tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -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';

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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, ''));

View File

@@ -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;

View File

@@ -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)';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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());

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);