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:
Anso
2026-05-24 15:44:12 -04:00
committed by GitHub
parent 009ec43638
commit d727a55a5f
5 changed files with 241 additions and 6 deletions
+21
View File
@@ -647,6 +647,7 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
try {
DatabaseService.getInstance().clearStackUpdateStatus(req.nodeId, stackName);
DatabaseService.getInstance().clearStackAutoUpdateSetting(req.nodeId, stackName);
DatabaseService.getInstance().clearStackScanAttempts(req.nodeId, stackName);
DatabaseService.getInstance().deleteRoleAssignmentsByResource('stack', stackName);
DatabaseService.getInstance().deleteGitSource(stackName);
if (debug) console.debug(`[Stacks:debug] Delete: db OK`, { stackName: sanitizedName });
@@ -1060,6 +1061,26 @@ stacksRouter.get('/:stackName/backup', async (req: Request, res: Response) => {
}
});
/**
* Returns the latest post-deploy scan attempt for this stack, or null if
* no scan has been attempted yet. Used by the editor UI to flag stacks
* whose latest deploy did not trigger a successful scan (Trivy missing,
* registry rejection, etc).
*/
stacksRouter.get('/:stackName/scan-status', (req: Request, res: Response): void => {
const stackName = req.params.stackName as string;
const row = DatabaseService.getInstance().getStackScanAttempt(req.nodeId, stackName);
if (!row) {
res.json({ status: null });
return;
}
res.json({
status: row.status,
attemptedAt: row.attempted_at,
errorMessage: row.error_message,
});
});
// ── File explorer endpoints ──
type FsErrorCode =