mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +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:
@@ -82,7 +82,13 @@ export async function triggerPostDeployScan(
|
||||
nodeId: number,
|
||||
): Promise<void> {
|
||||
const svc = TrivyService.getInstance();
|
||||
if (!svc.isTrivyAvailable()) return;
|
||||
const db = DatabaseService.getInstance();
|
||||
if (!svc.isTrivyAvailable()) {
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'skipped', 'Trivy is not available on this node');
|
||||
return;
|
||||
}
|
||||
let imageFailures = 0;
|
||||
let imageSuccesses = 0;
|
||||
try {
|
||||
const docker = DockerController.getInstance(nodeId).getDocker();
|
||||
const containers = await docker.listContainers({
|
||||
@@ -93,18 +99,23 @@ export async function triggerPostDeployScan(
|
||||
for (const c of containers as Array<{ Image?: string }>) {
|
||||
if (c.Image && !c.Image.startsWith('sha256:')) imageRefs.add(c.Image);
|
||||
}
|
||||
if (imageRefs.size === 0) return;
|
||||
|
||||
const db = DatabaseService.getInstance();
|
||||
if (imageRefs.size === 0) {
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'skipped', 'No images to scan');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const imageRef of imageRefs) {
|
||||
try {
|
||||
const digest = await svc.getImageDigest(imageRef, nodeId);
|
||||
if (digest) {
|
||||
const cached = db.getLatestScanByDigest(digest, 'vuln');
|
||||
if (cached && Date.now() - cached.scanned_at < DIGEST_CACHE_TTL_MS) continue;
|
||||
if (cached && Date.now() - cached.scanned_at < DIGEST_CACHE_TTL_MS) {
|
||||
imageSuccesses += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const scan = await svc.runScanAndPersist(imageRef, nodeId, 'deploy', stackName);
|
||||
imageSuccesses += 1;
|
||||
|
||||
if (scan.critical_count > 0 || scan.high_count > 0) {
|
||||
NotificationService.getInstance().dispatchAlert(
|
||||
@@ -115,6 +126,7 @@ export async function triggerPostDeployScan(
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
imageFailures += 1;
|
||||
const message = getErrorMessage(err, 'unknown error');
|
||||
console.error(`[Security] Post-deploy scan failed for ${imageRef}:`, message);
|
||||
NotificationService.getInstance().dispatchAlert(
|
||||
@@ -125,7 +137,17 @@ export async function triggerPostDeployScan(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFailures === 0) {
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'ok', null);
|
||||
} else if (imageSuccesses === 0) {
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'failed', `${imageFailures} image(s) failed to scan`);
|
||||
} else {
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'partial', `${imageFailures} of ${imageFailures + imageSuccesses} image(s) failed`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Security] triggerPostDeployScan error for %s:', sanitizeForLog(stackName), sanitizeForLog(getErrorMessage(err, 'unknown error')));
|
||||
const message = getErrorMessage(err, 'unknown error');
|
||||
console.error('[Security] triggerPostDeployScan error for %s:', sanitizeForLog(stackName), sanitizeForLog(message));
|
||||
db.recordStackScanAttempt(nodeId, stackName, 'failed', message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user