# SBOM Lineage Graph Architecture ## Overview The SBOM Lineage Graph provides a Git-like visualization of container image ancestry with hover-to-proof micro-interactions. It enables auditors and developers to explore SBOM/VEX deltas across artifact versions, turning evidence into an explorable UX. ## Core Concepts ### Lineage Graph A directed acyclic graph (DAG) where: - **Nodes** represent artifact versions (SBOM snapshots) - **Edges** represent relationships between versions ### Edge Types | Type | Description | Example | |------|-------------|---------| | `parent` | Direct version succession | v1.0 → v1.1 of same image | | `build` | Same CI build produced multiple artifacts | Multi-arch build | | `base` | Derived from base image | `FROM alpine:3.19` | ### Node Attributes ``` ┌─────────────────────────────────────┐ │ Node: sha256:abc123... │ ├─────────────────────────────────────┤ │ Artifact: registry/app:v1.2 │ │ Sequence: 42 │ │ Created: 2025-12-28T10:30:00Z │ │ Source: scanner │ ├─────────────────────────────────────┤ │ Badges: │ │ • 3 new vulns (🔴) │ │ • 2 resolved (🟢) │ │ • signature ✓ │ ├─────────────────────────────────────┤ │ Replay Hash: sha256:def456... │ └─────────────────────────────────────┘ ``` ## Data Flow ``` ┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ Scanner │────▶│ SbomService │────▶│ VexLens │ │ │ │ │ │ │ │ • OCI Parse │ │ • Ledger Store │ │ • Consensus │ │ • Ancestry │ │ • Edge Persist │ │ • Delta Compute │ │ • SBOM Gen │ │ • Diff Engine │ │ • Status Track │ └──────────────┘ └─────────────────┘ └──────────────────┘ │ │ │ └────────────────────┼───────────────────────┘ ▼ ┌─────────────────┐ │ Lineage API │ │ │ │ • Graph Query │ │ • Diff Compute │ │ • Export Pack │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Frontend UI │ │ │ │ • Lane View │ │ • Hover Cards │ │ • Compare Mode │ └─────────────────┘ ``` ## Component Architecture ### 1. OCI Ancestry Extractor (Scanner) Extracts parent/base image information from OCI manifests. ```csharp public interface IOciAncestryExtractor { ValueTask ExtractAncestryAsync( string imageReference, CancellationToken cancellationToken); } public sealed record OciAncestry( string ImageDigest, string? BaseImageDigest, string? BaseImageRef, IReadOnlyList LayerDigests, IReadOnlyList History); public sealed record OciHistoryEntry( string CreatedBy, DateTimeOffset Created, bool EmptyLayer); ``` **Implementation Notes:** - Parse OCI image config `history` field - Extract `FROM` instruction from first non-empty layer - Handle multi-stage builds by tracking layer boundaries - Fall back to layer digest heuristics when history unavailable ### 2. Lineage Edge Repository (SbomService) Persists relationships between artifact versions. ```csharp public interface ISbomLineageEdgeRepository { ValueTask AddAsync( LineageEdge edge, CancellationToken cancellationToken); ValueTask> GetChildrenAsync( string parentDigest, Guid tenantId, CancellationToken cancellationToken); ValueTask> GetParentsAsync( string childDigest, Guid tenantId, CancellationToken cancellationToken); ValueTask GetGraphAsync( string artifactDigest, Guid tenantId, int maxDepth, CancellationToken cancellationToken); } public sealed record LineageEdge( Guid Id, string ParentDigest, string ChildDigest, LineageRelationship Relationship, Guid TenantId, DateTimeOffset CreatedAt); public enum LineageRelationship { Parent, Build, Base } ``` ### 3. VEX Delta Repository (Excititor) Tracks VEX status changes between artifact versions. ```csharp public interface IVexDeltaRepository { ValueTask AddAsync( VexDelta delta, CancellationToken cancellationToken); ValueTask> GetDeltasAsync( string fromDigest, string toDigest, Guid tenantId, CancellationToken cancellationToken); ValueTask> GetDeltasByCveAsync( string cve, Guid tenantId, int limit, CancellationToken cancellationToken); } public sealed record VexDelta( Guid Id, string FromArtifactDigest, string ToArtifactDigest, string Cve, VexStatus FromStatus, VexStatus ToStatus, VexDeltaRationale Rationale, string ReplayHash, string? AttestationDigest, Guid TenantId, DateTimeOffset CreatedAt); public sealed record VexDeltaRationale( string Reason, string? EvidenceLink, IReadOnlyDictionary Metadata); ``` ### 4. SBOM-Verdict Link Repository (SbomService) Links SBOM versions to VEX consensus decisions. ```csharp public interface ISbomVerdictLinkRepository { ValueTask LinkAsync( SbomVerdictLink link, CancellationToken cancellationToken); ValueTask> GetVerdictsBySbomAsync( Guid sbomVersionId, Guid tenantId, CancellationToken cancellationToken); ValueTask> GetSbomsByCveAsync( string cve, Guid tenantId, int limit, CancellationToken cancellationToken); } public sealed record SbomVerdictLink( Guid SbomVersionId, string Cve, Guid ConsensusProjectionId, VexStatus VerdictStatus, decimal ConfidenceScore, Guid TenantId, DateTimeOffset LinkedAt); ``` ### 5. Lineage Graph Service (SbomService) Orchestrates lineage queries and diff computation. ```csharp public interface ILineageGraphService { ValueTask GetLineageAsync( string artifactDigest, Guid tenantId, LineageQueryOptions options, CancellationToken cancellationToken); ValueTask GetDiffAsync( string fromDigest, string toDigest, Guid tenantId, CancellationToken cancellationToken); ValueTask CompareAsync( string digestA, string digestB, Guid tenantId, CancellationToken cancellationToken); } public sealed record LineageQueryOptions( int MaxDepth = 10, bool IncludeVerdicts = true, bool IncludeBadges = true); ``` ## Database Schema ### sbom_lineage_edges ```sql CREATE TABLE sbom_lineage_edges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), parent_digest TEXT NOT NULL, child_digest TEXT NOT NULL, relationship TEXT NOT NULL CHECK (relationship IN ('parent', 'build', 'base')), tenant_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (parent_digest, child_digest, tenant_id) ); CREATE INDEX idx_lineage_edges_parent ON sbom_lineage_edges(parent_digest, tenant_id); CREATE INDEX idx_lineage_edges_child ON sbom_lineage_edges(child_digest, tenant_id); CREATE INDEX idx_lineage_edges_created ON sbom_lineage_edges(tenant_id, created_at DESC); ``` ### vex_deltas ```sql CREATE TABLE vex_deltas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_artifact_digest TEXT NOT NULL, to_artifact_digest TEXT NOT NULL, cve TEXT NOT NULL, from_status TEXT NOT NULL, to_status TEXT NOT NULL, rationale JSONB NOT NULL DEFAULT '{}', replay_hash TEXT NOT NULL, attestation_digest TEXT, tenant_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (from_artifact_digest, to_artifact_digest, cve, tenant_id) ); CREATE INDEX idx_vex_deltas_to ON vex_deltas(to_artifact_digest, tenant_id); CREATE INDEX idx_vex_deltas_cve ON vex_deltas(cve, tenant_id); CREATE INDEX idx_vex_deltas_created ON vex_deltas(tenant_id, created_at DESC); ``` ### sbom_verdict_links ```sql CREATE TABLE sbom_verdict_links ( sbom_version_id UUID NOT NULL, cve TEXT NOT NULL, consensus_projection_id UUID NOT NULL, verdict_status TEXT NOT NULL, confidence_score DECIMAL(5,4) NOT NULL, tenant_id UUID NOT NULL, linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (sbom_version_id, cve, tenant_id) ); CREATE INDEX idx_verdict_links_cve ON sbom_verdict_links(cve, tenant_id); CREATE INDEX idx_verdict_links_projection ON sbom_verdict_links(consensus_projection_id); ``` ## API Endpoints ### GET /api/v1/lineage/{artifactDigest} Returns the lineage graph for an artifact. **Response:** ```json { "artifact": "sha256:abc123...", "nodes": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "digest": "sha256:abc123...", "artifactRef": "registry/app:v1.2", "sequenceNumber": 42, "createdAt": "2025-12-28T10:30:00Z", "source": "scanner", "badges": { "newVulns": 3, "resolvedVulns": 2, "signatureStatus": "valid" }, "replayHash": "sha256:def456..." } ], "edges": [ { "from": "sha256:parent...", "to": "sha256:abc123...", "relationship": "parent" } ] } ``` ### GET /api/v1/lineage/diff Returns component and VEX diffs between two versions. **Query Parameters:** - `from` - Source artifact digest - `to` - Target artifact digest **Response:** ```json { "sbomDiff": { "added": [ {"purl": "pkg:npm/lodash@4.17.21", "version": "4.17.21", "license": "MIT"} ], "removed": [ {"purl": "pkg:npm/lodash@4.17.20", "version": "4.17.20", "license": "MIT"} ], "versionChanged": [ {"purl": "pkg:npm/axios@1.6.0", "fromVersion": "1.5.0", "toVersion": "1.6.0"} ] }, "vexDiff": [ { "cve": "CVE-2024-1234", "fromStatus": "affected", "toStatus": "not_affected", "reason": "Component removed", "evidenceLink": "/evidence/abc123" } ], "reachabilityDiff": [ { "cve": "CVE-2024-5678", "fromStatus": "reachable", "toStatus": "unreachable", "pathsRemoved": 3, "gatesAdded": ["auth_required"] } ], "replayHash": "sha256:ghi789..." } ``` ### POST /api/v1/lineage/export Exports evidence pack for artifact(s). **Request:** ```json { "artifactDigests": ["sha256:abc123..."], "includeAttestations": true, "sign": true } ``` **Response:** ```json { "downloadUrl": "/exports/pack-xyz.zip", "bundleDigest": "sha256:bundle...", "expiresAt": "2025-12-28T11:30:00Z" } ``` ## Caching Strategy ### Hover Card Cache (Valkey) - **Key:** `lineage:hover:{tenantId}:{artifactDigest}` - **TTL:** 5 minutes - **Invalidation:** On new SBOM version or VEX update - **Target:** <150ms response time ### Compare Cache (Valkey) - **Key:** `lineage:compare:{tenantId}:{digestA}:{digestB}` - **TTL:** 10 minutes - **Invalidation:** On new VEX data for either artifact ## Determinism Guarantees 1. **Node Ordering:** Sorted by `sequenceNumber DESC`, then `createdAt DESC` 2. **Edge Ordering:** Sorted by `(from, to, relationship)` lexicographically 3. **Component Diff:** Components sorted by `purl` (ordinal) 4. **VEX Diff:** Sorted by `cve` (ordinal) 5. **Replay Hash:** SHA256 of deterministically serialized inputs ## Security Considerations 1. **Tenant Isolation:** All queries scoped by `tenant_id` 2. **Digest Validation:** Verify artifact digest format before queries 3. **Rate Limiting:** Apply per-tenant rate limits on graph queries 4. **Export Authorization:** Verify `lineage:export` scope for pack generation ## Metrics | Metric | Type | Description | |--------|------|-------------| | `sbom_lineage_graph_queries_total` | Counter | Graph queries by tenant | | `sbom_lineage_diff_latency_seconds` | Histogram | Diff computation latency | | `sbom_lineage_hover_cache_hits_total` | Counter | Hover card cache hits | | `sbom_lineage_export_size_bytes` | Histogram | Evidence pack sizes | | `vex_deltas_created_total` | Counter | VEX deltas stored | ## Error Handling | Error Code | Description | HTTP Status | |------------|-------------|-------------| | `LINEAGE_NOT_FOUND` | Artifact not in lineage graph | 404 | | `LINEAGE_DEPTH_EXCEEDED` | Max depth limit reached | 400 | | `LINEAGE_DIFF_INVALID` | Same digest for from/to | 400 | | `LINEAGE_EXPORT_TOO_LARGE` | Pack exceeds size limit | 413 |