feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration

- Add RateLimitConfig for configuration management with YAML binding support.
- Introduce RateLimitDecision to encapsulate the result of rate limit checks.
- Implement RateLimitMetrics for OpenTelemetry metrics tracking.
- Create RateLimitMiddleware for enforcing rate limits on incoming requests.
- Develop RateLimitService to orchestrate instance and environment rate limit checks.
- Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

@@ -0,0 +1,117 @@
-- Migration: 006_score_replay_tables.sql
-- Sprint: SPRINT_3401_0002_0001
-- Tasks: SCORE-REPLAY-007 (scan_manifest), SCORE-REPLAY-009 (proof_bundle)
-- Description: Tables for score replay and proof bundle functionality
-- Scan manifests for deterministic replay
CREATE TABLE IF NOT EXISTS scan_manifest (
manifest_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scan_id UUID NOT NULL,
manifest_hash VARCHAR(128) NOT NULL, -- SHA-256 of manifest content
sbom_hash VARCHAR(128) NOT NULL, -- Hash of input SBOM
rules_hash VARCHAR(128) NOT NULL, -- Hash of rules snapshot
feed_hash VARCHAR(128) NOT NULL, -- Hash of advisory feed snapshot
policy_hash VARCHAR(128) NOT NULL, -- Hash of scoring policy
-- Evidence timing
scan_started_at TIMESTAMPTZ NOT NULL,
scan_completed_at TIMESTAMPTZ,
-- Content (stored as JSONB for query flexibility)
manifest_content JSONB NOT NULL,
-- Metadata
scanner_version VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT fk_scan_manifest_scan FOREIGN KEY (scan_id) REFERENCES scans(scan_id) ON DELETE CASCADE
);
-- Index for manifest hash lookups (for deduplication and verification)
CREATE INDEX IF NOT EXISTS idx_scan_manifest_hash ON scan_manifest(manifest_hash);
-- Index for scan lookups
CREATE INDEX IF NOT EXISTS idx_scan_manifest_scan_id ON scan_manifest(scan_id);
-- Index for temporal queries
CREATE INDEX IF NOT EXISTS idx_scan_manifest_created_at ON scan_manifest(created_at DESC);
-- Proof bundles for cryptographic evidence chains
CREATE TABLE IF NOT EXISTS proof_bundle (
scan_id UUID NOT NULL,
root_hash VARCHAR(128) NOT NULL, -- Merkle root of all evidence
bundle_type VARCHAR(32) NOT NULL DEFAULT 'standard', -- 'standard', 'extended', 'minimal'
-- DSSE envelope for the bundle
dsse_envelope JSONB, -- Full DSSE-signed envelope
signature_keyid VARCHAR(256), -- Key ID used for signing
signature_algorithm VARCHAR(64), -- e.g., 'ed25519', 'rsa-pss-sha256'
-- Bundle content
bundle_content BYTEA, -- ZIP archive or raw bundle data
bundle_hash VARCHAR(128) NOT NULL, -- SHA-256 of bundle_content
-- Component hashes for incremental verification
ledger_hash VARCHAR(128), -- Hash of proof ledger
manifest_hash VARCHAR(128), -- Reference to scan_manifest
sbom_hash VARCHAR(128),
vex_hash VARCHAR(128),
-- Metadata
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ, -- Optional TTL for retention
-- Primary key is (scan_id, root_hash) to allow multiple bundles per scan
PRIMARY KEY (scan_id, root_hash),
-- Foreign key
CONSTRAINT fk_proof_bundle_scan FOREIGN KEY (scan_id) REFERENCES scans(scan_id) ON DELETE CASCADE
);
-- Index for root hash lookups (for verification)
CREATE INDEX IF NOT EXISTS idx_proof_bundle_root_hash ON proof_bundle(root_hash);
-- Index for temporal queries
CREATE INDEX IF NOT EXISTS idx_proof_bundle_created_at ON proof_bundle(created_at DESC);
-- Index for expiration cleanup
CREATE INDEX IF NOT EXISTS idx_proof_bundle_expires_at ON proof_bundle(expires_at) WHERE expires_at IS NOT NULL;
-- Score replay history for tracking rescores
CREATE TABLE IF NOT EXISTS score_replay_history (
replay_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scan_id UUID NOT NULL,
-- What triggered the replay
trigger_type VARCHAR(32) NOT NULL, -- 'feed_update', 'policy_change', 'manual', 'scheduled'
trigger_reference VARCHAR(256), -- Feed snapshot ID, policy version, etc.
-- Before/after state
original_manifest_hash VARCHAR(128),
replayed_manifest_hash VARCHAR(128),
-- Score delta summary
score_delta_json JSONB, -- Summary of changed scores
findings_added INT DEFAULT 0,
findings_removed INT DEFAULT 0,
findings_rescored INT DEFAULT 0,
-- Timing
replayed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
duration_ms INT,
-- Foreign key
CONSTRAINT fk_score_replay_scan FOREIGN KEY (scan_id) REFERENCES scans(scan_id) ON DELETE CASCADE
);
-- Index for scan-based lookups
CREATE INDEX IF NOT EXISTS idx_score_replay_scan_id ON score_replay_history(scan_id);
-- Index for temporal queries
CREATE INDEX IF NOT EXISTS idx_score_replay_replayed_at ON score_replay_history(replayed_at DESC);
-- Comments for documentation
COMMENT ON TABLE scan_manifest IS 'Deterministic scan manifests for score replay. Each manifest captures all inputs needed to reproduce a scan result.';
COMMENT ON TABLE proof_bundle IS 'Cryptographically-signed evidence bundles for audit trails. Contains DSSE-wrapped proof chains.';
COMMENT ON TABLE score_replay_history IS 'History of score replays triggered by feed updates, policy changes, or manual requests.';

