|
188 | 188 | > |
189 | 189 | {{ finding.threat_level }} |
190 | 190 | </span> |
| 191 | + <span v-if="finding.severity" class="badge badge-xs badge-outline shrink-0 uppercase"> |
| 192 | + {{ finding.severity }} |
| 193 | + </span> |
191 | 194 | <span class="font-medium text-sm flex-1"> |
192 | 195 | {{ finding.rule_id || finding.title }} |
193 | 196 | </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 ×{{ findingSources(finding).length }} |
| 203 | + </span> |
194 | 204 | <span v-if="finding.package_name" class="font-mono text-xs text-base-content/50"> |
195 | 205 | {{ finding.package_name }} |
196 | 206 | </span> |
|
229 | 239 | <span class="text-base-content/50">Location:</span> |
230 | 240 | <code class="ml-1 bg-base-300 px-1 rounded">{{ finding.location }}</code> |
231 | 241 | </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> |
235 | 245 | </div> |
236 | 246 | </div> |
237 | 247 | <a |
|
285 | 295 | > |
286 | 296 | {{ finding.threat_level }} |
287 | 297 | </span> |
| 298 | + <span v-if="finding.severity" class="badge badge-xs badge-outline shrink-0 uppercase"> |
| 299 | + {{ finding.severity }} |
| 300 | + </span> |
288 | 301 | <span class="font-medium text-sm flex-1"> |
289 | 302 | {{ finding.rule_id || finding.title }} |
290 | 303 | </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 ×{{ findingSources(finding).length }} |
| 310 | + </span> |
291 | 311 | <span v-if="finding.package_name" class="font-mono text-xs text-base-content/50"> |
292 | 312 | {{ finding.package_name }} |
293 | 313 | </span> |
|
325 | 345 | <span class="text-base-content/50">Location:</span> |
326 | 346 | <code class="ml-1 bg-base-300 px-1 rounded">{{ finding.location }}</code> |
327 | 347 | </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> |
331 | 351 | </div> |
332 | 352 | </div> |
333 | 353 | <a |
@@ -615,6 +635,24 @@ function scannerDisplayName(id: string): string { |
615 | 635 | return scannerNames[id] || id |
616 | 636 | } |
617 | 637 |
|
| 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 | +
|
618 | 656 | // Scan context from the aggregated report (populated from job's ScanContext) |
619 | 657 | const scanContext = computed(() => { |
620 | 658 | return report.value?.scan_context || null |
|
0 commit comments