skillguard/artifacts/api-server/src/lib/scanEngine.ts

127 lines
3.3 KiB
TypeScript
Raw Normal View History

import { db } from "@workspace/db";
import {
rulesTable,
promptsTable,
aiProvidersTable,
type Prompt,
} from "@workspace/db";
import { eq } from "drizzle-orm";
import {
STATIC_RULES,
runStaticRule,
type ParsedFile,
type RawFinding,
type Severity,
} from "./ruleCatalog";
import type { FindingCounts as DbFindingCounts } from "@workspace/db";
import { runAiAnalysis } from "./aiAnalysis";
const SEVERITY_WEIGHT: Record<Severity, number> = {
critical: 50,
high: 18,
medium: 7,
low: 2,
info: 0,
};
export type EngineResult = {
findings: RawFinding[];
counts: DbFindingCounts;
riskScore: number;
verdict: "pass" | "review" | "block";
aiUsed: boolean;
aiError: string | null;
};
export function computeCounts(findings: RawFinding[]): DbFindingCounts {
const counts: DbFindingCounts = {
critical: 0,
high: 0,
medium: 0,
low: 0,
info: 0,
security: 0,
privacy: 0,
total: findings.length,
};
for (const f of findings) {
counts[f.severity] += 1;
counts[f.axis] += 1;
}
return counts;
}
export function computeScore(findings: RawFinding[]): number {
let score = 0;
for (const f of findings) score += SEVERITY_WEIGHT[f.severity];
return Math.min(100, score);
}
export function computeVerdict(
findings: RawFinding[],
score: number,
): "pass" | "review" | "block" {
const hasCritical = findings.some((f) => f.severity === "critical");
const hasHigh = findings.some((f) => f.severity === "high");
if (hasCritical || score >= 70) return "block";
if (hasHigh || score >= 20) return "review";
return "pass";
}
export async function analyzeSkill(
files: ParsedFile[],
useAi: boolean,
): Promise<EngineResult> {
const dbRules = await db.select().from(rulesTable);
const ruleConfig = new Map(
dbRules.map((r) => [r.ruleId, { enabled: r.enabled, severity: r.severity as Severity }]),
);
const findings: RawFinding[] = [];
for (const rule of STATIC_RULES) {
const cfg = ruleConfig.get(rule.ruleId);
if (cfg && !cfg.enabled) continue;
const severity = cfg?.severity ?? rule.defaultSeverity;
for (const file of files) {
findings.push(...runStaticRule(rule, file, severity));
}
}
let aiUsed = false;
let aiError: string | null = null;
if (useAi) {
const aiRulesEnabled = dbRules
.filter((r) => r.detectionType === "ai")
.some((r) => r.enabled);
const [provider] = await db
.select()
.from(aiProvidersTable)
.where(eq(aiProvidersTable.enabled, true))
.limit(1);
if (!aiRulesEnabled) {
aiError = "KI-Regeln sind im Regelwerk deaktiviert.";
} else if (!provider) {
aiError =
"Kein aktiver KI-Provider konfiguriert. Bitte im Admin-Bereich einrichten.";
} else if (!provider.apiToken) {
aiError = `Für den Provider "${provider.name}" ist kein API-Token hinterlegt.`;
} else {
const prompts: Prompt[] = await db.select().from(promptsTable);
const result = await runAiAnalysis(provider, prompts, files);
aiError = result.error;
if (!result.error) {
aiUsed = true;
findings.push(...result.findings);
}
}
}
const riskScore = computeScore(findings);
const counts = computeCounts(findings);
const verdict = computeVerdict(findings, riskScore);
return { findings, counts, riskScore, verdict, aiUsed, aiError };
}