feat(security): one-click managed Trivy install (#643)

* feat(security): one-click managed Trivy install

Add a Vulnerability Scanner card to Settings, Security with install,
update, uninstall, and auto-update controls (Admiral-only). The installer
downloads a verified Trivy release into the existing data volume at
/app/data/bin/trivy and defaults the cache to /app/data/trivy-cache, so
no host mounts or extra env vars are required. Detection probes the
managed path, a TRIVY_BIN override, and the host PATH, distinguishing
managed vs host installs. A daily scheduled check surfaces available
Trivy updates, installs them automatically when opted in, and dedupes
notifications per version.

* fix(frontend): silence react-hooks/set-state-in-effect in useTrivyStatus

The initial status fetch and managed-source update check both call
setState from the effect body. Match the existing pattern used in
useDashboardData / SSOSection and disable the rule at the call site.
This commit is contained in:
Anso
2026-04-16 21:29:44 -04:00
committed by GitHub
parent 759776792d
commit 61bac08027
11 changed files with 868 additions and 70 deletions
+61
View File
@@ -13,6 +13,10 @@ import { captureLocalNodeFiles, captureRemoteNodeFiles } from '../utils/snapshot
import { NodeRegistry } from './NodeRegistry';
import { NotificationService } from './NotificationService';
import TrivyService from './TrivyService';
import TrivyInstaller from './TrivyInstaller';
const TRIVY_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
const TRIVY_UPDATE_CHECK_STARTUP_DELAY_MS = 5 * 60 * 1000;
const TRIVY_REDETECT_INTERVAL_MS = 10 * 60 * 1000;
const STALE_SCAN_THRESHOLD_MS = 15 * 60 * 1000;
@@ -20,7 +24,10 @@ const STALE_SCAN_THRESHOLD_MS = 15 * 60 * 1000;
export class SchedulerService {
private static instance: SchedulerService;
private intervalId: ReturnType<typeof setInterval> | null = null;
private trivyUpdateIntervalId: ReturnType<typeof setInterval> | null = null;
private trivyUpdateStartupTimer: ReturnType<typeof setTimeout> | null = null;
private isProcessing = false;
private isCheckingTrivyUpdate = false;
private runningTasks = new Set<number>();
private lastTrivyRedetect = 0;
@@ -38,6 +45,8 @@ export class SchedulerService {
this.cleanupStaleRuns();
this.intervalId = setInterval(() => this.tick(), 60_000);
setTimeout(() => this.tick(), 10_000);
this.trivyUpdateStartupTimer = setTimeout(() => this.runTrivyUpdateCheck(), TRIVY_UPDATE_CHECK_STARTUP_DELAY_MS);
this.trivyUpdateIntervalId = setInterval(() => this.runTrivyUpdateCheck(), TRIVY_UPDATE_CHECK_INTERVAL_MS);
console.log('[SchedulerService] Started');
}
@@ -46,9 +55,61 @@ export class SchedulerService {
clearInterval(this.intervalId);
this.intervalId = null;
}
if (this.trivyUpdateIntervalId) {
clearInterval(this.trivyUpdateIntervalId);
this.trivyUpdateIntervalId = null;
}
if (this.trivyUpdateStartupTimer) {
clearTimeout(this.trivyUpdateStartupTimer);
this.trivyUpdateStartupTimer = null;
}
console.log('[SchedulerService] Stopped');
}
private async runTrivyUpdateCheck(): Promise<void> {
if (this.isCheckingTrivyUpdate) return;
this.isCheckingTrivyUpdate = true;
try {
const trivy = TrivyService.getInstance();
if (!trivy.isTrivyAvailable() || trivy.getSource() !== 'managed') return;
const db = DatabaseService.getInstance();
const settings = db.getGlobalSettings();
const autoUpdate = settings.trivy_auto_update === '1';
const installer = TrivyInstaller.getInstance();
if (installer.isBusy()) return;
const check = await installer.checkForUpdate(trivy.getVersion(), 'managed');
if (!check.updateAvailable) return;
if (autoUpdate) {
const previous = trivy.getVersion() ?? 'unknown';
console.log(`[SchedulerService] Auto-updating Trivy from ${previous} to ${check.latest}`);
try {
await installer.update();
await trivy.detectTrivy();
NotificationService.getInstance().dispatchAlert(
'info',
`Trivy updated from v${previous} to v${check.latest}`,
);
db.updateGlobalSetting('trivy_last_notified_version', check.latest);
} catch (err) {
console.error('[SchedulerService] Trivy auto-update failed:', getErrorMessage(err, 'unknown error'));
}
} else {
const lastNotified = settings.trivy_last_notified_version || '';
if (lastNotified === check.latest) return;
NotificationService.getInstance().dispatchAlert(
'info',
`Trivy update available: v${check.latest} (currently v${check.current ?? 'unknown'})`,
);
db.updateGlobalSetting('trivy_last_notified_version', check.latest);
}
} catch (err) {
console.warn('[SchedulerService] Trivy update check failed:', getErrorMessage(err, 'unknown error'));
} finally {
this.isCheckingTrivyUpdate = false;
}
}
private cleanupStaleRuns(): void {
try {
const count = DatabaseService.getInstance().markStaleRunsAsFailed();