tests fixes and sprints work
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
-- Release Orchestrator Schema Migration 012: Analytics Schema Foundation
|
||||
-- Creates analytics schema, version tracking, enums, and audit helpers.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-001)
|
||||
|
||||
-- ============================================================================
|
||||
-- Extensions
|
||||
-- ============================================================================
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ============================================================================
|
||||
-- Schema
|
||||
-- ============================================================================
|
||||
CREATE SCHEMA IF NOT EXISTS analytics;
|
||||
|
||||
COMMENT ON SCHEMA analytics IS 'Analytics star-schema for SBOM, attestation, and vulnerability data';
|
||||
|
||||
-- ============================================================================
|
||||
-- Version Tracking
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS analytics.schema_version (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
description TEXT
|
||||
);
|
||||
|
||||
INSERT INTO analytics.schema_version (version, description)
|
||||
VALUES ('1.0.0', 'Initial analytics schema foundation')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- Enums
|
||||
-- ============================================================================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'analytics_component_type') THEN
|
||||
CREATE TYPE analytics_component_type AS ENUM (
|
||||
'library',
|
||||
'application',
|
||||
'container',
|
||||
'framework',
|
||||
'operating-system',
|
||||
'device',
|
||||
'firmware',
|
||||
'file'
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'analytics_license_category') THEN
|
||||
CREATE TYPE analytics_license_category AS ENUM (
|
||||
'permissive',
|
||||
'copyleft-weak',
|
||||
'copyleft-strong',
|
||||
'proprietary',
|
||||
'unknown'
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'analytics_severity') THEN
|
||||
CREATE TYPE analytics_severity AS ENUM (
|
||||
'critical',
|
||||
'high',
|
||||
'medium',
|
||||
'low',
|
||||
'none',
|
||||
'unknown'
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'analytics_attestation_type') THEN
|
||||
CREATE TYPE analytics_attestation_type AS ENUM (
|
||||
'provenance',
|
||||
'sbom',
|
||||
'vex',
|
||||
'build',
|
||||
'scan',
|
||||
'policy'
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Audit Helpers
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION analytics.update_updated_at_column()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION analytics.update_updated_at_column IS
|
||||
'Trigger helper for analytics tables to keep updated_at current';
|
||||
@@ -0,0 +1,134 @@
|
||||
-- Release Orchestrator Schema Migration 013: Analytics Component Registry
|
||||
-- Creates analytics.components and normalization helpers.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-002)
|
||||
|
||||
-- ============================================================================
|
||||
-- Normalization Functions
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION analytics.normalize_supplier(raw_supplier TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
BEGIN
|
||||
IF raw_supplier IS NULL OR raw_supplier = '' THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
RETURN LOWER(TRIM(
|
||||
REGEXP_REPLACE(
|
||||
REGEXP_REPLACE(raw_supplier, '\s+(Inc\.?|LLC|Ltd\.?|Corp\.?|GmbH|B\.V\.|S\.A\.|PLC|Co\.)$', '', 'i'),
|
||||
'\s+', ' ', 'g'
|
||||
)
|
||||
));
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.categorize_license(license_expr TEXT)
|
||||
RETURNS analytics_license_category AS $$
|
||||
BEGIN
|
||||
IF license_expr IS NULL OR license_expr = '' THEN
|
||||
RETURN 'unknown';
|
||||
END IF;
|
||||
|
||||
IF license_expr ~* '(^GPL-[23]|AGPL|OSL|SSPL|EUPL|RPL|QPL|Sleepycat)' AND
|
||||
license_expr !~* 'WITH.*exception|WITH.*linking.*exception|WITH.*classpath.*exception' THEN
|
||||
RETURN 'copyleft-strong';
|
||||
END IF;
|
||||
|
||||
IF license_expr ~* '(LGPL|MPL|EPL|CPL|CDDL|Artistic|MS-RL|APSL|IPL|SPL)' THEN
|
||||
RETURN 'copyleft-weak';
|
||||
END IF;
|
||||
|
||||
IF license_expr ~* '(MIT|Apache|BSD|ISC|Zlib|Unlicense|CC0|WTFPL|0BSD|PostgreSQL|X11|Beerware|FTL|HPND|NTP|UPL)' THEN
|
||||
RETURN 'permissive';
|
||||
END IF;
|
||||
|
||||
IF license_expr ~* '(proprietary|commercial|all.rights.reserved|see.license|custom|confidential)' THEN
|
||||
RETURN 'proprietary';
|
||||
END IF;
|
||||
|
||||
IF license_expr ~* 'GPL.*WITH.*exception' THEN
|
||||
RETURN 'copyleft-weak';
|
||||
END IF;
|
||||
|
||||
RETURN 'unknown';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.parse_purl(purl TEXT)
|
||||
RETURNS TABLE (purl_type TEXT, purl_namespace TEXT, purl_name TEXT, purl_version TEXT) AS $$
|
||||
DECLARE
|
||||
name_part TEXT;
|
||||
BEGIN
|
||||
IF purl IS NULL OR purl = '' THEN
|
||||
RETURN QUERY SELECT NULL::TEXT, NULL::TEXT, NULL::TEXT, NULL::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
purl_type := SUBSTRING(purl FROM 'pkg:([^/]+)/');
|
||||
purl_version := SUBSTRING(purl FROM '@([^?#]+)');
|
||||
|
||||
name_part := REGEXP_REPLACE(purl, '@[^?#]+', '');
|
||||
name_part := REGEXP_REPLACE(name_part, '\?.*$', '');
|
||||
name_part := REGEXP_REPLACE(name_part, '#.*$', '');
|
||||
name_part := REGEXP_REPLACE(name_part, '^pkg:[^/]+/', '');
|
||||
|
||||
IF name_part ~ '/' THEN
|
||||
purl_namespace := SUBSTRING(name_part FROM '^([^/]+)/');
|
||||
purl_name := SUBSTRING(name_part FROM '/([^/]+)$');
|
||||
ELSE
|
||||
purl_namespace := NULL;
|
||||
purl_name := name_part;
|
||||
END IF;
|
||||
|
||||
RETURN QUERY SELECT purl_type, purl_namespace, purl_name, purl_version;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
|
||||
|
||||
-- ============================================================================
|
||||
-- Component Registry
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS analytics.components (
|
||||
component_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
purl TEXT NOT NULL,
|
||||
purl_type TEXT NOT NULL,
|
||||
purl_namespace TEXT,
|
||||
purl_name TEXT NOT NULL,
|
||||
purl_version TEXT,
|
||||
hash_sha256 TEXT,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT,
|
||||
description TEXT,
|
||||
component_type analytics_component_type NOT NULL DEFAULT 'library',
|
||||
supplier TEXT,
|
||||
supplier_normalized TEXT,
|
||||
license_declared TEXT,
|
||||
license_concluded TEXT,
|
||||
license_category analytics_license_category DEFAULT 'unknown',
|
||||
cpe TEXT,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
sbom_count INT NOT NULL DEFAULT 1,
|
||||
artifact_count INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (purl, hash_sha256)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_purl
|
||||
ON analytics.components(purl);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_supplier
|
||||
ON analytics.components(supplier_normalized);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_license
|
||||
ON analytics.components(license_category, license_concluded);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_type
|
||||
ON analytics.components(component_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_purl_type
|
||||
ON analytics.components(purl_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_hash
|
||||
ON analytics.components(hash_sha256)
|
||||
WHERE hash_sha256 IS NOT NULL;
|
||||
@@ -0,0 +1,47 @@
|
||||
-- Release Orchestrator Schema Migration 014: Analytics Artifacts
|
||||
-- Creates analytics.artifacts for container and application inventory.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-003)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.artifacts (
|
||||
artifact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artifact_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT,
|
||||
digest TEXT,
|
||||
purl TEXT,
|
||||
source_repo TEXT,
|
||||
source_ref TEXT,
|
||||
registry TEXT,
|
||||
environment TEXT,
|
||||
team TEXT,
|
||||
service TEXT,
|
||||
deployed_at TIMESTAMPTZ,
|
||||
sbom_digest TEXT,
|
||||
sbom_format TEXT,
|
||||
sbom_spec_version TEXT,
|
||||
component_count INT DEFAULT 0,
|
||||
vulnerability_count INT DEFAULT 0,
|
||||
critical_count INT DEFAULT 0,
|
||||
high_count INT DEFAULT 0,
|
||||
provenance_attested BOOLEAN DEFAULT FALSE,
|
||||
slsa_level INT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (digest)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_name_version
|
||||
ON analytics.artifacts(name, version);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_environment
|
||||
ON analytics.artifacts(environment);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_team
|
||||
ON analytics.artifacts(team);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_deployed
|
||||
ON analytics.artifacts(deployed_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_digest
|
||||
ON analytics.artifacts(digest);
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Release Orchestrator Schema Migration 015: Analytics Artifact-Component Bridge
|
||||
-- Creates analytics.artifact_components for SBOM component linkage.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-004)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.artifact_components (
|
||||
artifact_id UUID NOT NULL REFERENCES analytics.artifacts(artifact_id) ON DELETE CASCADE,
|
||||
component_id UUID NOT NULL REFERENCES analytics.components(component_id) ON DELETE CASCADE,
|
||||
bom_ref TEXT,
|
||||
scope TEXT,
|
||||
dependency_path TEXT[],
|
||||
depth INT DEFAULT 0,
|
||||
introduced_via TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (artifact_id, component_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifact_components_component
|
||||
ON analytics.artifact_components(component_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifact_components_depth
|
||||
ON analytics.artifact_components(depth);
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Release Orchestrator Schema Migration 016: Analytics Component Vulnerabilities
|
||||
-- Creates analytics.component_vulns for vulnerability correlation.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-005)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.component_vulns (
|
||||
component_id UUID NOT NULL REFERENCES analytics.components(component_id) ON DELETE CASCADE,
|
||||
vuln_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
severity analytics_severity NOT NULL,
|
||||
cvss_score NUMERIC(3,1),
|
||||
cvss_vector TEXT,
|
||||
epss_score NUMERIC(5,4),
|
||||
kev_listed BOOLEAN DEFAULT FALSE,
|
||||
affects BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
affected_versions TEXT,
|
||||
fixed_version TEXT,
|
||||
fix_available BOOLEAN DEFAULT FALSE,
|
||||
introduced_via TEXT,
|
||||
published_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (component_id, vuln_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_vuln
|
||||
ON analytics.component_vulns(vuln_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_severity
|
||||
ON analytics.component_vulns(severity, cvss_score DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_fixable
|
||||
ON analytics.component_vulns(fix_available)
|
||||
WHERE fix_available = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_kev
|
||||
ON analytics.component_vulns(kev_listed)
|
||||
WHERE kev_listed = TRUE;
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Release Orchestrator Schema Migration 017: Analytics Attestations
|
||||
-- Creates analytics.attestations for DSSE predicate tracking.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-006)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.attestations (
|
||||
attestation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artifact_id UUID REFERENCES analytics.artifacts(artifact_id) ON DELETE SET NULL,
|
||||
predicate_type analytics_attestation_type NOT NULL,
|
||||
predicate_uri TEXT NOT NULL,
|
||||
issuer TEXT,
|
||||
issuer_normalized TEXT,
|
||||
builder_id TEXT,
|
||||
slsa_level INT,
|
||||
dsse_payload_hash TEXT NOT NULL,
|
||||
dsse_sig_algorithm TEXT,
|
||||
rekor_log_id TEXT,
|
||||
rekor_log_index BIGINT,
|
||||
statement_time TIMESTAMPTZ,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verification_time TIMESTAMPTZ,
|
||||
materials_hash TEXT,
|
||||
source_uri TEXT,
|
||||
workflow_ref TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (dsse_payload_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_attestations_artifact
|
||||
ON analytics.attestations(artifact_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_attestations_type
|
||||
ON analytics.attestations(predicate_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_attestations_issuer
|
||||
ON analytics.attestations(issuer_normalized);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_attestations_rekor
|
||||
ON analytics.attestations(rekor_log_id)
|
||||
WHERE rekor_log_id IS NOT NULL;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Release Orchestrator Schema Migration 018: Analytics VEX Overrides
|
||||
-- Creates analytics.vex_overrides for attestation-based vulnerability decisions.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-007)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.vex_overrides (
|
||||
override_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
attestation_id UUID REFERENCES analytics.attestations(attestation_id) ON DELETE SET NULL,
|
||||
artifact_id UUID REFERENCES analytics.artifacts(artifact_id) ON DELETE CASCADE,
|
||||
vuln_id TEXT NOT NULL,
|
||||
component_purl TEXT,
|
||||
status TEXT NOT NULL,
|
||||
justification TEXT,
|
||||
justification_detail TEXT,
|
||||
impact TEXT,
|
||||
action_statement TEXT,
|
||||
operator_id TEXT,
|
||||
confidence NUMERIC(3,2),
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_until TIMESTAMPTZ,
|
||||
last_reviewed TIMESTAMPTZ,
|
||||
review_count INT DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_artifact_vuln
|
||||
ON analytics.vex_overrides(artifact_id, vuln_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_vuln
|
||||
ON analytics.vex_overrides(vuln_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_status
|
||||
ON analytics.vex_overrides(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_active
|
||||
ON analytics.vex_overrides(artifact_id, vuln_id)
|
||||
WHERE valid_until IS NULL OR valid_until > now();
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Release Orchestrator Schema Migration 019: Analytics Raw Payloads
|
||||
-- Creates raw SBOM and attestation storage tables for audit and reprocessing.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-008)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.raw_sboms (
|
||||
sbom_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artifact_id UUID REFERENCES analytics.artifacts(artifact_id) ON DELETE SET NULL,
|
||||
format TEXT NOT NULL,
|
||||
spec_version TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL UNIQUE,
|
||||
content_size BIGINT NOT NULL,
|
||||
storage_uri TEXT NOT NULL,
|
||||
ingest_version TEXT NOT NULL,
|
||||
schema_version TEXT NOT NULL,
|
||||
ingested_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_raw_sboms_artifact
|
||||
ON analytics.raw_sboms(artifact_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_raw_sboms_hash
|
||||
ON analytics.raw_sboms(content_hash);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.raw_attestations (
|
||||
raw_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
attestation_id UUID REFERENCES analytics.attestations(attestation_id) ON DELETE SET NULL,
|
||||
content_hash TEXT NOT NULL UNIQUE,
|
||||
content_size BIGINT NOT NULL,
|
||||
storage_uri TEXT NOT NULL,
|
||||
ingest_version TEXT NOT NULL,
|
||||
schema_version TEXT NOT NULL,
|
||||
ingested_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_raw_attestations_attestation
|
||||
ON analytics.raw_attestations(attestation_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_raw_attestations_hash
|
||||
ON analytics.raw_attestations(content_hash);
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Release Orchestrator Schema Migration 020: Analytics Rollups
|
||||
-- Creates daily rollup tables and compute function.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-009)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.daily_vulnerability_counts (
|
||||
snapshot_date DATE NOT NULL,
|
||||
environment TEXT NOT NULL,
|
||||
team TEXT,
|
||||
severity analytics_severity NOT NULL,
|
||||
total_vulns INT NOT NULL,
|
||||
fixable_vulns INT NOT NULL,
|
||||
vex_mitigated INT NOT NULL,
|
||||
kev_vulns INT NOT NULL,
|
||||
unique_cves INT NOT NULL,
|
||||
affected_artifacts INT NOT NULL,
|
||||
affected_components INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (snapshot_date, environment, COALESCE(team, ''), severity)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_daily_vuln_counts_date
|
||||
ON analytics.daily_vulnerability_counts (snapshot_date DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_daily_vuln_counts_env
|
||||
ON analytics.daily_vulnerability_counts (environment, snapshot_date DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics.daily_component_counts (
|
||||
snapshot_date DATE NOT NULL,
|
||||
environment TEXT NOT NULL,
|
||||
team TEXT,
|
||||
license_category analytics_license_category NOT NULL,
|
||||
component_type analytics_component_type NOT NULL,
|
||||
total_components INT NOT NULL,
|
||||
unique_suppliers INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (snapshot_date, environment, COALESCE(team, ''), license_category, component_type)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_daily_comp_counts_date
|
||||
ON analytics.daily_component_counts (snapshot_date DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.compute_daily_rollups(p_date DATE DEFAULT CURRENT_DATE)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO analytics.daily_vulnerability_counts (
|
||||
snapshot_date, environment, team, severity,
|
||||
total_vulns, fixable_vulns, vex_mitigated, kev_vulns,
|
||||
unique_cves, affected_artifacts, affected_components
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
cv.severity,
|
||||
COUNT(*) AS total_vulns,
|
||||
COUNT(*) FILTER (WHERE cv.fix_available = TRUE) AS fixable_vulns,
|
||||
COUNT(*) FILTER (WHERE EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = a.artifact_id AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
)) AS vex_mitigated,
|
||||
COUNT(*) FILTER (WHERE cv.kev_listed = TRUE) AS kev_vulns,
|
||||
COUNT(DISTINCT cv.vuln_id) AS unique_cves,
|
||||
COUNT(DISTINCT a.artifact_id) AS affected_artifacts,
|
||||
COUNT(DISTINCT cv.component_id) AS affected_components
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.component_vulns cv ON cv.component_id = ac.component_id AND cv.affects = TRUE
|
||||
GROUP BY a.environment, a.team, cv.severity
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), severity)
|
||||
DO UPDATE SET
|
||||
total_vulns = EXCLUDED.total_vulns,
|
||||
fixable_vulns = EXCLUDED.fixable_vulns,
|
||||
vex_mitigated = EXCLUDED.vex_mitigated,
|
||||
kev_vulns = EXCLUDED.kev_vulns,
|
||||
unique_cves = EXCLUDED.unique_cves,
|
||||
affected_artifacts = EXCLUDED.affected_artifacts,
|
||||
affected_components = EXCLUDED.affected_components,
|
||||
created_at = now();
|
||||
|
||||
INSERT INTO analytics.daily_component_counts (
|
||||
snapshot_date, environment, team, license_category, component_type,
|
||||
total_components, unique_suppliers
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
c.license_category,
|
||||
c.component_type,
|
||||
COUNT(DISTINCT c.component_id) AS total_components,
|
||||
COUNT(DISTINCT c.supplier_normalized) AS unique_suppliers
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.components c ON c.component_id = ac.component_id
|
||||
GROUP BY a.environment, a.team, c.license_category, c.component_type
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), license_category, component_type)
|
||||
DO UPDATE SET
|
||||
total_components = EXCLUDED.total_components,
|
||||
unique_suppliers = EXCLUDED.unique_suppliers,
|
||||
created_at = now();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,102 @@
|
||||
-- Release Orchestrator Schema Migration 021: Analytics Materialized Views
|
||||
-- Creates materialized views for dashboard queries.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-010..013)
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.mv_supplier_concentration AS
|
||||
SELECT
|
||||
c.supplier_normalized AS supplier,
|
||||
COUNT(DISTINCT c.component_id) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
COUNT(DISTINCT a.team) AS team_count,
|
||||
ARRAY_AGG(DISTINCT a.environment) FILTER (WHERE a.environment IS NOT NULL) AS environments,
|
||||
SUM(CASE WHEN cv.severity = 'critical' THEN 1 ELSE 0 END) AS critical_vuln_count,
|
||||
SUM(CASE WHEN cv.severity = 'high' THEN 1 ELSE 0 END) AS high_vuln_count,
|
||||
MAX(c.last_seen_at) AS last_seen_at
|
||||
FROM analytics.components c
|
||||
LEFT JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
LEFT JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.component_vulns cv ON cv.component_id = c.component_id AND cv.affects = TRUE
|
||||
WHERE c.supplier_normalized IS NOT NULL
|
||||
GROUP BY c.supplier_normalized
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_supplier_concentration_supplier
|
||||
ON analytics.mv_supplier_concentration (supplier);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.mv_license_distribution AS
|
||||
SELECT
|
||||
c.license_concluded,
|
||||
c.license_category,
|
||||
COUNT(*) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
ARRAY_AGG(DISTINCT c.purl_type) FILTER (WHERE c.purl_type IS NOT NULL) AS ecosystems
|
||||
FROM analytics.components c
|
||||
LEFT JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
GROUP BY c.license_concluded, c.license_category
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_license_distribution_license
|
||||
ON analytics.mv_license_distribution (COALESCE(license_concluded, ''), license_category);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.mv_vuln_exposure AS
|
||||
SELECT
|
||||
cv.vuln_id,
|
||||
cv.severity,
|
||||
cv.cvss_score,
|
||||
cv.epss_score,
|
||||
cv.kev_listed,
|
||||
cv.fix_available,
|
||||
COUNT(DISTINCT cv.component_id) AS raw_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS raw_artifact_count,
|
||||
COUNT(DISTINCT cv.component_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_artifact_count
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = cv.component_id
|
||||
WHERE cv.affects = TRUE
|
||||
GROUP BY cv.vuln_id, cv.severity, cv.cvss_score, cv.epss_score, cv.kev_listed, cv.fix_available
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_vuln_exposure_key
|
||||
ON analytics.mv_vuln_exposure (vuln_id, severity, cvss_score, epss_score, kev_listed, fix_available);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.mv_attestation_coverage AS
|
||||
SELECT
|
||||
a.environment,
|
||||
a.team,
|
||||
COUNT(*) AS total_artifacts,
|
||||
COUNT(*) FILTER (WHERE a.provenance_attested = TRUE) AS with_provenance,
|
||||
COUNT(*) FILTER (WHERE EXISTS (
|
||||
SELECT 1 FROM analytics.attestations att
|
||||
WHERE att.artifact_id = a.artifact_id AND att.predicate_type = 'sbom'
|
||||
)) AS with_sbom_attestation,
|
||||
COUNT(*) FILTER (WHERE EXISTS (
|
||||
SELECT 1 FROM analytics.attestations att
|
||||
WHERE att.artifact_id = a.artifact_id AND att.predicate_type = 'vex'
|
||||
)) AS with_vex_attestation,
|
||||
COUNT(*) FILTER (WHERE a.slsa_level >= 2) AS slsa_level_2_plus,
|
||||
COUNT(*) FILTER (WHERE a.slsa_level >= 3) AS slsa_level_3_plus,
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE a.provenance_attested = TRUE) / NULLIF(COUNT(*), 0), 1) AS provenance_pct,
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE a.slsa_level >= 2) / NULLIF(COUNT(*), 0), 1) AS slsa2_pct
|
||||
FROM analytics.artifacts a
|
||||
GROUP BY a.environment, a.team
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_attestation_coverage_key
|
||||
ON analytics.mv_attestation_coverage (environment, COALESCE(team, ''));
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Release Orchestrator Schema Migration 022: Analytics Refresh Procedures
|
||||
-- Creates helper procedures for refreshing analytics materialized views.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-010..013)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.refresh_all_views()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_supplier_concentration;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_license_distribution;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_vuln_exposure;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_attestation_coverage;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,198 @@
|
||||
-- Release Orchestrator Schema Migration 023: Analytics Stored Procedures
|
||||
-- Creates Day-1 query procedures returning JSON.
|
||||
-- Compliant with docs/db/analytics_schema.sql
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
-- Top suppliers by component count
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_top_suppliers(p_limit INT DEFAULT 20)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
supplier,
|
||||
component_count,
|
||||
artifact_count,
|
||||
team_count,
|
||||
critical_vuln_count,
|
||||
high_vuln_count,
|
||||
environments
|
||||
FROM analytics.mv_supplier_concentration
|
||||
ORDER BY component_count DESC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_top_suppliers IS
|
||||
'Get top suppliers by component count for supply chain risk analysis';
|
||||
|
||||
-- License distribution heatmap
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_license_heatmap()
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
license_category,
|
||||
license_concluded,
|
||||
component_count,
|
||||
artifact_count,
|
||||
ecosystems
|
||||
FROM analytics.mv_license_distribution
|
||||
ORDER BY component_count DESC
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_license_heatmap IS
|
||||
'Get license distribution for compliance heatmap';
|
||||
|
||||
-- CVE exposure adjusted by VEX
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_vuln_exposure(
|
||||
p_environment TEXT DEFAULT NULL,
|
||||
p_min_severity TEXT DEFAULT 'low'
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM analytics.mv_vuln_exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND severity::TEXT >= p_min_severity
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
ELSE 5
|
||||
END,
|
||||
effective_artifact_count DESC
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_vuln_exposure IS
|
||||
'Get CVE exposure with VEX-adjusted counts';
|
||||
|
||||
-- Fixable backlog
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_fixable_backlog(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
a.name AS service,
|
||||
a.environment,
|
||||
c.name AS component,
|
||||
c.version,
|
||||
cv.vuln_id,
|
||||
cv.severity::TEXT,
|
||||
cv.fixed_version
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.components c ON c.component_id = cv.component_id
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.vex_overrides vo ON vo.artifact_id = a.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.affects = TRUE
|
||||
AND cv.fix_available = TRUE
|
||||
AND vo.override_id IS NULL
|
||||
AND (p_environment IS NULL OR a.environment = p_environment)
|
||||
ORDER BY
|
||||
CASE cv.severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
a.name
|
||||
LIMIT 100
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_fixable_backlog IS
|
||||
'Get vulnerabilities with available fixes that are not VEX-mitigated';
|
||||
|
||||
-- Attestation coverage gaps
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_attestation_gaps(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
environment,
|
||||
team,
|
||||
total_artifacts,
|
||||
with_provenance,
|
||||
provenance_pct,
|
||||
slsa_level_2_plus,
|
||||
slsa2_pct,
|
||||
total_artifacts - with_provenance AS missing_provenance
|
||||
FROM analytics.mv_attestation_coverage
|
||||
WHERE (p_environment IS NULL OR environment = p_environment)
|
||||
ORDER BY provenance_pct ASC
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_attestation_gaps IS
|
||||
'Get attestation coverage gaps by environment/team';
|
||||
|
||||
-- MTTR by severity (simplified - requires proper remediation tracking)
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_mttr_by_severity(p_days INT DEFAULT 90)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
severity::TEXT,
|
||||
COUNT(*) AS total_vulns,
|
||||
AVG(EXTRACT(EPOCH FROM (vo.valid_from - cv.published_at)) / 86400)::NUMERIC(10,2) AS avg_days_to_mitigate
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.vex_overrides vo ON vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
WHERE cv.published_at >= now() - (p_days || ' days')::INTERVAL
|
||||
AND cv.published_at IS NOT NULL
|
||||
GROUP BY severity
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
ELSE 4
|
||||
END
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_mttr_by_severity IS
|
||||
'Get mean time to remediate by severity (last N days)';
|
||||
@@ -0,0 +1,141 @@
|
||||
-- Release Orchestrator Schema Migration 024: Analytics vulnerability exposure filters
|
||||
-- Updates sp_vuln_exposure to honor environment filter and severity ranking.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_vuln_exposure(
|
||||
p_environment TEXT DEFAULT NULL,
|
||||
p_min_severity TEXT DEFAULT 'low'
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
min_rank INT;
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
min_rank := CASE LOWER(COALESCE(NULLIF(p_min_severity, ''), 'low'))
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END;
|
||||
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM analytics.mv_vuln_exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM (
|
||||
SELECT
|
||||
cv.vuln_id,
|
||||
cv.severity,
|
||||
cv.cvss_score,
|
||||
cv.epss_score,
|
||||
cv.kev_listed,
|
||||
cv.fix_available,
|
||||
COUNT(DISTINCT cv.component_id) AS raw_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS raw_artifact_count,
|
||||
COUNT(DISTINCT cv.component_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_artifact_count
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = cv.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
WHERE cv.affects = TRUE
|
||||
AND a.environment = env
|
||||
GROUP BY cv.vuln_id, cv.severity, cv.cvss_score, cv.epss_score, cv.kev_listed, cv.fix_available
|
||||
) exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
COMMENT ON FUNCTION analytics.sp_vuln_exposure IS
|
||||
'Get CVE exposure with VEX-adjusted counts, optional environment filter, and severity threshold';
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Release Orchestrator Schema Migration 025: Analytics rollup retention
|
||||
-- Adds retention pruning to compute_daily_rollups.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-009)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.compute_daily_rollups(p_date DATE DEFAULT CURRENT_DATE)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO analytics.daily_vulnerability_counts (
|
||||
snapshot_date, environment, team, severity,
|
||||
total_vulns, fixable_vulns, vex_mitigated, kev_vulns,
|
||||
unique_cves, affected_artifacts, affected_components
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
cv.severity,
|
||||
COUNT(*) AS total_vulns,
|
||||
COUNT(*) FILTER (WHERE cv.fix_available = TRUE) AS fixable_vulns,
|
||||
COUNT(*) FILTER (WHERE EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = a.artifact_id AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
)) AS vex_mitigated,
|
||||
COUNT(*) FILTER (WHERE cv.kev_listed = TRUE) AS kev_vulns,
|
||||
COUNT(DISTINCT cv.vuln_id) AS unique_cves,
|
||||
COUNT(DISTINCT a.artifact_id) AS affected_artifacts,
|
||||
COUNT(DISTINCT cv.component_id) AS affected_components
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.component_vulns cv ON cv.component_id = ac.component_id AND cv.affects = TRUE
|
||||
GROUP BY a.environment, a.team, cv.severity
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), severity)
|
||||
DO UPDATE SET
|
||||
total_vulns = EXCLUDED.total_vulns,
|
||||
fixable_vulns = EXCLUDED.fixable_vulns,
|
||||
vex_mitigated = EXCLUDED.vex_mitigated,
|
||||
kev_vulns = EXCLUDED.kev_vulns,
|
||||
unique_cves = EXCLUDED.unique_cves,
|
||||
affected_artifacts = EXCLUDED.affected_artifacts,
|
||||
affected_components = EXCLUDED.affected_components,
|
||||
created_at = now();
|
||||
|
||||
INSERT INTO analytics.daily_component_counts (
|
||||
snapshot_date, environment, team, license_category, component_type,
|
||||
total_components, unique_suppliers
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
c.license_category,
|
||||
c.component_type,
|
||||
COUNT(DISTINCT c.component_id) AS total_components,
|
||||
COUNT(DISTINCT c.supplier_normalized) AS unique_suppliers
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.components c ON c.component_id = ac.component_id
|
||||
GROUP BY a.environment, a.team, c.license_category, c.component_type
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), license_category, component_type)
|
||||
DO UPDATE SET
|
||||
total_components = EXCLUDED.total_components,
|
||||
unique_suppliers = EXCLUDED.unique_suppliers,
|
||||
created_at = now();
|
||||
|
||||
DELETE FROM analytics.daily_vulnerability_counts
|
||||
WHERE snapshot_date < (p_date - INTERVAL '90 days');
|
||||
|
||||
DELETE FROM analytics.daily_component_counts
|
||||
WHERE snapshot_date < (p_date - INTERVAL '90 days');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Release Orchestrator Schema Migration 026: Analytics rollup VEX validity
|
||||
-- Ensures rollup VEX mitigation uses validity windows anchored to the snapshot date.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-009)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.compute_daily_rollups(p_date DATE DEFAULT CURRENT_DATE)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO analytics.daily_vulnerability_counts (
|
||||
snapshot_date, environment, team, severity,
|
||||
total_vulns, fixable_vulns, vex_mitigated, kev_vulns,
|
||||
unique_cves, affected_artifacts, affected_components
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
cv.severity,
|
||||
COUNT(*) AS total_vulns,
|
||||
COUNT(*) FILTER (WHERE cv.fix_available = TRUE) AS fixable_vulns,
|
||||
COUNT(*) FILTER (WHERE EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = a.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from::DATE <= p_date
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until::DATE >= p_date)
|
||||
)) AS vex_mitigated,
|
||||
COUNT(*) FILTER (WHERE cv.kev_listed = TRUE) AS kev_vulns,
|
||||
COUNT(DISTINCT cv.vuln_id) AS unique_cves,
|
||||
COUNT(DISTINCT a.artifact_id) AS affected_artifacts,
|
||||
COUNT(DISTINCT cv.component_id) AS affected_components
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.component_vulns cv ON cv.component_id = ac.component_id AND cv.affects = TRUE
|
||||
GROUP BY a.environment, a.team, cv.severity
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), severity)
|
||||
DO UPDATE SET
|
||||
total_vulns = EXCLUDED.total_vulns,
|
||||
fixable_vulns = EXCLUDED.fixable_vulns,
|
||||
vex_mitigated = EXCLUDED.vex_mitigated,
|
||||
kev_vulns = EXCLUDED.kev_vulns,
|
||||
unique_cves = EXCLUDED.unique_cves,
|
||||
affected_artifacts = EXCLUDED.affected_artifacts,
|
||||
affected_components = EXCLUDED.affected_components,
|
||||
created_at = now();
|
||||
|
||||
INSERT INTO analytics.daily_component_counts (
|
||||
snapshot_date, environment, team, license_category, component_type,
|
||||
total_components, unique_suppliers
|
||||
)
|
||||
SELECT
|
||||
p_date,
|
||||
a.environment,
|
||||
a.team,
|
||||
c.license_category,
|
||||
c.component_type,
|
||||
COUNT(DISTINCT c.component_id) AS total_components,
|
||||
COUNT(DISTINCT c.supplier_normalized) AS unique_suppliers
|
||||
FROM analytics.artifacts a
|
||||
JOIN analytics.artifact_components ac ON ac.artifact_id = a.artifact_id
|
||||
JOIN analytics.components c ON c.component_id = ac.component_id
|
||||
GROUP BY a.environment, a.team, c.license_category, c.component_type
|
||||
ON CONFLICT (snapshot_date, environment, COALESCE(team, ''), license_category, component_type)
|
||||
DO UPDATE SET
|
||||
total_components = EXCLUDED.total_components,
|
||||
unique_suppliers = EXCLUDED.unique_suppliers,
|
||||
created_at = now();
|
||||
|
||||
DELETE FROM analytics.daily_vulnerability_counts
|
||||
WHERE snapshot_date < (p_date - INTERVAL '90 days');
|
||||
|
||||
DELETE FROM analytics.daily_component_counts
|
||||
WHERE snapshot_date < (p_date - INTERVAL '90 days');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,234 @@
|
||||
-- Release Orchestrator Schema Migration 027: Analytics VEX validity filters
|
||||
-- Aligns exposure and backlog queries with VEX valid_from/valid_until windows.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
DROP FUNCTION IF EXISTS analytics.sp_vuln_exposure(TEXT, TEXT);
|
||||
DROP FUNCTION IF EXISTS analytics.refresh_all_views();
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS analytics.mv_vuln_exposure;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.mv_vuln_exposure AS
|
||||
SELECT
|
||||
cv.vuln_id,
|
||||
cv.severity,
|
||||
cv.cvss_score,
|
||||
cv.epss_score,
|
||||
cv.kev_listed,
|
||||
cv.fix_available,
|
||||
COUNT(DISTINCT cv.component_id) AS raw_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS raw_artifact_count,
|
||||
COUNT(DISTINCT cv.component_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_artifact_count
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = cv.component_id
|
||||
WHERE cv.affects = TRUE
|
||||
GROUP BY cv.vuln_id, cv.severity, cv.cvss_score, cv.epss_score, cv.kev_listed, cv.fix_available
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_vuln_exposure_key
|
||||
ON analytics.mv_vuln_exposure (vuln_id, severity, cvss_score, epss_score, kev_listed, fix_available);
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.refresh_all_views()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_supplier_concentration;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_license_distribution;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_vuln_exposure;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_attestation_coverage;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_vuln_exposure(
|
||||
p_environment TEXT DEFAULT NULL,
|
||||
p_min_severity TEXT DEFAULT 'low'
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
min_rank INT;
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
min_rank := CASE LOWER(COALESCE(NULLIF(p_min_severity, ''), 'low'))
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END;
|
||||
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM analytics.mv_vuln_exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM (
|
||||
SELECT
|
||||
cv.vuln_id,
|
||||
cv.severity,
|
||||
cv.cvss_score,
|
||||
cv.epss_score,
|
||||
cv.kev_listed,
|
||||
cv.fix_available,
|
||||
COUNT(DISTINCT cv.component_id) AS raw_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS raw_artifact_count,
|
||||
COUNT(DISTINCT cv.component_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_artifact_count
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = cv.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
WHERE cv.affects = TRUE
|
||||
AND a.environment = env
|
||||
GROUP BY cv.vuln_id, cv.severity, cv.cvss_score, cv.epss_score, cv.kev_listed, cv.fix_available
|
||||
) exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_fixable_backlog(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
a.name AS service,
|
||||
a.environment,
|
||||
c.name AS component,
|
||||
c.version,
|
||||
cv.vuln_id,
|
||||
cv.severity::TEXT,
|
||||
cv.fixed_version
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.components c ON c.component_id = cv.component_id
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.vex_overrides vo ON vo.artifact_id = a.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.affects = TRUE
|
||||
AND cv.fix_available = TRUE
|
||||
AND vo.override_id IS NULL
|
||||
AND (p_environment IS NULL OR a.environment = p_environment)
|
||||
ORDER BY
|
||||
CASE cv.severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
a.name
|
||||
LIMIT 100
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Release Orchestrator Schema Migration 028: Analytics VEX active index
|
||||
-- Aligns active override index with valid_from/valid_until window checks.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
DROP INDEX IF EXISTS ix_vex_overrides_active;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_active ON analytics.vex_overrides (artifact_id, vuln_id)
|
||||
WHERE valid_from <= now() AND (valid_until IS NULL OR valid_until > now());
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Release Orchestrator Schema Migration 029: Analytics MTTR validity filters
|
||||
-- Ensures MTTR calculations only consider active VEX overrides.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_mttr_by_severity(p_days INT DEFAULT 90)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
severity::TEXT,
|
||||
COUNT(*) AS total_vulns,
|
||||
AVG(EXTRACT(EPOCH FROM (vo.valid_from - cv.published_at)) / 86400)::NUMERIC(10,2) AS avg_days_to_mitigate
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.vex_overrides vo ON vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.published_at >= now() - (p_days || ' days')::INTERVAL
|
||||
AND cv.published_at IS NOT NULL
|
||||
GROUP BY severity
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
ELSE 4
|
||||
END
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Release Orchestrator Schema Migration 030: Analytics VEX override index fix
|
||||
-- Replaces the active override index with an immutable predicate.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
DROP INDEX IF EXISTS ix_vex_overrides_active;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_active
|
||||
ON analytics.vex_overrides (artifact_id, vuln_id, valid_from, valid_until)
|
||||
WHERE status = 'not_affected';
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Release Orchestrator Schema Migration 031: Analytics VEX override vuln index
|
||||
-- Adds a status-scoped index to speed MTTR and vulnerability exposure queries.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_vex_overrides_vuln_active
|
||||
ON analytics.vex_overrides (vuln_id, valid_from, valid_until)
|
||||
WHERE status = 'not_affected';
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Release Orchestrator Schema Migration 032: Analytics component vuln published index
|
||||
-- Adds a published_at index to speed MTTR and date-range queries.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_published
|
||||
ON analytics.component_vulns (published_at DESC)
|
||||
WHERE published_at IS NOT NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Release Orchestrator Schema Migration 033: Analytics component vuln EPSS index
|
||||
-- Adds an EPSS index for exposure prioritization queries.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-005)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_component_vulns_epss
|
||||
ON analytics.component_vulns (epss_score DESC)
|
||||
WHERE epss_score IS NOT NULL;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Release Orchestrator Schema Migration 034: Analytics attestations artifact/type index
|
||||
-- Speeds existence checks for attestation coverage views.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-006)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_attestations_artifact_type
|
||||
ON analytics.attestations (artifact_id, predicate_type);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Release Orchestrator Schema Migration 035: Analytics component counts env index
|
||||
-- Adds an environment/date index for component trend queries.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-009)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_daily_comp_counts_env
|
||||
ON analytics.daily_component_counts (environment, snapshot_date DESC);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Release Orchestrator Schema Migration 036: Analytics materialized view indexes
|
||||
-- Adds performance indexes for dashboard queries.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-010..013)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_supplier_concentration_component_count
|
||||
ON analytics.mv_supplier_concentration (component_count DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_license_distribution_component_count
|
||||
ON analytics.mv_license_distribution (component_count DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_vuln_exposure_severity_count
|
||||
ON analytics.mv_vuln_exposure (severity, effective_artifact_count DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_attestation_coverage_provenance
|
||||
ON analytics.mv_attestation_coverage (provenance_pct ASC);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Release Orchestrator Schema Migration 037: Analytics artifacts environment/name index
|
||||
-- Improves fixable backlog ordering when filtering by environment.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-003)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_environment_name
|
||||
ON analytics.artifacts (environment, name);
|
||||
@@ -0,0 +1,286 @@
|
||||
-- Release Orchestrator Schema Migration 038: Analytics stored procedure ordering
|
||||
-- Adds deterministic tie-breakers for stable analytics outputs.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
-- Top suppliers by component count
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_top_suppliers(p_limit INT DEFAULT 20)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
supplier,
|
||||
component_count,
|
||||
artifact_count,
|
||||
team_count,
|
||||
critical_vuln_count,
|
||||
high_vuln_count,
|
||||
environments
|
||||
FROM analytics.mv_supplier_concentration
|
||||
ORDER BY component_count DESC, supplier ASC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- License distribution heatmap
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_license_heatmap()
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
license_category,
|
||||
license_concluded,
|
||||
component_count,
|
||||
artifact_count,
|
||||
ecosystems
|
||||
FROM analytics.mv_license_distribution
|
||||
ORDER BY component_count DESC, license_category, COALESCE(license_concluded, '')
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- CVE exposure adjusted by VEX
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_vuln_exposure(
|
||||
p_environment TEXT DEFAULT NULL,
|
||||
p_min_severity TEXT DEFAULT 'low'
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
min_rank INT;
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
min_rank := CASE LOWER(COALESCE(NULLIF(p_min_severity, ''), 'low'))
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END;
|
||||
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM analytics.mv_vuln_exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC,
|
||||
vuln_id
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
vuln_id,
|
||||
severity::TEXT,
|
||||
cvss_score,
|
||||
epss_score,
|
||||
kev_listed,
|
||||
fix_available,
|
||||
raw_component_count,
|
||||
raw_artifact_count,
|
||||
effective_component_count,
|
||||
effective_artifact_count,
|
||||
raw_artifact_count - effective_artifact_count AS vex_mitigated
|
||||
FROM (
|
||||
SELECT
|
||||
cv.vuln_id,
|
||||
cv.severity,
|
||||
cv.cvss_score,
|
||||
cv.epss_score,
|
||||
cv.kev_listed,
|
||||
cv.fix_available,
|
||||
COUNT(DISTINCT cv.component_id) AS raw_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS raw_artifact_count,
|
||||
COUNT(DISTINCT cv.component_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) FILTER (
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM analytics.vex_overrides vo
|
||||
WHERE vo.artifact_id = ac.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
)
|
||||
) AS effective_artifact_count
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = cv.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
WHERE cv.affects = TRUE
|
||||
AND a.environment = env
|
||||
GROUP BY cv.vuln_id, cv.severity, cv.cvss_score, cv.epss_score, cv.kev_listed, cv.fix_available
|
||||
) exposure
|
||||
WHERE effective_artifact_count > 0
|
||||
AND CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END <= min_rank
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
WHEN 'none' THEN 5
|
||||
ELSE 6
|
||||
END,
|
||||
effective_artifact_count DESC,
|
||||
vuln_id
|
||||
LIMIT 50
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Fixable backlog
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_fixable_backlog(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
a.name AS service,
|
||||
a.environment,
|
||||
c.name AS component,
|
||||
c.version,
|
||||
cv.vuln_id,
|
||||
cv.severity::TEXT,
|
||||
cv.fixed_version
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.components c ON c.component_id = cv.component_id
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.vex_overrides vo ON vo.artifact_id = a.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.affects = TRUE
|
||||
AND cv.fix_available = TRUE
|
||||
AND vo.override_id IS NULL
|
||||
AND (p_environment IS NULL OR a.environment = p_environment)
|
||||
ORDER BY
|
||||
CASE cv.severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
a.name,
|
||||
c.name,
|
||||
c.version,
|
||||
cv.vuln_id
|
||||
LIMIT 100
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- Attestation coverage gaps
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_attestation_gaps(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
environment,
|
||||
team,
|
||||
total_artifacts,
|
||||
with_provenance,
|
||||
provenance_pct,
|
||||
slsa_level_2_plus,
|
||||
slsa2_pct,
|
||||
total_artifacts - with_provenance AS missing_provenance
|
||||
FROM analytics.mv_attestation_coverage
|
||||
WHERE (p_environment IS NULL OR environment = p_environment)
|
||||
ORDER BY provenance_pct ASC, COALESCE(environment, ''), COALESCE(team, '')
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- MTTR by severity (simplified - requires proper remediation tracking)
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_mttr_by_severity(p_days INT DEFAULT 90)
|
||||
RETURNS JSON AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
severity::TEXT,
|
||||
COUNT(*) AS total_vulns,
|
||||
AVG(EXTRACT(EPOCH FROM (vo.valid_from - cv.published_at)) / 86400)::NUMERIC(10,2) AS avg_days_to_mitigate
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.vex_overrides vo ON vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.published_at >= now() - (p_days || ' days')::INTERVAL
|
||||
AND cv.published_at IS NOT NULL
|
||||
GROUP BY severity
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
severity::TEXT
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,100 @@
|
||||
-- Release Orchestrator Schema Migration 039: Analytics supplier/license environment filters
|
||||
-- Adds optional environment filtering for supplier and license analytics.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
-- Top suppliers by component count (optional environment filter)
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_top_suppliers(
|
||||
p_limit INT DEFAULT 20,
|
||||
p_environment TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
supplier,
|
||||
component_count,
|
||||
artifact_count,
|
||||
team_count,
|
||||
critical_vuln_count,
|
||||
high_vuln_count,
|
||||
environments
|
||||
FROM analytics.mv_supplier_concentration
|
||||
ORDER BY component_count DESC, supplier ASC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
c.supplier_normalized AS supplier,
|
||||
COUNT(DISTINCT c.component_id) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
COUNT(DISTINCT a.team) AS team_count,
|
||||
ARRAY_AGG(DISTINCT a.environment) FILTER (WHERE a.environment IS NOT NULL) AS environments,
|
||||
SUM(CASE WHEN cv.severity = 'critical' THEN 1 ELSE 0 END) AS critical_vuln_count,
|
||||
SUM(CASE WHEN cv.severity = 'high' THEN 1 ELSE 0 END) AS high_vuln_count
|
||||
FROM analytics.components c
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.component_vulns cv ON cv.component_id = c.component_id AND cv.affects = TRUE
|
||||
WHERE c.supplier_normalized IS NOT NULL
|
||||
AND a.environment = env
|
||||
GROUP BY c.supplier_normalized
|
||||
ORDER BY component_count DESC, supplier ASC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- License distribution heatmap (optional environment filter)
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_license_heatmap(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
license_category,
|
||||
license_concluded,
|
||||
component_count,
|
||||
artifact_count,
|
||||
ecosystems
|
||||
FROM analytics.mv_license_distribution
|
||||
ORDER BY component_count DESC, license_category, COALESCE(license_concluded, '')
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
c.license_category,
|
||||
c.license_concluded,
|
||||
COUNT(*) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
ARRAY_AGG(DISTINCT c.purl_type) FILTER (WHERE c.purl_type IS NOT NULL) AS ecosystems
|
||||
FROM analytics.components c
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
WHERE a.environment = env
|
||||
GROUP BY c.license_concluded, c.license_category
|
||||
ORDER BY component_count DESC, license_category, COALESCE(c.license_concluded, '')
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Release Orchestrator Schema Migration 040: Analytics refresh function fix
|
||||
-- Replaces concurrent refreshes in a function (not allowed by PostgreSQL).
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-010..013)
|
||||
DROP FUNCTION IF EXISTS analytics.refresh_all_views();
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.refresh_all_views()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW analytics.mv_supplier_concentration;
|
||||
REFRESH MATERIALIZED VIEW analytics.mv_license_distribution;
|
||||
REFRESH MATERIALIZED VIEW analytics.mv_vuln_exposure;
|
||||
REFRESH MATERIALIZED VIEW analytics.mv_attestation_coverage;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION analytics.refresh_all_views IS
|
||||
'Refresh all analytics materialized views (non-concurrent; use PlatformAnalyticsMaintenanceService for concurrent refresh)';
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Release Orchestrator Schema Migration 041: Analytics deterministic array ordering
|
||||
-- Ensures array aggregations use stable ordering for deterministic output.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-010..011)
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS analytics.mv_supplier_concentration;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.mv_supplier_concentration AS
|
||||
SELECT
|
||||
c.supplier_normalized AS supplier,
|
||||
COUNT(DISTINCT c.component_id) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
COUNT(DISTINCT a.team) AS team_count,
|
||||
ARRAY_AGG(DISTINCT a.environment ORDER BY a.environment) FILTER (WHERE a.environment IS NOT NULL) AS environments,
|
||||
SUM(CASE WHEN cv.severity = 'critical' THEN 1 ELSE 0 END) AS critical_vuln_count,
|
||||
SUM(CASE WHEN cv.severity = 'high' THEN 1 ELSE 0 END) AS high_vuln_count,
|
||||
MAX(c.last_seen_at) AS last_seen_at
|
||||
FROM analytics.components c
|
||||
LEFT JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
LEFT JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.component_vulns cv ON cv.component_id = c.component_id AND cv.affects = TRUE
|
||||
WHERE c.supplier_normalized IS NOT NULL
|
||||
GROUP BY c.supplier_normalized
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_supplier_concentration_supplier
|
||||
ON analytics.mv_supplier_concentration (supplier);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_supplier_concentration_component_count
|
||||
ON analytics.mv_supplier_concentration (component_count DESC);
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS analytics.mv_license_distribution;
|
||||
|
||||
CREATE MATERIALIZED VIEW analytics.mv_license_distribution AS
|
||||
SELECT
|
||||
c.license_concluded,
|
||||
c.license_category,
|
||||
COUNT(*) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
ARRAY_AGG(DISTINCT c.purl_type ORDER BY c.purl_type) FILTER (WHERE c.purl_type IS NOT NULL) AS ecosystems
|
||||
FROM analytics.components c
|
||||
LEFT JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
GROUP BY c.license_concluded, c.license_category
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_mv_license_distribution_license
|
||||
ON analytics.mv_license_distribution (COALESCE(license_concluded, ''), license_category);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_mv_license_distribution_component_count
|
||||
ON analytics.mv_license_distribution (component_count DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_top_suppliers(
|
||||
p_limit INT DEFAULT 20,
|
||||
p_environment TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
supplier,
|
||||
component_count,
|
||||
artifact_count,
|
||||
team_count,
|
||||
critical_vuln_count,
|
||||
high_vuln_count,
|
||||
environments
|
||||
FROM analytics.mv_supplier_concentration
|
||||
ORDER BY component_count DESC, supplier ASC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
c.supplier_normalized AS supplier,
|
||||
COUNT(DISTINCT c.component_id) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
COUNT(DISTINCT a.team) AS team_count,
|
||||
ARRAY_AGG(DISTINCT a.environment ORDER BY a.environment) FILTER (WHERE a.environment IS NOT NULL) AS environments,
|
||||
SUM(CASE WHEN cv.severity = 'critical' THEN 1 ELSE 0 END) AS critical_vuln_count,
|
||||
SUM(CASE WHEN cv.severity = 'high' THEN 1 ELSE 0 END) AS high_vuln_count
|
||||
FROM analytics.components c
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.component_vulns cv ON cv.component_id = c.component_id AND cv.affects = TRUE
|
||||
WHERE c.supplier_normalized IS NOT NULL
|
||||
AND a.environment = env
|
||||
GROUP BY c.supplier_normalized
|
||||
ORDER BY component_count DESC, supplier ASC
|
||||
LIMIT p_limit
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_license_heatmap(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
IF env IS NULL THEN
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
license_category,
|
||||
license_concluded,
|
||||
component_count,
|
||||
artifact_count,
|
||||
ecosystems
|
||||
FROM analytics.mv_license_distribution
|
||||
ORDER BY component_count DESC, license_category, COALESCE(license_concluded, '')
|
||||
) t
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
c.license_category,
|
||||
c.license_concluded,
|
||||
COUNT(*) AS component_count,
|
||||
COUNT(DISTINCT ac.artifact_id) AS artifact_count,
|
||||
ARRAY_AGG(DISTINCT c.purl_type ORDER BY c.purl_type) FILTER (WHERE c.purl_type IS NOT NULL) AS ecosystems
|
||||
FROM analytics.components c
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
WHERE a.environment = env
|
||||
GROUP BY c.license_concluded, c.license_category
|
||||
ORDER BY component_count DESC, license_category, COALESCE(c.license_concluded, '')
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Release Orchestrator Schema Migration 042: Analytics environment normalization
|
||||
-- Normalizes environment parameters for backlog and attestation procedures.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-017)
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_fixable_backlog(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
a.name AS service,
|
||||
a.environment,
|
||||
c.name AS component,
|
||||
c.version,
|
||||
cv.vuln_id,
|
||||
cv.severity::TEXT,
|
||||
cv.fixed_version
|
||||
FROM analytics.component_vulns cv
|
||||
JOIN analytics.components c ON c.component_id = cv.component_id
|
||||
JOIN analytics.artifact_components ac ON ac.component_id = c.component_id
|
||||
JOIN analytics.artifacts a ON a.artifact_id = ac.artifact_id
|
||||
LEFT JOIN analytics.vex_overrides vo ON vo.artifact_id = a.artifact_id
|
||||
AND vo.vuln_id = cv.vuln_id
|
||||
AND vo.status = 'not_affected'
|
||||
AND vo.valid_from <= now()
|
||||
AND (vo.valid_until IS NULL OR vo.valid_until > now())
|
||||
WHERE cv.affects = TRUE
|
||||
AND cv.fix_available = TRUE
|
||||
AND vo.override_id IS NULL
|
||||
AND (env IS NULL OR a.environment = env)
|
||||
ORDER BY
|
||||
CASE cv.severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
a.name,
|
||||
c.name,
|
||||
c.version,
|
||||
cv.vuln_id
|
||||
LIMIT 100
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION analytics.sp_attestation_gaps(p_environment TEXT DEFAULT NULL)
|
||||
RETURNS JSON AS $$
|
||||
DECLARE
|
||||
env TEXT;
|
||||
BEGIN
|
||||
env := NULLIF(BTRIM(p_environment), '');
|
||||
RETURN (
|
||||
SELECT json_agg(row_to_json(t))
|
||||
FROM (
|
||||
SELECT
|
||||
environment,
|
||||
team,
|
||||
total_artifacts,
|
||||
with_provenance,
|
||||
provenance_pct,
|
||||
slsa_level_2_plus,
|
||||
slsa2_pct,
|
||||
total_artifacts - with_provenance AS missing_provenance
|
||||
FROM analytics.mv_attestation_coverage
|
||||
WHERE (env IS NULL OR environment = env)
|
||||
ORDER BY provenance_pct ASC, COALESCE(environment, ''), COALESCE(team, '')
|
||||
) t
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Release Orchestrator Schema Migration 043: Analytics schema alignment
|
||||
-- Aligns analytics schema objects with documented DDL.
|
||||
-- Sprint: SPRINT_20260120_030 (TASK-030-002, TASK-030-003)
|
||||
|
||||
ALTER TABLE analytics.artifacts
|
||||
ADD COLUMN IF NOT EXISTS medium_count INT DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS low_count INT DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_components_last_seen
|
||||
ON analytics.components (last_seen_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_environment_name
|
||||
ON analytics.artifacts (environment, name);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_artifacts_service
|
||||
ON analytics.artifacts (service);
|
||||
Reference in New Issue
Block a user