Skip to content

Commit 31adfd5

Browse files
committed
feat(039): expandable finding details + scanner execution logs
Each finding is now an expandable card with full details. Scanner Execution Logs section shows stderr/stdout per scanner. Fixed TS types to match Go API field names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e01f969 commit 31adfd5

25 files changed

Lines changed: 300 additions & 64 deletions

frontend/src/types/api.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,20 @@ export type ThreatType = 'tool_poisoning' | 'prompt_injection' | 'rug_pull' | 's
4848
export type ThreatLevel = 'dangerous' | 'warning' | 'info'
4949

5050
export interface SecurityScanFinding {
51-
id?: string
51+
rule_id?: string
52+
severity?: string // critical, high, medium, low, info
53+
category?: string
5254
threat_type: ThreatType
5355
threat_level: ThreatLevel
5456
title: string
5557
description: string
56-
help_uri?: string
58+
location?: string
59+
scanner?: string // Scanner that found this
60+
help_uri?: string // Link to CVE/advisory
61+
cvss_score?: number // CVSS score (0-10)
5762
package_name?: string
58-
package_version?: string
59-
fix_version?: string
60-
tool_name?: string
61-
scanner_id?: string
63+
installed_version?: string
64+
fixed_version?: string // Version with fix
6265
}
6366

