|
608 | 608 | <span class="badge badge-sm" :class="group.badgeClass">{{ group.findings.length }}</span> |
609 | 609 | </div> |
610 | 610 | <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> |
647 | 654 | </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> |
651 | 658 | </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> |
657 | 662 | </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 → |
| 676 | + </a> |
| 677 | + </div> |
| 678 | + </div> |
| 679 | + </div> |
667 | 680 | </div> |
668 | 681 | </div> |
669 | 682 | </div> |
|
695 | 708 | <span v-if="scanReport.duration_ms"> in {{ scanReport.duration_ms }}ms</span> |
696 | 709 | <span v-if="scanReport.scanners_used?.length"> using {{ scanReport.scanners_used.join(', ') }}</span> |
697 | 710 | </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> |
698 | 751 | </template> |
699 | 752 | </div> |
700 | 753 | </div> |
@@ -764,6 +817,7 @@ const quarantinedTools = computed(() => { |
764 | 817 |
|
765 | 818 | // Security scan (Spec 039) |
766 | 819 | const scanReport = ref<SecurityScanReport | null>(null) |
| 820 | +const scanStatus = ref<any>(null) |
767 | 821 | const scanLoading = ref(false) |
768 | 822 | const scanReportLoading = ref(false) |
769 | 823 | const scanError = ref<string | null>(null) |
@@ -1315,9 +1369,15 @@ async function loadScanReport() { |
1315 | 1369 | scanReportLoading.value = true |
1316 | 1370 | scanError.value = null |
1317 | 1371 | 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 |
1321 | 1381 | } |
1322 | 1382 | } catch (err) { |
1323 | 1383 | // Silently fail - report may not exist yet |
|
0 commit comments