WP-20/21: vulnerability management with Trivy and mail notifications
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Trivy scanner adapter (rootfs + image JSON, parsed and deduplicated),
findings repository, scan diff that keeps first_seen, marks disappeared
findings fixed and skips failed targets, vulnerability_scan job (daily by
default), digest mail for new findings at or above a configurable severity,
/api/vulnerabilities routes, Vulnerabilities page with severity tiles,
filters, details and acknowledge, notification threshold in settings.
Deploy script installs Trivy from the Aqua apt repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:45:57 +02:00
parent 5c6e09ad10
commit 872f4373ff
13 changed files with 801 additions and 94 deletions

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import type { Finding } from '../api/types'
defineProps<{ findings: Finding[]; canAct: boolean }>()
defineEmits<{ status: [f: Finding, status: 'open' | 'acknowledged']; select: [f: Finding] }>()
const sev: Record<string, string> = {
critical: 'bg-red-600 text-white',
high: 'bg-orange-500 text-white',
medium: 'bg-amber-300 text-amber-900',
low: 'bg-gray-200 text-gray-700',
unknown: 'bg-gray-100 text-gray-500',
}
</script>
<template>
<table class="w-full text-left text-sm">
<thead class="border-b border-gray-200 text-gray-500">
<tr>
<th class="py-2">Severity</th>
<th>CVE</th>
<th>Package</th>
<th>Installed</th>
<th>Fixed in</th>
<th>Target</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for="f in findings"
:key="f.id"
class="border-b border-gray-100"
:class="f.status === 'acknowledged' ? 'text-gray-500' : ''"
>
<td class="py-1.5">
<span class="rounded-full px-2 py-0.5 text-xs font-medium" :class="sev[f.severity]">{{
f.severity
}}</span>
</td>
<td class="font-mono">{{ f.cve_id }}</td>
<td class="font-mono">{{ f.package }}</td>
<td class="font-mono text-xs">{{ f.installed_version }}</td>
<td class="font-mono text-xs" :class="f.fixed_version ? 'text-green-700' : 'text-gray-400'">
{{ f.fixed_version ?? '–' }}
</td>
<td class="max-w-xs truncate font-mono text-xs" :title="f.target">{{ f.target }}</td>
<td>{{ f.status }}</td>
<td class="space-x-3 whitespace-nowrap text-right">
<button name="details" class="text-blue-600 hover:underline" @click="$emit('select', f)">
Details
</button>
<button
v-if="canAct"
name="ack"
class="text-blue-600 hover:underline"
@click="$emit('status', f, f.status === 'acknowledged' ? 'open' : 'acknowledged')"
>
{{ f.status === 'acknowledged' ? 'Reopen' : 'Acknowledge' }}
</button>
</td>
</tr>
<tr v-if="findings.length === 0">
<td colspan="8" class="py-6 text-center text-gray-500">No findings.</td>
</tr>
</tbody>
</table>
</template>