mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 18:56:53 +00:00
feat(stacks): surface post-deploy scan attempt status (#1198)
triggerPostDeployScan was fire-and-forget. When Trivy was missing on a
node, when the registry refused the digest lookup, or when a single
image scan threw, the failure went to console.error and the user
never learned. Open the security tab later, see stale data, no
indicator that the scan even tried.
Backend:
- New stack_scan_attempts table (node_id, stack_name, status,
attempted_at, error_message). One row per stack; latest attempt
overwrites the previous one.
- DatabaseService gains recordStackScanAttempt /
getStackScanAttempt / clearStackScanAttempts. Status is one of
'ok' | 'partial' | 'failed' | 'skipped'.
- triggerPostDeployScan in helpers/policyGate.ts now records every
exit path: 'skipped' when Trivy is unavailable or no images to
scan; 'failed' when container enumeration or all images fail;
'partial' when some images scan and others fail; 'ok' on full
success.
- New GET /api/stacks/:name/scan-status returns { status,
attemptedAt, errorMessage } or { status: null } when never tried.
- DELETE /:stackName cleanup chain now clears the row alongside
the existing update-status / auto-update cleanups.
Frontend:
- StackAnatomyPanel fetches /scan-status on stackName change.
- Renders a small warning strip below the update banner when
status !== 'ok' (failed / partial / skipped). Hidden when status
is 'ok' or unknown (never attempted). Title attribute carries
the full error message for hover inspection.
Cross-feature note: the audit doc flagged this as M-6 with a
coordination note for the pending Security feature audit. The
schema kept intentionally narrow (one row per stack, simple
status enum) so the Security audit can extend it (richer history,
per-image-row breakdown, etc.) without a destructive migration.
Resolves M-6 from the stack-management audit.
This commit is contained in:
@@ -746,6 +746,15 @@ export class DatabaseService {
|
||||
PRIMARY KEY (node_id, stack_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stack_scan_attempts (
|
||||
node_id INTEGER NOT NULL DEFAULT 0,
|
||||
stack_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
attempted_at INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
PRIMARY KEY (node_id, stack_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
@@ -2388,6 +2397,43 @@ export class DatabaseService {
|
||||
this.db.prepare('DELETE FROM stack_auto_update_settings WHERE node_id = ? AND stack_name = ?').run(nodeId, stackName);
|
||||
}
|
||||
|
||||
// --- Stack Scan Attempts ---
|
||||
//
|
||||
// Tracks the latest post-deploy scan attempt per (nodeId, stackName) so
|
||||
// operators can see when a scan was skipped or failed without scrolling
|
||||
// logs. One row per stack; the table is overwritten on every attempt.
|
||||
|
||||
public recordStackScanAttempt(
|
||||
nodeId: number,
|
||||
stackName: string,
|
||||
status: 'ok' | 'partial' | 'failed' | 'skipped',
|
||||
errorMessage: string | null,
|
||||
): void {
|
||||
this.db.prepare(
|
||||
`INSERT INTO stack_scan_attempts (node_id, stack_name, status, attempted_at, error_message)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(node_id, stack_name) DO UPDATE SET
|
||||
status = excluded.status,
|
||||
attempted_at = excluded.attempted_at,
|
||||
error_message = excluded.error_message`
|
||||
).run(nodeId, stackName, status, Date.now(), errorMessage);
|
||||
}
|
||||
|
||||
public getStackScanAttempt(nodeId: number, stackName: string): {
|
||||
status: string;
|
||||
attempted_at: number;
|
||||
error_message: string | null;
|
||||
} | null {
|
||||
const row = this.db.prepare(
|
||||
'SELECT status, attempted_at, error_message FROM stack_scan_attempts WHERE node_id = ? AND stack_name = ?'
|
||||
).get(nodeId, stackName) as { status: string; attempted_at: number; error_message: string | null } | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
public clearStackScanAttempts(nodeId: number, stackName: string): void {
|
||||
this.db.prepare('DELETE FROM stack_scan_attempts WHERE node_id = ? AND stack_name = ?').run(nodeId, stackName);
|
||||
}
|
||||
|
||||
public getNodeUpdateSummary(): Array<{ node_id: number; stacks_with_updates: number }> {
|
||||
return this.db.prepare(
|
||||
'SELECT node_id, SUM(has_update) as stacks_with_updates FROM stack_update_status WHERE has_update = 1 GROUP BY node_id'
|
||||
|
||||
Reference in New Issue
Block a user