diff --git a/artifacts/skillguard/public/opengraph.jpg b/artifacts/skillguard/public/opengraph.jpg index 2828bf9..01f6431 100644 Binary files a/artifacts/skillguard/public/opengraph.jpg and b/artifacts/skillguard/public/opengraph.jpg differ diff --git a/artifacts/skillguard/src/pages/scan-report.tsx b/artifacts/skillguard/src/pages/scan-report.tsx index c4f5b69..e144103 100644 --- a/artifacts/skillguard/src/pages/scan-report.tsx +++ b/artifacts/skillguard/src/pages/scan-report.tsx @@ -11,7 +11,8 @@ import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { VerdictBadge, SeverityBadge, AxisBadge } from "@/components/ui-helpers"; import { formatDate } from "@/lib/format"; -import { ShieldQuestion, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield } from "lucide-react"; +import { ShieldQuestion, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown } from "lucide-react"; +import type { ScanDetail } from "@workspace/api-client-react"; export default function ScanReport() { const [, params] = useRoute("/berichte/:id"); @@ -78,6 +79,19 @@ export default function ScanReport() { URL.revokeObjectURL(url); }; + const handleExportPdf = () => { + const html = buildReportHtml(data); + const printWindow = window.open("", "_blank"); + if (!printWindow) return; + printWindow.document.open(); + printWindow.document.write(html); + printWindow.document.close(); + printWindow.focus(); + printWindow.onload = () => { + printWindow.print(); + }; + }; + return (
@@ -100,10 +114,16 @@ export default function ScanReport() { )}
- +
+ + +
{data.aiError && ( @@ -337,3 +357,191 @@ export default function ScanReport() { ); } + +const VERDICT_LABELS: Record = { + pass: "Freigabe", + review: "Manuelle Prüfung", + block: "Blockieren", +}; + +const SEVERITY_LABELS: Record = { + critical: "Kritisch", + high: "Hoch", + medium: "Mittel", + low: "Niedrig", + info: "Info", +}; + +const AXIS_LABELS: Record = { + security: "IT-Sicherheit", + privacy: "Datenschutz", +}; + +const SOURCE_LABELS: Record = { + upload: "Upload", + url: "URL", + paste: "Einfügung", +}; + +const KIND_LABELS: Record = { + instruction: "Anweisung", + script: "Skript", + resource: "Ressource", +}; + +function escapeHtml(value: unknown): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function riskSummary(score: number): string { + if (score < 30) return "Geringes Risiko. Keine bedenklichen Muster gefunden."; + if (score < 70) return "Mittleres Risiko. Einige Auffälligkeiten erfordern Prüfung."; + return "Hohes Risiko. Kritische Sicherheitsprobleme erkannt."; +} + +function buildReportHtml(data: ScanDetail): string { + const title = data.name || `Scan #${data.id}`; + const verdict = VERDICT_LABELS[data.verdict] ?? data.verdict; + const source = SOURCE_LABELS[data.source] ?? data.source; + const counts = data.findingCounts; + + const findingsHtml = data.findings.length === 0 + ? `

Keine Auffälligkeiten gefunden. Der analysierte Skill entspricht den Sicherheits- und Datenschutzrichtlinien.

` + : data.findings.map((f, i) => { + const location = (f.file || f.line) + ? `
Fundstelle: ${escapeHtml(f.file || "unbekannt")}${f.line ? `:${escapeHtml(f.line)}` : ""}
` + : ""; + const snippet = f.snippet + ? `
${escapeHtml(f.snippet)}
` + : ""; + const remediation = f.remediation + ? `
Empfehlung: ${escapeHtml(f.remediation)}
` + : ""; + return ` +
+
+ ${i + 1}. + ${escapeHtml(f.title)} +
+
+ Schweregrad: ${escapeHtml(SEVERITY_LABELS[f.severity] ?? f.severity)} + Bereich: ${escapeHtml(AXIS_LABELS[f.axis] ?? f.axis)} + Regel: ${escapeHtml(f.ruleId)} + Erkennung: ${f.detectedBy === "ai" ? "KI" : "Statisch"} +
+ ${location} +

${escapeHtml(f.description)}

+ ${snippet} + ${remediation} +
`; + }).join(""); + + const aiWarning = data.aiError + ? `
KI-Analyse nicht durchgeführt: ${escapeHtml(data.aiError)}. Die statische Analyse wurde dennoch abgeschlossen.
` + : ""; + + return ` + + + +SkillGuard Bericht - ${escapeHtml(title)} + + + +

SkillGuard Sicherheitsbericht

+

+ ${escapeHtml(title)}  |  ${escapeHtml(verdict)}
+ Erstellt am ${escapeHtml(formatDate(data.createdAt))}  |  Quelle: ${escapeHtml(source)}  |  ${escapeHtml(data.fileCount)} ${data.fileCount === 1 ? "Datei" : "Dateien"}${data.aiUsed ? "  |  KI-Analyse aktiv" : ""} +

+ + ${aiWarning} + +

Risiko-Score

+
+
+
${escapeHtml(data.riskScore)}
+
/ 100
+
+
${riskSummary(data.riskScore)}
+
+ +

Achsen-Zusammenfassung

+ + + + + + + + + + + + + + +
KennzahlAnzahl
Kritisch${escapeHtml(counts.critical)}
Hoch${escapeHtml(counts.high)}
Mittel${escapeHtml(counts.medium)}
Niedrig${escapeHtml(counts.low)}
Info${escapeHtml(counts.info)}
IT-Sicherheit${escapeHtml(counts.security)}
Datenschutz${escapeHtml(counts.privacy)}
Gesamt${escapeHtml(counts.total)}
+ +

Auffälligkeiten (${data.findings.length})

+ ${findingsHtml} + +

Geprüfte Dateien (${data.files.length})

+ + + + + + ${data.files.length === 0 + ? `` + : data.files.map(file => ` + + + + + + `).join("")} + +
PfadTypSpracheGröße
Keine Dateien verfügbar.
${escapeHtml(file.path)}${escapeHtml(KIND_LABELS[file.kind] ?? file.kind)}${escapeHtml(file.language || "-")}${escapeHtml(file.size)} B
+ + + +`; +}