127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
|
|
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 };
|
||
|
|
}
|