diff --git a/docs/contracts/findings-ledger-rls.md b/docs/contracts/findings-ledger-rls.md index a8d7d41dc..84cd2e1fb 100644 --- a/docs/contracts/findings-ledger-rls.md +++ b/docs/contracts/findings-ledger-rls.md @@ -352,6 +352,8 @@ COMMIT; ### Rollback: `007_enable_rls_rollback.sql` +This rollback script remains a manual operator tool. It is not embedded into the service startup migration assembly, so forward startup will not flag it as a pending release migration. + ```sql BEGIN; diff --git a/docs/modules/findings-ledger/operations/rls-migration.md b/docs/modules/findings-ledger/operations/rls-migration.md index ee7f754ae..04bf8f25a 100644 --- a/docs/modules/findings-ledger/operations/rls-migration.md +++ b/docs/modules/findings-ledger/operations/rls-migration.md @@ -19,7 +19,7 @@ Migration `007_enable_rls.sql` enables Row-Level Security (RLS) on all Findings | File | Purpose | SHA256 | |------|---------|--------| | `007_enable_rls.sql` | Apply RLS policies | (generated at build time) | -| `007_enable_rls_rollback.sql` | Revert RLS policies | (generated at build time) | +| `007_enable_rls_rollback.sql` | Revert RLS policies | manual script on disk; excluded from embedded startup migrations | | `007_enable_rls.manifest.json` | Metadata for offline-kit | (generated at build time) | ## Protected Tables @@ -110,6 +110,8 @@ The rollback: - Removes the `findings_ledger_app` schema and tenant function - Does NOT drop the `findings_ledger_admin` role (preserves other grants) +`007_enable_rls_rollback.sql` is intentionally kept out of the service's embedded startup migrations so normal boot does not treat rollback as pending forward work. + ## Validation Checklist After applying the migration, verify: diff --git a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs index a02ba051c..baf8a3421 100644 --- a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs +++ b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs @@ -38,6 +38,7 @@ using StellaOps.Router.AspNet; using StellaOps.Signals.EvidenceWeightedScore; using StellaOps.Signals.EvidenceWeightedScore.Normalizers; using StellaOps.Telemetry.Core; +using StellaOps.Infrastructure.Postgres.Migrations; using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -97,6 +98,12 @@ builder.Services.AddOptions() .PostConfigure(options => options.Validate()) .ValidateOnStart(); +builder.Services.AddStartupMigrations( + schemaName: "findings", + moduleName: "FindingsLedger", + migrationsAssembly: typeof(LedgerDataSource).Assembly, + connectionStringSelector: options => options.Database.ConnectionString); + builder.Services.AddStellaOpsCrypto(); builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddProblemDetails(); @@ -2156,4 +2163,3 @@ namespace StellaOps.Findings.Ledger.WebService } - diff --git a/src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj b/src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj index d8b96a2cc..d2590803a 100644 --- a/src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj +++ b/src/Findings/StellaOps.Findings.Ledger.WebService/StellaOps.Findings.Ledger.WebService.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj b/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj index 19578643e..8ae5024f9 100644 --- a/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj +++ b/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Findings/StellaOps.Findings.Ledger/migrations/001_initial.sql b/src/Findings/StellaOps.Findings.Ledger/migrations/001_initial.sql index 6459ed18d..9aacdb8b1 100644 --- a/src/Findings/StellaOps.Findings.Ledger/migrations/001_initial.sql +++ b/src/Findings/StellaOps.Findings.Ledger/migrations/001_initial.sql @@ -6,32 +6,44 @@ SET search_path TO findings, public; BEGIN; -CREATE TYPE findings.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' -); +DO $$ +BEGIN + CREATE TYPE findings.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' + ); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$; -CREATE TYPE findings.ledger_action_type AS ENUM ( - 'assign', - 'comment', - 'attach_evidence', - 'link_ticket', - 'remediation_plan', - 'status_change', - 'accept_risk', - 'reopen', - 'close' -); +DO $$ +BEGIN + CREATE TYPE findings.ledger_action_type AS ENUM ( + 'assign', + 'comment', + 'attach_evidence', + 'link_ticket', + 'remediation_plan', + 'status_change', + 'accept_risk', + 'reopen', + 'close' + ); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$; -CREATE TABLE findings.ledger_events ( +CREATE TABLE IF NOT EXISTS findings.ledger_events ( tenant_id TEXT NOT NULL, chain_id UUID NOT NULL, sequence_no BIGINT NOT NULL, @@ -58,13 +70,13 @@ CREATE TABLE findings.ledger_events ( CONSTRAINT ck_ledger_events_actor_type CHECK (actor_type IN ('system', 'operator', 'integration')) ) PARTITION BY LIST (tenant_id); -CREATE TABLE findings.ledger_events_default PARTITION OF findings.ledger_events DEFAULT; +CREATE TABLE IF NOT EXISTS findings.ledger_events_default PARTITION OF findings.ledger_events DEFAULT; -CREATE INDEX ix_ledger_events_finding ON findings.ledger_events (tenant_id, finding_id, policy_version); -CREATE INDEX ix_ledger_events_type ON findings.ledger_events (tenant_id, event_type, recorded_at DESC); -CREATE INDEX ix_ledger_events_recorded_at ON findings.ledger_events (tenant_id, recorded_at DESC); +CREATE INDEX IF NOT EXISTS ix_ledger_events_finding ON findings.ledger_events (tenant_id, finding_id, policy_version); +CREATE INDEX IF NOT EXISTS ix_ledger_events_type ON findings.ledger_events (tenant_id, event_type, recorded_at DESC); +CREATE INDEX IF NOT EXISTS ix_ledger_events_recorded_at ON findings.ledger_events (tenant_id, recorded_at DESC); -CREATE TABLE findings.ledger_merkle_roots ( +CREATE TABLE IF NOT EXISTS findings.ledger_merkle_roots ( tenant_id TEXT NOT NULL, anchor_id UUID NOT NULL, window_start TIMESTAMPTZ NOT NULL, @@ -80,11 +92,11 @@ CREATE TABLE findings.ledger_merkle_roots ( CONSTRAINT ck_ledger_merkle_root_hash_hex CHECK (root_hash ~ '^[0-9a-f]{64}$') ) PARTITION BY LIST (tenant_id); -CREATE TABLE findings.ledger_merkle_roots_default PARTITION OF findings.ledger_merkle_roots DEFAULT; +CREATE TABLE IF NOT EXISTS findings.ledger_merkle_roots_default PARTITION OF findings.ledger_merkle_roots DEFAULT; -CREATE INDEX ix_merkle_sequences ON findings.ledger_merkle_roots (tenant_id, sequence_end DESC); +CREATE INDEX IF NOT EXISTS ix_merkle_sequences ON findings.ledger_merkle_roots (tenant_id, sequence_end DESC); -CREATE TABLE findings.findings_projection ( +CREATE TABLE IF NOT EXISTS findings.findings_projection ( tenant_id TEXT NOT NULL, finding_id TEXT NOT NULL, policy_version TEXT NOT NULL, @@ -99,12 +111,12 @@ CREATE TABLE findings.findings_projection ( CONSTRAINT ck_findings_projection_cycle_hash_hex CHECK (cycle_hash ~ '^[0-9a-f]{64}$') ) PARTITION BY LIST (tenant_id); -CREATE TABLE findings.findings_projection_default PARTITION OF findings.findings_projection DEFAULT; +CREATE TABLE IF NOT EXISTS findings.findings_projection_default PARTITION OF findings.findings_projection DEFAULT; -CREATE INDEX ix_projection_status ON findings.findings_projection (tenant_id, status, severity DESC); -CREATE INDEX ix_projection_labels_gin ON findings.findings_projection USING GIN (labels JSONB_PATH_OPS); +CREATE INDEX IF NOT EXISTS ix_projection_status ON findings.findings_projection (tenant_id, status, severity DESC); +CREATE INDEX IF NOT EXISTS ix_projection_labels_gin ON findings.findings_projection USING GIN (labels JSONB_PATH_OPS); -CREATE TABLE findings.finding_history ( +CREATE TABLE IF NOT EXISTS findings.finding_history ( tenant_id TEXT NOT NULL, finding_id TEXT NOT NULL, policy_version TEXT NOT NULL, @@ -117,11 +129,11 @@ CREATE TABLE findings.finding_history ( CONSTRAINT pk_finding_history PRIMARY KEY (tenant_id, finding_id, event_id) ) PARTITION BY LIST (tenant_id); -CREATE TABLE findings.finding_history_default PARTITION OF findings.finding_history DEFAULT; +CREATE TABLE IF NOT EXISTS findings.finding_history_default PARTITION OF findings.finding_history DEFAULT; -CREATE INDEX ix_finding_history_timeline ON findings.finding_history (tenant_id, finding_id, occurred_at DESC); +CREATE INDEX IF NOT EXISTS ix_finding_history_timeline ON findings.finding_history (tenant_id, finding_id, occurred_at DESC); -CREATE TABLE findings.triage_actions ( +CREATE TABLE IF NOT EXISTS findings.triage_actions ( tenant_id TEXT NOT NULL, action_id UUID NOT NULL, event_id UUID NOT NULL, @@ -133,9 +145,9 @@ CREATE TABLE findings.triage_actions ( CONSTRAINT pk_triage_actions PRIMARY KEY (tenant_id, action_id) ) PARTITION BY LIST (tenant_id); -CREATE TABLE findings.triage_actions_default PARTITION OF findings.triage_actions DEFAULT; +CREATE TABLE IF NOT EXISTS findings.triage_actions_default PARTITION OF findings.triage_actions DEFAULT; -CREATE INDEX ix_triage_actions_event ON findings.triage_actions (tenant_id, event_id); -CREATE INDEX ix_triage_actions_created_at ON findings.triage_actions (tenant_id, created_at DESC); +CREATE INDEX IF NOT EXISTS ix_triage_actions_event ON findings.triage_actions (tenant_id, event_id); +CREATE INDEX IF NOT EXISTS ix_triage_actions_created_at ON findings.triage_actions (tenant_id, created_at DESC); COMMIT; diff --git a/src/Findings/StellaOps.Findings.Ledger/migrations/002_add_evidence_bundle_ref.sql b/src/Findings/StellaOps.Findings.Ledger/migrations/002_add_evidence_bundle_ref.sql index 28fbae404..f930f21be 100644 --- a/src/Findings/StellaOps.Findings.Ledger/migrations/002_add_evidence_bundle_ref.sql +++ b/src/Findings/StellaOps.Findings.Ledger/migrations/002_add_evidence_bundle_ref.sql @@ -3,7 +3,7 @@ SET search_path TO findings, public; ALTER TABLE findings.ledger_events - ADD COLUMN evidence_bundle_ref text NULL; + ADD COLUMN IF NOT EXISTS evidence_bundle_ref text NULL; CREATE INDEX IF NOT EXISTS ix_ledger_events_finding_evidence_ref ON findings.ledger_events (tenant_id, finding_id, recorded_at DESC) diff --git a/src/Findings/StellaOps.Findings.Ledger/migrations/005_risk_fields.sql b/src/Findings/StellaOps.Findings.Ledger/migrations/005_risk_fields.sql index b5c8f3994..59898b466 100644 --- a/src/Findings/StellaOps.Findings.Ledger/migrations/005_risk_fields.sql +++ b/src/Findings/StellaOps.Findings.Ledger/migrations/005_risk_fields.sql @@ -13,6 +13,6 @@ ALTER TABLE findings.findings_projection ADD COLUMN IF NOT EXISTS risk_event_sequence bigint NULL; CREATE INDEX IF NOT EXISTS ix_findings_projection_risk - ON findings.findings_projection (tenant_id, risk_severity, risk_score DESC, recorded_at DESC); + ON findings.findings_projection (tenant_id, risk_severity, risk_score DESC, updated_at DESC); COMMIT;