mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 17:36:42 +00:00
fix: harden vulnerability scan scheduling (#1035)
This commit is contained in:
@@ -99,7 +99,7 @@ function toSuppressions(decision: Partial<SuppressionDecision>): SarifSuppressio
|
||||
{
|
||||
kind: 'external',
|
||||
status: 'accepted',
|
||||
justification: decision.suppression_reason?.trim() || 'Suppressed in Sencho',
|
||||
justification: 'Suppressed in Sencho',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -112,7 +112,7 @@ function toAckSuppressions(
|
||||
{
|
||||
kind: 'external',
|
||||
status: 'accepted',
|
||||
justification: decision.acknowledgement_reason?.trim() || 'Acknowledged in Sencho',
|
||||
justification: 'Acknowledged in Sencho',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -755,6 +755,13 @@ export class SchedulerService {
|
||||
if (task.node_id == null && isDebugEnabled()) {
|
||||
console.log(`[SchedulerService:debug] Scan task ${task.id}: no node_id specified, using default node ${nodeId}`);
|
||||
}
|
||||
const node = NodeRegistry.getInstance().getNode(nodeId);
|
||||
if (!node) {
|
||||
throw new Error('Scheduled vulnerability scans require an existing local node.');
|
||||
}
|
||||
if (node?.type === 'remote') {
|
||||
throw new Error('Scheduled vulnerability scans currently require a local node.');
|
||||
}
|
||||
|
||||
const scanStart = Date.now();
|
||||
if (isDebugEnabled()) console.log(`[SchedulerService:debug] executeScan start: task=${task.id} node=${nodeId}`);
|
||||
@@ -804,6 +811,13 @@ export function formatScanOutput(summary: ScanAllNodeImagesResult): string {
|
||||
header = parts.join('; ');
|
||||
}
|
||||
|
||||
if (summary.truncated) {
|
||||
const total = summary.totalImages ?? scanned + skipped + failed;
|
||||
const processed = summary.processedImages ?? scanned + skipped + failed;
|
||||
header += `. Scan limited after ${processed} of ${total} image(s)`;
|
||||
if (summary.limitReason) header += ` (${summary.limitReason})`;
|
||||
}
|
||||
|
||||
const severityTiers: Array<[string, number]> = [
|
||||
['critical', severity.critical],
|
||||
['high', severity.high],
|
||||
|
||||
@@ -25,6 +25,8 @@ const execFileAsync = promisify(execFile);
|
||||
const SCAN_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const SBOM_TIMEOUT_MS = 3 * 60 * 1000;
|
||||
export const DIGEST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const DEFAULT_SCAN_ALL_MAX_IMAGES = 100;
|
||||
const DEFAULT_SCAN_ALL_MAX_DURATION_MS = 30 * 60 * 1000;
|
||||
|
||||
const TRIVY_TEMP_DIR_PREFIX = 'sencho-trivy-';
|
||||
const TRIVY_TEMP_DIR_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||
@@ -136,6 +138,10 @@ export interface ScanAllNodeImagesResult {
|
||||
scanned: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
totalImages?: number;
|
||||
processedImages?: number;
|
||||
truncated?: boolean;
|
||||
limitReason?: string;
|
||||
severity: ScanAllNodeImagesSeverityTotals;
|
||||
/**
|
||||
* Policy violations observed across the freshly-scanned or cached rows.
|
||||
@@ -221,6 +227,12 @@ export interface TrivyComposeScanResult {
|
||||
|
||||
export type SbomFormat = 'spdx-json' | 'cyclonedx';
|
||||
|
||||
function positiveIntFromEnv(name: string, fallback: number): number {
|
||||
const value = Number(process.env[name]);
|
||||
if (!Number.isFinite(value) || value <= 0) return fallback;
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
// Keep scanners in canonical order so the DB value is comparable as-is.
|
||||
export function normalizeScanners(input?: readonly TrivyScanner[]): TrivyScanner[] {
|
||||
const set = new Set<TrivyScanner>(input && input.length > 0 ? input : ['vuln']);
|
||||
@@ -1029,10 +1041,16 @@ class TrivyService {
|
||||
if (tag && tag !== '<none>:<none>') imageRefs.add(tag);
|
||||
}
|
||||
}
|
||||
const refs = Array.from(imageRefs);
|
||||
const maxImages = positiveIntFromEnv('TRIVY_SCAN_ALL_MAX_IMAGES', DEFAULT_SCAN_ALL_MAX_IMAGES);
|
||||
const maxDurationMs = positiveIntFromEnv('TRIVY_SCAN_ALL_MAX_DURATION_MS', DEFAULT_SCAN_ALL_MAX_DURATION_MS);
|
||||
|
||||
let scanned = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
let processedImages = 0;
|
||||
let truncated = false;
|
||||
let limitReason: string | undefined;
|
||||
const severity = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 };
|
||||
const countedDigests = new Set<string>();
|
||||
const violations: ScanAllNodeImagesViolation[] = [];
|
||||
@@ -1068,7 +1086,19 @@ class TrivyService {
|
||||
}
|
||||
};
|
||||
|
||||
for (const ref of imageRefs) {
|
||||
for (const ref of refs) {
|
||||
if (processedImages >= maxImages) {
|
||||
truncated = true;
|
||||
limitReason = `image limit ${maxImages} reached`;
|
||||
break;
|
||||
}
|
||||
const elapsedMs = Date.now() - batchStartedAt;
|
||||
if (elapsedMs >= maxDurationMs) {
|
||||
truncated = true;
|
||||
limitReason = `duration limit ${maxDurationMs}ms reached`;
|
||||
break;
|
||||
}
|
||||
processedImages++;
|
||||
try {
|
||||
const digest = await this.getImageDigest(ref, nodeId);
|
||||
if (digest) {
|
||||
@@ -1097,9 +1127,19 @@ class TrivyService {
|
||||
diag(
|
||||
`scanAllNodeImages: nodeId=${nodeId} unique=${imageRefs.size} `
|
||||
+ `scanned=${scanned} skipped=${skipped} failed=${failed} `
|
||||
+ `violations=${violations.length} elapsedMs=${Date.now() - batchStartedAt}`,
|
||||
+ `violations=${violations.length} truncated=${truncated} elapsedMs=${Date.now() - batchStartedAt}`,
|
||||
);
|
||||
return { scanned, skipped, failed, severity, violations };
|
||||
return {
|
||||
scanned,
|
||||
skipped,
|
||||
failed,
|
||||
totalImages: refs.length,
|
||||
processedImages,
|
||||
truncated,
|
||||
limitReason,
|
||||
severity,
|
||||
violations,
|
||||
};
|
||||
}
|
||||
|
||||
async generateSBOM(imageRef: string, format: SbomFormat): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user