|
531 | 531 | </div> |
532 | 532 | </div> |
533 | 533 |
|
| 534 | + <!-- Scan Context Banner --> |
| 535 | + <div v-if="scanContext" class="mt-2"> |
| 536 | + <!-- No Docker Isolation (local process) --> |
| 537 | + <div v-if="!scanContext.docker_isolation && scanContext.source_method !== 'url' && scanContext.source_method !== 'none'" class="alert alert-warning"> |
| 538 | + <svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 539 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
| 540 | + </svg> |
| 541 | + <div> |
| 542 | + <h3 class="font-bold">No Docker Isolation</h3> |
| 543 | + <p class="text-sm">This server runs locally without Docker isolation.</p> |
| 544 | + <p class="text-sm"> |
| 545 | + Source: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code> |
| 546 | + <span v-if="scanContext.total_files"> ({{ scanContext.total_files }} files, {{ formatFileSize(scanContext.total_size_bytes) }})</span> |
| 547 | + </p> |
| 548 | + <p class="text-sm text-base-content/70"> |
| 549 | + Protocol: {{ scanContext.server_protocol }} |
| 550 | + <span v-if="scanContext.server_command"> • Command: {{ scanContext.server_command }}</span> |
| 551 | + </p> |
| 552 | + </div> |
| 553 | + </div> |
| 554 | + |
| 555 | + <!-- Docker Isolated --> |
| 556 | + <div v-else-if="scanContext.docker_isolation" class="alert alert-info"> |
| 557 | + <svg class="w-6 h-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 558 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /> |
| 559 | + </svg> |
| 560 | + <div> |
| 561 | + <h3 class="font-bold">Docker Isolated</h3> |
| 562 | + <p class="text-sm"> |
| 563 | + Source extracted from container<span v-if="scanContext.container_id">: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.container_id.substring(0, 12) }}...</code></span> |
| 564 | + </p> |
| 565 | + <p class="text-sm"> |
| 566 | + Source: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code> |
| 567 | + <span v-if="scanContext.total_files"> ({{ scanContext.total_files }} files, {{ formatFileSize(scanContext.total_size_bytes) }})</span> |
| 568 | + </p> |
| 569 | + <p class="text-sm text-base-content/70"> |
| 570 | + Protocol: {{ scanContext.server_protocol }} |
| 571 | + <span v-if="scanContext.server_command"> • Command: {{ scanContext.server_command }}</span> |
| 572 | + </p> |
| 573 | + </div> |
| 574 | + </div> |
| 575 | + |
| 576 | + <!-- HTTP Server (url) --> |
| 577 | + <div v-else-if="scanContext.source_method === 'url'" class="alert"> |
| 578 | + <svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 579 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /> |
| 580 | + </svg> |
| 581 | + <div> |
| 582 | + <h3 class="font-bold">HTTP Server</h3> |
| 583 | + <p class="text-sm">Behavioral scanning only (no filesystem to scan)</p> |
| 584 | + <p class="text-sm"> |
| 585 | + URL: <code class="bg-base-300 px-1 rounded text-xs">{{ scanContext.source_path }}</code> |
| 586 | + </p> |
| 587 | + </div> |
| 588 | + </div> |
| 589 | + |
| 590 | + <!-- No Source Available --> |
| 591 | + <div v-else-if="scanContext.source_method === 'none'" class="alert alert-error"> |
| 592 | + <svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 593 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 594 | + </svg> |
| 595 | + <div> |
| 596 | + <h3 class="font-bold">No Source Available</h3> |
| 597 | + <p class="text-sm">Could not resolve source files for scanning.</p> |
| 598 | + <p class="text-sm text-base-content/70">Server may be disconnected or not running in Docker.</p> |
| 599 | + </div> |
| 600 | + </div> |
| 601 | + </div> |
| 602 | + |
534 | 603 | <!-- Scan error --> |
535 | 604 | <div v-if="scanError" class="alert alert-error"> |
536 | 605 | <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
748 | 817 | </div> |
749 | 818 | </div> |
750 | 819 | </div> |
| 820 | + |
| 821 | + <!-- Scanned Files (lazy-loaded collapsible) --> |
| 822 | + <div v-if="scanContext && scanContext.source_method !== 'none' && scanContext.source_method !== 'url'" class="pt-4"> |
| 823 | + <div class="collapse collapse-arrow bg-base-100 shadow-md"> |
| 824 | + <input type="checkbox" @change="onScannedFilesToggle" /> |
| 825 | + <div class="collapse-title font-medium"> |
| 826 | + Scanned Files |
| 827 | + <span v-if="scanContext.total_files" class="text-base-content/60 font-normal"> |
| 828 | + ({{ scanContext.total_files }} files, {{ formatFileSize(scanContext.total_size_bytes) }}) |
| 829 | + </span> |
| 830 | + </div> |
| 831 | + <div class="collapse-content"> |
| 832 | + <div v-if="scanFilesLoading" class="text-center py-4"> |
| 833 | + <span class="loading loading-spinner loading-sm"></span> |
| 834 | + <span class="ml-2 text-sm">Loading file list...</span> |
| 835 | + </div> |
| 836 | + <div v-else-if="scanFiles.length === 0" class="text-sm text-base-content/40 py-2"> |
| 837 | + No file information available. |
| 838 | + </div> |
| 839 | + <ul v-else class="space-y-0.5 py-1"> |
| 840 | + <li |
| 841 | + v-for="(file, idx) in scanFiles" |
| 842 | + :key="file.path" |
| 843 | + class="flex items-center gap-2 py-0.5" |
| 844 | + > |
| 845 | + <span class="text-base-content/30 text-xs select-none w-4 text-right">{{ idx === scanFiles.length - 1 ? '\u2514' : '\u251C' }}</span> |
| 846 | + <code |
| 847 | + class="text-sm" |
| 848 | + :class="file.suspicious ? 'text-error font-semibold' : 'text-base-content/80'" |
| 849 | + >{{ file.path }}</code> |
| 850 | + <span |
| 851 | + v-if="file.suspicious && file.findings?.length" |
| 852 | + class="badge badge-error badge-xs gap-1" |
| 853 | + > |
| 854 | + {{ file.findings.join(', ') }} |
| 855 | + </span> |
| 856 | + </li> |
| 857 | + </ul> |
| 858 | + </div> |
| 859 | + </div> |
| 860 | + </div> |
751 | 861 | </template> |
752 | 862 | </div> |
753 | 863 | </div> |
@@ -824,6 +934,15 @@ const scanError = ref<string | null>(null) |
824 | 934 | const securityActionLoading = ref(false) |
825 | 935 | let scanPollTimer: ReturnType<typeof setInterval> | null = null |
826 | 936 |
|
| 937 | +// Scan context & files |
| 938 | +const scanFiles = ref<Array<{ path: string; suspicious: boolean; findings?: string[] }>>([]) |
| 939 | +const scanFilesLoading = ref(false) |
| 940 | +const scanFilesLoaded = ref(false) |
| 941 | +
|
| 942 | +const scanContext = computed(() => { |
| 943 | + return scanStatus.value?.scan_context || null |
| 944 | +}) |
| 945 | +
|
827 | 946 | // Logs |
828 | 947 | const serverLogs = ref<string[]>([]) |
829 | 948 | const logsLoading = ref(false) |
@@ -1341,6 +1460,31 @@ function viewToolSchema(tool: Tool) { |
1341 | 1460 | } |
1342 | 1461 |
|
1343 | 1462 | // Security scan functions (Spec 039) |
| 1463 | +function formatFileSize(bytes: number): string { |
| 1464 | + if (!bytes || bytes === 0) return '0 B' |
| 1465 | + if (bytes < 1024) return `${bytes} B` |
| 1466 | + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` |
| 1467 | + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` |
| 1468 | +} |
| 1469 | +
|
| 1470 | +async function onScannedFilesToggle(event: Event) { |
| 1471 | + const checkbox = event.target as HTMLInputElement |
| 1472 | + if (checkbox.checked && !scanFilesLoaded.value && server.value) { |
| 1473 | + scanFilesLoading.value = true |
| 1474 | + try { |
| 1475 | + const response = await api.getScanFiles(server.value.name) |
| 1476 | + if (response.success && response.data) { |
| 1477 | + scanFiles.value = response.data.files || [] |
| 1478 | + scanFilesLoaded.value = true |
| 1479 | + } |
| 1480 | + } catch { |
| 1481 | + // Silently fail |
| 1482 | + } finally { |
| 1483 | + scanFilesLoading.value = false |
| 1484 | + } |
| 1485 | + } |
| 1486 | +} |
| 1487 | +
|
1344 | 1488 | function formatRelativeTime(isoString: string): string { |
1345 | 1489 | const date = new Date(isoString) |
1346 | 1490 | const now = new Date() |
@@ -1391,6 +1535,8 @@ async function startSecurityScan() { |
1391 | 1535 |
|
1392 | 1536 | scanLoading.value = true |
1393 | 1537 | scanError.value = null |
| 1538 | + scanFiles.value = [] |
| 1539 | + scanFilesLoaded.value = false |
1394 | 1540 |
|
1395 | 1541 | try { |
1396 | 1542 | const response = await api.startScan(server.value.name) |
|
0 commit comments