docs consolidation, big sln build fixes, new advisories and sprints/tasks
This commit is contained in:
469
docs/modules/sbom-service/lineage/architecture.md
Normal file
469
docs/modules/sbom-service/lineage/architecture.md
Normal file
@@ -0,0 +1,469 @@
|
||||
# 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<OciAncestry> ExtractAncestryAsync(
|
||||
string imageReference,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record OciAncestry(
|
||||
string ImageDigest,
|
||||
string? BaseImageDigest,
|
||||
string? BaseImageRef,
|
||||
IReadOnlyList<string> LayerDigests,
|
||||
IReadOnlyList<OciHistoryEntry> 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<LineageEdge> AddAsync(
|
||||
LineageEdge edge,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<IReadOnlyList<LineageEdge>> GetChildrenAsync(
|
||||
string parentDigest,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<IReadOnlyList<LineageEdge>> GetParentsAsync(
|
||||
string childDigest,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<LineageGraph> 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<VexDelta> AddAsync(
|
||||
VexDelta delta,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<IReadOnlyList<VexDelta>> GetDeltasAsync(
|
||||
string fromDigest,
|
||||
string toDigest,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<IReadOnlyList<VexDelta>> 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<string, string> 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<IReadOnlyList<SbomVerdictLink>> GetVerdictsBySbomAsync(
|
||||
Guid sbomVersionId,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<IReadOnlyList<SbomVerdictLink>> 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<LineageGraphResponse> GetLineageAsync(
|
||||
string artifactDigest,
|
||||
Guid tenantId,
|
||||
LineageQueryOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<LineageDiffResponse> GetDiffAsync(
|
||||
string fromDigest,
|
||||
string toDigest,
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<LineageCompareResponse> 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 |
|
||||
319
docs/modules/sbom-service/lineage/schema.sql
Normal file
319
docs/modules/sbom-service/lineage/schema.sql
Normal file
@@ -0,0 +1,319 @@
|
||||
-- SBOM Lineage Graph Database Schema
|
||||
-- Version: 1.0.0
|
||||
-- Created: 2025-12-28
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: sbom_lineage_edges
|
||||
-- Purpose: Stores relationships between SBOM versions (parent/child, build, base)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sbom_lineage_edges (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Edge endpoints (using artifact digest as stable identifier)
|
||||
parent_digest TEXT NOT NULL,
|
||||
child_digest TEXT NOT NULL,
|
||||
|
||||
-- Relationship type
|
||||
relationship TEXT NOT NULL CHECK (relationship IN ('parent', 'build', 'base')),
|
||||
|
||||
-- Tenant isolation
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Prevent duplicate edges
|
||||
CONSTRAINT uq_lineage_edge UNIQUE (parent_digest, child_digest, tenant_id)
|
||||
);
|
||||
|
||||
-- Index for traversing from parent to children
|
||||
CREATE INDEX IF NOT EXISTS idx_lineage_edges_parent
|
||||
ON sbom_lineage_edges(parent_digest, tenant_id);
|
||||
|
||||
-- Index for traversing from child to parents
|
||||
CREATE INDEX IF NOT EXISTS idx_lineage_edges_child
|
||||
ON sbom_lineage_edges(child_digest, tenant_id);
|
||||
|
||||
-- Index for time-based queries
|
||||
CREATE INDEX IF NOT EXISTS idx_lineage_edges_created
|
||||
ON sbom_lineage_edges(tenant_id, created_at DESC);
|
||||
|
||||
-- Index for relationship filtering
|
||||
CREATE INDEX IF NOT EXISTS idx_lineage_edges_relationship
|
||||
ON sbom_lineage_edges(tenant_id, relationship);
|
||||
|
||||
COMMENT ON TABLE sbom_lineage_edges IS 'Stores directed edges between SBOM versions representing lineage relationships';
|
||||
COMMENT ON COLUMN sbom_lineage_edges.parent_digest IS 'SHA256 digest of parent artifact';
|
||||
COMMENT ON COLUMN sbom_lineage_edges.child_digest IS 'SHA256 digest of child artifact';
|
||||
COMMENT ON COLUMN sbom_lineage_edges.relationship IS 'Type of relationship: parent (version succession), build (same CI build), base (FROM instruction)';
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: vex_deltas
|
||||
-- Purpose: Tracks VEX status changes between artifact versions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vex_deltas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Artifact pair
|
||||
from_artifact_digest TEXT NOT NULL,
|
||||
to_artifact_digest TEXT NOT NULL,
|
||||
|
||||
-- Vulnerability
|
||||
cve TEXT NOT NULL,
|
||||
|
||||
-- Status transition
|
||||
from_status TEXT NOT NULL,
|
||||
to_status TEXT NOT NULL,
|
||||
|
||||
-- Explanation
|
||||
rationale JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Determinism
|
||||
replay_hash TEXT NOT NULL,
|
||||
|
||||
-- Signed attestation reference (if signed)
|
||||
attestation_digest TEXT,
|
||||
|
||||
-- Tenant isolation
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Prevent duplicate deltas
|
||||
CONSTRAINT uq_vex_delta UNIQUE (from_artifact_digest, to_artifact_digest, cve, tenant_id)
|
||||
);
|
||||
|
||||
-- Index for querying deltas by target artifact
|
||||
CREATE INDEX IF NOT EXISTS idx_vex_deltas_to
|
||||
ON vex_deltas(to_artifact_digest, tenant_id);
|
||||
|
||||
-- Index for querying deltas by CVE
|
||||
CREATE INDEX IF NOT EXISTS idx_vex_deltas_cve
|
||||
ON vex_deltas(cve, tenant_id);
|
||||
|
||||
-- Index for time-based queries
|
||||
CREATE INDEX IF NOT EXISTS idx_vex_deltas_created
|
||||
ON vex_deltas(tenant_id, created_at DESC);
|
||||
|
||||
-- Index for finding status transitions
|
||||
CREATE INDEX IF NOT EXISTS idx_vex_deltas_status
|
||||
ON vex_deltas(tenant_id, from_status, to_status);
|
||||
|
||||
-- GIN index for rationale JSON queries
|
||||
CREATE INDEX IF NOT EXISTS idx_vex_deltas_rationale
|
||||
ON vex_deltas USING GIN (rationale);
|
||||
|
||||
COMMENT ON TABLE vex_deltas IS 'Tracks VEX status changes between artifact versions with rationale';
|
||||
COMMENT ON COLUMN vex_deltas.rationale IS 'JSON object with reason, evidenceLink, and metadata';
|
||||
COMMENT ON COLUMN vex_deltas.replay_hash IS 'SHA256 hash of inputs for deterministic replay verification';
|
||||
COMMENT ON COLUMN vex_deltas.attestation_digest IS 'SHA256 digest of signed delta verdict attestation';
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: sbom_verdict_links
|
||||
-- Purpose: Links SBOM versions to VEX consensus decisions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sbom_verdict_links (
|
||||
-- SBOM version reference
|
||||
sbom_version_id UUID NOT NULL,
|
||||
|
||||
-- Vulnerability
|
||||
cve TEXT NOT NULL,
|
||||
|
||||
-- Consensus reference
|
||||
consensus_projection_id UUID NOT NULL,
|
||||
|
||||
-- Verdict snapshot
|
||||
verdict_status TEXT NOT NULL,
|
||||
confidence_score DECIMAL(5,4) NOT NULL CHECK (confidence_score >= 0 AND confidence_score <= 1),
|
||||
|
||||
-- Tenant isolation
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Audit
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Composite primary key
|
||||
PRIMARY KEY (sbom_version_id, cve, tenant_id)
|
||||
);
|
||||
|
||||
-- Index for querying by CVE
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_links_cve
|
||||
ON sbom_verdict_links(cve, tenant_id);
|
||||
|
||||
-- Index for querying by consensus projection
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_links_projection
|
||||
ON sbom_verdict_links(consensus_projection_id);
|
||||
|
||||
-- Index for time-based queries
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_links_linked
|
||||
ON sbom_verdict_links(tenant_id, linked_at DESC);
|
||||
|
||||
-- Index for finding specific statuses
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_links_status
|
||||
ON sbom_verdict_links(tenant_id, verdict_status);
|
||||
|
||||
COMMENT ON TABLE sbom_verdict_links IS 'Links SBOM versions to VEX consensus decisions for traceability';
|
||||
COMMENT ON COLUMN sbom_verdict_links.confidence_score IS 'Consensus confidence score (0.0-1.0)';
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE: vex_consensus_projections (migrated from in-memory VexLens)
|
||||
-- Purpose: Persistent storage for VEX consensus projections
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vex_consensus_projections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Target
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
product_key TEXT NOT NULL,
|
||||
|
||||
-- Tenant isolation
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Consensus result
|
||||
status TEXT NOT NULL,
|
||||
confidence_score DECIMAL(5,4) NOT NULL CHECK (confidence_score >= 0 AND confidence_score <= 1),
|
||||
outcome TEXT NOT NULL,
|
||||
|
||||
-- Statistics
|
||||
statement_count INT NOT NULL DEFAULT 0,
|
||||
conflict_count INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- Timestamps
|
||||
computed_at TIMESTAMPTZ NOT NULL,
|
||||
stored_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- History linkage
|
||||
previous_projection_id UUID REFERENCES vex_consensus_projections(id),
|
||||
status_changed BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- Unique constraint for latest projection per (vuln, product, tenant, time)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_consensus_unique
|
||||
ON vex_consensus_projections(tenant_id, vulnerability_id, product_key, computed_at);
|
||||
|
||||
-- Index for finding status changes
|
||||
CREATE INDEX IF NOT EXISTS idx_consensus_status_changed
|
||||
ON vex_consensus_projections(tenant_id, status_changed, computed_at DESC)
|
||||
WHERE status_changed = TRUE;
|
||||
|
||||
-- Index for history traversal
|
||||
CREATE INDEX IF NOT EXISTS idx_consensus_previous
|
||||
ON vex_consensus_projections(previous_projection_id)
|
||||
WHERE previous_projection_id IS NOT NULL;
|
||||
|
||||
-- Index for product queries
|
||||
CREATE INDEX IF NOT EXISTS idx_consensus_product
|
||||
ON vex_consensus_projections(product_key, tenant_id);
|
||||
|
||||
COMMENT ON TABLE vex_consensus_projections IS 'Persistent VEX consensus projections with full history';
|
||||
COMMENT ON COLUMN vex_consensus_projections.outcome IS 'Consensus outcome: Unanimous, Majority, Plurality, ConflictResolved, NoData';
|
||||
COMMENT ON COLUMN vex_consensus_projections.status_changed IS 'True if status differs from previous projection';
|
||||
|
||||
-- ============================================================================
|
||||
-- EXTENSION: Add replay_hash to sbom_snapshots (alter existing table)
|
||||
-- ============================================================================
|
||||
|
||||
-- Note: This ALTER should be applied to existing sbom_snapshots table
|
||||
-- ALTER TABLE sbom_snapshots ADD COLUMN IF NOT EXISTS replay_hash TEXT;
|
||||
-- CREATE INDEX IF NOT EXISTS idx_sbom_snapshots_replay ON sbom_snapshots(replay_hash) WHERE replay_hash IS NOT NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- FUNCTIONS: Helper functions for lineage queries
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get lineage depth from a starting node
|
||||
CREATE OR REPLACE FUNCTION get_lineage_depth(
|
||||
p_artifact_digest TEXT,
|
||||
p_tenant_id UUID,
|
||||
p_max_depth INT DEFAULT 10
|
||||
) RETURNS INT AS $$
|
||||
DECLARE
|
||||
v_depth INT := 0;
|
||||
v_current_count INT;
|
||||
BEGIN
|
||||
WITH RECURSIVE lineage AS (
|
||||
SELECT child_digest, 1 as depth
|
||||
FROM sbom_lineage_edges
|
||||
WHERE parent_digest = p_artifact_digest AND tenant_id = p_tenant_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT e.child_digest, l.depth + 1
|
||||
FROM sbom_lineage_edges e
|
||||
JOIN lineage l ON e.parent_digest = l.child_digest
|
||||
WHERE e.tenant_id = p_tenant_id AND l.depth < p_max_depth
|
||||
)
|
||||
SELECT COALESCE(MAX(depth), 0) INTO v_depth FROM lineage;
|
||||
|
||||
RETURN v_depth;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Function to get all ancestors of an artifact
|
||||
CREATE OR REPLACE FUNCTION get_ancestors(
|
||||
p_artifact_digest TEXT,
|
||||
p_tenant_id UUID,
|
||||
p_max_depth INT DEFAULT 10
|
||||
) RETURNS TABLE (
|
||||
ancestor_digest TEXT,
|
||||
depth INT,
|
||||
relationship TEXT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT parent_digest, 1 as depth, e.relationship
|
||||
FROM sbom_lineage_edges e
|
||||
WHERE child_digest = p_artifact_digest AND tenant_id = p_tenant_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT e.parent_digest, a.depth + 1, e.relationship
|
||||
FROM sbom_lineage_edges e
|
||||
JOIN ancestors a ON e.child_digest = a.parent_digest
|
||||
WHERE e.tenant_id = p_tenant_id AND a.depth < p_max_depth
|
||||
)
|
||||
SELECT parent_digest, ancestors.depth, ancestors.relationship
|
||||
FROM ancestors
|
||||
ORDER BY depth, parent_digest;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Function to get all descendants of an artifact
|
||||
CREATE OR REPLACE FUNCTION get_descendants(
|
||||
p_artifact_digest TEXT,
|
||||
p_tenant_id UUID,
|
||||
p_max_depth INT DEFAULT 10
|
||||
) RETURNS TABLE (
|
||||
descendant_digest TEXT,
|
||||
depth INT,
|
||||
relationship TEXT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT child_digest, 1 as depth, e.relationship
|
||||
FROM sbom_lineage_edges e
|
||||
WHERE parent_digest = p_artifact_digest AND tenant_id = p_tenant_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT e.child_digest, d.depth + 1, e.relationship
|
||||
FROM sbom_lineage_edges e
|
||||
JOIN descendants d ON e.parent_digest = d.child_digest
|
||||
WHERE e.tenant_id = p_tenant_id AND d.depth < p_max_depth
|
||||
)
|
||||
SELECT child_digest, descendants.depth, descendants.relationship
|
||||
FROM descendants
|
||||
ORDER BY depth, child_digest;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION get_lineage_depth IS 'Returns the maximum depth of descendants from an artifact';
|
||||
COMMENT ON FUNCTION get_ancestors IS 'Returns all ancestor artifacts up to max_depth';
|
||||
COMMENT ON FUNCTION get_descendants IS 'Returns all descendant artifacts up to max_depth';
|
||||
307
docs/modules/sbom-service/lineage/ui-architecture.md
Normal file
307
docs/modules/sbom-service/lineage/ui-architecture.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# Lineage Smart-Diff UI Architecture
|
||||
|
||||
> Sprint: SPRINT_20251229_001_FE_lineage_smartdiff_overview
|
||||
> Last Updated: 2025-12-29
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The Lineage Smart-Diff feature provides a comprehensive UI for visualizing SBOM lineage graphs, comparing artifact versions, and explaining security state changes between builds.
|
||||
|
||||
### 1.1 Completion Status
|
||||
|
||||
| Area | Completion | Notes |
|
||||
|------|------------|-------|
|
||||
| **Lineage Graph SVG** | 95% | Full DAG visualization with lanes, pan/zoom, nodes |
|
||||
| **Hover Cards** | 85% | Basic info displayed; needs CGS integration |
|
||||
| **SBOM Diff View** | 90% | 3-column diff exists; needs row expanders |
|
||||
| **VEX Diff View** | 90% | Status change display; needs reachability gates |
|
||||
| **Compare Mode** | 85% | Three-pane layout exists; needs explainer timeline |
|
||||
| **Export Dialog** | 80% | Basic export; needs audit pack format |
|
||||
| **Proof Tree** | 75% | Merkle tree viz; needs confidence breakdown |
|
||||
| **Reachability Diff** | 60% | Basic view; needs gate visualization |
|
||||
|
||||
## 2. UI Data Contracts
|
||||
|
||||
### 2.1 CGS/Lineage API Integration
|
||||
|
||||
```typescript
|
||||
// Lineage Graph Response
|
||||
interface LineageGraph {
|
||||
artifact: string;
|
||||
nodes: LineageNode[];
|
||||
edges: LineageEdge[];
|
||||
metadata: {
|
||||
totalNodes: number;
|
||||
maxDepth: number;
|
||||
generatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LineageNode {
|
||||
id: string;
|
||||
artifactDigest: string;
|
||||
artifactRef: string;
|
||||
sequenceNumber: number;
|
||||
createdAt: string;
|
||||
source: string;
|
||||
parentDigests: string[];
|
||||
badges: {
|
||||
newVulns: number;
|
||||
resolvedVulns: number;
|
||||
signatureStatus: 'valid' | 'invalid' | 'unknown';
|
||||
reachabilityStatus: 'analyzed' | 'pending' | 'unavailable';
|
||||
};
|
||||
replayHash: string;
|
||||
cgsHash?: string;
|
||||
}
|
||||
|
||||
interface LineageEdge {
|
||||
fromDigest: string;
|
||||
toDigest: string;
|
||||
relationship: 'parent' | 'build' | 'base' | 'derived';
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Diff Response Schema
|
||||
|
||||
```typescript
|
||||
interface LineageDiffResponse {
|
||||
fromDigest: string;
|
||||
toDigest: string;
|
||||
sbomDiff: {
|
||||
added: ComponentDiff[];
|
||||
removed: ComponentDiff[];
|
||||
versionChanged: VersionChange[];
|
||||
licenseChanged: LicenseChange[];
|
||||
};
|
||||
vexDiff: VexDelta[];
|
||||
reachabilityDiff: ReachabilityDelta[];
|
||||
replayHash: string;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
interface ComponentDiff {
|
||||
purl: string;
|
||||
name: string;
|
||||
version: string;
|
||||
ecosystem: string;
|
||||
license?: string;
|
||||
scope: 'runtime' | 'development' | 'optional';
|
||||
}
|
||||
|
||||
interface VersionChange {
|
||||
purl: string;
|
||||
name: string;
|
||||
fromVersion: string;
|
||||
toVersion: string;
|
||||
changeType: 'upgrade' | 'downgrade' | 'patch';
|
||||
}
|
||||
|
||||
interface VexDelta {
|
||||
cveId: string;
|
||||
purl: string;
|
||||
fromStatus: VexStatus;
|
||||
toStatus: VexStatus;
|
||||
justification?: string;
|
||||
effectiveAt: string;
|
||||
}
|
||||
|
||||
interface ReachabilityDelta {
|
||||
cveId: string;
|
||||
purl: string;
|
||||
fromReachable: boolean;
|
||||
toReachable: boolean;
|
||||
paths?: string[][];
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Explainer Timeline Requirements
|
||||
|
||||
### 3.1 Engine Steps
|
||||
|
||||
The explainer timeline shows the sequence of analysis steps:
|
||||
|
||||
| Step | Description | Visual Cue |
|
||||
|------|-------------|------------|
|
||||
| SBOM Parse | Initial SBOM ingestion | Document icon |
|
||||
| Component Match | CVE-to-component matching | Link icon |
|
||||
| VEX Lookup | VEX document resolution | Shield icon |
|
||||
| Reachability | Call graph analysis | Graph icon |
|
||||
| Policy Gate | Policy rule evaluation | Gate icon |
|
||||
| Verdict | Final determination | Checkmark/X |
|
||||
|
||||
### 3.2 Data Binding
|
||||
|
||||
```typescript
|
||||
interface ExplainerStep {
|
||||
stepId: string;
|
||||
stepType: 'sbom_parse' | 'component_match' | 'vex_lookup' |
|
||||
'reachability' | 'policy_gate' | 'verdict';
|
||||
timestamp: string;
|
||||
duration: number;
|
||||
status: 'success' | 'warning' | 'error';
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, unknown>;
|
||||
evidence?: {
|
||||
type: string;
|
||||
hash: string;
|
||||
downloadUrl?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Node Diff Table UX
|
||||
|
||||
### 4.1 Expander Pattern
|
||||
|
||||
Each diff row can expand to show:
|
||||
- Full component details (license, scope, dependencies)
|
||||
- CVE associations and status
|
||||
- Reachability paths (if analyzed)
|
||||
- VEX statements affecting the component
|
||||
|
||||
### 4.2 Visual Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Component │ Before │ After │ Status│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ▶ lodash │ 4.17.20 │ 4.17.21 │ ↑ │
|
||||
│ └─ CVE-2021-23337 (fixed in 4.17.21) │
|
||||
│ └─ License: MIT │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ▶ axios │ 0.21.1 │ 0.21.4 │ ↑ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ▶ express │ - │ 4.18.2 │ + NEW │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 5. Reachability Gate Diff UI
|
||||
|
||||
### 5.1 Visual Cues
|
||||
|
||||
| Gate Status | Icon | Color |
|
||||
|-------------|------|-------|
|
||||
| Reachable | ● | Red |
|
||||
| Not Reachable | ○ | Green |
|
||||
| Unknown | ? | Gray |
|
||||
| Changed | ↔ | Orange |
|
||||
|
||||
### 5.2 Path Display
|
||||
|
||||
When reachability changes, show the call path:
|
||||
```
|
||||
entrypoint.ts → handler.ts → vulnerable_fn.ts → lodash.get()
|
||||
```
|
||||
|
||||
## 6. Audit Pack Export UI
|
||||
|
||||
### 6.1 Export Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| Include SBOMs | Both before/after SBOMs | ✓ |
|
||||
| Include Diff | Component/VEX/reachability diff | ✓ |
|
||||
| Include Attestations | DSSE envelopes | ✓ |
|
||||
| Include Evidence | Supporting evidence files | ✗ |
|
||||
| Sign Bundle | Sign with tenant key | ✓ |
|
||||
|
||||
### 6.2 Manifest Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"generatedAt": "2025-12-29T10:00:00Z",
|
||||
"artifactA": { "digest": "sha256:...", "name": "...", "createdAt": "..." },
|
||||
"artifactB": { "digest": "sha256:...", "name": "...", "createdAt": "..." },
|
||||
"contents": [
|
||||
{ "type": "sbom", "filename": "before.cdx.json", "sha256": "..." },
|
||||
{ "type": "sbom", "filename": "after.cdx.json", "sha256": "..." },
|
||||
{ "type": "diff", "filename": "diff.json", "sha256": "..." },
|
||||
{ "type": "attestation", "filename": "attestations.dsse.json", "sha256": "..." }
|
||||
],
|
||||
"merkleRoot": "sha256:...",
|
||||
"summary": {
|
||||
"componentsAdded": 5,
|
||||
"componentsRemoved": 2,
|
||||
"vexUpdates": 3,
|
||||
"attestationCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Copy-Safe Workflow
|
||||
|
||||
### 7.1 Pinned Explanation UX
|
||||
|
||||
Operators can "pin" explanations for ticket export:
|
||||
|
||||
1. Click pin icon on any verdict/finding
|
||||
2. Explanation captures current state with hash
|
||||
3. Export as Markdown/JSON for JIRA/ServiceNow
|
||||
|
||||
### 7.2 Ticket Export Format
|
||||
|
||||
```markdown
|
||||
## Security Finding Report
|
||||
|
||||
**Artifact**: myorg/myapp:v1.2.3 (sha256:abc123...)
|
||||
**Generated**: 2025-12-29T10:00:00Z
|
||||
**Replay Hash**: sha256:def456...
|
||||
|
||||
### Finding: CVE-2021-23337 in lodash
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Not Affected |
|
||||
| Reason | Not Reachable |
|
||||
| Evidence | Call graph analysis |
|
||||
| VEX ID | VEX-2025-001 |
|
||||
|
||||
### Verification
|
||||
|
||||
To reproduce this verdict:
|
||||
```
|
||||
stella verdict replay --hash sha256:def456...
|
||||
```
|
||||
```
|
||||
|
||||
## 8. Confidence Breakdown Charts
|
||||
|
||||
### 8.1 Metrics Sources
|
||||
|
||||
| Metric | Source | Weight |
|
||||
|--------|--------|--------|
|
||||
| SBOM Completeness | Scanner analysis | 30% |
|
||||
| VEX Coverage | VEX Hub aggregation | 25% |
|
||||
| Reachability Depth | Call graph analysis | 25% |
|
||||
| Attestation Count | Sigstore/local | 20% |
|
||||
|
||||
### 8.2 Visualization
|
||||
|
||||
- Donut chart showing confidence breakdown
|
||||
- Hover for detailed explanations
|
||||
- Color coding: Green (>80%), Yellow (50-80%), Red (<50%)
|
||||
|
||||
## 9. Component Inventory
|
||||
|
||||
### 9.1 Lineage Feature Components
|
||||
|
||||
| Component | Location | Status |
|
||||
|-----------|----------|--------|
|
||||
| `LineageGraphComponent` | `lineage-graph.component.ts` | Complete |
|
||||
| `LineageNodeComponent` | `lineage-node.component.ts` | Complete |
|
||||
| `LineageEdgeComponent` | `lineage-edge.component.ts` | Complete |
|
||||
| `LineageHoverCardComponent` | `lineage-hover-card.component.ts` | Needs CGS |
|
||||
| `LineageMiniMapComponent` | `lineage-minimap.component.ts` | Complete |
|
||||
| `LineageControlsComponent` | `lineage-controls.component.ts` | Complete |
|
||||
| `LineageSbomDiffComponent` | `lineage-sbom-diff.component.ts` | Needs expanders |
|
||||
| `LineageVexDiffComponent` | `lineage-vex-diff.component.ts` | Needs gates |
|
||||
| `LineageCompareComponent` | `lineage-compare.component.ts` | Needs timeline |
|
||||
| `LineageExportDialogComponent` | `lineage-export-dialog.component.ts` | Needs audit pack |
|
||||
|
||||
## 10. Related Documentation
|
||||
|
||||
- [SbomService Lineage API](../sbomservice/lineage/architecture.md)
|
||||
- [UI Architecture](../ui/architecture.md)
|
||||
- [Graph Module Architecture](../graph/architecture.md)
|
||||
Reference in New Issue
Block a user