Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View 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 |

View 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';