6467
export interface SecurityScanReport {

frontend/src/views/ServerDetail.vue

Lines changed: 116 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -608,62 +608,75 @@
608608
<span class="badge badge-sm" :class="group.badgeClass">{{ group.findings.length }}</span>
609609
</div>
610610
<div class="collapse-content">
611-
<div class="overflow-x-auto">
612-
<table class="table table-sm">
613-
<thead>
614-
<tr>
615-
<th class="w-24">Severity</th>
616-
<th>Finding</th>
617-
<th class="w-32">Package</th>
618-
<th class="w-24">Fix</th>
619-
</tr>
620-
</thead>
621-
<tbody>
622-
<tr v-for="(finding, idx) in group.findings" :key="idx">
623-
<td>
624-
<span
625-
class="badge badge-sm"
626-
:class="{
627-
'badge-error': finding.threat_level === 'dangerous',
628-
'badge-warning': finding.threat_level === 'warning',
629-
'badge-info': finding.threat_level === 'info',
630-
}"
631-
>
632-
{{ finding.threat_level }}
633-
</span>
634-
</td>
635-
<td>
636-
<div class="font-medium">
637-
<a
638-
v-if="finding.help_uri"
639-
:href="finding.help_uri"
640-
target="_blank"
641-
rel="noopener noreferrer"
642-
class="link link-primary"
643-
>
644-
{{ finding.title }}
645-
</a>
646-
<span v-else>{{ finding.title }}</span>
611+
<div class="space-y-2">
612+
<div v-for="(finding, idx) in group.findings" :key="idx"
613+
class="collapse collapse-arrow bg-base-200 rounded-lg"
614+
>
615+
<input type="checkbox" />
616+
<div class="collapse-title py-2 px-4 min-h-0 flex items-center gap-3">
617+
<span
618+
class="badge badge-sm flex-shrink-0"
619+
:class="{
620+
'badge-error': finding.threat_level === 'dangerous',
621+
'badge-warning': finding.threat_level === 'warning',
622+
'badge-info': finding.threat_level === 'info',
623+
}"
624+
>
625+
{{ finding.threat_level }}
626+
</span>
627+
<span class="font-medium text-sm flex-1">
628+
{{ finding.rule_id || finding.title }}
629+
</span>
630+
<span v-if="finding.package_name" class="font-mono text-xs text-base-content/50">
631+
{{ finding.package_name }}
632+
</span>
633+
<span v-if="finding.fixed_version" class="badge badge-xs badge-success badge-outline">
634+
fix: {{ finding.fixed_version }}
635+
</span>
636+
</div>
637+
<div class="collapse-content px-4 pb-3">
638+
<div class="space-y-2 text-sm">
639+
<p class="text-base-content/80">{{ finding.description }}</p>
640+
<div class="grid grid-cols-2 gap-2 text-xs">
641+
<div v-if="finding.rule_id">
642+
<span class="text-base-content/50">Rule:</span>
643+
<code class="ml-1 bg-base-300 px-1 rounded">{{ finding.rule_id }}</code>
644+
</div>
645+
<div v-if="finding.severity">
646+
<span class="text-base-content/50">CVSS Severity:</span>
647+
<span class="ml-1 font-medium">{{ finding.severity }}</span>
648+
<span v-if="finding.cvss_score" class="ml-1">({{ finding.cvss_score }})</span>
649+
</div>
650+
<div v-if="finding.package_name">
651+
<span class="text-base-content/50">Package:</span>
652+
<span class="ml-1 font-mono">{{ finding.package_name }}</span>
653+
<span v-if="finding.installed_version" class="ml-1 text-base-content/50">v{{ finding.installed_version }}</span>
647654
</div>
648-
<div class="text-xs text-base-content/60 mt-0.5">{{ finding.description }}</div>
649-
<div v-if="finding.tool_name" class="text-xs text-base-content/50 mt-0.5">
650-
Tool: <span class="font-mono">{{ finding.tool_name }}</span>
655+
<div v-if="finding.fixed_version">
656+
<span class="text-base-content/50">Fixed in:</span>
657+
<span class="ml-1 font-mono text-success">{{ finding.fixed_version }}</span>
651658
</div>
652-
</td>
653-
<td>
654-
<div v-if="finding.package_name" class="text-sm">
655-
<div class="font-mono">{{ finding.package_name }}</div>
656-
<div v-if="finding.package_version" class="text-xs text-base-content/50">{{ finding.package_version }}</div>
659+
<div v-if="finding.location">
660+
<span class="text-base-content/50">Location:</span>
661+
<code class="ml-1 bg-base-300 px-1 rounded">{{ finding.location }}</code>
657662
</div>
658-
<span v-else class="text-base-content/30">-</span>
659-
</td>
660-
<td>
661-
<span v-if="finding.fix_version" class="badge badge-sm badge-success badge-outline">{{ finding.fix_version }}</span>
662-
<span v-else class="text-base-content/30">-</span>
663-
</td>
664-
</tr>
665-
</tbody>
666-
</table>
663+
<div v-if="finding.scanner">
664+
<span class="text-base-content/50">Scanner:</span>
665+
<span class="ml-1">{{ finding.scanner }}</span>
666+
</div>
667+
</div>
668+
<a
669+
v-if="finding.help_uri"
670+
:href="finding.help_uri"
671+
target="_blank"
672+
rel="noopener noreferrer"
673+
class="link link-primary text-xs inline-flex items-center gap-1"
674+
>
675+
View Advisory Details &rarr;
676+
</a>
677+
</div>
678+
</div>
679+
</div>
667680
</div>
668681
</div>
669682
</div>
@@ -695,6 +708,46 @@
695708
<span v-if="scanReport.duration_ms"> in {{ scanReport.duration_ms }}ms</span>
696709
<span v-if="scanReport.scanners_used?.length"> using {{ scanReport.scanners_used.join(', ') }}</span>
697710
</div>
711+
712+
<!-- Scanner Execution Logs -->
713+
<div v-if="scanStatus" class="pt-4">
714+
<div class="collapse collapse-arrow bg-base-100 shadow-md">
715+
<input type="checkbox" />
716+
<div class="collapse-title font-medium">
717+
Scanner Execution Logs
718+
<span class="badge badge-sm badge-ghost ml-2">{{ scanStatus.scanner_statuses?.length || 0 }} scanners</span>
719+
</div>
720+
<div class="collapse-content">
721+
<div v-for="ss in (scanStatus.scanner_statuses || [])" :key="ss.scanner_id" class="mb-4 last:mb-0">
722+
<div class="flex items-center gap-2 mb-1">
723+
<span class="font-mono font-medium text-sm">{{ ss.scanner_id }}</span>
724+
<span class="badge badge-xs" :class="{
725+
'badge-success': ss.status === 'completed',
726+
'badge-error': ss.status === 'failed',
727+
'badge-warning': ss.status === 'running',
728+
'badge-ghost': ss.status === 'pending',
729+
}">{{ ss.status }}</span>
730+
<span class="text-xs text-base-content/40">
731+
exit code: {{ ss.exit_code ?? '?' }}
732+
<template v-if="ss.findings_count"> | {{ ss.findings_count }} findings</template>
733+
</span>
734+
</div>
735+
<div v-if="ss.error" class="text-xs text-error mb-1 break-all">{{ ss.error }}</div>
736+
<div v-if="ss.stderr" class="mb-1">
737+
<div class="text-xs text-base-content/50 mb-0.5">stderr:</div>
738+
<pre class="bg-base-300 text-xs p-2 rounded max-h-40 overflow-auto whitespace-pre-wrap break-all">{{ ss.stderr }}</pre>
739+
</div>
740+
<div v-if="ss.stdout && !ss.stdout.startsWith('{')">
741+
<div class="text-xs text-base-content/50 mb-0.5">stdout:</div>
742+
<pre class="bg-base-300 text-xs p-2 rounded max-h-40 overflow-auto whitespace-pre-wrap break-all">{{ ss.stdout.substring(0, 2000) }}</pre>
743+
</div>
744+
</div>
745+
<div v-if="!scanStatus.scanner_statuses?.length" class="text-sm text-base-content/40">
746+
No scanner execution data available.
747+
</div>
748+
</div>
749+
</div>
750+
</div>
698751
</template>
699752
</div>
700753
</div>
@@ -764,6 +817,7 @@ const quarantinedTools = computed(() => {
764817
765818
// Security scan (Spec 039)
766819
const scanReport = ref<SecurityScanReport | null>(null)
820+
const scanStatus = ref<any>(null)
767821
const scanLoading = ref(false)
768822
const scanReportLoading = ref(false)
769823
const scanError = ref<string | null>(null)
@@ -1315,9 +1369,15 @@ async function loadScanReport() {
13151369
scanReportLoading.value = true
13161370
scanError.value = null
13171371
try {
1318-
const response = await api.getScanReport(server.value.name)
1319-
if (response.success && response.data) {
1320-
scanReport.value = response.data as SecurityScanReport
1372+
const [reportRes, statusRes] = await Promise.all([
1373+
api.getScanReport(server.value.name),
1374+
api.getScanStatus(server.value.name),
1375+
])
1376+
if (reportRes.success && reportRes.data) {
1377+
scanReport.value = reportRes.data as SecurityScanReport
1378+
}
1379+
if (statusRes.success && statusRes.data) {
1380+
scanStatus.value = statusRes.data
13211381
}
13221382
} catch (err) {
13231383
// Silently fail - report may not exist yet

web/frontend/dist/assets/Activity-NPd5xyPC.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/frontend/dist/assets/AdminDashboard-QJ4wDVEt.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)