diff --git a/backend/src/routes/fleet.ts b/backend/src/routes/fleet.ts index ac3ba626..06cb1845 100644 --- a/backend/src/routes/fleet.ts +++ b/backend/src/routes/fleet.ts @@ -20,6 +20,7 @@ import { getLatestVersion } from '../utils/version-check'; import { isValidStackName } from '../utils/validation'; import { isDebugEnabled } from '../utils/debug'; import { getErrorMessage } from '../utils/errors'; +import { POLICY_SEVERITIES } from '../utils/severity'; import { CloudBackupService } from '../services/CloudBackupService'; import { NotificationService } from '../services/NotificationService'; import { buildLocalConfigurationStatus, type ConfigurationStatus } from './dashboard'; @@ -47,7 +48,6 @@ const UPDATE_TIMEOUT_MSG = 'Node did not come back online within 5 minutes.'; const EARLY_FAIL_MS = 180 * 1000; // 3 minutes before declaring a probable pull failure const MAX_SYNC_ROWS = 5000; -const VALID_SEVERITY = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']); const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/; const isIntFlag = (v: unknown): v is 0 | 1 => v === 0 || v === 1; @@ -56,7 +56,7 @@ function validateScanPolicyRow(row: unknown): string | null { if (!row || typeof row !== 'object') return 'row must be an object'; const r = row as Record; if (typeof r.name !== 'string' || r.name.length === 0 || r.name.length > 200) return 'name must be a non-empty string'; - if (typeof r.max_severity !== 'string' || !VALID_SEVERITY.has(r.max_severity)) return 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW'; + if (typeof r.max_severity !== 'string' || !POLICY_SEVERITIES.has(r.max_severity)) return 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW'; if (r.stack_pattern !== null && typeof r.stack_pattern !== 'string') return 'stack_pattern must be a string or null'; if (typeof r.stack_pattern === 'string' && r.stack_pattern.length > 200) return 'stack_pattern is too long'; if (typeof r.node_identity !== 'string') return 'node_identity must be a string'; diff --git a/backend/src/routes/security.ts b/backend/src/routes/security.ts index bf50d930..e9c6bc94 100644 --- a/backend/src/routes/security.ts +++ b/backend/src/routes/security.ts @@ -12,6 +12,7 @@ import { applySuppressions } from '../utils/suppression-filter'; import { generateSarif } from '../services/SarifExporter'; import { getErrorMessage } from '../utils/errors'; import { isDebugEnabled } from '../utils/debug'; +import { FINDING_SEVERITIES, POLICY_SEVERITIES } from '../utils/severity'; const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/; @@ -269,8 +270,7 @@ securityRouter.get( const severity = typeof req.query.severity === 'string' ? (req.query.severity.toUpperCase() as 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN') : undefined; - const validSeverities = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'UNKNOWN']); - if (severity && !validSeverities.has(severity)) { + if (severity && !FINDING_SEVERITIES.has(severity)) { res.status(400).json({ error: 'Invalid severity filter' }); return; } const limit = req.query.limit ? Number(req.query.limit) : undefined; @@ -299,8 +299,7 @@ securityRouter.get( const severity = typeof req.query.severity === 'string' ? (req.query.severity.toUpperCase() as 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN') : undefined; - const validSeverities = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'UNKNOWN']); - if (severity && !validSeverities.has(severity)) { + if (severity && !FINDING_SEVERITIES.has(severity)) { res.status(400).json({ error: 'Invalid severity filter' }); return; } const limit = req.query.limit ? Number(req.query.limit) : undefined; @@ -326,8 +325,7 @@ securityRouter.get( const severity = typeof req.query.severity === 'string' ? (req.query.severity.toUpperCase() as 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN') : undefined; - const validSeverities = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'UNKNOWN']); - if (severity && !validSeverities.has(severity)) { + if (severity && !FINDING_SEVERITIES.has(severity)) { res.status(400).json({ error: 'Invalid severity filter' }); return; } const limit = req.query.limit ? Number(req.query.limit) : undefined; @@ -428,8 +426,7 @@ securityRouter.post('/policies', authMiddleware, (req: Request, res: Response): if (!name || typeof name !== 'string' || !name.trim()) { res.status(400).json({ error: 'Policy name is required' }); return; } - const validSeverities = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']); - if (!validSeverities.has(max_severity)) { + if (!POLICY_SEVERITIES.has(max_severity)) { res.status(400).json({ error: 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW' }); return; } try { @@ -473,8 +470,7 @@ securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response } if (body.stack_pattern !== undefined) updates.stack_pattern = body.stack_pattern ? String(body.stack_pattern) : null; if (body.max_severity !== undefined) { - const validSeverities = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']); - if (!validSeverities.has(body.max_severity)) { + if (!POLICY_SEVERITIES.has(body.max_severity)) { res.status(400).json({ error: 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW' }); return; } updates.max_severity = body.max_severity; diff --git a/backend/src/utils/severity.ts b/backend/src/utils/severity.ts index 9afe03d2..79443993 100644 --- a/backend/src/utils/severity.ts +++ b/backend/src/utils/severity.ts @@ -8,6 +8,21 @@ export const SEVERITY_ORDER: VulnSeverity[] = [ 'CRITICAL', ]; +export const FINDING_SEVERITIES: ReadonlySet = new Set([ + 'CRITICAL', + 'HIGH', + 'MEDIUM', + 'LOW', + 'UNKNOWN', +]); + +export const POLICY_SEVERITIES: ReadonlySet = new Set([ + 'CRITICAL', + 'HIGH', + 'MEDIUM', + 'LOW', +]); + export function severityRank(severity: VulnSeverity | null | undefined): number { if (!severity) return -1; return SEVERITY_ORDER.indexOf(severity);