-- ============================================================================= -- SCANNER SCHEMA - ProofSpine Audit Trail Tables -- Version: V3100_001 -- Sprint: SPRINT_3100_0001_0001 -- ============================================================================= CREATE SCHEMA IF NOT EXISTS scanner; -- ============================================================================= -- PROOF SPINES -- ============================================================================= -- Main proof spine table - represents a complete verifiable decision chain -- from SBOM through vulnerability matching to final VEX verdict CREATE TABLE scanner.proof_spines ( spine_id TEXT PRIMARY KEY, artifact_id TEXT NOT NULL, vuln_id TEXT NOT NULL, policy_profile_id TEXT NOT NULL, verdict TEXT NOT NULL CHECK (verdict IN ( 'not_affected', 'affected', 'fixed', 'under_investigation' )), verdict_reason TEXT, root_hash TEXT NOT NULL, scan_run_id TEXT NOT NULL, segment_count INT NOT NULL DEFAULT 0, created_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW(), superseded_by_spine_id TEXT REFERENCES scanner.proof_spines(spine_id), -- Deterministic spine ID = hash(artifact_id + vuln_id + policy_profile_id + root_hash) CONSTRAINT proof_spines_unique_decision UNIQUE (artifact_id, vuln_id, policy_profile_id, root_hash) ); -- Composite index for common lookups CREATE INDEX idx_proof_spines_lookup ON scanner.proof_spines(artifact_id, vuln_id, policy_profile_id); CREATE INDEX idx_proof_spines_scan_run ON scanner.proof_spines(scan_run_id); CREATE INDEX idx_proof_spines_created ON scanner.proof_spines(created_at_utc DESC); CREATE INDEX idx_proof_spines_verdict ON scanner.proof_spines(verdict); -- ============================================================================= -- PROOF SEGMENTS -- ============================================================================= -- Individual segments within a spine - each segment is DSSE-signed CREATE TABLE scanner.proof_segments ( segment_id TEXT PRIMARY KEY, spine_id TEXT NOT NULL REFERENCES scanner.proof_spines(spine_id) ON DELETE CASCADE, idx INT NOT NULL, segment_type TEXT NOT NULL CHECK (segment_type IN ( 'SbomSlice', 'Match', 'Reachability', 'GuardAnalysis', 'RuntimeObservation', 'PolicyEval' )), input_hash TEXT NOT NULL, result_hash TEXT NOT NULL, prev_segment_hash TEXT, envelope_json TEXT NOT NULL, -- DSSE envelope as JSON tool_id TEXT NOT NULL, tool_version TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'Pending' CHECK (status IN ( 'Pending', 'Verified', 'Partial', 'Invalid', 'Untrusted' )), created_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT proof_segments_unique_idx UNIQUE (spine_id, idx) ); CREATE INDEX idx_proof_segments_spine ON scanner.proof_segments(spine_id); CREATE INDEX idx_proof_segments_type ON scanner.proof_segments(segment_type); CREATE INDEX idx_proof_segments_status ON scanner.proof_segments(status); -- ============================================================================= -- PROOF SPINE HISTORY -- ============================================================================= -- Audit trail for spine lifecycle events (creation, supersession, verification) CREATE TABLE scanner.proof_spine_history ( history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), spine_id TEXT NOT NULL REFERENCES scanner.proof_spines(spine_id), action TEXT NOT NULL CHECK (action IN ( 'created', 'superseded', 'verified', 'invalidated' )), actor TEXT, reason TEXT, occurred_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_proof_spine_history_spine ON scanner.proof_spine_history(spine_id); CREATE INDEX idx_proof_spine_history_action ON scanner.proof_spine_history(action); CREATE INDEX idx_proof_spine_history_occurred ON scanner.proof_spine_history(occurred_at_utc DESC); -- ============================================================================= -- VERIFICATION CACHE -- ============================================================================= -- Caches verification results to avoid re-verifying unchanged spines CREATE TABLE scanner.proof_spine_verification_cache ( spine_id TEXT PRIMARY KEY REFERENCES scanner.proof_spines(spine_id) ON DELETE CASCADE, verified_at_utc TIMESTAMPTZ NOT NULL DEFAULT NOW(), verifier_version TEXT NOT NULL, all_segments_valid BOOLEAN NOT NULL, invalid_segment_ids TEXT[], signature_algorithm TEXT NOT NULL, key_fingerprint TEXT NOT NULL ); CREATE INDEX idx_verification_cache_verified ON scanner.proof_spine_verification_cache(verified_at_utc DESC); -- ============================================================================= -- FUNCTIONS -- ============================================================================= -- Function to update segment count after segment insert CREATE OR REPLACE FUNCTION scanner.update_spine_segment_count() RETURNS TRIGGER AS $$ BEGIN UPDATE scanner.proof_spines SET segment_count = ( SELECT COUNT(*) FROM scanner.proof_segments WHERE spine_id = NEW.spine_id ) WHERE spine_id = NEW.spine_id; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to maintain segment count CREATE TRIGGER trg_update_segment_count AFTER INSERT OR DELETE ON scanner.proof_segments FOR EACH ROW EXECUTE FUNCTION scanner.update_spine_segment_count(); -- Function to record history on spine events CREATE OR REPLACE FUNCTION scanner.record_spine_history() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO scanner.proof_spine_history (spine_id, action, reason) VALUES (NEW.spine_id, 'created', 'Spine created'); ELSIF TG_OP = 'UPDATE' AND NEW.superseded_by_spine_id IS NOT NULL AND OLD.superseded_by_spine_id IS NULL THEN INSERT INTO scanner.proof_spine_history (spine_id, action, reason) VALUES (OLD.spine_id, 'superseded', 'Superseded by ' || NEW.superseded_by_spine_id); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to record spine history CREATE TRIGGER trg_record_spine_history AFTER INSERT OR UPDATE ON scanner.proof_spines FOR EACH ROW EXECUTE FUNCTION scanner.record_spine_history(); -- ============================================================================= -- COMMENTS -- ============================================================================= COMMENT ON TABLE scanner.proof_spines IS 'Verifiable decision chains from SBOM to VEX verdict with cryptographic integrity'; COMMENT ON TABLE scanner.proof_segments IS 'Individual DSSE-signed evidence segments within a proof spine'; COMMENT ON TABLE scanner.proof_spine_history IS 'Audit trail for spine lifecycle events'; COMMENT ON COLUMN scanner.proof_spines.root_hash IS 'SHA256 hash of concatenated segment result hashes for tamper detection'; COMMENT ON COLUMN scanner.proof_segments.prev_segment_hash IS 'Hash chain linking - NULL for first segment, result_hash of previous segment otherwise'; COMMENT ON COLUMN scanner.proof_segments.envelope_json IS 'DSSE envelope containing signed segment payload';