Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ async def init_db():
package_name TEXT,
package_version TEXT,
ml_score REAL,
risk_score REAL,
created_at TEXT DEFAULT (datetime('now'))
)
""")
Expand Down Expand Up @@ -86,6 +87,9 @@ async def init_db():
if "ml_score" not in columns:
await db.execute("ALTER TABLE findings ADD COLUMN ml_score REAL")

if "risk_score" not in columns:
await db.execute("ALTER TABLE findings ADD COLUMN risk_score REAL")

cursor = await db.execute("PRAGMA table_info(jobs)")
job_columns = [row["name"] for row in await cursor.fetchall()]

Expand Down
25 changes: 17 additions & 8 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,18 @@ def health():
}


def _prioritize_findings(findings: List[Finding]) -> List[Finding]:
def score(f: Finding) -> int:
def _calculate_risk_scores(findings: List[Finding]) -> None:
for f in findings:
sev = {"CRITICAL": 100, "HIGH": 80, "MEDIUM": 50, "LOW": 20, "INFO": 5}.get(
f.severity, 10
)
tw = {"dependency": 25, "secret": 35, "sast": 20}.get(f.category, 10)
return sev + tw

return sorted(findings, key=score, reverse=True)
base_score = sev + tw
ml_bonus = (f.ml_score * 50.0) if getattr(f, "ml_score", None) is not None else 0.0
total = float(base_score) + ml_bonus
if getattr(f, "reachability", None) and getattr(f.reachability, "reachable", False):
total *= 1.5
f.risk_score = round(total, 2)


def _extract_dependencies(repo_dir: Path) -> List[tuple[str, str]]:
Expand Down Expand Up @@ -216,13 +219,18 @@ def _scan_repo_dir(repo_dir: Path, progress_cb=None):

findings = scoring_function(findings, RANKER)

_calculate_risk_scores(findings)

if RANKER:
findings.sort(
key=lambda f: getattr(f, "ml_score", 0.0),
reverse=True,
)
else:
findings = _prioritize_findings(findings)
findings.sort(
key=lambda f: getattr(f, "risk_score", 0.0),
reverse=True,
)

return semgrep, osv, gitleaks, entropy, findings

Expand Down Expand Up @@ -447,11 +455,12 @@ def update_progress(phase, status):
pkg_name,
pkg_version,
f.ml_score,
f.risk_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, ml_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO findings (id, job_id, rule_id, severity, category, file_path, line_number, cwe, scanner, message, package_name, package_version, ml_score, risk_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
rows,
)
await db.execute(
Expand Down Expand Up @@ -803,7 +812,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, ml_score
line_number, cwe, scanner, message, package_name, package_version, created_at, ml_score, risk_score
FROM findings
WHERE job_id = ?
ORDER BY created_at
Expand Down
1 change: 1 addition & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Finding(BaseModel):
reachability: Optional[Reachability] = None
features: Optional[Dict[str, Any]] = Field(default_factory=dict)
ml_score: Optional[float] = None
risk_score: Optional[float] = None


class ScanResponse(BaseModel):
Expand Down
25 changes: 24 additions & 1 deletion backend/app/reports/evidence_pack.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

import json
import sqlite3
import zipfile
from datetime import datetime, timezone
from pathlib import Path

from ..utils.exec import run_cmd
from ..db import DB_PATH


def build_evidence_pack(
Expand Down Expand Up @@ -38,6 +41,19 @@ def build_evidence_pack(
gitleaks.get("stdout", ""), encoding="utf-8"
)

# Dump prioritized findings
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM findings WHERE job_id = ? ORDER BY risk_score DESC", (job_id,)
).fetchall()
conn.close()

findings_list = [dict(r) for r in rows]
(pack_root / "prioritized_findings.json").write_text(
json.dumps(findings_list, indent=2), encoding="utf-8"
)

report_md = _render_report(project_name=project_name, job_id=job_id)
(pack_root / "REPORT.md").write_text(report_md, encoding="utf-8")

Expand All @@ -58,14 +74,21 @@ def _render_report(project_name: str, job_id: str) -> str:
**Generated:** {datetime.now(timezone.utc).isoformat()}

## What this pack contains
- `prioritized_findings.json` — All findings scored by the Risk-Based Prioritization Engine
- `raw/semgrep.json` — SAST scan results (Semgrep)
- `raw/osv.json` — Dependency vulnerability results (OSV-Scanner)
- `raw/gitleaks.json` — Secret detection results (Gitleaks)
- This `REPORT.md` summary

## Risk Score Methodology
The Risk-Based Prioritization Engine calculates a `risk_score` for each finding using the following criteria:
1. **Base Score**: Severity weight + Category weight (e.g. CRITICAL=100, Secret=35).
2. **ML Modifier**: Machine Learning confidence score (+ up to 50 points).
3. **Reachability**: If a finding is verifiably reachable in code, its total score is multiplied by 1.5.

## Methodology (high-level)
1. Scan codebase for vulnerabilities (SAST, dependency CVEs, secrets).
2. Prioritize findings by severity and likely impact.
2. Prioritize findings using the Risk Engine.
3. Apply or suggest minimal remediation steps.
4. Provide verification artifacts and re-scan outputs.

Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/data/sample-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface Finding {
suggestedFix?: string;
references?: string[];
ml_score?: number;
risk_score?: number;
}

export interface Job {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ features?: Record<string, unknown>;
suggested_fix?: string;
references?: string[];
ml_score?: number;
risk_score?: number;
};

export type ScanInitResponse = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/lib/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ export function mapBackendFindingToUi(f: BackendFinding): Finding {
suggestedFix: f.suggested_fix,
references: f.references ?? [],
ml_score: f.ml_score,
risk_score: f.risk_score,
};
}
58 changes: 56 additions & 2 deletions frontend/src/app/pages/findings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,28 @@ export function MlScorePill({ score }: { score: number }) {
);
}

export function RiskScorePill({ score }: { score: number }) {
let colorClasses = "";
if (score >= 100) {
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 >= 50) {
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 (
<span
className={cn(
"inline-flex items-center px-1.5 py-0.5 rounded border text-[10px] font-bold font-mono tracking-wide shadow-sm select-none",
colorClasses
)}
>
Risk: {score}
</span>
);
}

export function Findings() {
const navigate = useNavigate();

Expand Down Expand Up @@ -166,7 +188,7 @@ export function Findings() {
const [selectedFindings, setSelectedFindings] = useState<Set<string>>(new Set());
const [detailFinding, setDetailFinding] = useState<Finding | null>(null);
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [sortBy, setSortBy] = useState<"severity" | "ml_score">("severity");
const [sortBy, setSortBy] = useState<"severity" | "ml_score" | "risk_score">("risk_score");

const handleStatusUpdate = async (findingId: string, newStatus: "open" | "accepted" | "ignored") => {
setIsUpdatingStatus(true);
Expand Down Expand Up @@ -282,7 +304,18 @@ export function Findings() {
info: 0,
};

if (sortBy === "ml_score") {
if (sortBy === "risk_score") {
filtered.sort((a, b) => {
const scoreA = a.risk_score ?? 0;
const scoreB = b.risk_score ?? 0;
if (scoreB !== scoreA) {
return scoreB - scoreA;
}
const sevA = severityOrder[a.severity] ?? 0;
const sevB = severityOrder[b.severity] ?? 0;
return sevB - sevA;
});
} else if (sortBy === "ml_score") {
filtered.sort((a, b) => {
const scoreA = a.ml_score ?? 0;
const scoreB = b.ml_score ?? 0;
Expand Down Expand Up @@ -377,6 +410,18 @@ export function Findings() {
>
Severity
</button>
<button
type="button"
onClick={() => setSortBy("risk_score")}
className={cn(
"inline-flex items-center justify-center rounded-md px-3 py-1.5 text-xs font-semibold ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
sortBy === "risk_score"
? "bg-background text-foreground shadow-sm"
: "hover:bg-muted/50 hover:text-foreground"
)}
>
Risk Score
</button>
<button
type="button"
onClick={() => setSortBy("ml_score")}
Expand Down Expand Up @@ -468,6 +513,9 @@ export function Findings() {
{finding.ml_score !== undefined && finding.ml_score !== null && (
<MlScorePill score={finding.ml_score} />
)}
{finding.risk_score !== undefined && finding.risk_score !== null && (
<RiskScorePill score={finding.risk_score} />
)}
</div>
</TableCell>
<TableCell>
Expand Down Expand Up @@ -537,6 +585,9 @@ export function Findings() {
{finding.ml_score !== undefined && finding.ml_score !== null && (
<MlScorePill score={finding.ml_score} />
)}
{finding.risk_score !== undefined && finding.risk_score !== null && (
<RiskScorePill score={finding.risk_score} />
)}
<ToolBadge tool={finding.tool} />
</div>
<div className="font-medium mb-1 line-clamp-2">
Expand Down Expand Up @@ -569,6 +620,9 @@ export function Findings() {
{detailFinding.ml_score !== undefined && detailFinding.ml_score !== null && (
<MlScorePill score={detailFinding.ml_score} />
)}
{detailFinding.risk_score !== undefined && detailFinding.risk_score !== null && (
<RiskScorePill score={detailFinding.risk_score} />
)}
<ToolBadge tool={detailFinding.tool} />
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-muted/30 text-muted-foreground text-[11px] font-bold uppercase tracking-wider">
{detailFinding.confidence}% Confidence
Expand Down
Loading