View File

@@ -0,0 +1,64 @@
-- Migration: 007_unknowns_ranking_containment.sql
-- Sprint: SPRINT_3600_0002_0001
-- Task: UNK-RANK-005 - Add blast_radius, containment columns to unknowns table
-- Description: Extend unknowns table with ranking signals for containment-aware scoring
-- Add blast radius columns
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS blast_dependents INT DEFAULT 0;
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS blast_net_facing BOOLEAN DEFAULT false;
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS blast_privilege TEXT DEFAULT 'user';
-- Add exploit pressure columns
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS epss DOUBLE PRECISION;
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS kev BOOLEAN DEFAULT false;
-- Add containment signal columns
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS containment_seccomp TEXT DEFAULT 'unknown';
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS containment_fs TEXT DEFAULT 'unknown';
-- Add proof reference for ranking explanation
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS proof_ref TEXT;
-- Add evidence scarcity column (0-1 range)
ALTER TABLE unknowns ADD COLUMN IF NOT EXISTS evidence_scarcity DOUBLE PRECISION DEFAULT 0.5;
-- Update score index for efficient sorting
DROP INDEX IF EXISTS ix_unknowns_score_desc;
CREATE INDEX IF NOT EXISTS ix_unknowns_score_desc ON unknowns(score DESC);
-- Composite index for common query patterns
DROP INDEX IF EXISTS ix_unknowns_artifact_score;
CREATE INDEX IF NOT EXISTS ix_unknowns_artifact_score ON unknowns(artifact_digest, score DESC);
-- Index for filtering by containment state
DROP INDEX IF EXISTS ix_unknowns_containment;
CREATE INDEX IF NOT EXISTS ix_unknowns_containment ON unknowns(containment_seccomp, containment_fs);
-- Index for KEV filtering (high priority unknowns)
DROP INDEX IF EXISTS ix_unknowns_kev;
CREATE INDEX IF NOT EXISTS ix_unknowns_kev ON unknowns(kev) WHERE kev = true;
-- Comments for documentation
COMMENT ON COLUMN unknowns.blast_dependents IS 'Number of dependent packages affected by this unknown';
COMMENT ON COLUMN unknowns.blast_net_facing IS 'Whether the affected code is network-facing';
COMMENT ON COLUMN unknowns.blast_privilege IS 'Privilege level: root, user, unprivileged';
COMMENT ON COLUMN unknowns.epss IS 'EPSS score if available (0.0-1.0)';
COMMENT ON COLUMN unknowns.kev IS 'True if vulnerability is in CISA KEV catalog';
COMMENT ON COLUMN unknowns.containment_seccomp IS 'Seccomp state: enforced, permissive, unknown';
COMMENT ON COLUMN unknowns.containment_fs IS 'Filesystem state: ro (read-only), rw, unknown';
COMMENT ON COLUMN unknowns.proof_ref IS 'Path to proof bundle explaining ranking factors';
COMMENT ON COLUMN unknowns.evidence_scarcity IS 'Evidence scarcity factor (0=full evidence, 1=no evidence)';
-- Check constraint for valid privilege values
ALTER TABLE unknowns DROP CONSTRAINT IF EXISTS chk_unknowns_privilege;
ALTER TABLE unknowns ADD CONSTRAINT chk_unknowns_privilege
CHECK (blast_privilege IN ('root', 'user', 'unprivileged'));
-- Check constraint for valid containment values
ALTER TABLE unknowns DROP CONSTRAINT IF EXISTS chk_unknowns_seccomp;
ALTER TABLE unknowns ADD CONSTRAINT chk_unknowns_seccomp
CHECK (containment_seccomp IN ('enforced', 'permissive', 'unknown'));
ALTER TABLE unknowns DROP CONSTRAINT IF EXISTS chk_unknowns_fs;
ALTER TABLE unknowns ADD CONSTRAINT chk_unknowns_fs
CHECK (containment_fs IN ('ro', 'rw', 'unknown'));

