save progress
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user