feat: security center page (/security)

Project-scoped security surface: scan trigger (dependency /
configuration / comprehensive, optional service), security score and
vulnerability metric cards, findings list with severity badges and
resolve/ignore actions, and scan history.

Fixed a latent bug in GET /projects/:id/security/metrics: it selected a
non-existent `score` column (the score lives in the security_scans
summary jsonb), so the endpoint 500'd on every project.

Verified live: comprehensive scan produced 4 real findings (insecure
public URL, curl|sh build, http source, unpinned tag) with score 47;
resolve flow and metrics aggregation confirmed.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 18:24:19 +02:00
1 parent d5c99acb8c
commit d5e6bfc40d
6 files changed
+345 -9

No files matched your search

+56
View File
@@ -1602,3 +1602,59 @@ export async function listHealthResults(): Promise<HAHealthResult[]> {
const payload = await requestJson<{ results?: HAHealthResult[] }>('/ha/health/results');
return payload.results ?? [];
}
export type SecurityScan = components['schemas']['SecurityScan'];
export type Vulnerability = components['schemas']['Vulnerability'];
export type SecurityMetrics = {
vulnerabilities?: {
total?: number; critical?: number; high?: number; medium?: number;
low?: number; open?: number; resolved?: number;
};
latest_scan?: { id?: string; score?: number; scanned_at?: string; status?: string };
compliance?: { overall_status?: string; score?: number; last_assessed?: string };
security_score?: number;
};
export async function startSecurityScan(input: {
project_id: string;
service_id?: string;
scan_type: 'dependency' | 'configuration' | 'comprehensive';
}): Promise<SecurityScan> {
return requestJson<SecurityScan>('/security/scans', {
method: 'POST',
body: JSON.stringify(input),
});
}
export async function getSecurityScan(scanId: string): Promise<SecurityScan> {
const payload = await requestJson<{ scan?: SecurityScan } | SecurityScan>(
`/security/scans/${encodeURIComponent(scanId)}`,
);
return 'scan' in payload && payload.scan ? payload.scan : (payload as SecurityScan);
}
export async function getSecurityHistory(projectId: string): Promise<SecurityScan[]> {
const payload = await requestJson<{ scans?: SecurityScan[] }>(
`/projects/${encodeURIComponent(projectId)}/security/history`,
);
return payload.scans ?? [];
}
export async function listVulnerabilities(projectId: string): Promise<Vulnerability[]> {
const payload = await requestJson<{ vulnerabilities?: Vulnerability[] }>(
`/projects/${encodeURIComponent(projectId)}/vulnerabilities`,
);
return payload.vulnerabilities ?? [];
}
export async function updateVulnerability(id: string, status: 'open' | 'resolved' | 'ignored'): Promise<void> {
await requestJson(`/vulnerabilities/${encodeURIComponent(id)}`, {
method: 'PUT',
body: JSON.stringify({ status }),
});
}
export async function getSecurityMetrics(projectId: string): Promise<SecurityMetrics> {
return requestJson<SecurityMetrics>(`/projects/${encodeURIComponent(projectId)}/security/metrics`);
}