View File

@@ -0,0 +1,292 @@
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Sprint: Advisory-derived
-- Task: EPSS Integration - Database Schema
-- Description: Creates tables for EPSS (Exploit Prediction Scoring System) integration
-- with time-series storage and change detection
-- ============================================================================
-- EPSS Import Provenance
-- ============================================================================
-- Tracks all EPSS import runs with full provenance for audit and replay
CREATE TABLE IF NOT EXISTS epss_import_runs (
import_run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
model_date DATE NOT NULL,
source_uri TEXT NOT NULL,
retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
file_sha256 TEXT NOT NULL,
decompressed_sha256 TEXT,
row_count INT NOT NULL,
model_version_tag TEXT, -- e.g., v2025.03.14 from leading # comment
published_date DATE, -- from leading # comment if present
status TEXT NOT NULL CHECK (status IN ('PENDING', 'SUCCEEDED', 'FAILED')),
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT epss_import_runs_model_date_unique UNIQUE (model_date)
);
CREATE INDEX IF NOT EXISTS idx_epss_import_runs_model_date
ON epss_import_runs (model_date DESC);
CREATE INDEX IF NOT EXISTS idx_epss_import_runs_status
ON epss_import_runs (status);
COMMENT ON TABLE epss_import_runs IS 'Provenance tracking for all EPSS import operations';
COMMENT ON COLUMN epss_import_runs.model_date IS 'The date of the EPSS model snapshot';
COMMENT ON COLUMN epss_import_runs.source_uri IS 'Source URL or bundle:// URI for the import';
COMMENT ON COLUMN epss_import_runs.file_sha256 IS 'SHA256 hash of the compressed file';
COMMENT ON COLUMN epss_import_runs.decompressed_sha256 IS 'SHA256 hash of the decompressed CSV';
-- ============================================================================
-- EPSS Time-Series Scores (Partitioned)
-- ============================================================================
-- Immutable append-only storage for all EPSS scores by date
-- Partitioned by month for efficient querying and maintenance
CREATE TABLE IF NOT EXISTS epss_scores (
model_date DATE NOT NULL,
cve_id TEXT NOT NULL,
epss_score DOUBLE PRECISION NOT NULL CHECK (epss_score >= 0 AND epss_score <= 1),
percentile DOUBLE PRECISION NOT NULL CHECK (percentile >= 0 AND percentile <= 1),
import_run_id UUID NOT NULL REFERENCES epss_import_runs(import_run_id),
PRIMARY KEY (model_date, cve_id)
) PARTITION BY RANGE (model_date);
-- Create partitions for current and next 6 months
-- Additional partitions should be created via scheduled maintenance
CREATE TABLE IF NOT EXISTS epss_scores_2025_12 PARTITION OF epss_scores
FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
CREATE TABLE IF NOT EXISTS epss_scores_2026_01 PARTITION OF epss_scores
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE IF NOT EXISTS epss_scores_2026_02 PARTITION OF epss_scores
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE IF NOT EXISTS epss_scores_2026_03 PARTITION OF epss_scores
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
CREATE TABLE IF NOT EXISTS epss_scores_2026_04 PARTITION OF epss_scores
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE IF NOT EXISTS epss_scores_2026_05 PARTITION OF epss_scores
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
-- Default partition for dates outside defined ranges
CREATE TABLE IF NOT EXISTS epss_scores_default PARTITION OF epss_scores DEFAULT;
CREATE INDEX IF NOT EXISTS idx_epss_scores_cve_id
ON epss_scores (cve_id);
CREATE INDEX IF NOT EXISTS idx_epss_scores_score_desc
ON epss_scores (epss_score DESC);
CREATE INDEX IF NOT EXISTS idx_epss_scores_cve_date
ON epss_scores (cve_id, model_date DESC);
COMMENT ON TABLE epss_scores IS 'Immutable time-series storage for all EPSS scores';
COMMENT ON COLUMN epss_scores.epss_score IS 'EPSS probability score (0.0 to 1.0)';
COMMENT ON COLUMN epss_scores.percentile IS 'Percentile rank vs all CVEs (0.0 to 1.0)';
-- ============================================================================
-- EPSS Current Projection (Fast Lookup)
-- ============================================================================
-- Materialized current EPSS for fast O(1) lookup
-- Updated during each import after delta computation
CREATE TABLE IF NOT EXISTS epss_current (
cve_id TEXT PRIMARY KEY,
epss_score DOUBLE PRECISION NOT NULL CHECK (epss_score >= 0 AND epss_score <= 1),
percentile DOUBLE PRECISION NOT NULL CHECK (percentile >= 0 AND percentile <= 1),
model_date DATE NOT NULL,
import_run_id UUID NOT NULL REFERENCES epss_import_runs(import_run_id),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_epss_current_score_desc
ON epss_current (epss_score DESC);
CREATE INDEX IF NOT EXISTS idx_epss_current_percentile_desc
ON epss_current (percentile DESC);
CREATE INDEX IF NOT EXISTS idx_epss_current_model_date
ON epss_current (model_date);
COMMENT ON TABLE epss_current IS 'Fast lookup projection of latest EPSS scores';
-- ============================================================================
-- EPSS Change Detection (Partitioned)
-- ============================================================================
-- Tracks daily changes to enable efficient targeted enrichment
CREATE TABLE IF NOT EXISTS epss_changes (
model_date DATE NOT NULL,
cve_id TEXT NOT NULL,
old_score DOUBLE PRECISION,
new_score DOUBLE PRECISION NOT NULL,
delta_score DOUBLE PRECISION,
old_percentile DOUBLE PRECISION,
new_percentile DOUBLE PRECISION NOT NULL,
delta_percentile DOUBLE PRECISION,
flags INT NOT NULL DEFAULT 0,
import_run_id UUID NOT NULL REFERENCES epss_import_runs(import_run_id),
PRIMARY KEY (model_date, cve_id)
) PARTITION BY RANGE (model_date);
-- Create partitions matching epss_scores
CREATE TABLE IF NOT EXISTS epss_changes_2025_12 PARTITION OF epss_changes
FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
CREATE TABLE IF NOT EXISTS epss_changes_2026_01 PARTITION OF epss_changes
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE IF NOT EXISTS epss_changes_2026_02 PARTITION OF epss_changes
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE IF NOT EXISTS epss_changes_2026_03 PARTITION OF epss_changes
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
CREATE TABLE IF NOT EXISTS epss_changes_2026_04 PARTITION OF epss_changes
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE IF NOT EXISTS epss_changes_2026_05 PARTITION OF epss_changes
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE IF NOT EXISTS epss_changes_default PARTITION OF epss_changes DEFAULT;
-- Flags bitmask values:
-- 0x01 = NEW_SCORED (CVE newly scored)
-- 0x02 = CROSSED_HIGH (crossed above high score threshold)
-- 0x04 = CROSSED_LOW (crossed below high score threshold)
-- 0x08 = BIG_JUMP_UP (delta > 0.10 upward)
-- 0x10 = BIG_JUMP_DOWN (delta > 0.10 downward)
-- 0x20 = TOP_PERCENTILE (entered top 5%)
-- 0x40 = LEFT_TOP_PERCENTILE (left top 5%)
CREATE INDEX IF NOT EXISTS idx_epss_changes_flags
ON epss_changes (flags) WHERE flags > 0;
CREATE INDEX IF NOT EXISTS idx_epss_changes_delta
ON epss_changes (ABS(delta_score) DESC) WHERE delta_score IS NOT NULL;
COMMENT ON TABLE epss_changes IS 'Daily change detection for targeted enrichment';
COMMENT ON COLUMN epss_changes.flags IS 'Bitmask: 0x01=NEW, 0x02=CROSSED_HIGH, 0x04=CROSSED_LOW, 0x08=BIG_UP, 0x10=BIG_DOWN, 0x20=TOP_PCT';
-- ============================================================================
-- EPSS Configuration
-- ============================================================================
-- Per-org or global thresholds for notification and scoring
CREATE TABLE IF NOT EXISTS epss_config (
config_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID, -- NULL for global defaults
high_percentile DOUBLE PRECISION NOT NULL DEFAULT 0.95,
high_score DOUBLE PRECISION NOT NULL DEFAULT 0.50,
big_jump_delta DOUBLE PRECISION NOT NULL DEFAULT 0.10,
score_weight DOUBLE PRECISION NOT NULL DEFAULT 0.25,
notify_on_new_high BOOLEAN NOT NULL DEFAULT true,
notify_on_crossing BOOLEAN NOT NULL DEFAULT true,
notify_on_big_jump BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT epss_config_org_unique UNIQUE (org_id)
);
-- Insert global defaults
INSERT INTO epss_config (org_id, high_percentile, high_score, big_jump_delta, score_weight)
VALUES (NULL, 0.95, 0.50, 0.10, 0.25)
ON CONFLICT (org_id) DO NOTHING;
COMMENT ON TABLE epss_config IS 'EPSS notification and scoring thresholds';
COMMENT ON COLUMN epss_config.high_percentile IS 'Threshold for top percentile alerts (default: 0.95 = top 5%)';
COMMENT ON COLUMN epss_config.high_score IS 'Threshold for high score alerts (default: 0.50)';
COMMENT ON COLUMN epss_config.big_jump_delta IS 'Threshold for significant daily change (default: 0.10)';
-- ============================================================================
-- EPSS Evidence on Scan Findings
-- ============================================================================
-- Add EPSS-at-scan columns to existing scan_findings if not exists
-- This preserves immutable evidence for replay
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'scan_findings' AND column_name = 'epss_score_at_scan'
) THEN
ALTER TABLE scan_findings
ADD COLUMN epss_score_at_scan DOUBLE PRECISION,
ADD COLUMN epss_percentile_at_scan DOUBLE PRECISION,
ADD COLUMN epss_model_date_at_scan DATE,
ADD COLUMN epss_import_run_id_at_scan UUID;
END IF;
END $$;
-- ============================================================================
-- Helper Functions
-- ============================================================================
-- Function to compute change flags
CREATE OR REPLACE FUNCTION compute_epss_change_flags(
p_old_score DOUBLE PRECISION,
p_new_score DOUBLE PRECISION,
p_old_percentile DOUBLE PRECISION,
p_new_percentile DOUBLE PRECISION,
p_high_score DOUBLE PRECISION DEFAULT 0.50,
p_high_percentile DOUBLE PRECISION DEFAULT 0.95,
p_big_jump DOUBLE PRECISION DEFAULT 0.10
) RETURNS INT AS $$
DECLARE
v_flags INT := 0;
v_delta DOUBLE PRECISION;
BEGIN
-- NEW_SCORED
IF p_old_score IS NULL THEN
v_flags := v_flags | 1; -- 0x01
END IF;
-- CROSSED_HIGH (score)
IF p_old_score IS NOT NULL AND p_old_score < p_high_score AND p_new_score >= p_high_score THEN
v_flags := v_flags | 2; -- 0x02
END IF;
-- CROSSED_LOW (score)
IF p_old_score IS NOT NULL AND p_old_score >= p_high_score AND p_new_score < p_high_score THEN
v_flags := v_flags | 4; -- 0x04
END IF;
-- BIG_JUMP_UP
IF p_old_score IS NOT NULL THEN
v_delta := p_new_score - p_old_score;
IF v_delta > p_big_jump THEN
v_flags := v_flags | 8; -- 0x08
END IF;
-- BIG_JUMP_DOWN
IF v_delta < -p_big_jump THEN
v_flags := v_flags | 16; -- 0x10
END IF;
END IF;
-- TOP_PERCENTILE (entered)
IF (p_old_percentile IS NULL OR p_old_percentile < p_high_percentile)
AND p_new_percentile >= p_high_percentile THEN
v_flags := v_flags | 32; -- 0x20
END IF;
-- LEFT_TOP_PERCENTILE
IF p_old_percentile IS NOT NULL AND p_old_percentile >= p_high_percentile
AND p_new_percentile < p_high_percentile THEN
v_flags := v_flags | 64; -- 0x40
END IF;
RETURN v_flags;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
COMMENT ON FUNCTION compute_epss_change_flags IS 'Computes bitmask flags for EPSS change detection';
-- Function to create monthly partition
CREATE OR REPLACE FUNCTION create_epss_partition(p_year INT, p_month INT)
RETURNS VOID AS $$
DECLARE
v_start DATE;
v_end DATE;
v_partition_name TEXT;
BEGIN
v_start := make_date(p_year, p_month, 1);
v_end := v_start + INTERVAL '1 month';
v_partition_name := format('epss_scores_%s_%s', p_year, LPAD(p_month::TEXT, 2, '0'));
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF epss_scores FOR VALUES FROM (%L) TO (%L)',
v_partition_name, v_start, v_end
);
v_partition_name := format('epss_changes_%s_%s', p_year, LPAD(p_month::TEXT, 2, '0'));
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF epss_changes FOR VALUES FROM (%L) TO (%L)',
v_partition_name, v_start, v_end
);
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION create_epss_partition IS 'Creates monthly partitions for EPSS tables';

View File

@@ -6,4 +6,8 @@ internal static class MigrationIds
public const string ProofSpineTables = "002_proof_spine_tables.sql";
public const string ClassificationHistory = "003_classification_history.sql";
public const string ScanMetrics = "004_scan_metrics.sql";
public const string SmartDiffTables = "005_smart_diff_tables.sql";
public const string ScoreReplayTables = "006_score_replay_tables.sql";
public const string UnknownsRankingContainment = "007_unknowns_ranking_containment.sql";
public const string EpssIntegration = "008_epss_integration.sql";
}