diff --git a/backend/app/db.py b/backend/app/db.py index 38e1672..13530bd 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -3,7 +3,10 @@ import aiosqlite -DB_PATH = os.path.join(os.path.dirname(__file__), "..", "patchpilot.db") +DB_PATH = os.environ.get( + "PATCHPILOT_DB_PATH", + os.path.join(os.path.dirname(__file__), "..", "patchpilot.db"), +) async def init_db(): @@ -30,6 +33,7 @@ async def init_db(): message TEXT, package_name TEXT, package_version TEXT, + ml_score REAL, created_at TEXT DEFAULT (datetime('now')) ) """) @@ -79,6 +83,9 @@ async def init_db(): await db.execute("ALTER TABLE findings ADD COLUMN package_name TEXT") await db.execute("ALTER TABLE findings ADD COLUMN package_version TEXT") + if "ml_score" not in columns: + await db.execute("ALTER TABLE findings ADD COLUMN ml_score REAL") + cursor = await db.execute("PRAGMA table_info(jobs)") job_columns = [row["name"] for row in await cursor.fetchall()] diff --git a/backend/app/main.py b/backend/app/main.py index d7d7629..1457236 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -446,11 +446,12 @@ def update_progress(phase, status): message, pkg_name, pkg_version, + f.ml_score, ) ) if rows: await db.executemany( - "INSERT INTO findings (id, job_id, rule_id, severity, category, file_path, line_number, cwe, scanner, message, package_name, package_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO findings (id, job_id, rule_id, severity, category, file_path, line_number, cwe, scanner, message, package_name, package_version, ml_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", rows, ) await db.execute( @@ -802,7 +803,7 @@ async def get_findings(job_id: str): cur = await db.execute( """ SELECT id, rule_id, severity, category, file_path, - line_number, cwe, scanner, message, package_name, package_version, created_at + line_number, cwe, scanner, message, package_name, package_version, created_at, ml_score FROM findings WHERE job_id = ? ORDER BY created_at @@ -1089,12 +1090,13 @@ async def _run_repo_scan_task( message, pkg_name, pkg_version, + f.ml_score, ) ) if rows: await db.executemany( - "INSERT INTO findings (id, job_id, rule_id, severity, category, file_path, line_number, cwe, scanner, message, package_name, package_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO findings (id, job_id, rule_id, severity, category, file_path, line_number, cwe, scanner, message, package_name, package_version, ml_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", rows, ) @@ -1404,7 +1406,8 @@ async def get_org_findings(org_job_id: str): f.severity, f.file_path, f.line_number, - f.cwe + f.cwe, + f.ml_score FROM findings f JOIN jobs j ON f.job_id = j.job_id WHERE j.org_job_id = ? diff --git a/backend/tests/test_job_endpoints.py b/backend/tests/test_job_endpoints.py index d616092..2d15faa 100644 --- a/backend/tests/test_job_endpoints.py +++ b/backend/tests/test_job_endpoints.py @@ -20,7 +20,10 @@ "cwe", "scanner", "message", + "package_name", + "package_version", "created_at", + "ml_score", ) VERIFY_COLS = ("id", "job_id", "passed", "new_issues_introduced", "verified_at") @@ -35,7 +38,10 @@ None, "semgrep", "Hardcoded secret detected", + None, + None, "2024-01-01 00:00:00", + 0.85, ), ( str(uuid.uuid4()), @@ -47,7 +53,10 @@ None, "osv", "Vulnerable dependency", + None, + None, "2024-01-01 00:00:01", + None, ), ( str(uuid.uuid4()), @@ -59,7 +68,10 @@ None, "gitleaks", "API key exposed", + None, + None, "2024-01-01 00:00:02", + 0.95, ), ] @@ -140,7 +152,15 @@ def test_finding_fields(self): f = res.json()["findings"][0] assert all( k in f - for k in ("id", "rule_id", "severity", "category", "scanner", "message") + for k in ( + "id", + "rule_id", + "severity", + "category", + "scanner", + "message", + "ml_score", + ) ) diff --git a/frontend/src/app/data/sample-data.ts b/frontend/src/app/data/sample-data.ts index 4ed192b..2dba3ff 100644 --- a/frontend/src/app/data/sample-data.ts +++ b/frontend/src/app/data/sample-data.ts @@ -16,6 +16,7 @@ export interface Finding { code: string; suggestedFix?: string; references?: string[]; + ml_score?: number; } export interface Job { @@ -59,6 +60,7 @@ export const sampleFindings: Finding[] = [ "CWE-89: SQL Injection", "OWASP Top 10: A03:2021 – Injection", ], + ml_score: 0.92, }, { id: "F-002", @@ -118,6 +120,7 @@ export async function getAwsConfig() { "AWS Security Best Practices", "CWE-798: Use of Hard-coded Credentials", ], + ml_score: 0.96, }, { id: "F-004", @@ -145,6 +148,7 @@ app.get('/download', (req, res) => { "CWE-22: Path Traversal", "OWASP: Path Traversal", ], + ml_score: 0.78, }, { id: "F-005", @@ -166,6 +170,7 @@ app.get('/download', (req, res) => { "GHSA-7fh5-64p2-3v2j", "CVE-2020-8203", ], + ml_score: 0.65, }, { id: "F-006", @@ -191,6 +196,7 @@ function UserProfile({ user }) { "CWE-79: Cross-site Scripting (XSS)", "React Security Best Practices", ], + ml_score: 0.58, }, { id: "F-007", @@ -214,6 +220,7 @@ function generateToken() { references: [ "CWE-330: Use of Insufficiently Random Values", ], + ml_score: 0.35, }, { id: "F-008", @@ -232,6 +239,7 @@ function generateToken() { references: [ "CWE-209: Information Exposure Through an Error Message", ], + ml_score: 0.22, }, { id: "F-009", @@ -249,6 +257,7 @@ function generateToken() { references: [ "Node.js Crypto Documentation", ], + ml_score: 0.12, }, ]; diff --git a/frontend/src/app/lib/api.ts b/frontend/src/app/lib/api.ts index 62a9d2d..867d90c 100644 --- a/frontend/src/app/lib/api.ts +++ b/frontend/src/app/lib/api.ts @@ -54,6 +54,7 @@ features?: Record; code?: string; suggested_fix?: string; references?: string[]; + ml_score?: number; }; export type ScanInitResponse = { diff --git a/frontend/src/app/lib/mappers.ts b/frontend/src/app/lib/mappers.ts index 5f34a15..19aac71 100644 --- a/frontend/src/app/lib/mappers.ts +++ b/frontend/src/app/lib/mappers.ts @@ -41,5 +41,6 @@ export function mapBackendFindingToUi(f: BackendFinding): Finding { code: f.code ?? "", suggestedFix: f.suggested_fix, references: f.references ?? [], + ml_score: f.ml_score, }; } \ No newline at end of file diff --git a/frontend/src/app/pages/findings.tsx b/frontend/src/app/pages/findings.tsx index 82f1c70..284ab9d 100644 --- a/frontend/src/app/pages/findings.tsx +++ b/frontend/src/app/pages/findings.tsx @@ -113,6 +113,30 @@ function ExportReportButton({ scanId }: { scanId: string }) { ); } +export function MlScorePill({ score }: { score: number }) { + const percentage = Math.round(score * 100); + + let colorClasses = ""; + if (score >= 0.75) { + colorClasses = "bg-rose-500/10 border-rose-500/20 text-rose-600 dark:bg-rose-500/20 dark:border-rose-500/30 dark:text-rose-400"; + } else if (score >= 0.5) { + colorClasses = "bg-amber-500/10 border-amber-500/20 text-amber-600 dark:bg-amber-500/20 dark:border-amber-500/30 dark:text-amber-400"; + } else { + colorClasses = "bg-slate-500/10 border-slate-500/20 text-slate-600 dark:bg-slate-500/20 dark:border-slate-500/30 dark:text-slate-400"; + } + + return ( + + {percentage}% + + ); +} + export function Findings() { const navigate = useNavigate(); @@ -142,6 +166,7 @@ export function Findings() { const [selectedFindings, setSelectedFindings] = useState>(new Set()); const [detailFinding, setDetailFinding] = useState(null); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + const [sortBy, setSortBy] = useState<"severity" | "ml_score">("severity"); const handleStatusUpdate = async (findingId: string, newStatus: "open" | "accepted" | "ignored") => { setIsUpdatingStatus(true); @@ -223,7 +248,7 @@ export function Findings() { const filteredFindings = useMemo(() => { const q = searchQuery.trim().toLowerCase(); - return findings.filter((f) => { + const filtered = findings.filter((f) => { const matchesQuery = q.length === 0 || f.title.toLowerCase().includes(q) || @@ -235,7 +260,43 @@ export function Findings() { return matchesQuery && matchesSeverity; }); - }, [findings, searchQuery, activeSeverities]); + + const severityOrder: Record = { + critical: 4, + high: 3, + medium: 2, + low: 1, + info: 0, + }; + + if (sortBy === "ml_score") { + filtered.sort((a, b) => { + const scoreA = a.ml_score ?? 0; + const scoreB = b.ml_score ?? 0; + if (scoreB !== scoreA) { + return scoreB - scoreA; + } + // secondary sort: severity + const sevA = severityOrder[a.severity] ?? 0; + const sevB = severityOrder[b.severity] ?? 0; + return sevB - sevA; + }); + } else { + filtered.sort((a, b) => { + const sevA = severityOrder[a.severity] ?? 0; + const sevB = severityOrder[b.severity] ?? 0; + if (sevB !== sevA) { + return sevB - sevA; + } + // secondary sort: ml_score + const scoreA = a.ml_score ?? 0; + const scoreB = b.ml_score ?? 0; + return scoreB - scoreA; + }); + } + + return filtered; + }, [findings, searchQuery, activeSeverities, sortBy]); if (isLoadingFindings) { return ( @@ -289,10 +350,38 @@ export function Findings() { className="pl-9" /> - +
+
+ + +
+ +
@@ -361,7 +450,12 @@ export function Findings() { /> - +
+ + {finding.ml_score !== undefined && finding.ml_score !== null && ( + + )} +
@@ -427,6 +521,9 @@ export function Findings() {
+ {finding.ml_score !== undefined && finding.ml_score !== null && ( + + )}
@@ -456,6 +553,9 @@ export function Findings() {
+ {detailFinding.ml_score !== undefined && detailFinding.ml_score !== null && ( + + )} {detailFinding.confidence}% Confidence