Skip to content

Commit 2a3ae2c

Browse files
committed
feat(webui): render unified findings with severity + source attribution
Spec 077 US2 (T023). Surface the merged, deduplicated finding list in the scan report: each finding shows its severity, its contributing source(s), and a "consensus xN" badge when two or more scanners independently agreed. Adds the additive `sources`/`tier`/`confidence`/`signals` fields to SecurityScanFinding. ## Changes - api.ts: extend SecurityScanFinding with sources/tier/confidence/signals. - ScanReport.vue: severity badge + consensus badge in the finding header; Source(s) row (falls back to the single scanner id for legacy findings). ## Testing - vue-tsc + vite build clean. Related: Spec 077 (specs/077-scanner-simplification)
1 parent 8ec7130 commit 2a3ae2c

2 files changed

Lines changed: 49 additions & 6 deletions

File tree

frontend/src/types/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export interface SecurityScanFinding {
6666
scan_pass?: number // 1 = security scan, 2 = supply chain audit
6767
evidence?: string // Text/content that triggered the finding
6868
supply_chain_audit?: boolean // True for real CVE/package findings — routes to the Supply Chain (CVEs) section regardless of scan_pass
69+
// Spec 077 unified report — additive fields.
70+
sources?: string[] // Contributing scanner ids; ≥2 means scanners agreed (consensus)
71+
tier?: string // "hard" (gates approval) | "soft" (review-only)
72+
confidence?: number // 0.0–1.0; raised when independent sources agree
73+
signals?: string[] // Deterministic detect check ids that fired
6974
}
7075

7176
export interface SecurityScanReport {

frontend/src/views/ScanReport.vue

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,19 @@
188188
>
189189
{{ finding.threat_level }}
190190
</span>
191+
<span v-if="finding.severity" class="badge badge-xs badge-outline shrink-0 uppercase">
192+
{{ finding.severity }}
193+
</span>
191194
<span class="font-medium text-sm flex-1">
192195
{{ finding.rule_id || finding.title }}
193196
</span>
197+
<span
198+
v-if="findingHasConsensus(finding)"
199+
class="badge badge-xs badge-primary badge-outline shrink-0"
200+
:title="`Flagged independently by ${findingSources(finding).length} scanners`"
201+
>
202+
consensus &times;{{ findingSources(finding).length }}
203+
</span>
194204
<span v-if="finding.package_name" class="font-mono text-xs text-base-content/50">
195205
{{ finding.package_name }}
196206
</span>
@@ -229,9 +239,9 @@
229239
<span class="text-base-content/50">Location:</span>
230240
<code class="ml-1 bg-base-300 px-1 rounded">{{ finding.location }}</code>
231241
</div>
232-
<div v-if="finding.scanner">
233-
<span class="text-base-content/50">Scanner:</span>
234-
<span class="ml-1">{{ scannerDisplayName(finding.scanner) }}</span>
242+
<div v-if="findingSources(finding).length">
243+
<span class="text-base-content/50">{{ findingSources(finding).length > 1 ? 'Sources:' : 'Source:' }}</span>
244+
<span class="ml-1">{{ findingSourcesLabel(finding) }}</span>
235245
</div>
236246
</div>
237247
<a
@@ -285,9 +295,19 @@
285295
>
286296
{{ finding.threat_level }}
287297
</span>
298+
<span v-if="finding.severity" class="badge badge-xs badge-outline shrink-0 uppercase">
299+
{{ finding.severity }}
300+
</span>
288301
<span class="font-medium text-sm flex-1">
289302
{{ finding.rule_id || finding.title }}
290303
</span>
304+
<span
305+
v-if="findingHasConsensus(finding)"
306+
class="badge badge-xs badge-primary badge-outline shrink-0"
307+
:title="`Flagged independently by ${findingSources(finding).length} scanners`"
308+
>
309+
consensus &times;{{ findingSources(finding).length }}
310+
</span>
291311
<span v-if="finding.package_name" class="font-mono text-xs text-base-content/50">
292312
{{ finding.package_name }}
293313
</span>
@@ -325,9 +345,9 @@
325345
<span class="text-base-content/50">Location:</span>
326346
<code class="ml-1 bg-base-300 px-1 rounded">{{ finding.location }}</code>
327347
</div>
328-
<div v-if="finding.scanner">
329-
<span class="text-base-content/50">Scanner:</span>
330-
<span class="ml-1">{{ scannerDisplayName(finding.scanner) }}</span>
348+
<div v-if="findingSources(finding).length">
349+
<span class="text-base-content/50">{{ findingSources(finding).length > 1 ? 'Sources:' : 'Source:' }}</span>
350+
<span class="ml-1">{{ findingSourcesLabel(finding) }}</span>
331351
</div>
332352
</div>
333353
<a
@@ -615,6 +635,24 @@ function scannerDisplayName(id: string): string {
615635
return scannerNames[id] || id
616636
}
617637
638+
// Spec 077 unified report — a finding's contributing sources. Prefers the
639+
// merged `sources` list; falls back to the single `scanner` id for legacy
640+
// findings that predate multi-source attribution.
641+
function findingSources(finding: SecurityScanFinding): string[] {
642+
if (finding.sources && finding.sources.length > 0) return finding.sources
643+
if (finding.scanner) return [finding.scanner]
644+
return []
645+
}
646+
647+
// True when ≥2 independent scanners agreed on a finding (consensus).
648+
function findingHasConsensus(finding: SecurityScanFinding): boolean {
649+
return findingSources(finding).length > 1
650+
}
651+
652+
function findingSourcesLabel(finding: SecurityScanFinding): string {
653+
return findingSources(finding).map(scannerDisplayName).join(', ')
654+
}
655+
618656
// Scan context from the aggregated report (populated from job's ScanContext)
619657
const scanContext = computed(() => {
620658
return report.value?.scan_context || null

0 commit comments

Comments
 (0)