-- Policy exceptions schema bootstrap for compose environments. -- Ensures exception endpoints can start against a clean database. CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE SCHEMA IF NOT EXISTS policy; CREATE TABLE IF NOT EXISTS policy.recheck_policies ( policy_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, name TEXT NOT NULL, conditions JSONB NOT NULL, default_action TEXT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_recheck_policies_tenant ON policy.recheck_policies (tenant_id, is_active); CREATE TABLE IF NOT EXISTS policy.exceptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, rule_pattern TEXT, resource_pattern TEXT, artifact_pattern TEXT, project_id TEXT, reason TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('proposed', 'approved', 'active', 'expired', 'revoked')), expires_at TIMESTAMPTZ, approved_by TEXT, approved_at TIMESTAMPTZ, revoked_by TEXT, revoked_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by TEXT, exception_id TEXT NOT NULL UNIQUE, version INTEGER NOT NULL DEFAULT 1, type TEXT NOT NULL DEFAULT 'policy' CHECK (type IN ('vulnerability', 'policy', 'unknown', 'component')), artifact_digest TEXT, purl_pattern TEXT, vulnerability_id TEXT, policy_rule_id TEXT, environments TEXT[] NOT NULL DEFAULT '{}', owner_id TEXT, requester_id TEXT, approver_ids TEXT[] NOT NULL DEFAULT '{}', reason_code TEXT DEFAULT 'other' CHECK (reason_code IN ( 'false_positive', 'accepted_risk', 'compensating_control', 'test_only', 'vendor_not_affected', 'scheduled_fix', 'deprecation_in_progress', 'runtime_mitigation', 'network_isolation', 'other' )), rationale TEXT, evidence_refs JSONB NOT NULL DEFAULT '[]', compensating_controls JSONB NOT NULL DEFAULT '[]', ticket_ref TEXT, recheck_policy_id TEXT REFERENCES policy.recheck_policies(policy_id), last_recheck_result JSONB, last_recheck_at TIMESTAMPTZ, UNIQUE (tenant_id, name) ); CREATE INDEX IF NOT EXISTS idx_exceptions_tenant ON policy.exceptions(tenant_id); CREATE INDEX IF NOT EXISTS idx_exceptions_status ON policy.exceptions(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_exceptions_expires ON policy.exceptions(expires_at) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_exceptions_project ON policy.exceptions(tenant_id, project_id); CREATE INDEX IF NOT EXISTS idx_exceptions_vuln_id ON policy.exceptions(vulnerability_id) WHERE vulnerability_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_exceptions_purl ON policy.exceptions(purl_pattern) WHERE purl_pattern IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_exceptions_artifact ON policy.exceptions(artifact_digest) WHERE artifact_digest IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_exceptions_policy_rule ON policy.exceptions(policy_rule_id) WHERE policy_rule_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_exceptions_owner ON policy.exceptions(owner_id) WHERE owner_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_exceptions_recheck_policy ON policy.exceptions(tenant_id, recheck_policy_id) WHERE recheck_policy_id IS NOT NULL; CREATE TABLE IF NOT EXISTS policy.exception_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), exception_id TEXT NOT NULL REFERENCES policy.exceptions(exception_id) ON DELETE CASCADE, sequence_number INTEGER NOT NULL, event_type TEXT NOT NULL CHECK (event_type IN ( 'created', 'updated', 'approved', 'activated', 'extended', 'revoked', 'expired', 'evidence_attached', 'compensating_control_added', 'rejected' )), actor_id TEXT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), previous_status TEXT, new_status TEXT NOT NULL, new_version INTEGER NOT NULL, description TEXT, details JSONB NOT NULL DEFAULT '{}', client_info TEXT, UNIQUE (exception_id, sequence_number) ); CREATE INDEX IF NOT EXISTS idx_exception_events_exception ON policy.exception_events(exception_id); CREATE INDEX IF NOT EXISTS idx_exception_events_time ON policy.exception_events USING BRIN (occurred_at);