+
+
+
+
+ @if (loading()) {
+
+
+
Loading coverage data...
+
+ }
+
+
+ @if (error()) {
+
+ !
+ {{ error() }}
+
+
+ }
+
+
+ @if (!loading() && !error()) {
+
+ @if (viewMode() !== 'heatmap') {
+
+ }
+
+
+ @if (viewMode() === 'heatmap') {
+
+
+
+
+ {{ totalCves() }}
+ Total CVEs
+
+
+ {{ avgCoverage() | number:'1.1-1' }}%
+ Avg Coverage
+
+
+ {{ criticalCount() }}
+ Critical (<25%)
+
+
+ {{ safeCount() }}
+ Safe (>90%)
+
+
+
+
+
+ @for (cell of heatmapCells(); track cell.cveId) {
+
+ }
+
+
+
+ @if (totalCves() > pageSize) {
+
+ }
+
+ }
+
+
+ @if (viewMode() === 'details' && coverageDetails()) {
+
+
+
+
+
+
+ {{ coverageDetails()!.summary.vulnerableImages }}
+ Vulnerable
+
+
+ {{ coverageDetails()!.summary.patchedImages }}
+ Patched
+
+
+ {{ coverageDetails()!.summary.unknownImages }}
+ Unknown
+
+
+ {{ coverageDetails()!.summary.overallCoverage | number:'1.1-1' }}%
+ Coverage
+
+
+
+
+
+
+
+ | Symbol |
+ Soname |
+ Vulnerable |
+ Patched |
+ Unknown |
+ Delta |
+ Actions |
+
+
+
+ @for (fn of coverageDetails()!.functions; track fn.symbolName) {
+
+ | {{ fn.symbolName }} |
+ {{ fn.soname ?? '-' }} |
+ {{ fn.vulnerableCount }} |
+ {{ fn.patchedCount }} |
+ {{ fn.unknownCount }} |
+
+ @if (fn.hasDelta) {
+ D
+ } @else {
+ -
+ }
+ |
+
+
+ |
+
+ }
+
+
+
+ }
+
+
+ @if (viewMode() === 'matches' && matchPage()) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Binary Key |
+ Symbol |
+ State |
+ Confidence |
+ Scanned |
+
+
+
+ @for (match of matchPage()!.matches; track match.matchId) {
+
+ |
+ {{ truncateBinaryKey(match.binaryKey) }}
+ |
+ {{ match.symbolName }} |
+
+
+ {{ match.matchState }}
+
+ |
+ {{ (match.confidence * 100) | number:'1.0-0' }}% |
+ {{ formatDate(match.scannedAt) }} |
+
+ }
+
+
+
+
+ @if (matchPage()!.totalCount > matchPageSize) {
+
+ }
+
+ }
+ }
+
+ `,
+ styles: [`
+ .patch-map-container {
+ padding: 24px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ /* Header */
+ .patch-map-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ flex-wrap: wrap;
+ gap: 16px;
+ }
+
+ .page-title {
+ font-size: 24px;
+ font-weight: 600;
+ margin: 0 0 4px 0;
+ }
+
+ .page-subtitle {
+ font-size: 14px;
+ color: #666;
+ margin: 0;
+ }
+
+ .header-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .filter-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .filter-label {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .filter-input {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ width: 180px;
+ }
+
+ .action-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ background: white;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.15s;
+ }
+
+ .action-btn:hover:not(:disabled) {
+ border-color: #007bff;
+ color: #007bff;
+ }
+
+ .action-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ /* Loading/Error states */
+ .loading-state, .error-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 48px;
+ text-align: center;
+ }
+
+ .loading-spinner {
+ width: 24px;
+ height: 24px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #007bff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ to { transform: rotate(360deg); }
+ }
+
+ .error-state {
+ background: #f8d7da;
+ border-radius: 8px;
+ color: #721c24;
+ }
+
+ .error-icon {
+ font-weight: bold;
+ font-size: 18px;
+ }
+
+ .retry-btn {
+ padding: 6px 12px;
+ background: #721c24;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ /* Breadcrumb */
+ .breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ font-size: 14px;
+ }
+
+ .breadcrumb-link {
+ background: none;
+ border: none;
+ color: #007bff;
+ cursor: pointer;
+ padding: 0;
+ }
+
+ .breadcrumb-link:hover {
+ text-decoration: underline;
+ }
+
+ .breadcrumb-separator {
+ color: #999;
+ }
+
+ .breadcrumb-current {
+ font-weight: 500;
+ }
+
+ /* Summary bar */
+ .summary-bar {
+ display: flex;
+ gap: 24px;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ margin-bottom: 24px;
+ }
+
+ .stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .stat-value {
+ font-size: 24px;
+ font-weight: 600;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #666;
+ }
+
+ .stat-item.critical .stat-value { color: #dc3545; }
+ .stat-item.safe .stat-value { color: #28a745; }
+
+ /* Heatmap grid */
+ .heatmap-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 12px;
+ }
+
+ .heatmap-cell {
+ display: flex;
+ flex-direction: column;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ background: white;
+ cursor: pointer;
+ text-align: left;
+ transition: all 0.15s;
+ }
+
+ .heatmap-cell:hover {
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+ transform: translateY(-2px);
+ }
+
+ .heatmap-cell:focus {
+ outline: 2px solid #007bff;
+ outline-offset: 2px;
+ }
+
+ /* Coverage colors */
+ .heatmap-cell.coverage-critical {
+ border-left: 4px solid #dc3545;
+ background: #fff5f5;
+ }
+
+ .heatmap-cell.coverage-high {
+ border-left: 4px solid #fd7e14;
+ background: #fff8f0;
+ }
+
+ .heatmap-cell.coverage-medium {
+ border-left: 4px solid #ffc107;
+ background: #fffef0;
+ }
+
+ .heatmap-cell.coverage-low {
+ border-left: 4px solid #20c997;
+ background: #f0fff8;
+ }
+
+ .heatmap-cell.coverage-safe {
+ border-left: 4px solid #28a745;
+ background: #f0fff0;
+ }
+
+ .cell-cve {
+ font-weight: 600;
+ font-size: 14px;
+ margin-bottom: 2px;
+ }
+
+ .cell-package {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .cell-coverage {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .cell-bar {
+ height: 4px;
+ background: #e9ecef;
+ border-radius: 2px;
+ overflow: hidden;
+ }
+
+ .cell-bar-fill {
+ height: 100%;
+ background: currentColor;
+ transition: width 0.3s;
+ }
+
+ .coverage-critical .cell-bar-fill { background: #dc3545; }
+ .coverage-high .cell-bar-fill { background: #fd7e14; }
+ .coverage-medium .cell-bar-fill { background: #ffc107; }
+ .coverage-low .cell-bar-fill { background: #20c997; }
+ .coverage-safe .cell-bar-fill { background: #28a745; }
+
+ /* Pagination */
+ .pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ margin-top: 24px;
+ }
+
+ .page-btn {
+ padding: 8px 16px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ background: white;
+ cursor: pointer;
+ }
+
+ .page-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .page-info {
+ font-size: 14px;
+ color: #666;
+ }
+
+ /* Details section */
+ .details-section {
+ background: white;
+ border: 1px solid #e9ecef;
+ border-radius: 12px;
+ padding: 24px;
+ }
+
+ .details-header {
+ margin-bottom: 24px;
+ }
+
+ .details-title {
+ font-size: 20px;
+ font-weight: 600;
+ margin: 0 0 4px 0;
+ }
+
+ .details-package {
+ font-size: 14px;
+ color: #666;
+ }
+
+ .details-summary {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 24px;
+ }
+
+ .summary-card {
+ flex: 1;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ text-align: center;
+ }
+
+ .summary-card.vulnerable { background: #f8d7da; }
+ .summary-card.patched { background: #d4edda; }
+ .summary-card.unknown { background: #e2e3e5; }
+
+ .card-value {
+ display: block;
+ font-size: 24px;
+ font-weight: 600;
+ }
+
+ .card-label {
+ font-size: 12px;
+ color: #666;
+ }
+
+ /* Tables */
+ .function-table, .matches-table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+
+ .function-table th, .function-table td,
+ .matches-table th, .matches-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #e9ecef;
+ }
+
+ .function-table th, .matches-table th {
+ font-weight: 600;
+ font-size: 13px;
+ color: #666;
+ text-transform: uppercase;
+ }
+
+ .symbol-name {
+ font-family: monospace;
+ font-size: 13px;
+ }
+
+ .count.vulnerable { color: #dc3545; }
+ .count.patched { color: #28a745; }
+ .count.unknown { color: #6c757d; }
+
+ .delta-badge {
+ display: inline-block;
+ padding: 2px 6px;
+ background: #007bff;
+ color: white;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+ }
+
+ .no-delta {
+ color: #999;
+ }
+
+ .view-btn {
+ padding: 4px 8px;
+ font-size: 12px;
+ border: 1px solid #007bff;
+ border-radius: 4px;
+ background: white;
+ color: #007bff;
+ cursor: pointer;
+ }
+
+ .view-btn:hover {
+ background: #007bff;
+ color: white;
+ }
+
+ /* Matches section */
+ .matches-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ }
+
+ .matches-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0;
+ }
+
+ .matches-count {
+ font-size: 14px;
+ color: #666;
+ }
+
+ .matches-filters {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ }
+
+ .state-select {
+ padding: 6px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ }
+
+ .binary-key {
+ font-family: monospace;
+ font-size: 12px;
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .state-badge {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ }
+
+ .state-vulnerable { background: #f8d7da; color: #721c24; }
+ .state-patched { background: #d4edda; color: #155724; }
+ .state-unknown { background: #e2e3e5; color: #383d41; }
+
+ .scanned-at {
+ font-size: 12px;
+ color: #666;
+ }
+
+ /* Responsive */
+ @media (max-width: 768px) {
+ .patch-map-header {
+ flex-direction: column;
+ }
+
+ .header-right {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .heatmap-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .details-summary {
+ flex-wrap: wrap;
+ }
+
+ .summary-card {
+ min-width: calc(50% - 8px);
+ }
+
+ .function-table, .matches-table {
+ display: block;
+ overflow-x: auto;
+ }
+ }
+ `]
+})
+export class PatchMapComponent implements OnInit {
+ private readonly api = inject(PATCH_COVERAGE_API);
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+
+ protected readonly Math = Math;
+
+ // State signals
+ readonly loading = signal(false);
+ readonly error = signal