feat: add Attestation Chain and Triage Evidence API clients and models

- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
This commit is contained in:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,249 @@
-- Stella Ops Triage Schema Migration
-- Generated from docs/db/triage_schema.sql
-- Version: 1.0.0
BEGIN;
-- Extensions
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Enums
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_lane') THEN
CREATE TYPE triage_lane AS ENUM (
'ACTIVE',
'BLOCKED',
'NEEDS_EXCEPTION',
'MUTED_REACH',
'MUTED_VEX',
'COMPENSATED'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_verdict') THEN
CREATE TYPE triage_verdict AS ENUM ('SHIP', 'BLOCK', 'EXCEPTION');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_reachability') THEN
CREATE TYPE triage_reachability AS ENUM ('YES', 'NO', 'UNKNOWN');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_vex_status') THEN
CREATE TYPE triage_vex_status AS ENUM ('affected', 'not_affected', 'under_investigation', 'unknown');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_decision_kind') THEN
CREATE TYPE triage_decision_kind AS ENUM ('MUTE_REACH', 'MUTE_VEX', 'ACK', 'EXCEPTION');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_snapshot_trigger') THEN
CREATE TYPE triage_snapshot_trigger AS ENUM (
'FEED_UPDATE',
'VEX_UPDATE',
'SBOM_UPDATE',
'RUNTIME_TRACE',
'POLICY_UPDATE',
'DECISION',
'RESCAN'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'triage_evidence_type') THEN
CREATE TYPE triage_evidence_type AS ENUM (
'SBOM_SLICE',
'VEX_DOC',
'PROVENANCE',
'CALLSTACK_SLICE',
'REACHABILITY_PROOF',
'REPLAY_MANIFEST',
'POLICY',
'SCAN_LOG',
'OTHER'
);
END IF;
END $$;
-- Core: finding (caseId == findingId)
CREATE TABLE IF NOT EXISTS triage_finding (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
asset_id uuid NOT NULL,
environment_id uuid NULL,
asset_label text NOT NULL,
purl text NOT NULL,
cve_id text NULL,
rule_id text NULL,
first_seen_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (asset_id, environment_id, purl, cve_id, rule_id)
);
CREATE INDEX IF NOT EXISTS ix_triage_finding_last_seen ON triage_finding (last_seen_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_finding_asset_label ON triage_finding (asset_label);
CREATE INDEX IF NOT EXISTS ix_triage_finding_purl ON triage_finding (purl);
CREATE INDEX IF NOT EXISTS ix_triage_finding_cve ON triage_finding (cve_id);
-- Effective VEX (post-merge)
CREATE TABLE IF NOT EXISTS triage_effective_vex (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
status triage_vex_status NOT NULL,
source_domain text NOT NULL,
source_ref text NOT NULL,
pruned_sources jsonb NULL,
dsse_envelope_hash text NULL,
signature_ref text NULL,
issuer text NULL,
valid_from timestamptz NOT NULL DEFAULT now(),
valid_to timestamptz NULL,
collected_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_triage_effective_vex_finding ON triage_effective_vex (finding_id, collected_at DESC);
-- Reachability results
CREATE TABLE IF NOT EXISTS triage_reachability_result (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
reachable triage_reachability NOT NULL,
confidence smallint NOT NULL CHECK (confidence >= 0 AND confidence <= 100),
static_proof_ref text NULL,
runtime_proof_ref text NULL,
inputs_hash text NOT NULL,
computed_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_triage_reachability_finding ON triage_reachability_result (finding_id, computed_at DESC);
-- Risk/lattice result
CREATE TABLE IF NOT EXISTS triage_risk_result (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
policy_id text NOT NULL,
policy_version text NOT NULL,
inputs_hash text NOT NULL,
score int NOT NULL CHECK (score >= 0 AND score <= 100),
verdict triage_verdict NOT NULL,
lane triage_lane NOT NULL,
why text NOT NULL,
explanation jsonb NULL,
computed_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (finding_id, policy_id, policy_version, inputs_hash)
);
CREATE INDEX IF NOT EXISTS ix_triage_risk_finding ON triage_risk_result (finding_id, computed_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_risk_lane ON triage_risk_result (lane, computed_at DESC);
-- Signed Decisions
CREATE TABLE IF NOT EXISTS triage_decision (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
kind triage_decision_kind NOT NULL,
reason_code text NOT NULL,
note text NULL,
policy_ref text NULL,
ttl timestamptz NULL,
actor_subject text NOT NULL,
actor_display text NULL,
signature_ref text NULL,
dsse_hash text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz NULL,
revoke_reason text NULL,
revoke_signature_ref text NULL,
revoke_dsse_hash text NULL
);
CREATE INDEX IF NOT EXISTS ix_triage_decision_finding ON triage_decision (finding_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_decision_kind ON triage_decision (kind, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_decision_active ON triage_decision (finding_id) WHERE revoked_at IS NULL;
-- Evidence artifacts
CREATE TABLE IF NOT EXISTS triage_evidence_artifact (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
type triage_evidence_type NOT NULL,
title text NOT NULL,
issuer text NULL,
signed boolean NOT NULL DEFAULT false,
signed_by text NULL,
content_hash text NOT NULL,
signature_ref text NULL,
media_type text NULL,
uri text NOT NULL,
size_bytes bigint NULL,
metadata jsonb NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (finding_id, type, content_hash)
);
CREATE INDEX IF NOT EXISTS ix_triage_evidence_finding ON triage_evidence_artifact (finding_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_evidence_type ON triage_evidence_artifact (type, created_at DESC);
-- Snapshots for Smart-Diff
CREATE TABLE IF NOT EXISTS triage_snapshot (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL REFERENCES triage_finding(id) ON DELETE CASCADE,
trigger triage_snapshot_trigger NOT NULL,
from_inputs_hash text NULL,
to_inputs_hash text NOT NULL,
summary text NOT NULL,
diff_json jsonb NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (finding_id, to_inputs_hash, created_at)
);
CREATE INDEX IF NOT EXISTS ix_triage_snapshot_finding ON triage_snapshot (finding_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_triage_snapshot_trigger ON triage_snapshot (trigger, created_at DESC);
-- Current-case view
CREATE OR REPLACE VIEW v_triage_case_current AS
WITH latest_risk AS (
SELECT DISTINCT ON (finding_id)
finding_id, policy_id, policy_version, inputs_hash, score, verdict, lane, why, computed_at
FROM triage_risk_result
ORDER BY finding_id, computed_at DESC
),
latest_reach AS (
SELECT DISTINCT ON (finding_id)
finding_id, reachable, confidence, static_proof_ref, runtime_proof_ref, computed_at
FROM triage_reachability_result
ORDER BY finding_id, computed_at DESC
),
latest_vex AS (
SELECT DISTINCT ON (finding_id)
finding_id, status, issuer, signature_ref, source_domain, source_ref, collected_at
FROM triage_effective_vex
ORDER BY finding_id, collected_at DESC
)
SELECT
f.id AS case_id,
f.asset_id,
f.environment_id,
f.asset_label,
f.purl,
f.cve_id,
f.rule_id,
f.first_seen_at,
f.last_seen_at,
r.policy_id,
r.policy_version,
r.inputs_hash,
r.score,
r.verdict,
r.lane,
r.why,
r.computed_at AS risk_computed_at,
coalesce(re.reachable, 'UNKNOWN'::triage_reachability) AS reachable,
re.confidence AS reach_confidence,
v.status AS vex_status,
v.issuer AS vex_issuer,
v.signature_ref AS vex_signature_ref,
v.source_domain AS vex_source_domain,
v.source_ref AS vex_source_ref
FROM triage_finding f
LEFT JOIN latest_risk r ON r.finding_id = f.id
LEFT JOIN latest_reach re ON re.finding_id = f.id
LEFT JOIN latest_vex v ON v.finding_id = f.id;
COMMIT;