566 lines
22 KiB
PL/PgSQL
566 lines
22 KiB
PL/PgSQL
-- Findings Ledger: Consolidated init from migrations 001-009
|
|
-- Auto-generated for docker-compose postgres-init
|
|
-- Creates all tables required by stellaops-findings-ledger-web
|
|
|
|
-- ============================================================================
|
|
-- 001_initial.sql - Bootstrap schema (LEDGER-29-001)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE TYPE ledger_event_type AS ENUM (
|
|
'finding.created',
|
|
'finding.status_changed',
|
|
'finding.severity_changed',
|
|
'finding.tag_updated',
|
|
'finding.comment_added',
|
|
'finding.assignment_changed',
|
|
'finding.accepted_risk',
|
|
'finding.remediation_plan_added',
|
|
'finding.attachment_added',
|
|
'finding.closed'
|
|
);
|
|
|
|
CREATE TYPE ledger_action_type AS ENUM (
|
|
'assign',
|
|
'comment',
|
|
'attach_evidence',
|
|
'link_ticket',
|
|
'remediation_plan',
|
|
'status_change',
|
|
'accept_risk',
|
|
'reopen',
|
|
'close'
|
|
);
|
|
|
|
CREATE TABLE ledger_events (
|
|
tenant_id TEXT NOT NULL,
|
|
chain_id UUID NOT NULL,
|
|
sequence_no BIGINT NOT NULL,
|
|
event_id UUID NOT NULL,
|
|
event_type ledger_event_type NOT NULL,
|
|
policy_version TEXT NOT NULL,
|
|
finding_id TEXT NOT NULL,
|
|
artifact_id TEXT NOT NULL,
|
|
source_run_id UUID,
|
|
actor_id TEXT NOT NULL,
|
|
actor_type TEXT NOT NULL,
|
|
occurred_at TIMESTAMPTZ NOT NULL,
|
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
event_body JSONB NOT NULL,
|
|
event_hash CHAR(64) NOT NULL,
|
|
previous_hash CHAR(64) NOT NULL,
|
|
merkle_leaf_hash CHAR(64) NOT NULL,
|
|
CONSTRAINT pk_ledger_events PRIMARY KEY (tenant_id, chain_id, sequence_no),
|
|
CONSTRAINT uq_ledger_events_event_id UNIQUE (tenant_id, event_id),
|
|
CONSTRAINT uq_ledger_events_chain_hash UNIQUE (tenant_id, chain_id, event_hash),
|
|
CONSTRAINT ck_ledger_events_event_hash_hex CHECK (event_hash ~ '^[0-9a-f]{64}$'),
|
|
CONSTRAINT ck_ledger_events_previous_hash_hex CHECK (previous_hash ~ '^[0-9a-f]{64}$'),
|
|
CONSTRAINT ck_ledger_events_leaf_hash_hex CHECK (merkle_leaf_hash ~ '^[0-9a-f]{64}$'),
|
|
CONSTRAINT ck_ledger_events_actor_type CHECK (actor_type IN ('system', 'operator', 'integration'))
|
|
) PARTITION BY LIST (tenant_id);
|
|
|
|
CREATE TABLE ledger_events_default PARTITION OF ledger_events DEFAULT;
|
|
|
|
CREATE INDEX ix_ledger_events_finding ON ledger_events (tenant_id, finding_id, policy_version);
|
|
CREATE INDEX ix_ledger_events_type ON ledger_events (tenant_id, event_type, recorded_at DESC);
|
|
CREATE INDEX ix_ledger_events_recorded_at ON ledger_events (tenant_id, recorded_at DESC);
|
|
|
|
CREATE TABLE ledger_merkle_roots (
|
|
tenant_id TEXT NOT NULL,
|
|
anchor_id UUID NOT NULL,
|
|
window_start TIMESTAMPTZ NOT NULL,
|
|
window_end TIMESTAMPTZ NOT NULL,
|
|
sequence_start BIGINT NOT NULL,
|
|
sequence_end BIGINT NOT NULL,
|
|
root_hash CHAR(64) NOT NULL,
|
|
leaf_count INTEGER NOT NULL,
|
|
anchored_at TIMESTAMPTZ NOT NULL,
|
|
anchor_reference TEXT,
|
|
CONSTRAINT pk_ledger_merkle_roots PRIMARY KEY (tenant_id, anchor_id),
|
|
CONSTRAINT uq_ledger_merkle_root_hash UNIQUE (tenant_id, root_hash),
|
|
CONSTRAINT ck_ledger_merkle_root_hash_hex CHECK (root_hash ~ '^[0-9a-f]{64}$')
|
|
) PARTITION BY LIST (tenant_id);
|
|
|
|
CREATE TABLE ledger_merkle_roots_default PARTITION OF ledger_merkle_roots DEFAULT;
|
|
|
|
CREATE INDEX ix_merkle_sequences ON ledger_merkle_roots (tenant_id, sequence_end DESC);
|
|
|
|
CREATE TABLE findings_projection (
|
|
tenant_id TEXT NOT NULL,
|
|
finding_id TEXT NOT NULL,
|
|
policy_version TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
severity NUMERIC(6,3),
|
|
labels JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
current_event_id UUID NOT NULL,
|
|
explain_ref TEXT,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
cycle_hash CHAR(64) NOT NULL,
|
|
CONSTRAINT pk_findings_projection PRIMARY KEY (tenant_id, finding_id, policy_version),
|
|
CONSTRAINT ck_findings_projection_cycle_hash_hex CHECK (cycle_hash ~ '^[0-9a-f]{64}$')
|
|
) PARTITION BY LIST (tenant_id);
|
|
|
|
CREATE TABLE findings_projection_default PARTITION OF findings_projection DEFAULT;
|
|
|
|
CREATE INDEX ix_projection_status ON findings_projection (tenant_id, status, severity DESC);
|
|
CREATE INDEX ix_projection_labels_gin ON findings_projection USING GIN (labels JSONB_PATH_OPS);
|
|
|
|
CREATE TABLE finding_history (
|
|
tenant_id TEXT NOT NULL,
|
|
finding_id TEXT NOT NULL,
|
|
policy_version TEXT NOT NULL,
|
|
event_id UUID NOT NULL,
|
|
status TEXT NOT NULL,
|
|
severity NUMERIC(6,3),
|
|
actor_id TEXT NOT NULL,
|
|
comment TEXT,
|
|
occurred_at TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT pk_finding_history PRIMARY KEY (tenant_id, finding_id, event_id)
|
|
) PARTITION BY LIST (tenant_id);
|
|
|
|
CREATE TABLE finding_history_default PARTITION OF finding_history DEFAULT;
|
|
|
|
CREATE INDEX ix_finding_history_timeline ON finding_history (tenant_id, finding_id, occurred_at DESC);
|
|
|
|
CREATE TABLE triage_actions (
|
|
tenant_id TEXT NOT NULL,
|
|
action_id UUID NOT NULL,
|
|
event_id UUID NOT NULL,
|
|
finding_id TEXT NOT NULL,
|
|
action_type ledger_action_type NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_by TEXT NOT NULL,
|
|
CONSTRAINT pk_triage_actions PRIMARY KEY (tenant_id, action_id)
|
|
) PARTITION BY LIST (tenant_id);
|
|
|
|
CREATE TABLE triage_actions_default PARTITION OF triage_actions DEFAULT;
|
|
|
|
CREATE INDEX ix_triage_actions_event ON triage_actions (tenant_id, event_id);
|
|
CREATE INDEX ix_triage_actions_created_at ON triage_actions (tenant_id, created_at DESC);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 002_projection_offsets.sql - Projection worker checkpoints (LEDGER-29-003)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_projection_offsets (
|
|
worker_id TEXT NOT NULL PRIMARY KEY,
|
|
last_recorded_at TIMESTAMPTZ NOT NULL,
|
|
last_event_id UUID NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
INSERT INTO ledger_projection_offsets (worker_id, last_recorded_at, last_event_id, updated_at)
|
|
VALUES (
|
|
'default',
|
|
'1970-01-01T00:00:00Z',
|
|
'00000000-0000-0000-0000-000000000000',
|
|
NOW())
|
|
ON CONFLICT (worker_id) DO NOTHING;
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 002_add_evidence_bundle_ref.sql - Evidence bundle references (LEDGER-OBS-53-001)
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE ledger_events
|
|
ADD COLUMN evidence_bundle_ref text NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_events_finding_evidence_ref
|
|
ON ledger_events (tenant_id, finding_id, recorded_at DESC)
|
|
WHERE evidence_bundle_ref IS NOT NULL;
|
|
|
|
-- ============================================================================
|
|
-- 003_policy_rationale.sql - Policy rationale column (LEDGER-29-004)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
ALTER TABLE findings_projection
|
|
ADD COLUMN IF NOT EXISTS policy_rationale JSONB NOT NULL DEFAULT '[]'::JSONB;
|
|
|
|
ALTER TABLE findings_projection
|
|
ALTER COLUMN policy_rationale SET DEFAULT '[]'::JSONB;
|
|
|
|
UPDATE findings_projection
|
|
SET policy_rationale = '[]'::JSONB
|
|
WHERE policy_rationale IS NULL;
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 004_ledger_attestations.sql - Attestation verification exports (LEDGER-OBS-54-001)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_attestations (
|
|
tenant_id text NOT NULL,
|
|
attestation_id uuid NOT NULL,
|
|
artifact_id text NOT NULL,
|
|
finding_id text NULL,
|
|
verification_status text NOT NULL,
|
|
verification_time timestamptz NOT NULL,
|
|
dsse_digest text NOT NULL,
|
|
rekor_entry_id text NULL,
|
|
evidence_bundle_ref text NULL,
|
|
ledger_event_id uuid NOT NULL,
|
|
recorded_at timestamptz NOT NULL,
|
|
merkle_leaf_hash text NOT NULL,
|
|
root_hash text NOT NULL,
|
|
cycle_hash text NOT NULL,
|
|
projection_version text NOT NULL
|
|
);
|
|
|
|
ALTER TABLE ledger_attestations
|
|
ADD CONSTRAINT pk_ledger_attestations PRIMARY KEY (tenant_id, attestation_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestations_recorded
|
|
ON ledger_attestations (tenant_id, recorded_at, attestation_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestations_artifact
|
|
ON ledger_attestations (tenant_id, artifact_id, recorded_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestations_finding
|
|
ON ledger_attestations (tenant_id, finding_id, recorded_at DESC)
|
|
WHERE finding_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestations_status
|
|
ON ledger_attestations (tenant_id, verification_status, recorded_at DESC);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 004_risk_fields.sql - Risk scoring fields (LEDGER-RISK-66-001/002)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
ALTER TABLE findings_projection
|
|
ADD COLUMN IF NOT EXISTS risk_score NUMERIC(6,3),
|
|
ADD COLUMN IF NOT EXISTS risk_severity TEXT,
|
|
ADD COLUMN IF NOT EXISTS risk_profile_version TEXT,
|
|
ADD COLUMN IF NOT EXISTS risk_explanation_id UUID,
|
|
ADD COLUMN IF NOT EXISTS risk_event_sequence BIGINT;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_projection_risk ON findings_projection (tenant_id, risk_severity, risk_score DESC);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 005_risk_fields.sql - Risk scoring fields idempotent re-add (LEDGER-RISK-66-001)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
ALTER TABLE findings_projection
|
|
ADD COLUMN IF NOT EXISTS risk_score numeric(6,2) NULL,
|
|
ADD COLUMN IF NOT EXISTS risk_severity text NULL,
|
|
ADD COLUMN IF NOT EXISTS risk_profile_version text NULL,
|
|
ADD COLUMN IF NOT EXISTS risk_explanation_id text NULL,
|
|
ADD COLUMN IF NOT EXISTS risk_event_sequence bigint NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_findings_projection_risk
|
|
ON findings_projection (tenant_id, risk_severity, risk_score DESC);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 006_orchestrator_airgap.sql - Export and import provenance (LEDGER-34-101, LEDGER-AIRGAP-56-001)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS orchestrator_exports
|
|
(
|
|
tenant_id TEXT NOT NULL,
|
|
run_id UUID NOT NULL,
|
|
job_type TEXT NOT NULL,
|
|
artifact_hash TEXT NOT NULL,
|
|
policy_hash TEXT NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
completed_at TIMESTAMPTZ,
|
|
status TEXT NOT NULL,
|
|
manifest_path TEXT,
|
|
logs_path TEXT,
|
|
merkle_root CHAR(64) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
PRIMARY KEY (tenant_id, run_id)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_orchestrator_exports_artifact_run
|
|
ON orchestrator_exports (tenant_id, artifact_hash, run_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_orchestrator_exports_artifact
|
|
ON orchestrator_exports (tenant_id, artifact_hash);
|
|
|
|
CREATE TABLE IF NOT EXISTS airgap_imports
|
|
(
|
|
tenant_id TEXT NOT NULL,
|
|
bundle_id TEXT NOT NULL,
|
|
mirror_generation TEXT,
|
|
merkle_root TEXT NOT NULL,
|
|
time_anchor TIMESTAMPTZ NOT NULL,
|
|
publisher TEXT,
|
|
hash_algorithm TEXT,
|
|
contents JSONB,
|
|
imported_at TIMESTAMPTZ NOT NULL,
|
|
import_operator TEXT,
|
|
ledger_event_id UUID,
|
|
PRIMARY KEY (tenant_id, bundle_id, time_anchor)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_airgap_imports_bundle
|
|
ON airgap_imports (tenant_id, bundle_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_airgap_imports_event
|
|
ON airgap_imports (tenant_id, ledger_event_id);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 007_enable_rls.sql - Row-Level Security (LEDGER-TEN-48-001-DEV)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE SCHEMA IF NOT EXISTS findings_ledger_app;
|
|
|
|
CREATE OR REPLACE FUNCTION findings_ledger_app.require_current_tenant()
|
|
RETURNS TEXT
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
AS $$
|
|
DECLARE
|
|
tenant_text TEXT;
|
|
BEGIN
|
|
tenant_text := current_setting('app.current_tenant', true);
|
|
IF tenant_text IS NULL OR length(trim(tenant_text)) = 0 THEN
|
|
RAISE EXCEPTION 'app.current_tenant is not set for the current session'
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
RETURN tenant_text;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION findings_ledger_app.require_current_tenant() IS
|
|
'Returns the current tenant ID from session variable, raises exception if not set';
|
|
|
|
ALTER TABLE ledger_events ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE ledger_events FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS ledger_events_tenant_isolation ON ledger_events;
|
|
CREATE POLICY ledger_events_tenant_isolation
|
|
ON ledger_events
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE ledger_merkle_roots ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE ledger_merkle_roots FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS ledger_merkle_roots_tenant_isolation ON ledger_merkle_roots;
|
|
CREATE POLICY ledger_merkle_roots_tenant_isolation
|
|
ON ledger_merkle_roots
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE findings_projection ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE findings_projection FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS findings_projection_tenant_isolation ON findings_projection;
|
|
CREATE POLICY findings_projection_tenant_isolation
|
|
ON findings_projection
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE finding_history ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE finding_history FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS finding_history_tenant_isolation ON finding_history;
|
|
CREATE POLICY finding_history_tenant_isolation
|
|
ON finding_history
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE triage_actions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE triage_actions FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS triage_actions_tenant_isolation ON triage_actions;
|
|
CREATE POLICY triage_actions_tenant_isolation
|
|
ON triage_actions
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE ledger_attestations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE ledger_attestations FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS ledger_attestations_tenant_isolation ON ledger_attestations;
|
|
CREATE POLICY ledger_attestations_tenant_isolation
|
|
ON ledger_attestations
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE orchestrator_exports ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE orchestrator_exports FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS orchestrator_exports_tenant_isolation ON orchestrator_exports;
|
|
CREATE POLICY orchestrator_exports_tenant_isolation
|
|
ON orchestrator_exports
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
ALTER TABLE airgap_imports ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE airgap_imports FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS airgap_imports_tenant_isolation ON airgap_imports;
|
|
CREATE POLICY airgap_imports_tenant_isolation
|
|
ON airgap_imports
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'findings_ledger_admin') THEN
|
|
CREATE ROLE findings_ledger_admin NOLOGIN BYPASSRLS;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON ROLE findings_ledger_admin IS
|
|
'Admin role that bypasses RLS for migrations and cross-tenant operations';
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 008_attestation_pointers.sql - Finding-to-attestation pointers (LEDGER-ATTEST-73-001)
|
|
-- ============================================================================
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_attestation_pointers (
|
|
tenant_id text NOT NULL,
|
|
pointer_id uuid NOT NULL,
|
|
finding_id text NOT NULL,
|
|
attestation_type text NOT NULL,
|
|
relationship text NOT NULL,
|
|
attestation_ref jsonb NOT NULL,
|
|
verification_result jsonb NULL,
|
|
created_at timestamptz NOT NULL,
|
|
created_by text NOT NULL,
|
|
metadata jsonb NULL,
|
|
ledger_event_id uuid NULL
|
|
);
|
|
|
|
ALTER TABLE ledger_attestation_pointers
|
|
ADD CONSTRAINT pk_ledger_attestation_pointers PRIMARY KEY (tenant_id, pointer_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_finding
|
|
ON ledger_attestation_pointers (tenant_id, finding_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_digest
|
|
ON ledger_attestation_pointers (tenant_id, (attestation_ref->>'digest'));
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_type
|
|
ON ledger_attestation_pointers (tenant_id, attestation_type, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_verified
|
|
ON ledger_attestation_pointers (tenant_id, ((verification_result->>'verified')::boolean))
|
|
WHERE verification_result IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_signer
|
|
ON ledger_attestation_pointers (tenant_id, (attestation_ref->'signer_info'->>'subject'))
|
|
WHERE attestation_ref->'signer_info' IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_ledger_attestation_pointers_predicate
|
|
ON ledger_attestation_pointers (tenant_id, (attestation_ref->>'predicate_type'))
|
|
WHERE attestation_ref->>'predicate_type' IS NOT NULL;
|
|
|
|
ALTER TABLE ledger_attestation_pointers ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE ledger_attestation_pointers FORCE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS ledger_attestation_pointers_tenant_isolation ON ledger_attestation_pointers;
|
|
CREATE POLICY ledger_attestation_pointers_tenant_isolation
|
|
ON ledger_attestation_pointers
|
|
FOR ALL
|
|
USING (tenant_id = findings_ledger_app.require_current_tenant())
|
|
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
|
|
|
|
COMMENT ON TABLE ledger_attestation_pointers IS
|
|
'Links findings to verification reports and attestation envelopes for explainability (LEDGER-ATTEST-73-001)';
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================================================
|
|
-- 009_snapshots.sql - Ledger snapshots for time-travel
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_snapshots (
|
|
tenant_id TEXT NOT NULL,
|
|
snapshot_id UUID NOT NULL,
|
|
label TEXT,
|
|
description TEXT,
|
|
status TEXT NOT NULL DEFAULT 'Creating',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
sequence_number BIGINT NOT NULL,
|
|
snapshot_timestamp TIMESTAMPTZ NOT NULL,
|
|
findings_count BIGINT NOT NULL DEFAULT 0,
|
|
vex_statements_count BIGINT NOT NULL DEFAULT 0,
|
|
advisories_count BIGINT NOT NULL DEFAULT 0,
|
|
sboms_count BIGINT NOT NULL DEFAULT 0,
|
|
events_count BIGINT NOT NULL DEFAULT 0,
|
|
size_bytes BIGINT NOT NULL DEFAULT 0,
|
|
merkle_root TEXT,
|
|
dsse_digest TEXT,
|
|
metadata JSONB,
|
|
include_entity_types JSONB,
|
|
sign_requested BOOLEAN NOT NULL DEFAULT FALSE,
|
|
PRIMARY KEY (tenant_id, snapshot_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_snapshots_status
|
|
ON ledger_snapshots (tenant_id, status, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_snapshots_expires
|
|
ON ledger_snapshots (expires_at)
|
|
WHERE expires_at IS NOT NULL AND status = 'Available';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_snapshots_sequence
|
|
ON ledger_snapshots (tenant_id, sequence_number);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_snapshots_label
|
|
ON ledger_snapshots (tenant_id, label)
|
|
WHERE label IS NOT NULL;
|
|
|
|
ALTER TABLE ledger_snapshots ENABLE ROW LEVEL SECURITY;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE tablename = 'ledger_snapshots'
|
|
AND policyname = 'ledger_snapshots_tenant_isolation'
|
|
) THEN
|
|
CREATE POLICY ledger_snapshots_tenant_isolation ON ledger_snapshots
|
|
USING (tenant_id = current_setting('app.tenant_id', true))
|
|
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));
|
|
END IF;
|
|
END $$;
|
|
|
|
COMMENT ON TABLE ledger_snapshots IS 'Point-in-time snapshots of ledger state for time-travel queries';
|