stela ops usage fixes roles propagation and timoeut, one account to support multi tenants, migrations consolidation, search to support documentation, doctor and open api vector db search
This commit is contained in:
611
devops/compose/postgres-init/04-authority-schema.sql
Normal file
611
devops/compose/postgres-init/04-authority-schema.sql
Normal file
@@ -0,0 +1,611 @@
|
||||
-- Authority Schema: Consolidated Initial Schema
|
||||
-- Consolidated from migrations 001-005 (pre_1.0 archived)
|
||||
-- Creates the complete authority schema for IAM, tenants, users, tokens, RLS, and audit
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 1: Schema Creation
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS authority;
|
||||
CREATE SCHEMA IF NOT EXISTS authority_app;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 2: Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION authority.update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Tenant context helper function for RLS
|
||||
CREATE OR REPLACE FUNCTION authority_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'
|
||||
USING HINT = 'Set via: SELECT set_config(''app.tenant_id'', ''<tenant>'', false)',
|
||||
ERRCODE = 'P0001';
|
||||
END IF;
|
||||
RETURN v_tenant;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION authority_app.require_current_tenant() FROM PUBLIC;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 3: Core Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Tenants table (NOT RLS-protected - defines tenant boundaries)
|
||||
CREATE TABLE IF NOT EXISTS authority.tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'deleted')),
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tenants_status ON authority.tenants(status);
|
||||
CREATE INDEX idx_tenants_created_at ON authority.tenants(created_at);
|
||||
|
||||
COMMENT ON TABLE authority.tenants IS
|
||||
'Tenant registry. Not RLS-protected - defines tenant boundaries for the system.';
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS authority.users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
username TEXT NOT NULL,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
password_hash TEXT,
|
||||
password_salt TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
password_algorithm TEXT DEFAULT 'argon2id',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'locked', 'deleted')),
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mfa_secret TEXT,
|
||||
mfa_backup_codes TEXT,
|
||||
failed_login_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
password_changed_at TIMESTAMPTZ,
|
||||
last_password_change_at TIMESTAMPTZ,
|
||||
password_expires_at TIMESTAMPTZ,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
updated_by TEXT,
|
||||
UNIQUE(tenant_id, username),
|
||||
UNIQUE(tenant_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_tenant_id ON authority.users(tenant_id);
|
||||
CREATE INDEX idx_users_status ON authority.users(tenant_id, status);
|
||||
CREATE INDEX idx_users_email ON authority.users(tenant_id, email);
|
||||
|
||||
-- Roles table
|
||||
CREATE TABLE IF NOT EXISTS authority.roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_roles_tenant_id ON authority.roles(tenant_id);
|
||||
|
||||
-- Permissions table
|
||||
CREATE TABLE IF NOT EXISTS authority.permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
name TEXT NOT NULL,
|
||||
resource TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_permissions_tenant_id ON authority.permissions(tenant_id);
|
||||
CREATE INDEX idx_permissions_resource ON authority.permissions(tenant_id, resource);
|
||||
|
||||
-- Role-Permission assignments
|
||||
CREATE TABLE IF NOT EXISTS authority.role_permissions (
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
permission_id UUID NOT NULL REFERENCES authority.permissions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
-- User-Role assignments
|
||||
CREATE TABLE IF NOT EXISTS authority.user_roles (
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES authority.roles(id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
granted_by TEXT,
|
||||
expires_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
-- API Keys table
|
||||
CREATE TABLE IF NOT EXISTS authority.api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'expired')),
|
||||
last_used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_api_keys_tenant_id ON authority.api_keys(tenant_id);
|
||||
CREATE INDEX idx_api_keys_key_prefix ON authority.api_keys(key_prefix);
|
||||
CREATE INDEX idx_api_keys_user_id ON authority.api_keys(user_id);
|
||||
CREATE INDEX idx_api_keys_status ON authority.api_keys(tenant_id, status);
|
||||
|
||||
-- Tokens table (access tokens)
|
||||
CREATE TABLE IF NOT EXISTS authority.tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
token_type TEXT NOT NULL DEFAULT 'access' CHECK (token_type IN ('access', 'refresh', 'api')),
|
||||
scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tokens_tenant_id ON authority.tokens(tenant_id);
|
||||
CREATE INDEX idx_tokens_user_id ON authority.tokens(user_id);
|
||||
CREATE INDEX idx_tokens_expires_at ON authority.tokens(expires_at);
|
||||
CREATE INDEX idx_tokens_token_hash ON authority.tokens(token_hash);
|
||||
|
||||
-- Refresh Tokens table
|
||||
CREATE TABLE IF NOT EXISTS authority.refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
access_token_id UUID REFERENCES authority.tokens(id) ON DELETE SET NULL,
|
||||
client_id TEXT,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by TEXT,
|
||||
replaced_by UUID,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_refresh_tokens_tenant_id ON authority.refresh_tokens(tenant_id);
|
||||
CREATE INDEX idx_refresh_tokens_user_id ON authority.refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_expires_at ON authority.refresh_tokens(expires_at);
|
||||
|
||||
-- Sessions table
|
||||
CREATE TABLE IF NOT EXISTS authority.sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL REFERENCES authority.tenants(tenant_id),
|
||||
user_id UUID NOT NULL REFERENCES authority.users(id) ON DELETE CASCADE,
|
||||
session_token_hash TEXT NOT NULL UNIQUE,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ,
|
||||
end_reason TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_tenant_id ON authority.sessions(tenant_id);
|
||||
CREATE INDEX idx_sessions_user_id ON authority.sessions(user_id);
|
||||
CREATE INDEX idx_sessions_expires_at ON authority.sessions(expires_at);
|
||||
|
||||
-- Audit log table
|
||||
CREATE TABLE IF NOT EXISTS authority.audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id UUID,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
old_value JSONB,
|
||||
new_value JSONB,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_tenant_id ON authority.audit(tenant_id);
|
||||
CREATE INDEX idx_audit_user_id ON authority.audit(user_id);
|
||||
CREATE INDEX idx_audit_action ON authority.audit(action);
|
||||
CREATE INDEX idx_audit_resource ON authority.audit(resource_type, resource_id);
|
||||
CREATE INDEX idx_audit_created_at ON authority.audit(created_at);
|
||||
CREATE INDEX idx_audit_correlation_id ON authority.audit(correlation_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4: OIDC and Mongo Store Equivalent Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Bootstrap invites
|
||||
CREATE TABLE IF NOT EXISTS authority.bootstrap_invites (
|
||||
id TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
provider TEXT,
|
||||
target TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
issued_by TEXT,
|
||||
reserved_until TIMESTAMPTZ,
|
||||
reserved_by TEXT,
|
||||
consumed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Service accounts
|
||||
CREATE TABLE IF NOT EXISTS authority.service_accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL UNIQUE,
|
||||
tenant TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
authorized_clients TEXT[] NOT NULL DEFAULT '{}',
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_service_accounts_tenant ON authority.service_accounts(tenant);
|
||||
|
||||
-- Clients
|
||||
CREATE TABLE IF NOT EXISTS authority.clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
client_id TEXT NOT NULL UNIQUE,
|
||||
client_secret TEXT,
|
||||
secret_hash TEXT,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
plugin TEXT,
|
||||
sender_constraint TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
||||
post_logout_redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
||||
allowed_scopes TEXT[] NOT NULL DEFAULT '{}',
|
||||
allowed_grant_types TEXT[] NOT NULL DEFAULT '{}',
|
||||
require_client_secret BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
require_pkce BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
allow_plain_text_pkce BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
client_type TEXT,
|
||||
properties JSONB NOT NULL DEFAULT '{}',
|
||||
certificate_bindings JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Revocations
|
||||
CREATE TABLE IF NOT EXISTS authority.revocations (
|
||||
id TEXT PRIMARY KEY,
|
||||
category TEXT NOT NULL,
|
||||
revocation_id TEXT NOT NULL,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
token_id TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
reason_description TEXT,
|
||||
revoked_at TIMESTAMPTZ NOT NULL,
|
||||
effective_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_revocations_category_revocation_id
|
||||
ON authority.revocations(category, revocation_id);
|
||||
|
||||
-- Login attempts
|
||||
CREATE TABLE IF NOT EXISTS authority.login_attempts (
|
||||
id TEXT PRIMARY KEY,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
event_type TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_login_attempts_subject ON authority.login_attempts(subject_id, occurred_at DESC);
|
||||
|
||||
-- OIDC tokens
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT NOT NULL UNIQUE,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
token_type TEXT NOT NULL,
|
||||
reference_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
redeemed_at TIMESTAMPTZ,
|
||||
payload TEXT,
|
||||
properties JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_subject ON authority.oidc_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_client ON authority.oidc_tokens(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_tokens_reference ON authority.oidc_tokens(reference_id);
|
||||
|
||||
-- OIDC refresh tokens
|
||||
CREATE TABLE IF NOT EXISTS authority.oidc_refresh_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT NOT NULL UNIQUE,
|
||||
subject_id TEXT,
|
||||
client_id TEXT,
|
||||
handle TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
payload TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_subject ON authority.oidc_refresh_tokens(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_handle ON authority.oidc_refresh_tokens(handle);
|
||||
|
||||
-- Airgap audit
|
||||
CREATE TABLE IF NOT EXISTS authority.airgap_audit (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_type TEXT NOT NULL,
|
||||
operator_id TEXT,
|
||||
component_id TEXT,
|
||||
outcome TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
properties JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_airgap_audit_occurred_at ON authority.airgap_audit(occurred_at DESC);
|
||||
|
||||
-- Revocation export state (singleton row with optimistic concurrency)
|
||||
CREATE TABLE IF NOT EXISTS authority.revocation_export_state (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
sequence BIGINT NOT NULL DEFAULT 0,
|
||||
bundle_id TEXT,
|
||||
issued_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Offline Kit Audit
|
||||
CREATE TABLE IF NOT EXISTS authority.offline_kit_audit (
|
||||
event_id UUID PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
details JSONB NOT NULL,
|
||||
result TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_ts ON authority.offline_kit_audit(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_type ON authority.offline_kit_audit(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_tenant_ts ON authority.offline_kit_audit(tenant_id, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_result ON authority.offline_kit_audit(tenant_id, result, timestamp DESC);
|
||||
|
||||
-- Verdict manifests table
|
||||
CREATE TABLE IF NOT EXISTS authority.verdict_manifests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
manifest_id TEXT NOT NULL,
|
||||
tenant TEXT NOT NULL,
|
||||
asset_digest TEXT NOT NULL,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
inputs_json JSONB NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('affected', 'not_affected', 'fixed', 'under_investigation')),
|
||||
confidence DOUBLE PRECISION NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
|
||||
result_json JSONB NOT NULL,
|
||||
policy_hash TEXT NOT NULL,
|
||||
lattice_version TEXT NOT NULL,
|
||||
evaluated_at TIMESTAMPTZ NOT NULL,
|
||||
manifest_digest TEXT NOT NULL,
|
||||
signature_base64 TEXT,
|
||||
rekor_log_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_verdict_manifest_id UNIQUE (tenant, manifest_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_asset_vuln
|
||||
ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_policy
|
||||
ON authority.verdict_manifests(tenant, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_time
|
||||
ON authority.verdict_manifests USING BRIN (evaluated_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_verdict_replay
|
||||
ON authority.verdict_manifests(tenant, asset_digest, vulnerability_id, policy_hash, lattice_version);
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_digest
|
||||
ON authority.verdict_manifests(manifest_digest);
|
||||
|
||||
COMMENT ON TABLE authority.verdict_manifests IS 'VEX verdict manifests for deterministic replay verification';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 5: Triggers
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TRIGGER trg_tenants_updated_at
|
||||
BEFORE UPDATE ON authority.tenants
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_users_updated_at
|
||||
BEFORE UPDATE ON authority.users
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_roles_updated_at
|
||||
BEFORE UPDATE ON authority.roles
|
||||
FOR EACH ROW EXECUTE FUNCTION authority.update_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6: Row-Level Security
|
||||
-- ============================================================================
|
||||
|
||||
-- authority.users
|
||||
ALTER TABLE authority.users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.users FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY users_tenant_isolation ON authority.users
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.roles
|
||||
ALTER TABLE authority.roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY roles_tenant_isolation ON authority.roles
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.permissions
|
||||
ALTER TABLE authority.permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY permissions_tenant_isolation ON authority.permissions
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.role_permissions (FK-based, inherits from roles)
|
||||
ALTER TABLE authority.role_permissions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.role_permissions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY role_permissions_tenant_isolation ON authority.role_permissions
|
||||
FOR ALL
|
||||
USING (
|
||||
role_id IN (
|
||||
SELECT id FROM authority.roles
|
||||
WHERE tenant_id = authority_app.require_current_tenant()
|
||||
)
|
||||
);
|
||||
|
||||
-- authority.user_roles (FK-based, inherits from users)
|
||||
ALTER TABLE authority.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.user_roles FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY user_roles_tenant_isolation ON authority.user_roles
|
||||
FOR ALL
|
||||
USING (
|
||||
user_id IN (
|
||||
SELECT id FROM authority.users
|
||||
WHERE tenant_id = authority_app.require_current_tenant()
|
||||
)
|
||||
);
|
||||
|
||||
-- authority.api_keys
|
||||
ALTER TABLE authority.api_keys ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.api_keys FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY api_keys_tenant_isolation ON authority.api_keys
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.tokens
|
||||
ALTER TABLE authority.tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tokens_tenant_isolation ON authority.tokens
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.refresh_tokens
|
||||
ALTER TABLE authority.refresh_tokens ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.refresh_tokens FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY refresh_tokens_tenant_isolation ON authority.refresh_tokens
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.sessions
|
||||
ALTER TABLE authority.sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.sessions FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY sessions_tenant_isolation ON authority.sessions
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.audit
|
||||
ALTER TABLE authority.audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY audit_tenant_isolation ON authority.audit
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.offline_kit_audit
|
||||
ALTER TABLE authority.offline_kit_audit ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE authority.offline_kit_audit FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY offline_kit_audit_tenant_isolation ON authority.offline_kit_audit
|
||||
FOR ALL
|
||||
USING (tenant_id = authority_app.require_current_tenant())
|
||||
WITH CHECK (tenant_id = authority_app.require_current_tenant());
|
||||
|
||||
-- authority.verdict_manifests
|
||||
ALTER TABLE authority.verdict_manifests ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY verdict_tenant_isolation ON authority.verdict_manifests
|
||||
USING (tenant = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant = current_setting('app.current_tenant', true));
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 7: Roles and Permissions
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authority_admin') THEN
|
||||
CREATE ROLE authority_admin WITH NOLOGIN BYPASSRLS;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Grant permissions (if role exists)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'stellaops_app') THEN
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON authority.verdict_manifests TO stellaops_app;
|
||||
GRANT USAGE ON SCHEMA authority TO stellaops_app;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
114
devops/compose/postgres-init/05-policy-exceptions-schema.sql
Normal file
114
devops/compose/postgres-init/05-policy-exceptions-schema.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
-- 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);
|
||||
245
devops/compose/postgres-init/06-attestor-proofchain-schema.sql
Normal file
245
devops/compose/postgres-init/06-attestor-proofchain-schema.sql
Normal file
@@ -0,0 +1,245 @@
|
||||
-- Attestor Schema Migration 001: Initial Schema (Compacted)
|
||||
-- Consolidated from 20251214000001_AddProofChainSchema.sql and 20251216_001_create_rekor_submission_queue.sql
|
||||
-- for 1.0.0 release
|
||||
-- Creates the proofchain schema for proof chain persistence and attestor schema for Rekor queue
|
||||
|
||||
-- ============================================================================
|
||||
-- Extensions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ============================================================================
|
||||
-- Schema Creation
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS proofchain;
|
||||
CREATE SCHEMA IF NOT EXISTS attestor;
|
||||
|
||||
-- ============================================================================
|
||||
-- Enum Types
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'verification_result' AND typnamespace = 'proofchain'::regnamespace) THEN
|
||||
CREATE TYPE proofchain.verification_result AS ENUM ('pass', 'fail', 'pending');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- ProofChain Schema Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Trust anchors table (create first - no dependencies)
|
||||
CREATE TABLE IF NOT EXISTS proofchain.trust_anchors (
|
||||
anchor_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
purl_pattern TEXT NOT NULL,
|
||||
allowed_keyids TEXT[] NOT NULL,
|
||||
allowed_predicate_types TEXT[],
|
||||
policy_ref TEXT,
|
||||
policy_version TEXT,
|
||||
revoked_keys TEXT[] DEFAULT '{}',
|
||||
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_trust_anchors_pattern ON proofchain.trust_anchors(purl_pattern);
|
||||
CREATE INDEX IF NOT EXISTS idx_trust_anchors_active ON proofchain.trust_anchors(is_active) WHERE is_active = TRUE;
|
||||
|
||||
COMMENT ON TABLE proofchain.trust_anchors IS 'Trust anchor configurations for dependency verification';
|
||||
COMMENT ON COLUMN proofchain.trust_anchors.purl_pattern IS 'PURL glob pattern (e.g., pkg:npm/*)';
|
||||
COMMENT ON COLUMN proofchain.trust_anchors.revoked_keys IS 'Key IDs that have been revoked but may appear in old proofs';
|
||||
|
||||
-- SBOM entries table
|
||||
CREATE TABLE IF NOT EXISTS proofchain.sbom_entries (
|
||||
entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
bom_digest VARCHAR(64) NOT NULL,
|
||||
purl TEXT NOT NULL,
|
||||
version TEXT,
|
||||
artifact_digest VARCHAR(64),
|
||||
trust_anchor_id UUID REFERENCES proofchain.trust_anchors(anchor_id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_sbom_entry UNIQUE (bom_digest, purl, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sbom_entries_bom_digest ON proofchain.sbom_entries(bom_digest);
|
||||
CREATE INDEX IF NOT EXISTS idx_sbom_entries_purl ON proofchain.sbom_entries(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_sbom_entries_artifact ON proofchain.sbom_entries(artifact_digest);
|
||||
CREATE INDEX IF NOT EXISTS idx_sbom_entries_anchor ON proofchain.sbom_entries(trust_anchor_id);
|
||||
|
||||
COMMENT ON TABLE proofchain.sbom_entries IS 'SBOM component entries with content-addressed identifiers';
|
||||
COMMENT ON COLUMN proofchain.sbom_entries.bom_digest IS 'SHA-256 hash of the parent SBOM document';
|
||||
COMMENT ON COLUMN proofchain.sbom_entries.purl IS 'Package URL (PURL) of the component';
|
||||
COMMENT ON COLUMN proofchain.sbom_entries.artifact_digest IS 'SHA-256 hash of the component artifact if available';
|
||||
|
||||
-- DSSE envelopes table
|
||||
CREATE TABLE IF NOT EXISTS proofchain.dsse_envelopes (
|
||||
env_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entry_id UUID NOT NULL REFERENCES proofchain.sbom_entries(entry_id) ON DELETE CASCADE,
|
||||
predicate_type TEXT NOT NULL,
|
||||
signer_keyid TEXT NOT NULL,
|
||||
body_hash VARCHAR(64) NOT NULL,
|
||||
envelope_blob_ref TEXT NOT NULL,
|
||||
signed_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_dsse_envelope UNIQUE (entry_id, predicate_type, body_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dsse_entry_predicate ON proofchain.dsse_envelopes(entry_id, predicate_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_dsse_signer ON proofchain.dsse_envelopes(signer_keyid);
|
||||
CREATE INDEX IF NOT EXISTS idx_dsse_body_hash ON proofchain.dsse_envelopes(body_hash);
|
||||
|
||||
COMMENT ON TABLE proofchain.dsse_envelopes IS 'Signed DSSE envelopes for proof chain statements';
|
||||
COMMENT ON COLUMN proofchain.dsse_envelopes.predicate_type IS 'Predicate type URI (e.g., evidence.stella/v1)';
|
||||
COMMENT ON COLUMN proofchain.dsse_envelopes.envelope_blob_ref IS 'Reference to blob storage (OCI, S3, file)';
|
||||
|
||||
-- Spines table
|
||||
CREATE TABLE IF NOT EXISTS proofchain.spines (
|
||||
entry_id UUID PRIMARY KEY REFERENCES proofchain.sbom_entries(entry_id) ON DELETE CASCADE,
|
||||
bundle_id VARCHAR(64) NOT NULL,
|
||||
evidence_ids TEXT[] NOT NULL,
|
||||
reasoning_id VARCHAR(64) NOT NULL,
|
||||
vex_id VARCHAR(64) NOT NULL,
|
||||
anchor_id UUID REFERENCES proofchain.trust_anchors(anchor_id) ON DELETE SET NULL,
|
||||
policy_version TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_spine_bundle UNIQUE (bundle_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_spines_bundle ON proofchain.spines(bundle_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_spines_anchor ON proofchain.spines(anchor_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_spines_policy ON proofchain.spines(policy_version);
|
||||
|
||||
COMMENT ON TABLE proofchain.spines IS 'Proof spines linking evidence to verdicts via merkle aggregation';
|
||||
COMMENT ON COLUMN proofchain.spines.bundle_id IS 'ProofBundleID (merkle root of all components)';
|
||||
COMMENT ON COLUMN proofchain.spines.evidence_ids IS 'Array of EvidenceIDs in sorted order';
|
||||
|
||||
-- Rekor entries table
|
||||
CREATE TABLE IF NOT EXISTS proofchain.rekor_entries (
|
||||
dsse_sha256 VARCHAR(64) PRIMARY KEY,
|
||||
log_index BIGINT NOT NULL,
|
||||
log_id TEXT NOT NULL,
|
||||
uuid TEXT NOT NULL,
|
||||
integrated_time BIGINT NOT NULL,
|
||||
inclusion_proof JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
env_id UUID REFERENCES proofchain.dsse_envelopes(env_id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_log_index ON proofchain.rekor_entries(log_index);
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_log_id ON proofchain.rekor_entries(log_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_uuid ON proofchain.rekor_entries(uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_env ON proofchain.rekor_entries(env_id);
|
||||
|
||||
COMMENT ON TABLE proofchain.rekor_entries IS 'Rekor transparency log entries for verification';
|
||||
COMMENT ON COLUMN proofchain.rekor_entries.inclusion_proof IS 'Merkle inclusion proof from Rekor';
|
||||
|
||||
-- Audit log table
|
||||
CREATE TABLE IF NOT EXISTS proofchain.audit_log (
|
||||
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
operation TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
actor TEXT,
|
||||
details JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_entity ON proofchain.audit_log(entity_type, entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_created ON proofchain.audit_log(created_at DESC);
|
||||
|
||||
COMMENT ON TABLE proofchain.audit_log IS 'Audit log for proof chain operations';
|
||||
|
||||
-- ============================================================================
|
||||
-- Attestor Schema Tables
|
||||
-- ============================================================================
|
||||
|
||||
-- Rekor submission queue table
|
||||
CREATE TABLE IF NOT EXISTS attestor.rekor_submission_queue (
|
||||
id UUID PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
bundle_sha256 TEXT NOT NULL,
|
||||
dsse_payload BYTEA NOT NULL,
|
||||
backend TEXT NOT NULL DEFAULT 'primary',
|
||||
|
||||
-- Status lifecycle: pending -> submitting -> submitted | retrying -> dead_letter
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'submitting', 'retrying', 'submitted', 'dead_letter')),
|
||||
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 5,
|
||||
next_retry_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Populated on success
|
||||
rekor_uuid TEXT,
|
||||
rekor_index BIGINT,
|
||||
|
||||
-- Populated on failure
|
||||
last_error TEXT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE attestor.rekor_submission_queue IS
|
||||
'Durable retry queue for Rekor transparency log submissions';
|
||||
COMMENT ON COLUMN attestor.rekor_submission_queue.status IS
|
||||
'Submission lifecycle: pending -> submitting -> (submitted | retrying -> dead_letter)';
|
||||
COMMENT ON COLUMN attestor.rekor_submission_queue.backend IS
|
||||
'Target Rekor backend (primary or mirror)';
|
||||
COMMENT ON COLUMN attestor.rekor_submission_queue.dsse_payload IS
|
||||
'Serialized DSSE envelope to submit';
|
||||
|
||||
-- Index for dequeue operations (status + next_retry_at for SKIP LOCKED queries)
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_queue_dequeue
|
||||
ON attestor.rekor_submission_queue (status, next_retry_at)
|
||||
WHERE status IN ('pending', 'retrying');
|
||||
|
||||
-- Index for tenant-scoped queries
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_queue_tenant
|
||||
ON attestor.rekor_submission_queue (tenant_id);
|
||||
|
||||
-- Index for bundle lookup (deduplication check)
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_queue_bundle
|
||||
ON attestor.rekor_submission_queue (tenant_id, bundle_sha256);
|
||||
|
||||
-- Index for dead letter management
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_queue_dead_letter
|
||||
ON attestor.rekor_submission_queue (status, updated_at)
|
||||
WHERE status = 'dead_letter';
|
||||
|
||||
-- Index for cleanup of completed submissions
|
||||
CREATE INDEX IF NOT EXISTS idx_rekor_queue_completed
|
||||
ON attestor.rekor_submission_queue (status, updated_at)
|
||||
WHERE status = 'submitted';
|
||||
|
||||
-- ============================================================================
|
||||
-- Trigger Functions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION proofchain.update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Apply updated_at trigger to trust_anchors
|
||||
DROP TRIGGER IF EXISTS update_trust_anchors_updated_at ON proofchain.trust_anchors;
|
||||
CREATE TRIGGER update_trust_anchors_updated_at
|
||||
BEFORE UPDATE ON proofchain.trust_anchors
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION proofchain.update_updated_at_column();
|
||||
|
||||
-- Apply updated_at trigger to rekor_submission_queue
|
||||
DROP TRIGGER IF EXISTS update_rekor_queue_updated_at ON attestor.rekor_submission_queue;
|
||||
CREATE TRIGGER update_rekor_queue_updated_at
|
||||
BEFORE UPDATE ON attestor.rekor_submission_queue
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION proofchain.update_updated_at_column();
|
||||
@@ -0,0 +1,95 @@
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Migration: 20260129_001_create_identity_watchlist
|
||||
-- Sprint: SPRINT_0129_001_ATTESTOR_identity_watchlist_alerting
|
||||
-- Task: WATCH-004
|
||||
-- Description: Creates identity watchlist and alert deduplication tables.
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Watchlist entries table
|
||||
CREATE TABLE IF NOT EXISTS attestor.identity_watchlist (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'Tenant',
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Identity matching fields (at least one required)
|
||||
issuer TEXT,
|
||||
subject_alternative_name TEXT,
|
||||
key_id TEXT,
|
||||
match_mode TEXT NOT NULL DEFAULT 'Exact',
|
||||
|
||||
-- Alert configuration
|
||||
severity TEXT NOT NULL DEFAULT 'Warning',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
channel_overrides JSONB,
|
||||
suppress_duplicates_minutes INT NOT NULL DEFAULT 60,
|
||||
|
||||
-- Metadata
|
||||
tags TEXT[],
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT NOT NULL,
|
||||
updated_by TEXT NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT chk_at_least_one_identity CHECK (
|
||||
issuer IS NOT NULL OR
|
||||
subject_alternative_name IS NOT NULL OR
|
||||
key_id IS NOT NULL
|
||||
),
|
||||
CONSTRAINT chk_scope_valid CHECK (scope IN ('Tenant', 'Global', 'System')),
|
||||
CONSTRAINT chk_match_mode_valid CHECK (match_mode IN ('Exact', 'Prefix', 'Glob', 'Regex')),
|
||||
CONSTRAINT chk_severity_valid CHECK (severity IN ('Info', 'Warning', 'Critical')),
|
||||
CONSTRAINT chk_suppress_duplicates_positive CHECK (suppress_duplicates_minutes >= 1)
|
||||
);
|
||||
|
||||
-- Performance indexes for active entry lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_tenant_enabled
|
||||
ON attestor.identity_watchlist(tenant_id)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_scope_enabled
|
||||
ON attestor.identity_watchlist(scope)
|
||||
WHERE enabled = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_issuer
|
||||
ON attestor.identity_watchlist(issuer)
|
||||
WHERE enabled = TRUE AND issuer IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_san
|
||||
ON attestor.identity_watchlist(subject_alternative_name)
|
||||
WHERE enabled = TRUE AND subject_alternative_name IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_keyid
|
||||
ON attestor.identity_watchlist(key_id)
|
||||
WHERE enabled = TRUE AND key_id IS NOT NULL;
|
||||
|
||||
-- Alert deduplication table
|
||||
CREATE TABLE IF NOT EXISTS attestor.identity_alert_dedup (
|
||||
watchlist_id UUID NOT NULL,
|
||||
identity_hash TEXT NOT NULL,
|
||||
last_alert_at TIMESTAMPTZ NOT NULL,
|
||||
alert_count INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (watchlist_id, identity_hash)
|
||||
);
|
||||
|
||||
-- Index for cleanup
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_dedup_last_alert
|
||||
ON attestor.identity_alert_dedup(last_alert_at);
|
||||
|
||||
-- Comment documentation
|
||||
COMMENT ON TABLE attestor.identity_watchlist IS
|
||||
'Watchlist entries for monitoring signing identity appearances in transparency logs.';
|
||||
|
||||
COMMENT ON COLUMN attestor.identity_watchlist.scope IS
|
||||
'Visibility scope: Tenant (owning tenant only), Global (all tenants), System (read-only).';
|
||||
|
||||
COMMENT ON COLUMN attestor.identity_watchlist.match_mode IS
|
||||
'Pattern matching mode: Exact, Prefix, Glob, or Regex.';
|
||||
|
||||
COMMENT ON COLUMN attestor.identity_watchlist.suppress_duplicates_minutes IS
|
||||
'Deduplication window in minutes. Alerts for same identity within window are suppressed.';
|
||||
|
||||
COMMENT ON TABLE attestor.identity_alert_dedup IS
|
||||
'Tracks alert deduplication state to prevent alert storms.';
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Attestor Schema Migration 002: Predicate Type Registry
|
||||
-- Sprint: SPRINT_20260219_010 (PSR-01)
|
||||
-- Creates discoverable, versioned registry for all predicate types
|
||||
|
||||
-- ============================================================================
|
||||
-- Predicate Type Registry Table
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS proofchain.predicate_type_registry (
|
||||
registry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
predicate_type_uri TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL DEFAULT '1.0.0',
|
||||
category TEXT NOT NULL DEFAULT 'stella-core'
|
||||
CHECK (category IN ('stella-core', 'stella-proof', 'stella-delta', 'ecosystem', 'intoto', 'custom')),
|
||||
json_schema JSONB,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
validation_mode TEXT NOT NULL DEFAULT 'log-only'
|
||||
CHECK (validation_mode IN ('log-only', 'warn', 'reject')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_predicate_type_version UNIQUE (predicate_type_uri, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_uri
|
||||
ON proofchain.predicate_type_registry(predicate_type_uri);
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_category
|
||||
ON proofchain.predicate_type_registry(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_predicate_registry_active
|
||||
ON proofchain.predicate_type_registry(is_active) WHERE is_active = TRUE;
|
||||
|
||||
-- Apply updated_at trigger
|
||||
DROP TRIGGER IF EXISTS update_predicate_registry_updated_at ON proofchain.predicate_type_registry;
|
||||
CREATE TRIGGER update_predicate_registry_updated_at
|
||||
BEFORE UPDATE ON proofchain.predicate_type_registry
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION proofchain.update_updated_at_column();
|
||||
|
||||
COMMENT ON TABLE proofchain.predicate_type_registry IS 'Discoverable registry of all predicate types accepted by the Attestor';
|
||||
COMMENT ON COLUMN proofchain.predicate_type_registry.predicate_type_uri IS 'Canonical URI for the predicate type (e.g., https://stella-ops.org/predicates/evidence/v1)';
|
||||
COMMENT ON COLUMN proofchain.predicate_type_registry.validation_mode IS 'How mismatches are handled: log-only (default), warn, or reject';
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-core predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://stella-ops.org/predicates/sbom-linkage/v1', 'SBOM Linkage', '1.0.0', 'stella-core', 'Links SBOM components to evidence and proof spines'),
|
||||
('https://stella-ops.org/predicates/vex-verdict/v1', 'VEX Verdict', '1.0.0', 'stella-core', 'VEX consensus verdict for an artifact+advisory tuple'),
|
||||
('https://stella-ops.org/predicates/evidence/v1', 'Evidence', '1.0.0', 'stella-core', 'Generic evidence attestation linking scan results to artifacts'),
|
||||
('https://stella-ops.org/predicates/reasoning/v1', 'Reasoning', '1.0.0', 'stella-core', 'Policy reasoning chain for a release decision'),
|
||||
('https://stella-ops.org/predicates/proof-spine/v1', 'Proof Spine', '1.0.0', 'stella-core', 'Merkle-aggregated proof spine linking evidence to verdicts'),
|
||||
('https://stella-ops.org/predicates/reachability-drift/v1', 'Reachability Drift', '1.0.0', 'stella-core', 'Reachability state changes between consecutive scans'),
|
||||
('https://stella-ops.org/predicates/reachability-subgraph/v1', 'Reachability Subgraph', '1.0.0', 'stella-core', 'Call graph subgraph for a specific vulnerability path'),
|
||||
('https://stella-ops.org/predicates/delta-verdict/v1', 'Delta Verdict', '1.0.0', 'stella-core', 'Verdict differences between two scan runs'),
|
||||
('https://stella-ops.org/predicates/policy-decision/v1', 'Policy Decision', '1.0.0', 'stella-core', 'Policy engine evaluation result for a release gate'),
|
||||
('https://stella-ops.org/predicates/unknowns-budget/v1', 'Unknowns Budget', '1.0.0', 'stella-core', 'Budget check for unknown reachability components'),
|
||||
('https://stella-ops.org/predicates/ai-code-guard/v1', 'AI Code Guard', '1.0.0', 'stella-core', 'AI-assisted code security analysis results'),
|
||||
('https://stella-ops.org/predicates/fix-chain/v1', 'Fix Chain', '1.0.0', 'stella-core', 'Linked chain of fix commits from vulnerability to resolution'),
|
||||
('https://stella-ops.org/attestation/graph-root/v1', 'Graph Root', '1.0.0', 'stella-core', 'Root attestation for a complete call graph')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-proof predicates (ProofChain)
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://stella.ops/predicates/path-witness/v1', 'Path Witness', '1.0.0', 'stella-proof', 'Entrypoint-to-sink call path witness with gate detection'),
|
||||
('https://stella.ops/predicates/runtime-witness/v1', 'Runtime Witness', '1.0.0', 'stella-proof', 'Runtime micro-witness from eBPF/ETW observations'),
|
||||
('https://stella.ops/predicates/policy-decision@v2', 'Policy Decision v2', '2.0.0', 'stella-proof', 'Enhanced policy decision with reachability context'),
|
||||
('https://stellaops.dev/predicates/binary-micro-witness@v1', 'Binary Micro-Witness', '1.0.0', 'stella-proof', 'Binary-level micro-witness with build ID correlation'),
|
||||
('https://stellaops.dev/predicates/binary-fingerprint-evidence@v1', 'Binary Fingerprint', '1.0.0', 'stella-proof', 'Binary fingerprint evidence for patch detection'),
|
||||
('https://stellaops.io/attestation/budget-check/v1', 'Budget Check', '1.0.0', 'stella-proof', 'Unknowns budget check attestation'),
|
||||
('https://stellaops.dev/attestation/vex/v1', 'VEX Attestation', '1.0.0', 'stella-proof', 'DSSE-signed VEX statement attestation'),
|
||||
('https://stellaops.dev/attestations/vex-override/v1', 'VEX Override', '1.0.0', 'stella-proof', 'Manual VEX override decision with justification'),
|
||||
('https://stellaops.dev/predicates/trust-verdict@v1', 'Trust Verdict', '1.0.0', 'stella-proof', 'Trust lattice verdict combining P/C/R vectors'),
|
||||
('https://stellaops.io/attestation/v1/signed-exception', 'Signed Exception', '1.0.0', 'stella-proof', 'Manually approved exception with expiry'),
|
||||
('https://stellaops.dev/attestation/verification-report/v1', 'Verification Report', '1.0.0', 'stella-proof', 'QA verification report attestation')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: stella-delta predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('stella.ops/changetrace@v1', 'Change Trace', '1.0.0', 'stella-delta', 'File-level change trace between SBOM versions'),
|
||||
('stella.ops/vex-delta@v1', 'VEX Delta', '1.0.0', 'stella-delta', 'VEX statement differences between consecutive ingestions'),
|
||||
('stella.ops/sbom-delta@v1', 'SBOM Delta', '1.0.0', 'stella-delta', 'Component differences between two SBOM versions'),
|
||||
('stella.ops/verdict-delta@v1', 'Verdict Delta', '1.0.0', 'stella-delta', 'Verdict changes between policy evaluations'),
|
||||
('stellaops.binarydiff.v1', 'Binary Diff', '1.0.0', 'stella-delta', 'Binary diff signatures for patch detection')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: ecosystem predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://spdx.dev/Document', 'SPDX Document', '2.3.0', 'ecosystem', 'SPDX 2.x document attestation'),
|
||||
('https://cyclonedx.org/bom', 'CycloneDX BOM', '1.7.0', 'ecosystem', 'CycloneDX BOM attestation'),
|
||||
('https://slsa.dev/provenance', 'SLSA Provenance', '1.0.0', 'ecosystem', 'SLSA v1.0 build provenance')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Seed: in-toto standard predicates
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO proofchain.predicate_type_registry (predicate_type_uri, display_name, version, category, description) VALUES
|
||||
('https://in-toto.io/Statement/v1', 'In-Toto Statement', '1.0.0', 'intoto', 'In-toto attestation statement wrapper'),
|
||||
('https://in-toto.io/Link/v1', 'In-Toto Link', '1.0.0', 'intoto', 'In-toto supply chain link'),
|
||||
('https://in-toto.io/Layout/v1', 'In-Toto Layout', '1.0.0', 'intoto', 'In-toto supply chain layout')
|
||||
ON CONFLICT (predicate_type_uri, version) DO NOTHING;
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Migration 003: Artifact Canonical Record materialized view
|
||||
-- Sprint: SPRINT_20260219_009 (CID-04)
|
||||
-- Purpose: Unified read projection joining sbom_entries + dsse_envelopes + rekor_entries
|
||||
-- for the Evidence Thread API (GET /api/v1/evidence/thread/{canonical_id}).
|
||||
|
||||
-- Materialized view: one row per canonical_id with aggregated attestation evidence.
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS proofchain.artifact_canonical_records AS
|
||||
SELECT
|
||||
se.bom_digest AS canonical_id,
|
||||
'cyclonedx-jcs:1'::text AS format,
|
||||
se.artifact_digest,
|
||||
se.purl,
|
||||
se.created_at,
|
||||
COALESCE(
|
||||
jsonb_agg(
|
||||
DISTINCT jsonb_build_object(
|
||||
'predicate_type', de.predicate_type,
|
||||
'dsse_digest', de.body_hash,
|
||||
'signer_keyid', de.signer_keyid,
|
||||
'rekor_entry_id', re.uuid,
|
||||
'rekor_tile', re.log_id,
|
||||
'signed_at', de.signed_at
|
||||
)
|
||||
) FILTER (WHERE de.env_id IS NOT NULL),
|
||||
'[]'::jsonb
|
||||
) AS attestations
|
||||
FROM proofchain.sbom_entries se
|
||||
LEFT JOIN proofchain.dsse_envelopes de ON de.entry_id = se.entry_id
|
||||
LEFT JOIN proofchain.rekor_entries re ON re.env_id = de.env_id
|
||||
GROUP BY se.entry_id, se.bom_digest, se.artifact_digest, se.purl, se.created_at;
|
||||
|
||||
-- Unique index for CONCURRENTLY refresh and fast lookup.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_acr_canonical_id
|
||||
ON proofchain.artifact_canonical_records (canonical_id);
|
||||
|
||||
-- Index for PURL-based lookup (Evidence Thread by PURL).
|
||||
CREATE INDEX IF NOT EXISTS idx_acr_purl
|
||||
ON proofchain.artifact_canonical_records (purl)
|
||||
WHERE purl IS NOT NULL;
|
||||
|
||||
COMMENT ON MATERIALIZED VIEW proofchain.artifact_canonical_records IS
|
||||
'Unified read projection for the Evidence Thread API. Joins SBOM entries, DSSE envelopes, and Rekor entries into one row per canonical_id. Refresh via REFRESH MATERIALIZED VIEW CONCURRENTLY.';
|
||||
83
devops/compose/postgres-init/10-attestor-verdict-ledger.sql
Normal file
83
devops/compose/postgres-init/10-attestor-verdict-ledger.sql
Normal file
@@ -0,0 +1,83 @@
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 001_verdict_ledger_initial.sql
|
||||
-- Sprint: SPRINT_20260118_015_Attestor_verdict_ledger_foundation
|
||||
-- Task: VL-001 - Create VerdictLedger database schema
|
||||
-- Description: Append-only verdict ledger with SHA-256 hash chaining
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Create verdict decision enum
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE verdict_decision AS ENUM ('unknown', 'approve', 'reject', 'pending');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Create the verdict_ledger table
|
||||
CREATE TABLE IF NOT EXISTS verdict_ledger (
|
||||
ledger_id UUID PRIMARY KEY,
|
||||
bom_ref VARCHAR(2048) NOT NULL,
|
||||
cyclonedx_serial VARCHAR(512),
|
||||
rekor_uuid VARCHAR(128),
|
||||
decision verdict_decision NOT NULL DEFAULT 'unknown',
|
||||
reason TEXT NOT NULL,
|
||||
policy_bundle_id VARCHAR(256) NOT NULL,
|
||||
policy_bundle_hash VARCHAR(64) NOT NULL,
|
||||
verifier_image_digest VARCHAR(256) NOT NULL,
|
||||
signer_keyid VARCHAR(512) NOT NULL,
|
||||
prev_hash VARCHAR(64), -- SHA-256 hex, null for genesis entry
|
||||
verdict_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT verdict_hash_format CHECK (verdict_hash ~ '^[a-f0-9]{64}$'),
|
||||
CONSTRAINT prev_hash_format CHECK (prev_hash IS NULL OR prev_hash ~ '^[a-f0-9]{64}$'),
|
||||
CONSTRAINT policy_hash_format CHECK (policy_bundle_hash ~ '^[a-f0-9]{64}$')
|
||||
);
|
||||
|
||||
-- Indexes for common query patterns
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_bom_ref
|
||||
ON verdict_ledger (bom_ref);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_rekor_uuid
|
||||
ON verdict_ledger (rekor_uuid)
|
||||
WHERE rekor_uuid IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_created_at
|
||||
ON verdict_ledger (created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_tenant_created
|
||||
ON verdict_ledger (tenant_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_prev_hash
|
||||
ON verdict_ledger (prev_hash)
|
||||
WHERE prev_hash IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_decision
|
||||
ON verdict_ledger (decision);
|
||||
|
||||
-- Composite index for chain walking
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_ledger_chain
|
||||
ON verdict_ledger (tenant_id, verdict_hash);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE verdict_ledger IS 'Append-only ledger of release verdicts with SHA-256 hash chaining for cryptographic audit trail';
|
||||
COMMENT ON COLUMN verdict_ledger.ledger_id IS 'Unique identifier for this ledger entry';
|
||||
COMMENT ON COLUMN verdict_ledger.bom_ref IS 'Package URL (purl) or container digest reference';
|
||||
COMMENT ON COLUMN verdict_ledger.cyclonedx_serial IS 'CycloneDX serialNumber URN linking to SBOM';
|
||||
COMMENT ON COLUMN verdict_ledger.rekor_uuid IS 'Transparency log entry UUID for external verification';
|
||||
COMMENT ON COLUMN verdict_ledger.decision IS 'The release decision: unknown, approve, reject, or pending';
|
||||
COMMENT ON COLUMN verdict_ledger.reason IS 'Human-readable explanation for the decision';
|
||||
COMMENT ON COLUMN verdict_ledger.policy_bundle_id IS 'Reference to the policy configuration used';
|
||||
COMMENT ON COLUMN verdict_ledger.policy_bundle_hash IS 'SHA-256 hash of the policy bundle for reproducibility';
|
||||
COMMENT ON COLUMN verdict_ledger.verifier_image_digest IS 'Container digest of the verifier service';
|
||||
COMMENT ON COLUMN verdict_ledger.signer_keyid IS 'Key ID that signed this verdict';
|
||||
COMMENT ON COLUMN verdict_ledger.prev_hash IS 'SHA-256 hash of previous entry (null for genesis)';
|
||||
COMMENT ON COLUMN verdict_ledger.verdict_hash IS 'SHA-256 hash of this entry canonical JSON form';
|
||||
COMMENT ON COLUMN verdict_ledger.created_at IS 'Timestamp when this verdict was recorded';
|
||||
COMMENT ON COLUMN verdict_ledger.tenant_id IS 'Tenant identifier for multi-tenancy';
|
||||
|
||||
-- Revoke UPDATE and DELETE for application role (append-only enforcement)
|
||||
-- This should be run after creating the appropriate role
|
||||
-- REVOKE UPDATE, DELETE ON verdict_ledger FROM stellaops_app;
|
||||
-- GRANT INSERT, SELECT ON verdict_ledger TO stellaops_app;
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Migration: 001_CreateVerdictAttestations
|
||||
-- Description: Create verdict_attestations table for storing signed policy verdict attestations
|
||||
-- Author: Evidence Locker Guild
|
||||
-- Date: 2025-12-23
|
||||
|
||||
-- Create schema if not exists
|
||||
CREATE SCHEMA IF NOT EXISTS evidence_locker;
|
||||
|
||||
-- Create verdict_attestations table
|
||||
CREATE TABLE IF NOT EXISTS evidence_locker.verdict_attestations (
|
||||
verdict_id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
policy_id TEXT NOT NULL,
|
||||
policy_version INTEGER NOT NULL,
|
||||
finding_id TEXT NOT NULL,
|
||||
verdict_status TEXT NOT NULL CHECK (verdict_status IN ('passed', 'warned', 'blocked', 'quieted', 'ignored')),
|
||||
verdict_severity TEXT NOT NULL CHECK (verdict_severity IN ('critical', 'high', 'medium', 'low', 'info', 'none')),
|
||||
verdict_score NUMERIC(5, 2) NOT NULL CHECK (verdict_score >= 0 AND verdict_score <= 100),
|
||||
evaluated_at TIMESTAMPTZ NOT NULL,
|
||||
envelope JSONB NOT NULL,
|
||||
predicate_digest TEXT NOT NULL,
|
||||
determinism_hash TEXT,
|
||||
rekor_log_index BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes for common query patterns
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_run
|
||||
ON evidence_locker.verdict_attestations(run_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_finding
|
||||
ON evidence_locker.verdict_attestations(finding_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_tenant_evaluated
|
||||
ON evidence_locker.verdict_attestations(tenant_id, evaluated_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_tenant_status
|
||||
ON evidence_locker.verdict_attestations(tenant_id, verdict_status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_tenant_severity
|
||||
ON evidence_locker.verdict_attestations(tenant_id, verdict_severity);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_policy
|
||||
ON evidence_locker.verdict_attestations(policy_id, policy_version);
|
||||
|
||||
-- Create GIN index for JSONB envelope queries
|
||||
CREATE INDEX IF NOT EXISTS idx_verdict_attestations_envelope
|
||||
ON evidence_locker.verdict_attestations USING gin(envelope);
|
||||
|
||||
-- Create function for updating updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION evidence_locker.update_verdict_attestations_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to auto-update updated_at
|
||||
CREATE TRIGGER trigger_verdict_attestations_updated_at
|
||||
BEFORE UPDATE ON evidence_locker.verdict_attestations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION evidence_locker.update_verdict_attestations_updated_at();
|
||||
|
||||
-- Create view for verdict summary (without full envelope)
|
||||
CREATE OR REPLACE VIEW evidence_locker.verdict_attestations_summary AS
|
||||
SELECT
|
||||
verdict_id,
|
||||
tenant_id,
|
||||
run_id,
|
||||
policy_id,
|
||||
policy_version,
|
||||
finding_id,
|
||||
verdict_status,
|
||||
verdict_severity,
|
||||
verdict_score,
|
||||
evaluated_at,
|
||||
predicate_digest,
|
||||
determinism_hash,
|
||||
rekor_log_index,
|
||||
created_at
|
||||
FROM evidence_locker.verdict_attestations;
|
||||
|
||||
-- Grant permissions (adjust as needed)
|
||||
-- GRANT SELECT, INSERT ON evidence_locker.verdict_attestations TO evidence_locker_app;
|
||||
-- GRANT SELECT ON evidence_locker.verdict_attestations_summary TO evidence_locker_app;
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE evidence_locker.verdict_attestations IS
|
||||
'Stores DSSE-signed policy verdict attestations for audit and verification';
|
||||
|
||||
COMMENT ON COLUMN evidence_locker.verdict_attestations.verdict_id IS
|
||||
'Unique verdict identifier (format: verdict:run:{runId}:finding:{findingId})';
|
||||
|
||||
COMMENT ON COLUMN evidence_locker.verdict_attestations.envelope IS
|
||||
'DSSE envelope containing signed verdict predicate';
|
||||
|
||||
COMMENT ON COLUMN evidence_locker.verdict_attestations.predicate_digest IS
|
||||
'SHA256 digest of the canonical JSON predicate payload';
|
||||
|
||||
COMMENT ON COLUMN evidence_locker.verdict_attestations.determinism_hash IS
|
||||
'Determinism hash computed from sorted evidence digests and verdict components';
|
||||
|
||||
COMMENT ON COLUMN evidence_locker.verdict_attestations.rekor_log_index IS
|
||||
'Rekor transparency log index (if anchored), null for offline/air-gap deployments';
|
||||
41
devops/compose/postgres-init/12-integrations-table.sql
Normal file
41
devops/compose/postgres-init/12-integrations-table.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Integrations catalog bootstrap schema for compose environments.
|
||||
-- Creates the EF-backed integrations table when running without EF migrations.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS integrations
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
description VARCHAR(1024),
|
||||
type INTEGER NOT NULL,
|
||||
provider INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
endpoint VARCHAR(2048) NOT NULL,
|
||||
auth_ref_uri VARCHAR(1024),
|
||||
organization_id VARCHAR(256),
|
||||
config_json JSONB,
|
||||
last_health_status INTEGER NOT NULL DEFAULT 0,
|
||||
last_health_check_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
created_by VARCHAR(256),
|
||||
updated_by VARCHAR(256),
|
||||
tenant_id VARCHAR(128),
|
||||
tags JSONB,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_type
|
||||
ON integrations (type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_provider
|
||||
ON integrations (provider);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_status
|
||||
ON integrations (status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_integrations_tenant
|
||||
ON integrations (tenant_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_integrations_tenant_name_active
|
||||
ON integrations (tenant_id, name)
|
||||
WHERE is_deleted = FALSE;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE SCHEMA IF NOT EXISTS advisoryai;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pgvector extension unavailable in test DB image; AKS falls back to array embeddings.';
|
||||
END
|
||||
$$;
|
||||
Reference in New Issue
Block a user