import { Router, type IRouter } from "express"; import { db } from "@workspace/db"; import { scansTable, scanFilesTable, findingsTable, type Scan, type ScanFile, type Finding, } from "@workspace/db"; import { eq, desc } from "drizzle-orm"; import { ListScansResponse, CreateScanBody, GetScanParams, GetScanResponse, DeleteScanParams, } from "@workspace/api-zod"; import { parseZip, parseSingleFile, parseText, deriveScanName, } from "../lib/skillParser"; import { analyzeSkill } from "../lib/scanEngine"; import type { ParsedFile } from "../lib/ruleCatalog"; import { logger } from "../lib/logger"; const router: IRouter = Router(); export function serializeScan(scan: Scan) { return { id: scan.id, name: scan.name, source: scan.source, status: scan.status, verdict: scan.verdict, riskScore: scan.riskScore, fileCount: scan.fileCount, aiUsed: scan.aiUsed, aiError: scan.aiError, findingCounts: scan.findingCounts, createdAt: scan.createdAt.toISOString(), }; } function serializeFile(f: ScanFile) { return { path: f.path, kind: f.kind, language: f.language, size: f.size, }; } function serializeFinding(f: Finding) { return { id: f.id, ruleId: f.ruleId, axis: f.axis, severity: f.severity, title: f.title, description: f.description, remediation: f.remediation, file: f.file, line: f.line, snippet: f.snippet, detectedBy: f.detectedBy, }; } router.get("/scans", async (_req, res) => { const rows = await db .select() .from(scansTable) .orderBy(desc(scansTable.createdAt)); res.json(ListScansResponse.parse(rows.map(serializeScan))); }); router.post("/scans", async (req, res) => { const parsed = CreateScanBody.safeParse(req.body); if (!parsed.success) { return res .status(400) .json({ message: "Ungültige Eingabe", details: parsed.error.issues }); } const input = parsed.data; let files: ParsedFile[] = []; try { if (input.source === "zip") { if (!input.contentBase64) return res.status(400).json({ message: "ZIP-Inhalt fehlt." }); files = parseZip(Buffer.from(input.contentBase64, "base64")); } else if (input.source === "file") { if (!input.contentBase64) return res.status(400).json({ message: "Dateiinhalt fehlt." }); files = [ parseSingleFile( input.filename ?? "datei", Buffer.from(input.contentBase64, "base64"), ), ]; } else { if (!input.text || !input.text.trim()) return res.status(400).json({ message: "Text fehlt." }); files = [parseText(input.text, input.name ?? "SKILL.md")]; } } catch (err) { logger.error({ err }, "Skill-Parsing fehlgeschlagen"); return res.status(400).json({ message: "Das Skill konnte nicht gelesen werden. Bitte prüfen Sie das Format (gültiges ZIP / Textdatei).", }); } if (files.length === 0) { return res .status(400) .json({ message: "Keine analysierbaren Dateien gefunden." }); } const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill"); const result = await analyzeSkill(files, input.useAi); const [scan] = await db .insert(scansTable) .values({ name, source: input.source, status: "completed", verdict: result.verdict, riskScore: result.riskScore, fileCount: files.length, aiUsed: result.aiUsed, aiError: result.aiError, findingCounts: result.counts, }) .returning(); let insertedFiles: ScanFile[] = []; if (files.length > 0) { insertedFiles = await db .insert(scanFilesTable) .values( files.map((f) => ({ scanId: scan.id, path: f.path, kind: f.kind, language: f.language, size: f.size, })), ) .returning(); } let insertedFindings: Finding[] = []; if (result.findings.length > 0) { insertedFindings = await db .insert(findingsTable) .values( result.findings.map((f) => ({ scanId: scan.id, ruleId: f.ruleId, axis: f.axis, severity: f.severity, title: f.title, description: f.description, remediation: f.remediation, file: f.file, line: f.line, snippet: f.snippet, detectedBy: f.detectedBy, })), ) .returning(); } const payload = { ...serializeScan(scan), files: insertedFiles.map(serializeFile), findings: insertedFindings .sort((a, b) => a.id - b.id) .map(serializeFinding), }; return res.status(201).json(GetScanResponse.parse(payload)); }); router.get("/scans/:id", async (req, res) => { const params = GetScanParams.safeParse(req.params); if (!params.success) return res.status(400).json({ message: "Ungültige ID" }); const [scan] = await db .select() .from(scansTable) .where(eq(scansTable.id, params.data.id)); if (!scan) return res.status(404).json({ message: "Scan nicht gefunden" }); const files = await db .select() .from(scanFilesTable) .where(eq(scanFilesTable.scanId, scan.id)); const findings = await db .select() .from(findingsTable) .where(eq(findingsTable.scanId, scan.id)) .orderBy(findingsTable.id); const payload = { ...serializeScan(scan), files: files.map(serializeFile), findings: findings.map(serializeFinding), }; return res.json(GetScanResponse.parse(payload)); }); router.delete("/scans/:id", async (req, res) => { const params = DeleteScanParams.safeParse(req.params); if (!params.success) return res.status(400).json({ message: "Ungültige ID" }); await db.delete(scansTable).where(eq(scansTable.id, params.data.id)); return res.status(204).send(); }); export default router;