- Add RpmVersionComparer for RPM version comparison with epoch, version, and release handling. - Introduce DebianVersion for parsing Debian EVR (Epoch:Version-Release) strings. - Create ApkVersion for parsing Alpine APK version strings with suffix support. - Define IVersionComparator interface for version comparison with proof-line generation. - Implement VersionComparisonResult struct to encapsulate comparison results and proof lines. - Add tests for Debian and RPM version comparers to ensure correct functionality and edge case handling. - Create project files for the version comparison library and its tests.
16 KiB
Smart-Diff UI Architecture
Version: 1.0 Status: Draft Last Updated: 2025-12-22 Sprint Reference: SPRINT_4200_0002_0003
Overview
The Smart-Diff UI provides a dedicated comparison experience for analyzing material risk changes between container image versions. It implements a "diff-first" approach to vulnerability triage, enabling users to focus on what changed rather than reviewing entire vulnerability lists.
Design Principles
1. Diff-First Triage
The primary question in any release is: "What changed that affects risk?" The UI defaults to showing delta information rather than full vulnerability lists.
2. Proof-Carrying Evidence
Every verdict and comparison includes cryptographic evidence. Users can verify determinism, trace decisions to policy rules, and replay computations.
3. Baseline Transparency
Comparisons require explicit baselines with auditor-friendly rationale. The system never uses "magic" to select baselines without explanation.
4. Role-Based Defaults
Different personas (Developer, Security, Audit) see different default views while retaining access to all information.
Component Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ SMART-DIFF UI ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ COMPARE VIEW CONTAINER │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ Baseline │ │ Trust │ │ Export │ │ │
│ │ │ Selector │ │ Indicators │ │ Actions │ │ │
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
│ │ │ DELTA SUMMARY STRIP │ │ │
│ │ │ [+N added] [-N removed] [~N changed] [Policy: v1.2] [Feed: 2h]│ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ THREE-PANE LAYOUT │ │ │
│ │ │ ┌──────────┐ ┌────────────────┐ ┌────────────────────────┐ │ │ │
│ │ │ │Categories│ │ Items │ │ Proof Panel │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ ● SBOM │ │ CVE-2024-1234 │ │ ┌────────────────────┐ │ │ │ │
│ │ │ │ ● Reach │ │ lodash@4.17.20 │ │ │ Witness Path │ │ │ │ │
│ │ │ │ ● VEX │ │ +reachable │ │ │ main() → parse() │ │ │ │ │
│ │ │ │ ● Policy │ │ Priority: 0.85 │ │ │ → vuln_func() │ │ │ │ │
│ │ │ │ ● Unknwn │ │ │ │ └────────────────────┘ │ │ │ │
│ │ │ │ │ │ CVE-2024-5678 │ │ ┌────────────────────┐ │ │ │ │
│ │ │ │ │ │ requests@2.28 │ │ │ VEX Merge │ │ │ │ │
│ │ │ │ │ │ +KEV │ │ │ vendor: affected │ │ │ │ │
│ │ │ │ │ │ Priority: 0.95 │ │ │ distro: not_aff │ │ │ │ │
│ │ │ │ │ │ │ │ │ → Result: affected │ │ │ │ │
│ │ │ │ │ │ │ │ └────────────────────┘ │ │ │ │
│ │ │ └──────────┘ └────────────────┘ └────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ACTIONABLES PANEL │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ What to do next: │ │ │ │
│ │ │ │ 1. [CRITICAL] Upgrade lodash → 4.17.21 │ │ │ │
│ │ │ │ 2. [HIGH] Add VEX statement for urllib3 (not affected) │ │ │ │
│ │ │ │ 3. [MEDIUM] Resolve unknown: missing SBOM for module A │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Component Hierarchy
CompareViewComponent
├── BaselineSelectorComponent
│ └── BaselineRationaleComponent
├── TrustIndicatorsComponent
│ ├── DeterminismHashDisplay
│ ├── PolicyVersionDisplay
│ ├── FeedSnapshotDisplay
│ ├── SignatureStatusDisplay
│ └── PolicyDriftIndicator
├── DeltaSummaryStripComponent
├── ThreePaneLayoutComponent
│ ├── CategoriesPaneComponent
│ ├── ItemsPaneComponent
│ └── ProofPaneComponent
│ ├── WitnessPathComponent
│ ├── VexMergeExplanationComponent
│ └── EnvelopeHashesComponent
├── ActionablesPanelComponent
└── ExportActionsComponent
State Management
Signals-Based State
The compare view uses Angular signals for reactive state management:
// Core state
currentTarget = signal<CompareTarget | null>(null);
baselineTarget = signal<CompareTarget | null>(null);
delta = signal<DeltaVerdictResponse | null>(null);
// UI state
selectedCategory = signal<string | null>(null);
selectedItem = signal<DeltaItem | null>(null);
viewMode = signal<'side-by-side' | 'unified'>('side-by-side');
userRole = signal<'developer' | 'security' | 'audit'>('developer');
// Computed state
filteredItems = computed(() => {
const cat = this.selectedCategory();
const items = this.delta()?.Items ?? [];
return cat ? items.filter(i => i.category === cat) : items;
});
deltaSummary = computed(() => this.delta()?.Summary);
trustIndicators = computed(() => this.delta()?.TrustIndicators);
Data Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Route │───►│ Component │───►│ Service │
│ Params │ │ Init │ │ Calls │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Signals │◄───│ Backend │
│ Update │ │ Response │
└─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Computed │
│ Values │
└─────────────┘
│
▼
┌─────────────┐
│ Template │
│ Render │
└─────────────┘
API Integration
Backend Endpoints
| Endpoint | Purpose |
|---|---|
GET /api/v1/baselines/recommendations/{digest} |
Get recommended baselines |
GET /api/v1/baselines/rationale/{base}/{head} |
Get baseline selection rationale |
POST /api/v1/delta/compute |
Compute delta (idempotent) |
GET /api/v1/delta/{deltaId} |
Get delta results |
GET /api/v1/delta/{deltaId}/trust-indicators |
Get trust indicators |
GET /api/v1/actionables/delta/{deltaId} |
Get actionable recommendations |
GET /api/v1/evidence/delta/{deltaId}/items/{itemId} |
Get item evidence |
GET /api/v1/evidence/delta/{deltaId}/witness-paths |
Get witness paths |
GET /api/v1/evidence/delta/{deltaId}/vex-merge/{vulnId} |
Get VEX merge explanation |
Service Layer
@Injectable({ providedIn: 'root' })
export class CompareService {
constructor(private http: HttpClient) {}
getRecommendedBaselines(digest: string): Observable<BaselineRecommendationsResponse> {
return this.http.get<BaselineRecommendationsResponse>(
`/api/v1/baselines/recommendations/${digest}`
);
}
computeDelta(request: DeltaComputeRequest): Observable<DeltaVerdictResponse> {
return this.http.post<DeltaVerdictResponse>('/api/v1/delta/compute', request);
}
getActionables(deltaId: string): Observable<ActionablesResponse> {
return this.http.get<ActionablesResponse>(`/api/v1/actionables/delta/${deltaId}`);
}
getItemEvidence(deltaId: string, itemId: string): Observable<DeltaItemEvidenceResponse> {
return this.http.get<DeltaItemEvidenceResponse>(
`/api/v1/evidence/delta/${deltaId}/items/${itemId}`
);
}
}
Routing
// app.routes.ts additions
{
path: 'releases/:releaseId',
children: [
{ path: '', redirectTo: 'detail', pathMatch: 'full' },
{ path: 'detail', component: ReleaseFlowComponent },
{
path: 'compare',
component: CompareViewComponent,
data: { requireBaseline: false }
},
{
path: 'compare/:baselineId',
component: CompareViewComponent,
data: { requireBaseline: true }
}
]
},
{
path: 'compare',
children: [
{
path: ':currentDigest',
component: CompareViewComponent
},
{
path: ':currentDigest/:baselineDigest',
component: CompareViewComponent
}
]
}
Role-Based Views
Default Tab by Role
| Role | Default Tab | Visible Features |
|---|---|---|
| Developer | Actionables | Actionables, Witness Paths, Upgrade Suggestions |
| Security | Claims | VEX Merge, Policy Reasoning, Claim Sources, Actionables |
| Audit | Attestations | Signatures, Replay, Evidence Pack, Envelope Hashes |
Implementation
const ROLE_DEFAULTS: Record<UserRole, RoleConfig> = {
developer: {
defaultTab: 'actionables',
showFeatures: ['actionables', 'witness-paths', 'upgrade-suggestions'],
expandedPanels: ['actionables']
},
security: {
defaultTab: 'claims',
showFeatures: ['vex-merge', 'policy-reasoning', 'claim-sources', 'actionables'],
expandedPanels: ['vex-merge', 'policy']
},
audit: {
defaultTab: 'attestations',
showFeatures: ['signatures', 'replay', 'evidence-pack', 'envelope-hashes'],
expandedPanels: ['trust-indicators', 'signatures']
}
};
Trust Indicators
Determinism Verification
The UI displays and enables verification of:
- Determinism Hash - SHA-256 of normalized delta output
- Policy Version/Hash - Active policy at scan time
- Feed Snapshot - Vulnerability feed timestamp and hash
- Signature Status - DSSE envelope verification result
Degraded Mode
When signature verification fails, the UI:
- Displays a prominent warning banner
- Disables "Approve" actions
- Shows detailed verification failure reason
- Provides replay command for local verification
Accessibility
Keyboard Navigation
Tab/Shift+Tab: Navigate between panesArrow Up/Down: Navigate items within paneEnter: Select item / expand detailEscape: Close expanded detailC: Copy replay command (when focused on trust indicators)
Screen Reader Support
- ARIA labels on all interactive elements
- Live regions for delta summary updates
- Semantic heading structure
Performance Considerations
Lazy Loading
- Evidence panel loads on-demand when item selected
- Witness paths collapse by default (expand on click)
- VEX merge details in expansion panel
Caching
- Delta computations cached by (base_hash, head_hash, policy_hash)
- Baseline recommendations cached per session
- Trust indicators cached with delta
Virtual Scrolling
For large deltas (> 100 items), the items pane uses virtual scrolling:
<cdk-virtual-scroll-viewport itemSize="56" class="items-viewport">
<mat-list-item *cdkVirtualFor="let item of filteredItems()">
<!-- item content -->
</mat-list-item>
</cdk-virtual-scroll-viewport>
Testing Strategy
Unit Tests
- Component behavior (selection, filtering, expansion)
- Computed signal derivations
- Role-based view switching
Integration Tests
- API service calls and response handling
- Navigation and routing
- State persistence across route changes
E2E Tests
- Full comparison workflow
- Baseline selection and rationale display
- Export functionality
- Role-based default verification