Skip to content
Merged
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
9 changes: 8 additions & 1 deletion backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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'))
)
""")
Expand Down Expand Up @@ -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()]

Expand Down
11 changes: 7 additions & 4 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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 = ?
Expand Down
22 changes: 21 additions & 1 deletion backend/tests/test_job_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -35,7 +38,10 @@
None,
"semgrep",
"Hardcoded secret detected",
None,
None,
"2024-01-01 00:00:00",
0.85,
),
(
str(uuid.uuid4()),
Expand All @@ -47,7 +53,10 @@
None,
"osv",
"Vulnerable dependency",
None,
None,
"2024-01-01 00:00:01",
None,
),
(
str(uuid.uuid4()),
Expand All @@ -59,7 +68,10 @@
None,
"gitleaks",
"API key exposed",
None,
None,
"2024-01-01 00:00:02",
0.95,
),
]

Expand Down Expand Up @@ -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",
)
)


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

export interface Job {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -145,6 +148,7 @@ app.get('/download', (req, res) => {
"CWE-22: Path Traversal",
"OWASP: Path Traversal",
],
ml_score: 0.78,
},
{
id: "F-005",
Expand All @@ -166,6 +170,7 @@ app.get('/download', (req, res) => {
"GHSA-7fh5-64p2-3v2j",
"CVE-2020-8203",
],
ml_score: 0.65,
},
{
id: "F-006",
Expand All @@ -191,6 +196,7 @@ function UserProfile({ user }) {
"CWE-79: Cross-site Scripting (XSS)",
"React Security Best Practices",
],
ml_score: 0.58,
},
{
id: "F-007",
Expand All @@ -214,6 +220,7 @@ function generateToken() {
references: [
"CWE-330: Use of Insufficiently Random Values",
],
ml_score: 0.35,
},
{
id: "F-008",
Expand All @@ -232,6 +239,7 @@ function generateToken() {
references: [
"CWE-209: Information Exposure Through an Error Message",
],
ml_score: 0.22,
},
{
id: "F-009",
Expand All @@ -249,6 +257,7 @@ function generateToken() {
references: [
"Node.js Crypto Documentation",
],
ml_score: 0.12,
},
];

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 @@ -54,6 +54,7 @@ features?: Record<string, unknown>;
code?: string;
suggested_fix?: string;
references?: string[];
ml_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 @@ -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,
};
}
114 changes: 107 additions & 7 deletions frontend/src/app/pages/findings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<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
)}
>
{percentage}%
</span>
);
}

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

Expand Down Expand Up @@ -142,6 +166,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 handleStatusUpdate = async (findingId: string, newStatus: "open" | "accepted" | "ignored") => {
setIsUpdatingStatus(true);
Expand Down Expand Up @@ -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) ||
Expand All @@ -235,7 +260,43 @@ export function Findings() {

return matchesQuery && matchesSeverity;
});
}, [findings, searchQuery, activeSeverities]);

const severityOrder: Record<string, number> = {
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 (
Expand Down Expand Up @@ -289,10 +350,38 @@ export function Findings() {
className="pl-9"
/>
</div>
<Button variant="outline">
<Filter className="h-4 w-4 mr-2" />
More Filters
</Button>
<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex items-center rounded-lg border border-border bg-muted/20 p-1 text-muted-foreground">
<button
type="button"
onClick={() => setSortBy("severity")}
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 === "severity"
? "bg-background text-foreground shadow-sm"
: "hover:bg-muted/50 hover:text-foreground"
)}
>
Severity
</button>
<button
type="button"
onClick={() => setSortBy("ml_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 === "ml_score"
? "bg-background text-foreground shadow-sm"
: "hover:bg-muted/50 hover:text-foreground"
)}
>
ML Score
</button>
</div>
<Button variant="outline">
<Filter className="h-4 w-4 mr-2" />
More Filters
</Button>
</div>
</div>
<div className="mt-4">
<FilterChips filters={filters} onToggle={toggleFilter} />
Expand Down Expand Up @@ -361,7 +450,12 @@ export function Findings() {
/>
</TableCell>
<TableCell>
<SeverityChip severity={finding.severity} />
<div className="flex items-center gap-2">
<SeverityChip severity={finding.severity} />
{finding.ml_score !== undefined && finding.ml_score !== null && (
<MlScorePill score={finding.ml_score} />
)}
</div>
</TableCell>
<TableCell>
<div className="font-medium max-w-md truncate">
Expand Down Expand Up @@ -427,6 +521,9 @@ export function Findings() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<SeverityChip severity={finding.severity} />
{finding.ml_score !== undefined && finding.ml_score !== null && (
<MlScorePill score={finding.ml_score} />
)}
<ToolBadge tool={finding.tool} />
</div>
<div className="font-medium mb-1 line-clamp-2">
Expand Down Expand Up @@ -456,6 +553,9 @@ export function Findings() {
<div className="flex-1 overflow-y-auto mt-6 space-y-8">
<div className="flex flex-wrap items-center gap-3">
<SeverityChip severity={detailFinding.severity} />
{detailFinding.ml_score !== undefined && detailFinding.ml_score !== null && (
<MlScorePill score={detailFinding.ml_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