save progress

This commit is contained in:
StellaOps Bot
2025-12-20 12:15:16 +02:00
parent 439f10966b
commit 0ada1b583f
95 changed files with 12400 additions and 65 deletions

View File

@@ -41,11 +41,13 @@ This document specifies the PostgreSQL database design for StellaOps control-pla
| `vex` | Excititor | VEX statements, graphs, observations, evidence |
| `scheduler` | Scheduler | Job definitions, triggers, execution history |
| `notify` | Notify | Channels, rules, deliveries, escalations |
| `policy` | Policy | Policy packs, rules, risk profiles, evaluations |
| `policy` | Policy | Policy packs, rules, risk profiles, evaluations, reachability verdicts, unknowns queue, score proofs |
| `packs` | PacksRegistry | Package attestations, mirrors, lifecycle |
| `issuer` | IssuerDirectory | Trust anchors, issuer keys, certificates |
| `proofchain` | Attestor | Content-addressed proof/evidence chain (entries, DSSE envelopes, spines, trust anchors, Rekor) |
| `unknowns` | Unknowns | Bitemporal ambiguity tracking for scan gaps |
| `scanner` | Scanner | Scan orchestration, manifests, call-graphs, proof bundles, entrypoints, runtime samples |
| `shared` | Scanner + Policy | SBOM component to symbol mapping |
| `audit` | Shared | Cross-cutting audit log (optional) |
**ProofChain references:**
@@ -1134,6 +1136,306 @@ See [schemas/notify.sql](./schemas/notify.sql) for the complete schema definitio
See [schemas/policy.sql](./schemas/policy.sql) for the complete schema definition.
Policy schema extensions for score proofs and reachability:
```sql
-- Score proof segments for deterministic replay
CREATE TABLE policy.proof_segments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
spine_id UUID NOT NULL, -- Reference to proofchain.proof_spines
idx INT NOT NULL, -- Segment index within spine
segment_type TEXT NOT NULL CHECK (segment_type IN ('score_delta', 'reachability', 'vex_claim', 'unknown_band')),
payload_hash TEXT NOT NULL, -- SHA-256 of canonical JSON payload
payload JSONB NOT NULL, -- Canonical JSON segment data
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (spine_id, idx)
);
-- Unknowns queue for ambiguity tracking
CREATE TABLE policy.unknowns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
pkg_id TEXT NOT NULL, -- PURL base (without version)
pkg_version TEXT NOT NULL, -- Specific version
band TEXT NOT NULL CHECK (band IN ('HOT', 'WARM', 'COLD', 'RESOLVED')),
score DECIMAL(5,2) NOT NULL, -- 2-factor ranking score (0.00-100.00)
uncertainty_factor DECIMAL(5,4) NOT NULL, -- Missing data signal (0.0000-1.0000)
exploit_pressure DECIMAL(5,4) NOT NULL, -- KEV/EPSS pressure (0.0000-1.0000)
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
resolution_reason TEXT, -- NULL until resolved
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Reachability verdicts per finding
CREATE TABLE policy.reachability_finding (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL, -- Reference to scanner.scan_manifest
finding_id UUID NOT NULL, -- Reference to finding in findings ledger
status TEXT NOT NULL CHECK (status IN ('reachable', 'unreachable', 'unknown', 'partial')),
path_count INT NOT NULL DEFAULT 0,
shortest_path_depth INT,
entrypoint_ids UUID[], -- References to scanner.entrypoint
evidence_hash TEXT, -- SHA-256 of path evidence
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Reachability component mapping
CREATE TABLE policy.reachability_component (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL,
component_purl TEXT NOT NULL,
symbol_count INT NOT NULL DEFAULT 0,
reachable_symbol_count INT NOT NULL DEFAULT 0,
unreachable_symbol_count INT NOT NULL DEFAULT 0,
unknown_symbol_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scan_id, component_purl)
);
-- Indexes for proof_segments
CREATE INDEX idx_proof_segments_spine ON policy.proof_segments(spine_id, idx);
CREATE INDEX idx_proof_segments_tenant ON policy.proof_segments(tenant_id);
-- Indexes for unknowns
CREATE INDEX idx_unknowns_score ON policy.unknowns(score DESC) WHERE band = 'HOT';
CREATE INDEX idx_unknowns_pkg ON policy.unknowns(pkg_id, pkg_version);
CREATE INDEX idx_unknowns_tenant_band ON policy.unknowns(tenant_id, band);
-- Indexes for reachability_finding
CREATE INDEX idx_reachability_finding_scan ON policy.reachability_finding(scan_id, status);
CREATE INDEX idx_reachability_finding_tenant ON policy.reachability_finding(tenant_id);
-- Indexes for reachability_component
CREATE INDEX idx_reachability_component_scan ON policy.reachability_component(scan_id);
CREATE INDEX idx_reachability_component_purl ON policy.reachability_component(component_purl);
```
### 5.7 Scanner Schema
The scanner schema owns scan orchestration, manifests, call-graphs, and proof bundles.
```sql
CREATE SCHEMA IF NOT EXISTS scanner;
CREATE SCHEMA IF NOT EXISTS scanner_app;
-- RLS helper function
CREATE OR REPLACE FUNCTION scanner_app.require_current_tenant()
RETURNS TEXT
LANGUAGE plpgsql STABLE SECURITY DEFINER
AS $$
DECLARE
v_tenant TEXT;
BEGIN
v_tenant := current_setting('app.tenant_id', true);
IF v_tenant IS NULL OR v_tenant = '' THEN
RAISE EXCEPTION 'app.tenant_id session variable not set';
END IF;
RETURN v_tenant;
END;
$$;
-- Scan manifest: captures frozen feed state for deterministic replay
CREATE TABLE scanner.scan_manifest (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
artifact_digest TEXT NOT NULL, -- OCI digest of scanned artifact
artifact_purl TEXT, -- PURL if resolvable
sbom_digest TEXT, -- SHA-256 of input SBOM
concelier_snapshot_hash TEXT NOT NULL, -- Frozen vuln feed hash
excititor_snapshot_hash TEXT NOT NULL, -- Frozen VEX feed hash
scanner_version TEXT NOT NULL, -- Scanner version for replay
scan_config JSONB NOT NULL DEFAULT '{}', -- Frozen scan configuration
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'replaying')),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Proof bundle: content-addressed proof ledger per scan
CREATE TABLE scanner.proof_bundle (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL REFERENCES scanner.scan_manifest(id) ON DELETE CASCADE,
proof_root_hash TEXT NOT NULL, -- Merkle root of proof ledger
proof_ledger BYTEA NOT NULL, -- CBOR-encoded canonical proof ledger
dsse_envelope JSONB, -- Optional DSSE signature envelope
rekor_log_index BIGINT, -- Optional Rekor transparency log index
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scan_id)
);
-- Call-graph nodes: symbols/methods in the analyzed artifact
CREATE TABLE scanner.cg_node (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL REFERENCES scanner.scan_manifest(id) ON DELETE CASCADE,
node_type TEXT NOT NULL CHECK (node_type IN ('method', 'function', 'class', 'module', 'entrypoint')),
qualified_name TEXT NOT NULL, -- Fully qualified symbol name
file_path TEXT, -- Source file path if available
line_start INT,
line_end INT,
component_purl TEXT, -- PURL of owning component
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scan_id, qualified_name)
);
-- Call-graph edges: call relationships between nodes
CREATE TABLE scanner.cg_edge (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL REFERENCES scanner.scan_manifest(id) ON DELETE CASCADE,
from_node_id UUID NOT NULL REFERENCES scanner.cg_node(id) ON DELETE CASCADE,
to_node_id UUID NOT NULL REFERENCES scanner.cg_node(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('static', 'virtual', 'interface', 'dynamic', 'reflection')),
call_site_file TEXT,
call_site_line INT,
confidence DECIMAL(3,2) DEFAULT 1.00, -- 0.00-1.00 for speculative edges
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Entrypoints: discovered entry points (controllers, handlers, main methods)
CREATE TABLE scanner.entrypoint (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL REFERENCES scanner.scan_manifest(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES scanner.cg_node(id) ON DELETE CASCADE,
entrypoint_type TEXT NOT NULL CHECK (entrypoint_type IN (
'aspnet_controller', 'aspnet_minimal_api', 'grpc_service',
'spring_controller', 'spring_handler', 'jaxrs_resource',
'main_method', 'cli_command', 'lambda_handler', 'azure_function',
'message_handler', 'scheduled_job', 'test_method'
)),
route_pattern TEXT, -- HTTP route if applicable
http_method TEXT, -- GET/POST/etc if applicable
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scan_id, node_id)
);
-- Runtime samples: optional runtime evidence for dynamic reachability
CREATE TABLE scanner.runtime_sample (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL REFERENCES scanner.scan_manifest(id) ON DELETE CASCADE,
sample_type TEXT NOT NULL CHECK (sample_type IN ('trace', 'coverage', 'profile')),
collected_at TIMESTAMPTZ NOT NULL,
duration_ms INT,
frames JSONB NOT NULL, -- Array of stack frames/coverage data
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (collected_at);
-- Create initial partitions for runtime_sample (monthly)
CREATE TABLE scanner.runtime_sample_2025_12 PARTITION OF scanner.runtime_sample
FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
CREATE TABLE scanner.runtime_sample_2026_01 PARTITION OF scanner.runtime_sample
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE scanner.runtime_sample_2026_02 PARTITION OF scanner.runtime_sample
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE scanner.runtime_sample_2026_03 PARTITION OF scanner.runtime_sample
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Indexes for scan_manifest
CREATE INDEX idx_scan_manifest_artifact ON scanner.scan_manifest(artifact_digest);
CREATE INDEX idx_scan_manifest_snapshots ON scanner.scan_manifest(concelier_snapshot_hash, excititor_snapshot_hash);
CREATE INDEX idx_scan_manifest_tenant ON scanner.scan_manifest(tenant_id);
CREATE INDEX idx_scan_manifest_status ON scanner.scan_manifest(tenant_id, status) WHERE status IN ('pending', 'running');
-- Indexes for proof_bundle
CREATE INDEX idx_proof_bundle_scan ON scanner.proof_bundle(scan_id);
CREATE INDEX idx_proof_bundle_root ON scanner.proof_bundle(proof_root_hash);
-- Indexes for cg_node
CREATE INDEX idx_cg_node_scan ON scanner.cg_node(scan_id);
CREATE INDEX idx_cg_node_purl ON scanner.cg_node(component_purl);
CREATE INDEX idx_cg_node_type ON scanner.cg_node(scan_id, node_type);
-- Indexes for cg_edge
CREATE INDEX idx_cg_edge_from ON scanner.cg_edge(scan_id, from_node_id);
CREATE INDEX idx_cg_edge_to ON scanner.cg_edge(scan_id, to_node_id);
CREATE INDEX idx_cg_edge_kind ON scanner.cg_edge(scan_id, kind) WHERE kind = 'static';
-- Indexes for entrypoint
CREATE INDEX idx_entrypoint_scan ON scanner.entrypoint(scan_id);
CREATE INDEX idx_entrypoint_type ON scanner.entrypoint(scan_id, entrypoint_type);
-- Indexes for runtime_sample (BRIN for time-ordered data)
CREATE INDEX idx_runtime_sample_scan ON scanner.runtime_sample(scan_id, collected_at DESC);
CREATE INDEX idx_runtime_sample_frames ON scanner.runtime_sample USING GIN(frames);
-- RLS policies
ALTER TABLE scanner.scan_manifest ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.scan_manifest FORCE ROW LEVEL SECURITY;
CREATE POLICY scan_manifest_tenant_isolation ON scanner.scan_manifest
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
ALTER TABLE scanner.proof_bundle ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.proof_bundle FORCE ROW LEVEL SECURITY;
CREATE POLICY proof_bundle_tenant_isolation ON scanner.proof_bundle
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
ALTER TABLE scanner.cg_node ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.cg_node FORCE ROW LEVEL SECURITY;
CREATE POLICY cg_node_tenant_isolation ON scanner.cg_node
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
ALTER TABLE scanner.cg_edge ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.cg_edge FORCE ROW LEVEL SECURITY;
CREATE POLICY cg_edge_tenant_isolation ON scanner.cg_edge
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
ALTER TABLE scanner.entrypoint ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.entrypoint FORCE ROW LEVEL SECURITY;
CREATE POLICY entrypoint_tenant_isolation ON scanner.entrypoint
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
ALTER TABLE scanner.runtime_sample ENABLE ROW LEVEL SECURITY;
ALTER TABLE scanner.runtime_sample FORCE ROW LEVEL SECURITY;
CREATE POLICY runtime_sample_tenant_isolation ON scanner.runtime_sample
FOR ALL USING (tenant_id::text = scanner_app.require_current_tenant())
WITH CHECK (tenant_id::text = scanner_app.require_current_tenant());
```
### 5.8 Shared Schema
The shared schema contains cross-module lookup tables used by both Scanner and Policy.
```sql
CREATE SCHEMA IF NOT EXISTS shared;
-- SBOM component to symbol mapping
CREATE TABLE shared.symbol_component_map (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
scan_id UUID NOT NULL,
node_id UUID NOT NULL, -- Reference to scanner.cg_node
purl TEXT NOT NULL, -- PURL of the component
component_name TEXT NOT NULL,
component_version TEXT,
confidence DECIMAL(3,2) DEFAULT 1.00, -- Mapping confidence
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scan_id, node_id)
);
-- Indexes
CREATE INDEX idx_symbol_component_scan ON shared.symbol_component_map(scan_id, node_id);
CREATE INDEX idx_symbol_component_purl ON shared.symbol_component_map(purl);
CREATE INDEX idx_symbol_component_tenant ON shared.symbol_component_map(tenant_id);
```
---
## 6. Indexing Strategy