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
+63 -21
View File
@@ -1,28 +1,70 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { apiFetch } from '@/lib/api';
import type { TrivyStatus } from '@/types/security';
import type { TrivyStatus, TrivyUpdateCheck } from '@/types/security';
export function useTrivyStatus(): TrivyStatus {
const [status, setStatus] = useState<TrivyStatus>({ available: false, version: null });
const INITIAL_STATUS: TrivyStatus = {
available: false,
version: null,
source: 'none',
autoUpdate: false,
busy: false,
};
useEffect(() => {
let cancelled = false;
apiFetch('/security/trivy-status')
.then((r) => (r.ok ? r.json() : null))
.then((d) => {
if (cancelled || !d) return;
setStatus({
available: !!d.available,
version: typeof d.version === 'string' ? d.version : null,
});
})
.catch((err) => {
console.error('Failed to fetch Trivy status:', err);
export interface UseTrivyStatusResult {
status: TrivyStatus;
updateCheck: TrivyUpdateCheck | null;
refresh: () => Promise<void>;
refreshUpdateCheck: () => Promise<void>;
}
export function useTrivyStatus(): UseTrivyStatusResult {
const [status, setStatus] = useState<TrivyStatus>(INITIAL_STATUS);
const [updateCheck, setUpdateCheck] = useState<TrivyUpdateCheck | null>(null);
const refresh = useCallback(async () => {
try {
const r = await apiFetch('/security/trivy-status');
if (!r.ok) return;
const d = await r.json();
setStatus({
available: !!d.available,
version: typeof d.version === 'string' ? d.version : null,
source: d.source === 'managed' || d.source === 'host' ? d.source : 'none',
autoUpdate: !!d.autoUpdate,
busy: !!d.busy,
});
return () => {
cancelled = true;
};
} catch (err) {
console.error('Failed to fetch Trivy status:', err);
}
}, []);
return status;
const refreshUpdateCheck = useCallback(async () => {
try {
const r = await apiFetch('/security/trivy-update-check');
if (!r.ok) {
setUpdateCheck(null);
return;
}
const d = (await r.json()) as TrivyUpdateCheck;
setUpdateCheck(d);
} catch {
setUpdateCheck(null);
}
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
void refresh();
}, [refresh]);
useEffect(() => {
if (status.source === 'managed') {
// eslint-disable-next-line react-hooks/set-state-in-effect
void refreshUpdateCheck();
} else {
setUpdateCheck(null);
}
}, [status.source, refreshUpdateCheck]);
return { status, updateCheck, refresh, refreshUpdateCheck };
}