mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 23:56:39 +00:00
feat: add dedicated Security page and policy-pack foundation (#1362)
* feat: add dedicated Security page and policy-pack foundation Bring vulnerability scanning, scan history, suppressions, Compose risks, secrets, policy packs, and scanner setup into one node-scoped Security command center instead of scattering them across Resources and Settings. - New top-level Security view with Overview, Images, Compose risks, Secrets, Policies, Suppressions, History, and Scanner setup tabs (status masthead + signal rail; controlled tabs with deep-link support). - Backend: GET /security/overview rollup and GET /security/policy-packs static catalog (auth-only, Community). DatabaseService gains an uncapped scan-status count and a node-eligible block-policy count, and getImageScanSummaries now projects secret and misconfig counts. - Reuse existing surfaces: the scan-history sheet, the control-governed suppression and acknowledgement panels, and the scan-detail sheet (now with an initial-tab prop so it opens on the matching finding type). - Extract a shared SeverityBadge (from Resources) and a TrivyManager (from Settings) so both surfaces render identical controls. - Resources "Scan history" now links into the Security page History tab. - Docs for the new Security surface and tests for the new endpoints, helpers, nav wiring, and tabs. * refactor: consolidate scanner and policy management onto the Security page Remove the Settings "Vulnerability Scanning" section now that the Security page covers the same ground, with every option preserved: - Scanner install / update / uninstall / auto-update live on the Scanner setup tab (TrivyManager). - Scan policies, the honor-suppressions toggle, and the replica managed-by-control / demote controls move into a new ScanPolicyManager on the Policies tab (paid; Community sees only the policy-pack catalog). - CVE suppressions and acknowledgements remain on the Suppressions tab. Wiring removed: the registry section and the now-empty Security settings group, the SectionId, the SettingsSectionContent case and the isPaid prop it was the sole consumer of, and SecuritySection itself. The dashboard configuration-status "Vulnerability scanning" row now navigates to the Security page Policies tab. Docs that pointed at "Settings -> Security -> Vulnerability Scanning" are swept to the relevant Security page tabs. * fix: harden Security page scanner refresh, policy-load errors, and secret-only badges Address independent-review findings on the Security page: - Scanner setup now refreshes Trivy state when the active node changes, so the displayed scanner status matches the node TrivyManager's actions target (both follow x-node-id). Previously, switching nodes on the tab left stale state. - ScanPolicyManager surfaces an explicit error state on a failed policy fetch instead of falling through to a false "No scan policies configured". - The shared SeverityBadge and the Images findings column no longer label a scan "clean" when it has secrets or misconfigurations but no CVE severity (highest_severity is derived from vulnerabilities only); they show a "Findings" state and the secret/misconfig counts instead. - The Overview enforcement note points to the Policies tab, not the removed Settings section. - The History tab auto-opens the scan-history sheet only on a deep-link (mount with the History tab active), not on every manual tab selection. Adds tests for the badge secret/misconfig state and the policy-load error state.
This commit is contained in:
@@ -17,6 +17,7 @@ import { isDebugEnabled } from '../utils/debug';
|
||||
import { blockIfReplica } from '../middleware/fleetSyncGuards';
|
||||
import { validateStackPatternForRedos } from './fleet';
|
||||
import { FINDING_SEVERITIES, POLICY_SEVERITIES } from '../utils/severity';
|
||||
import { DEFAULT_POLICY_PACKS } from '../services/policy-packs';
|
||||
|
||||
const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/;
|
||||
// Trivy emits misconfig rule ids in two shapes that Sencho persists verbatim:
|
||||
@@ -113,6 +114,28 @@ function shapeScanForResponse(scan: VulnerabilityScan): Omit<VulnerabilityScan,
|
||||
return { ...rest, policy_evaluation: parsePolicyEvaluation(policy_evaluation) };
|
||||
}
|
||||
|
||||
// A completed scan whose latest run is older than this is considered "stale" in
|
||||
// the Security overview. Named so the route and its tests share one value.
|
||||
export const STALE_SCAN_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
// Shape of the /overview response. Mirrors the frontend `SecurityOverview` type
|
||||
// (frontend/src/types/security.ts); annotating the response below makes a
|
||||
// renamed or dropped field a compile error here instead of an undefined read in
|
||||
// the UI. Keep the two in sync.
|
||||
interface SecurityOverviewResponse {
|
||||
scannedImages: number;
|
||||
critical: number;
|
||||
high: number;
|
||||
fixable: number;
|
||||
secrets: number;
|
||||
misconfigs: number;
|
||||
staleScans: number;
|
||||
failedScans: number;
|
||||
lastSuccessfulScanAt: number | null;
|
||||
scanner: { available: boolean; version: string | null; source: 'managed' | 'host' | 'none'; autoUpdate: boolean };
|
||||
deployEnforcement: { honorSuppressionsOnDeploy: boolean; eligibleBlockPolicies: number };
|
||||
}
|
||||
|
||||
export const securityRouter = Router();
|
||||
|
||||
securityRouter.get('/trivy-status', authMiddleware, (_req: Request, res: Response) => {
|
||||
@@ -437,6 +460,83 @@ securityRouter.get('/image-summaries', authMiddleware, (req: Request, res: Respo
|
||||
}
|
||||
});
|
||||
|
||||
// Node-scoped security posture rollup for the Security page Overview. Read-only,
|
||||
// auth-only (Community). Counts derive from the latest-completed-scan-per-image
|
||||
// summaries plus two precise helpers; the deploy-enforcement block is this
|
||||
// node's read-only posture, not policy management.
|
||||
securityRouter.get('/overview', authMiddleware, (req: Request, res: Response): void => {
|
||||
try {
|
||||
const db = DatabaseService.getInstance();
|
||||
const summaries = Object.values(db.getImageScanSummaries(req.nodeId));
|
||||
const settings = db.getGlobalSettings();
|
||||
const svc = TrivyService.getInstance();
|
||||
|
||||
const now = Date.now();
|
||||
let scannedImages = 0;
|
||||
let critical = 0;
|
||||
let high = 0;
|
||||
let fixable = 0;
|
||||
let secrets = 0;
|
||||
let misconfigs = 0;
|
||||
let staleScans = 0;
|
||||
let lastSuccessfulScanAt: number | null = null;
|
||||
|
||||
for (const s of summaries) {
|
||||
// Severity, secret, and misconfig totals are summed across every summary
|
||||
// (real images and stack/config scans alike). Only scannedImages excludes
|
||||
// the stack/config rows (stored under a "stack:" image_ref), since those
|
||||
// are stacks, not images.
|
||||
if (!s.image_ref.startsWith('stack:')) scannedImages += 1;
|
||||
critical += s.critical;
|
||||
high += s.high;
|
||||
fixable += s.fixable;
|
||||
secrets += s.secret_count;
|
||||
misconfigs += s.misconfig_count;
|
||||
if (now - s.scanned_at > STALE_SCAN_THRESHOLD_MS) staleScans += 1;
|
||||
if (lastSuccessfulScanAt === null || s.scanned_at > lastSuccessfulScanAt) {
|
||||
lastSuccessfulScanAt = s.scanned_at;
|
||||
}
|
||||
}
|
||||
|
||||
const overview: SecurityOverviewResponse = {
|
||||
scannedImages,
|
||||
critical,
|
||||
high,
|
||||
fixable,
|
||||
secrets,
|
||||
misconfigs,
|
||||
staleScans,
|
||||
failedScans: db.countScansByStatus(req.nodeId, 'failed'),
|
||||
lastSuccessfulScanAt,
|
||||
scanner: {
|
||||
available: svc.isTrivyAvailable(),
|
||||
version: svc.getVersion(),
|
||||
source: svc.getSource(),
|
||||
autoUpdate: settings.trivy_auto_update === '1',
|
||||
},
|
||||
deployEnforcement: {
|
||||
honorSuppressionsOnDeploy: settings.deploy_block_honor_suppressions === '1',
|
||||
eligibleBlockPolicies: db.countEligibleBlockPolicies(
|
||||
req.nodeId,
|
||||
FleetSyncService.getRole(),
|
||||
FleetSyncService.getSelfIdentity(),
|
||||
),
|
||||
},
|
||||
};
|
||||
res.json(overview);
|
||||
} catch (error) {
|
||||
console.error('[Security] Failed to build overview:', error);
|
||||
res.status(500).json({ error: 'Failed to build security overview' });
|
||||
}
|
||||
});
|
||||
|
||||
// Static, read-only policy-pack catalog. Auth-only (Community), no DB, no
|
||||
// enforcement. The frontend fetches this with localOnly so the global catalog
|
||||
// is available regardless of which node is active.
|
||||
securityRouter.get('/policy-packs', authMiddleware, (_req: Request, res: Response): void => {
|
||||
res.json(DEFAULT_POLICY_PACKS);
|
||||
});
|
||||
|
||||
securityRouter.post('/sbom', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
const svc = TrivyService.getInstance();
|
||||
|
||||
Reference in New Issue
Block a user