docs consolidation and others
This commit is contained in:
45
docs/modules/risk-engine/guides/api.md
Normal file
45
docs/modules/risk-engine/guides/api.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Risk API
|
||||
|
||||
> Based on `CONTRACT-RISK-SCORING-002` (2025-12-05). Examples are frozen in `docs/modules/risk-engine/samples/api/risk-api-samples.json` with hashes in `SHA256SUMS`. Keep ETags and error payloads deterministic.
|
||||
|
||||
## Purpose
|
||||
- Document risk-related endpoints for profile management, simulation, scoring results, explainability retrieval, and export.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: API consumers, SDK authors, platform integrators.
|
||||
- In scope: endpoint list, methods, request/response schemas, auth/tenancy headers, rate limits, feature flags, error model.
|
||||
- Out of scope: console/UI workflow details (see `explainability.md`).
|
||||
|
||||
## Endpoints (v1)
|
||||
- `POST /api/v1/risk/jobs` — submit scoring job (body: job request); returns `202` with `job_id` and `status` (`queued`). Sample: `risk-api-samples.json#submit_job_request`.
|
||||
- `GET /api/v1/risk/jobs/{job_id}` — job status + results array (sample: `get_job_status`).
|
||||
- `GET /api/v1/risk/explain/{job_id}` — explainability payload (sample references `../explain/explain-trace.json`). Optional `If-None-Match` for caching.
|
||||
- `GET /api/v1/risk/profiles` — list profiles (tenant-filtered); includes `profile_hash`, `version`, `etag` (see error-catalog headers).
|
||||
- `POST /api/v1/risk/profiles` — create/update profile with DSSE/attestation metadata; returns `201` with `etag`.
|
||||
- `POST /api/v1/risk/simulations` — dry-run scoring with fixtures; returns explain + contributions without persisting results.
|
||||
- `GET /api/v1/risk/export/{job_id}` — export bundle (JSON + CSV + manifest) for auditors.
|
||||
- Feature flags: `risk.jobs`, `risk.explain`, `risk.simulations`, `risk.export` (toggle exposure per tenant).
|
||||
|
||||
## Auth & Tenancy
|
||||
- Required headers: `X-Stella-Tenant`, `Authorization: Bearer <token>`, optional `X-Stella-Scope` for imposed rule reminders.
|
||||
- Imposed rule reminder must be present in responses where tenant-bound resources are returned.
|
||||
|
||||
## Error Model
|
||||
- Envelope: `code`, `message`, `correlation_id`, `severity`, `remediation`; sample catalog in `docs/modules/risk-engine/samples/api/error-catalog.json`.
|
||||
- Rate-limit headers: `Retry-After`, `X-RateLimit-Remaining`; caching headers include `ETag` for explain/results/profile GETs.
|
||||
|
||||
## Determinism & Offline Posture
|
||||
- Samples: `docs/modules/risk-engine/samples/api/risk-api-samples.json` (hashes in `SHA256SUMS`); explain sample reused via relative reference.
|
||||
- No live dependencies; use frozen fixtures. Keep ordering of fields stable in docs and samples.
|
||||
|
||||
## Open Items
|
||||
- Add ETag examples for profile list/create once generators emit them.
|
||||
- Populate error/code catalog and SDK targets once available.
|
||||
- Align feature flag names with deployment config.
|
||||
|
||||
## References
|
||||
- `docs/modules/risk-engine/guides/overview.md`
|
||||
- `docs/modules/risk-engine/guides/profiles.md`
|
||||
- `docs/modules/risk-engine/guides/factors.md`
|
||||
- `docs/modules/risk-engine/guides/formulas.md`
|
||||
- `docs/modules/risk-engine/guides/explainability.md`
|
||||
817
docs/modules/risk-engine/guides/epss-integration-v4.md
Normal file
817
docs/modules/risk-engine/guides/epss-integration-v4.md
Normal file
@@ -0,0 +1,817 @@
|
||||
# EPSS v4 Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
EPSS (Exploit Prediction Scoring System) v4 is a machine learning-based vulnerability scoring system developed by FIRST.org that predicts the probability a CVE will be exploited in the wild within the next 30 days. StellaOps integrates EPSS as a **probabilistic threat signal** alongside CVSS v4's **deterministic severity assessment**, enabling more accurate vulnerability prioritization.
|
||||
|
||||
**Key Concepts**:
|
||||
- **EPSS Score**: Probability (0.0-1.0) that a CVE will be exploited in next 30 days
|
||||
- **EPSS Percentile**: Ranking (0.0-1.0) of this CVE relative to all scored CVEs
|
||||
- **Model Date**: Date for which EPSS scores were computed
|
||||
- **Immutable at-scan**: EPSS evidence captured at scan time never changes (deterministic replay)
|
||||
- **Current EPSS**: Live projection for triage (updated daily)
|
||||
|
||||
---
|
||||
|
||||
## EPSS Versioning Clarification
|
||||
|
||||
> **Note on "EPSS v4" Terminology**
|
||||
>
|
||||
> The term "EPSS v4" used in this document is a conceptual identifier aligning with CVSS v4 integration, **not** an official FIRST.org version number. FIRST.org's EPSS does not use explicit version numbers like "v1", "v2", etc.
|
||||
>
|
||||
> **How EPSS Versioning Actually Works:**
|
||||
> - EPSS models are identified by **model_date** (e.g., `2025-12-16`)
|
||||
> - Each daily CSV release represents a new model trained on updated threat data
|
||||
> - The EPSS specification itself evolves without formal version increments
|
||||
>
|
||||
> **StellaOps Implementation:**
|
||||
> - Tracks `model_date` for each EPSS score ingested
|
||||
> - Does not assume a formal EPSS version number
|
||||
> - Evidence replay uses the `model_date` from scan time
|
||||
>
|
||||
> For authoritative EPSS methodology, see: [FIRST.org EPSS Documentation](https://www.first.org/epss/)
|
||||
|
||||
---
|
||||
|
||||
## How EPSS Works
|
||||
|
||||
EPSS uses machine learning to predict exploitation probability based on:
|
||||
|
||||
1. **Vulnerability Characteristics**: CVSS metrics, CWE, affected products
|
||||
2. **Social Signals**: Twitter/GitHub mentions, security blog posts
|
||||
3. **Exploit Database Entries**: Exploit-DB, Metasploit, etc.
|
||||
4. **Historical Exploitation**: Past exploitation patterns
|
||||
|
||||
EPSS is updated **daily** by FIRST.org based on fresh threat intelligence.
|
||||
|
||||
### EPSS vs CVSS
|
||||
|
||||
| Dimension | CVSS v4 | EPSS v4 |
|
||||
|-----------|---------|---------|
|
||||
| **Nature** | Deterministic severity | Probabilistic threat |
|
||||
| **Scale** | 0.0-10.0 (severity) | 0.0-1.0 (probability) |
|
||||
| **Update Frequency** | Static (per CVE version) | Daily (live threat data) |
|
||||
| **Purpose** | Impact assessment | Likelihood assessment |
|
||||
| **Source** | Vendor/NVD | FIRST.org ML model |
|
||||
|
||||
**Example**:
|
||||
- **CVE-2024-1234**: CVSS 9.8 (Critical) + EPSS 0.01 (1st percentile)
|
||||
- Interpretation: Severe impact if exploited, but very unlikely to be exploited
|
||||
- Priority: **Medium** (deprioritize despite high CVSS)
|
||||
|
||||
- **CVE-2024-5678**: CVSS 6.5 (Medium) + EPSS 0.95 (98th percentile)
|
||||
- Interpretation: Moderate impact, but actively being exploited
|
||||
- Priority: **High** (escalate despite moderate CVSS)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ EPSS Data Lifecycle in StellaOps │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
|
||||
1. INGESTION (Daily 00:05 UTC)
|
||||
┌───────────────────┐
|
||||
│ FIRST.org │ Daily CSV: epss_scores-YYYY-MM-DD.csv.gz
|
||||
│ (300k CVEs) │ ~15MB compressed
|
||||
└────────┬──────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ Concelier: EpssIngestJob │
|
||||
│ - Download/Import CSV │
|
||||
│ - Parse (handle # comment, validate bounds) │
|
||||
│ - Bulk insert: epss_scores (partitioned by month) │
|
||||
│ - Compute delta: epss_changes (flags for enrichment) │
|
||||
│ - Upsert: epss_current (latest projection) │
|
||||
│ - Emit event: "epss.updated" │
|
||||
└────────┬──────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
[PostgreSQL: concelier.epss_*]
|
||||
│
|
||||
├─────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
|
||||
2. AT-SCAN CAPTURE (Immutable Evidence)
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Scanner: On new scan │
|
||||
│ - Bulk query: epss_current for CVE list │
|
||||
│ - Store immutable evidence: │
|
||||
│ * epss_score_at_scan │
|
||||
│ * epss_percentile_at_scan │
|
||||
│ * epss_model_date_at_scan │
|
||||
│ * epss_import_run_id_at_scan │
|
||||
│ - Use in lattice decision (SR→CR if EPSS≥90th) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
3. LIVE ENRICHMENT (Existing Findings)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Concelier: EpssEnrichmentJob (on "epss.updated") │
|
||||
│ - Read: epss_changes WHERE flags IN (CROSSED_HIGH, BIG_JUMP)│
|
||||
│ - Find impacted: vuln_instance_triage BY cve_id │
|
||||
│ - Update: current_epss_score, current_epss_percentile │
|
||||
│ - If priority band changed → emit "vuln.priority.changed" │
|
||||
└────────┬────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Notify: On "vuln.priority.changed" │
|
||||
│ - Check tenant notification rules │
|
||||
│ - Send: Slack / Email / Teams / In-app │
|
||||
│ - Payload: EPSS delta, threshold crossed │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
4. POLICY SCORING
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Policy Engine: Risk Score Formula │
|
||||
│ risk_score = (cvss/10) + epss_bonus + kev_bonus + reach_mult│
|
||||
│ │
|
||||
│ EPSS Bonus (Simple Profile): │
|
||||
│ - Percentile ≥99th: +10% │
|
||||
│ - Percentile ≥90th: +5% │
|
||||
│ - Percentile ≥50th: +2% │
|
||||
│ - Percentile <50th: 0% │
|
||||
│ │
|
||||
│ VEX Lattice Rules: │
|
||||
│ - SR + EPSS≥90th → Escalate to CR (Confirmed Reachable) │
|
||||
│ - DV + EPSS≥95th → Flag for review (vendor denial) │
|
||||
│ - U + EPSS≥95th → Prioritize for reachability analysis │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
**Location**: `concelier` database
|
||||
|
||||
#### epss_import_runs (Provenance)
|
||||
|
||||
Tracks each EPSS import with full provenance for audit trail.
|
||||
|
||||
```sql
|
||||
CREATE TABLE concelier.epss_import_runs (
|
||||
import_run_id UUID PRIMARY KEY,
|
||||
model_date DATE NOT NULL UNIQUE,
|
||||
source_uri TEXT NOT NULL,
|
||||
file_sha256 TEXT NOT NULL,
|
||||
row_count INT NOT NULL,
|
||||
model_version_tag TEXT NULL,
|
||||
published_date DATE NULL,
|
||||
status TEXT NOT NULL, -- IN_PROGRESS, SUCCEEDED, FAILED
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
#### epss_scores (Time-Series, Partitioned)
|
||||
|
||||
Immutable append-only history of daily EPSS scores.
|
||||
|
||||
```sql
|
||||
CREATE TABLE concelier.epss_scores (
|
||||
model_date DATE NOT NULL,
|
||||
cve_id TEXT NOT NULL,
|
||||
epss_score DOUBLE PRECISION NOT NULL,
|
||||
percentile DOUBLE PRECISION NOT NULL,
|
||||
import_run_id UUID NOT NULL,
|
||||
PRIMARY KEY (model_date, cve_id)
|
||||
) PARTITION BY RANGE (model_date);
|
||||
```
|
||||
|
||||
**Partitions**: Monthly (e.g., `epss_scores_2025_12`)
|
||||
|
||||
#### epss_current (Latest Projection)
|
||||
|
||||
Materialized view of latest EPSS score per CVE for fast lookups.
|
||||
|
||||
```sql
|
||||
CREATE TABLE concelier.epss_current (
|
||||
cve_id TEXT PRIMARY KEY,
|
||||
epss_score DOUBLE PRECISION NOT NULL,
|
||||
percentile DOUBLE PRECISION NOT NULL,
|
||||
model_date DATE NOT NULL,
|
||||
import_run_id UUID NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
**Usage**: Scanner bulk queries this table for new scans.
|
||||
|
||||
#### epss_changes (Delta Tracking, Partitioned)
|
||||
|
||||
Tracks material EPSS changes for targeted enrichment.
|
||||
|
||||
```sql
|
||||
CREATE TABLE concelier.epss_changes (
|
||||
model_date DATE NOT NULL,
|
||||
cve_id TEXT NOT NULL,
|
||||
old_score DOUBLE PRECISION NULL,
|
||||
new_score DOUBLE PRECISION NOT NULL,
|
||||
delta_score DOUBLE PRECISION NULL,
|
||||
old_percentile DOUBLE PRECISION NULL,
|
||||
new_percentile DOUBLE PRECISION NOT NULL,
|
||||
delta_percentile DOUBLE PRECISION NULL,
|
||||
flags INT NOT NULL, -- Bitmask
|
||||
PRIMARY KEY (model_date, cve_id)
|
||||
) PARTITION BY RANGE (model_date);
|
||||
```
|
||||
|
||||
**Flags** (bitmask):
|
||||
- `1` = NEW_SCORED (CVE newly appeared)
|
||||
- `2` = CROSSED_HIGH (percentile ≥95th)
|
||||
- `4` = BIG_JUMP (|Δscore| ≥0.10)
|
||||
- `8` = DROPPED_LOW (percentile <50th)
|
||||
- `16` = SCORE_INCREASED
|
||||
- `32` = SCORE_DECREASED
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Scheduler Configuration
|
||||
|
||||
**File**: `etc/scheduler.yaml`
|
||||
|
||||
```yaml
|
||||
scheduler:
|
||||
jobs:
|
||||
- name: epss.ingest
|
||||
schedule: "0 5 0 * * *" # Daily at 00:05 UTC
|
||||
worker: concelier
|
||||
args:
|
||||
source: online
|
||||
date: null # Auto: yesterday
|
||||
timeout: 600s
|
||||
retry:
|
||||
max_attempts: 3
|
||||
backoff: exponential
|
||||
```
|
||||
|
||||
### Concelier Configuration
|
||||
|
||||
**File**: `etc/concelier.yaml`
|
||||
|
||||
```yaml
|
||||
concelier:
|
||||
epss:
|
||||
enabled: true
|
||||
online_source:
|
||||
base_url: "https://epss.empiricalsecurity.com/"
|
||||
url_pattern: "epss_scores-{date:yyyy-MM-dd}.csv.gz"
|
||||
timeout: 180s
|
||||
bundle_source:
|
||||
path: "/opt/stellaops/bundles/epss/"
|
||||
thresholds:
|
||||
high_percentile: 0.95 # Top 5%
|
||||
high_score: 0.50 # 50% probability
|
||||
big_jump_delta: 0.10 # 10 percentage points
|
||||
low_percentile: 0.50 # Median
|
||||
enrichment:
|
||||
enabled: true
|
||||
batch_size: 1000
|
||||
flags_to_process:
|
||||
- NEW_SCORED
|
||||
- CROSSED_HIGH
|
||||
- BIG_JUMP
|
||||
```
|
||||
|
||||
### Scanner Configuration
|
||||
|
||||
**File**: `etc/scanner.yaml`
|
||||
|
||||
```yaml
|
||||
scanner:
|
||||
epss:
|
||||
enabled: true
|
||||
provider: postgres
|
||||
cache_ttl: 3600
|
||||
fallback_on_missing: unknown # Options: unknown, zero, skip
|
||||
```
|
||||
|
||||
### Policy Configuration
|
||||
|
||||
**File**: `etc/policy.yaml`
|
||||
|
||||
```yaml
|
||||
policy:
|
||||
scoring:
|
||||
epss:
|
||||
enabled: true
|
||||
profile: simple # Options: simple, advanced, custom
|
||||
simple_bonuses:
|
||||
percentile_99: 0.10 # +10%
|
||||
percentile_90: 0.05 # +5%
|
||||
percentile_50: 0.02 # +2%
|
||||
lattice:
|
||||
epss_escalation:
|
||||
enabled: true
|
||||
sr_to_cr_threshold: 0.90 # SR→CR if EPSS≥90th percentile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daily Operation
|
||||
|
||||
### Automated Ingestion
|
||||
|
||||
EPSS data is ingested automatically daily at **00:05 UTC** via Scheduler.
|
||||
|
||||
**Workflow**:
|
||||
1. Scheduler triggers `epss.ingest` job at 00:05 UTC
|
||||
2. Concelier downloads `epss_scores-YYYY-MM-DD.csv.gz` from FIRST.org
|
||||
3. CSV parsed (comment line → metadata, rows → scores)
|
||||
4. Bulk insert into `epss_scores` partition (NpgsqlBinaryImporter)
|
||||
5. Compute delta: `epss_changes` (compare vs `epss_current`)
|
||||
6. Upsert `epss_current` (latest projection)
|
||||
7. Emit `epss.updated` event
|
||||
8. Enrichment job updates impacted vulnerability instances
|
||||
9. Notifications sent if priority bands changed
|
||||
|
||||
**Monitoring**:
|
||||
```bash
|
||||
# Check latest model date
|
||||
stellaops epss status
|
||||
|
||||
# Output:
|
||||
# EPSS Status:
|
||||
# Latest Model Date: 2025-12-16
|
||||
# Import Time: 2025-12-17 00:07:32 UTC
|
||||
# CVE Count: 231,417
|
||||
# Staleness: FRESH (1 day)
|
||||
```
|
||||
|
||||
### Manual Triggering
|
||||
|
||||
```bash
|
||||
# Trigger manual ingest (force re-import)
|
||||
stellaops concelier job trigger epss.ingest --date 2025-12-16 --force
|
||||
|
||||
# Backfill historical data (last 30 days)
|
||||
stellaops epss backfill --from 2025-11-17 --to 2025-12-16
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Air-Gapped Operation
|
||||
|
||||
### Bundle Structure
|
||||
|
||||
EPSS data for offline deployments is packaged in risk bundles:
|
||||
|
||||
```
|
||||
risk-bundle-2025-12-16/
|
||||
├── manifest.json
|
||||
├── epss/
|
||||
│ ├── epss_scores-2025-12-16.csv.zst # ZSTD compressed
|
||||
│ └── epss_metadata.json
|
||||
├── kev/
|
||||
│ └── kev-catalog.json
|
||||
└── signatures/
|
||||
└── bundle.dsse.json
|
||||
```
|
||||
|
||||
### EPSS Metadata
|
||||
|
||||
**File**: `epss/epss_metadata.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"model_date": "2025-12-16",
|
||||
"model_version": "v2025.12.16",
|
||||
"published_date": "2025-12-16",
|
||||
"row_count": 231417,
|
||||
"sha256": "abc123...",
|
||||
"source_uri": "https://epss.empiricalsecurity.com/epss_scores-2025-12-16.csv.gz",
|
||||
"created_at": "2025-12-16T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Import Procedure
|
||||
|
||||
```bash
|
||||
# 1. Transfer bundle to air-gapped system
|
||||
scp risk-bundle-2025-12-16.tar.zst airgap-host:/opt/stellaops/bundles/
|
||||
|
||||
# 2. Import bundle
|
||||
stellaops offline import --bundle /opt/stellaops/bundles/risk-bundle-2025-12-16.tar.zst
|
||||
|
||||
# 3. Verify import
|
||||
stellaops epss status
|
||||
|
||||
# Output:
|
||||
# EPSS Status:
|
||||
# Latest Model Date: 2025-12-16
|
||||
# Source: bundle://risk-bundle-2025-12-16
|
||||
# CVE Count: 231,417
|
||||
# Staleness: ACCEPTABLE (within 7 days)
|
||||
```
|
||||
|
||||
### Update Cadence
|
||||
|
||||
**Recommended**:
|
||||
- **Online**: Daily (automatic)
|
||||
- **Air-gapped**: Weekly (manual bundle import)
|
||||
|
||||
**Staleness Thresholds**:
|
||||
- **FRESH**: ≤1 day
|
||||
- **ACCEPTABLE**: ≤7 days
|
||||
- **STALE**: ≤14 days
|
||||
- **VERY_STALE**: >14 days (alert, fallback to CVSS-only)
|
||||
|
||||
---
|
||||
|
||||
## Scanner Integration
|
||||
|
||||
### EPSS Evidence in Scan Findings
|
||||
|
||||
Every scan finding includes **immutable EPSS-at-scan** evidence:
|
||||
|
||||
```json
|
||||
{
|
||||
"finding_id": "CVE-2024-12345-pkg:npm/lodash@4.17.21",
|
||||
"cve_id": "CVE-2024-12345",
|
||||
"product": "pkg:npm/lodash@4.17.21",
|
||||
"scan_id": "scan-abc123",
|
||||
"scan_timestamp": "2025-12-17T10:30:00Z",
|
||||
"evidence": {
|
||||
"cvss_v4": {
|
||||
"vector_string": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
|
||||
"base_score": 9.3,
|
||||
"severity": "CRITICAL"
|
||||
},
|
||||
"epss_at_scan": {
|
||||
"epss_score": 0.42357,
|
||||
"percentile": 0.88234,
|
||||
"model_date": "2025-12-16",
|
||||
"import_run_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
},
|
||||
"epss_current": {
|
||||
"epss_score": 0.45123,
|
||||
"percentile": 0.89456,
|
||||
"model_date": "2025-12-17",
|
||||
"delta_score": 0.02766,
|
||||
"delta_percentile": 0.01222,
|
||||
"trend": "RISING"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- **epss_at_scan**: Immutable, captured at scan time (deterministic replay)
|
||||
- **epss_current**: Mutable, updated daily for live triage
|
||||
- **Replay**: Historical scans always use `epss_at_scan` for consistent policy evaluation
|
||||
|
||||
### Bulk Query Optimization
|
||||
|
||||
Scanner queries EPSS for all CVEs in a single database call:
|
||||
|
||||
```sql
|
||||
SELECT cve_id, epss_score, percentile, model_date, import_run_id
|
||||
FROM concelier.epss_current
|
||||
WHERE cve_id = ANY(@cve_ids);
|
||||
```
|
||||
|
||||
**Performance**: <500ms for 10k CVEs (P95)
|
||||
|
||||
---
|
||||
|
||||
## Policy Engine Integration
|
||||
|
||||
### Risk Score Formula
|
||||
|
||||
**Simple Profile**:
|
||||
|
||||
```
|
||||
risk_score = (cvss_base / 10) + epss_bonus + kev_bonus
|
||||
```
|
||||
|
||||
**EPSS Bonus Table**:
|
||||
|
||||
| EPSS Percentile | Bonus | Rationale |
|
||||
|----------------|-------|-----------|
|
||||
| ≥99th | +10% | Top 1% most likely to be exploited |
|
||||
| ≥90th | +5% | Top 10% high exploitation probability |
|
||||
| ≥50th | +2% | Above median moderate risk |
|
||||
| <50th | 0% | Below median no bonus |
|
||||
|
||||
**Advanced Profile**:
|
||||
|
||||
Adds:
|
||||
- **KEV synergy**: If in KEV catalog → multiply EPSS bonus by 1.5
|
||||
- **Uncertainty penalty**: Missing EPSS → -5%
|
||||
- **Temporal decay**: EPSS >30 days stale → reduce bonus by 50%
|
||||
|
||||
### VEX Lattice Rules
|
||||
|
||||
**Escalation**:
|
||||
- **SR (Static Reachable) + EPSS≥90th** → Auto-escalate to **CR (Confirmed Reachable)**
|
||||
- Rationale: High exploit probability warrants confirmation
|
||||
|
||||
**Review Flags**:
|
||||
- **DV (Denied by Vendor VEX) + EPSS≥95th** → Flag for manual review
|
||||
- Rationale: Vendor denial contradicted by active exploitation signals
|
||||
|
||||
**Prioritization**:
|
||||
- **U (Unknown) + EPSS≥95th** → Prioritize for reachability analysis
|
||||
- Rationale: High exploit probability justifies effort
|
||||
|
||||
### SPL (Stella Policy Language) Syntax
|
||||
|
||||
```yaml
|
||||
# Custom policy using EPSS
|
||||
rules:
|
||||
- name: high_epss_escalation
|
||||
condition: |
|
||||
epss.percentile >= 0.95 AND
|
||||
lattice.state == "SR" AND
|
||||
runtime.exposed == true
|
||||
action: escalate_to_cr
|
||||
reason: "High EPSS (top 5%) + Static Reachable + Runtime Exposed"
|
||||
|
||||
- name: epss_trend_alert
|
||||
condition: |
|
||||
epss.delta_score >= 0.10 AND
|
||||
cvss.base_score >= 7.0
|
||||
action: notify
|
||||
channels: [slack, email]
|
||||
reason: "EPSS jumped by 10+ points (was {epss.old_score}, now {epss.new_score})"
|
||||
```
|
||||
|
||||
**Available Fields**:
|
||||
- `epss.score` - Current EPSS score (0.0-1.0)
|
||||
- `epss.percentile` - Current percentile (0.0-1.0)
|
||||
- `epss.model_date` - Model date
|
||||
- `epss.delta_score` - Change vs previous scan
|
||||
- `epss.trend` - RISING, FALLING, STABLE
|
||||
- `epss.at_scan.score` - Immutable score at scan time
|
||||
- `epss.at_scan.percentile` - Immutable percentile at scan time
|
||||
|
||||
---
|
||||
|
||||
## Notification Integration
|
||||
|
||||
### Event: vuln.priority.changed
|
||||
|
||||
Emitted when EPSS change causes priority band shift.
|
||||
|
||||
**Payload**:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "vuln.priority.changed",
|
||||
"vulnerability_id": "CVE-2024-12345",
|
||||
"product_key": "pkg:npm/lodash@4.17.21",
|
||||
"old_priority_band": "medium",
|
||||
"new_priority_band": "high",
|
||||
"reason": "EPSS percentile crossed 95th (was 88th, now 96th)",
|
||||
"epss_change": {
|
||||
"old_score": 0.42,
|
||||
"new_score": 0.78,
|
||||
"delta_score": 0.36,
|
||||
"old_percentile": 0.88,
|
||||
"new_percentile": 0.96,
|
||||
"model_date": "2025-12-16"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Rules
|
||||
|
||||
**File**: `etc/notify.yaml`
|
||||
|
||||
```yaml
|
||||
notify:
|
||||
rules:
|
||||
- name: epss_crossed_high
|
||||
event_type: vuln.priority.changed
|
||||
condition: "payload.epss_change.new_percentile >= 0.95"
|
||||
channels: [slack, email]
|
||||
template: epss_high_alert
|
||||
digest: false # Immediate
|
||||
|
||||
- name: epss_big_jump
|
||||
event_type: vuln.priority.changed
|
||||
condition: "payload.epss_change.delta_score >= 0.10"
|
||||
channels: [slack]
|
||||
template: epss_rising_threat
|
||||
digest: true
|
||||
digest_time: "09:00" # Daily digest at 9 AM
|
||||
```
|
||||
|
||||
### Slack Template Example
|
||||
|
||||
```
|
||||
🚨 **High EPSS Alert**
|
||||
|
||||
**CVE**: CVE-2024-12345
|
||||
**Product**: pkg:npm/lodash@4.17.21
|
||||
**EPSS**: 0.78 (96th percentile) ⬆️ from 0.42 (88th percentile)
|
||||
**Delta**: +0.36 (36 percentage points)
|
||||
**Priority**: Medium → **High**
|
||||
|
||||
**Action Required**: Review and prioritize remediation.
|
||||
|
||||
[View in StellaOps →](https://stellaops.example.com/vulns/CVE-2024-12345)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### EPSS Data Not Available
|
||||
|
||||
**Symptom**: Scans show "EPSS: N/A"
|
||||
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check EPSS status
|
||||
stellaops epss status
|
||||
|
||||
# Check import runs
|
||||
stellaops concelier jobs list --type epss.ingest --limit 10
|
||||
```
|
||||
|
||||
**Resolution**:
|
||||
1. **No imports**: Trigger manual ingest
|
||||
```bash
|
||||
stellaops concelier job trigger epss.ingest
|
||||
```
|
||||
|
||||
2. **Import failed**: Check logs
|
||||
```bash
|
||||
stellaops concelier logs --job-id <id> --level ERROR
|
||||
```
|
||||
|
||||
3. **FIRST.org down**: Use air-gapped bundle
|
||||
```bash
|
||||
stellaops offline import --bundle /path/to/risk-bundle.tar.zst
|
||||
```
|
||||
|
||||
### Stale EPSS Data
|
||||
|
||||
**Symptom**: UI shows "EPSS stale (14 days)"
|
||||
|
||||
**Diagnosis**:
|
||||
```sql
|
||||
SELECT * FROM concelier.epss_model_staleness;
|
||||
-- Output: days_stale: 14, staleness_status: STALE
|
||||
```
|
||||
|
||||
**Resolution**:
|
||||
1. **Online**: Check scheduler job status
|
||||
```bash
|
||||
stellaops scheduler jobs status epss.ingest
|
||||
```
|
||||
|
||||
2. **Air-gapped**: Import fresh bundle
|
||||
```bash
|
||||
stellaops offline import --bundle /path/to/latest-bundle.tar.zst
|
||||
```
|
||||
|
||||
3. **Fallback**: Disable EPSS temporarily (uses CVSS-only)
|
||||
```yaml
|
||||
# etc/scanner.yaml
|
||||
scanner:
|
||||
epss:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
### High Memory Usage During Ingest
|
||||
|
||||
**Symptom**: Concelier worker OOM during EPSS ingest
|
||||
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check memory metrics
|
||||
stellaops metrics query 'process_resident_memory_bytes{service="concelier"}'
|
||||
```
|
||||
|
||||
**Resolution**:
|
||||
1. **Increase worker memory limit**:
|
||||
```yaml
|
||||
# Kubernetes deployment
|
||||
resources:
|
||||
limits:
|
||||
memory: 1Gi # Was 512Mi
|
||||
```
|
||||
|
||||
2. **Verify streaming parser** (should not load full CSV into memory):
|
||||
```bash
|
||||
# Check logs for "EPSS CSV parsed: rows_yielded="
|
||||
stellaops concelier logs --job-type epss.ingest | grep "CSV parsed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Combine Signals (Never Use EPSS Alone)
|
||||
|
||||
❌ **Don't**: `if epss > 0.95 then CRITICAL`
|
||||
|
||||
✅ **Do**: `if cvss >= 8.0 AND epss >= 0.95 AND runtime_exposed then CRITICAL`
|
||||
|
||||
### 2. Review High EPSS Manually
|
||||
|
||||
Manually review vulnerabilities with EPSS ≥95th percentile, especially if:
|
||||
- CVSS is low (<7.0) but EPSS is high
|
||||
- Vendor VEX denies exploitability but EPSS is high
|
||||
|
||||
### 3. Track Trends
|
||||
|
||||
Monitor EPSS changes over time:
|
||||
- Rising EPSS → increasing threat
|
||||
- Falling EPSS → threat subsiding
|
||||
|
||||
### 4. Update Regularly
|
||||
|
||||
- **Online**: Daily (automatic)
|
||||
- **Air-gapped**: Weekly minimum, daily preferred
|
||||
|
||||
### 5. Verify During Audits
|
||||
|
||||
For compliance audits, use EPSS-at-scan (immutable) not current EPSS:
|
||||
```sql
|
||||
SELECT epss_score_at_scan, epss_model_date_at_scan
|
||||
FROM scan_findings
|
||||
WHERE scan_id = 'audit-scan-20251217';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Query Current EPSS
|
||||
|
||||
```bash
|
||||
# Single CVE
|
||||
stellaops epss get CVE-2024-12345
|
||||
|
||||
# Output:
|
||||
# CVE-2024-12345
|
||||
# Score: 0.42357 (42.4% probability)
|
||||
# Percentile: 88.2th
|
||||
# Model Date: 2025-12-16
|
||||
# Status: FRESH
|
||||
```
|
||||
|
||||
### Batch Query
|
||||
|
||||
```bash
|
||||
# From file
|
||||
stellaops epss batch --file cves.txt --output epss-scores.json
|
||||
|
||||
# cves.txt:
|
||||
# CVE-2024-1
|
||||
# CVE-2024-2
|
||||
# CVE-2024-3
|
||||
```
|
||||
|
||||
### Query History
|
||||
|
||||
```bash
|
||||
# Last 180 days
|
||||
stellaops epss history CVE-2024-12345 --days 180 --format csv
|
||||
|
||||
# Output: epss-history-CVE-2024-12345.csv
|
||||
# model_date,epss_score,percentile
|
||||
# 2025-12-17,0.45123,0.89456
|
||||
# 2025-12-16,0.42357,0.88234
|
||||
# ...
|
||||
```
|
||||
|
||||
### Top CVEs by EPSS
|
||||
|
||||
```bash
|
||||
# Top 100
|
||||
stellaops epss top --limit 100 --format table
|
||||
|
||||
# Output:
|
||||
# Rank | CVE | Score | Percentile | CVSS
|
||||
# -----|---------------|--------|------------|------
|
||||
# 1 | CVE-2024-9999 | 0.9872 | 99.9th | 9.8
|
||||
# 2 | CVE-2024-8888 | 0.9654 | 99.8th | 8.1
|
||||
# ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **FIRST EPSS Homepage**: https://www.first.org/epss/
|
||||
- **EPSS Data & Stats**: https://www.first.org/epss/data_stats
|
||||
- **EPSS API Docs**: https://www.first.org/epss/api
|
||||
- **CVSS v4.0 Spec**: https://www.first.org/cvss/v4.0/specification-document
|
||||
- **StellaOps Policy Guide**: `docs/policy/overview.md`
|
||||
- **StellaOps Reachability Guide**: `docs/modules/scanner/reachability.md`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-17
|
||||
**Version**: 1.0
|
||||
**Maintainer**: StellaOps Security Team
|
||||
290
docs/modules/risk-engine/guides/epss-integration.md
Normal file
290
docs/modules/risk-engine/guides/epss-integration.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# EPSS Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
EPSS (Exploit Prediction Scoring System) is a FIRST.org initiative that provides probability scores for vulnerability exploitation within 30 days. StellaOps integrates EPSS as a risk signal alongside CVSS and KEV (Known Exploited Vulnerabilities) to provide more accurate vulnerability prioritization.
|
||||
|
||||
## How EPSS Works
|
||||
|
||||
EPSS uses machine learning to predict the probability that a CVE will be exploited in the wild within the next 30 days. The model considers:
|
||||
- Vulnerability characteristics (CVSS metrics, CWE, etc.)
|
||||
- Social signals (Twitter mentions, GitHub issues, etc.)
|
||||
- Exploit database entries
|
||||
- Historical exploitation patterns
|
||||
|
||||
EPSS outputs two values:
|
||||
- **Score** (0.0-1.0): Probability of exploitation in next 30 days
|
||||
- **Percentile** (0-100): Ranking relative to all other CVEs
|
||||
|
||||
## How EPSS Affects Risk Scoring in StellaOps
|
||||
|
||||
### Combined Risk Formula
|
||||
|
||||
StellaOps combines CVSS, KEV, and EPSS signals into a unified risk score:
|
||||
|
||||
```
|
||||
risk_score = clamp01(
|
||||
(cvss / 10) + # Base severity (0-1)
|
||||
kevBonus + # +0.20 if in CISA KEV
|
||||
epssBonus # +0.02 to +0.10 based on percentile
|
||||
)
|
||||
```
|
||||
|
||||
### EPSS Bonus Thresholds
|
||||
|
||||
| EPSS Percentile | Bonus | Rationale |
|
||||
|-----------------|-------|-----------|
|
||||
| >= 99th | +10% | Top 1% most likely to be exploited; urgent priority |
|
||||
| >= 90th | +5% | Top 10%; high exploitation probability |
|
||||
| >= 50th | +2% | Above median; moderate additional risk |
|
||||
| < 50th | 0% | Below median; no bonus applied |
|
||||
|
||||
### Example Calculations
|
||||
|
||||
| CVE | CVSS | KEV | EPSS Percentile | Risk Score |
|
||||
|-----|------|-----|-----------------|------------|
|
||||
| CVE-2024-1234 | 9.8 | Yes | 99.5th | 1.00 (clamped) |
|
||||
| CVE-2024-5678 | 7.5 | No | 95th | 0.80 |
|
||||
| CVE-2024-9012 | 6.0 | No | 60th | 0.62 |
|
||||
| CVE-2024-3456 | 8.0 | No | 30th | 0.80 |
|
||||
|
||||
## Implementation Reference
|
||||
|
||||
### IEpssSource Interface
|
||||
|
||||
```csharp
|
||||
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/IEpssSources.cs
|
||||
|
||||
public interface IEpssSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns EPSS data for the given CVE identifier, or null if unknown.
|
||||
/// </summary>
|
||||
Task<EpssData?> GetEpssAsync(string cveId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record EpssData(double Score, double Percentile, DateTimeOffset? ModelVersion = null);
|
||||
```
|
||||
|
||||
### Risk Providers
|
||||
|
||||
**EpssProvider** - Uses EPSS score directly as risk (0.0-1.0):
|
||||
```csharp
|
||||
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/EpssProvider.cs
|
||||
public const string ProviderName = "epss";
|
||||
```
|
||||
|
||||
**CvssKevEpssProvider** - Combined provider using all three signals:
|
||||
```csharp
|
||||
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/EpssProvider.cs
|
||||
public const string ProviderName = "cvss-kev-epss";
|
||||
```
|
||||
|
||||
## Policy Configuration
|
||||
|
||||
### Enabling EPSS Integration
|
||||
|
||||
```yaml
|
||||
# etc/risk-engine.yaml
|
||||
risk:
|
||||
providers:
|
||||
- name: cvss-kev-epss
|
||||
enabled: true
|
||||
priority: 1
|
||||
|
||||
epss:
|
||||
enabled: true
|
||||
source: database # or "api" for live FIRST API
|
||||
cache_ttl: 24h
|
||||
|
||||
# Percentile-based bonus thresholds
|
||||
thresholds:
|
||||
- percentile: 99
|
||||
bonus: 0.10
|
||||
- percentile: 90
|
||||
bonus: 0.05
|
||||
- percentile: 50
|
||||
bonus: 0.02
|
||||
```
|
||||
|
||||
### Custom Threshold Configuration
|
||||
|
||||
Organizations can customize EPSS bonus thresholds based on their risk tolerance:
|
||||
|
||||
```yaml
|
||||
# More aggressive (higher bonuses for high-risk vulns)
|
||||
epss:
|
||||
thresholds:
|
||||
- percentile: 99
|
||||
bonus: 0.15
|
||||
- percentile: 95
|
||||
bonus: 0.10
|
||||
- percentile: 75
|
||||
bonus: 0.05
|
||||
|
||||
# More conservative (smaller bonuses)
|
||||
epss:
|
||||
thresholds:
|
||||
- percentile: 99
|
||||
bonus: 0.05
|
||||
- percentile: 95
|
||||
bonus: 0.02
|
||||
```
|
||||
|
||||
## EPSS in Lattice Decisions
|
||||
|
||||
EPSS influences VEX lattice state transitions for vulnerability triage:
|
||||
|
||||
| Current State | EPSS >= 90th Percentile | Recommended Action |
|
||||
|---------------|-------------------------|-------------------|
|
||||
| SR (Static Reachable) | Yes | Escalate to CR (Confirmed Reachable) priority |
|
||||
| SU (Static Unreachable) | Yes | Flag for review - high exploit probability despite unreachable |
|
||||
| DV (Denied by Vendor VEX) | Yes | Review denial validity - exploit activity contradicts vendor |
|
||||
| U (Unknown) | Yes | Prioritize for reachability analysis |
|
||||
|
||||
### VEX Policy Example
|
||||
|
||||
```yaml
|
||||
# etc/vex-policy.yaml
|
||||
lattice:
|
||||
transitions:
|
||||
- from: SR
|
||||
to: CR
|
||||
condition:
|
||||
epss_percentile: ">= 90"
|
||||
action: auto_escalate
|
||||
|
||||
- from: SU
|
||||
to: REVIEW
|
||||
condition:
|
||||
epss_percentile: ">= 95"
|
||||
action: flag_for_review
|
||||
reason: "High EPSS despite static unreachability"
|
||||
```
|
||||
|
||||
## Offline EPSS Data
|
||||
|
||||
EPSS data is included in offline risk bundles for air-gapped environments.
|
||||
|
||||
### Bundle Structure
|
||||
|
||||
```
|
||||
risk-bundle-2025-12-14/
|
||||
├── manifest.json
|
||||
├── kev/
|
||||
│ └── kev-catalog.json
|
||||
├── epss/
|
||||
│ ├── epss-scores.csv.zst # Compressed EPSS data
|
||||
│ └── epss-metadata.json # Model date, row count, checksum
|
||||
└── signatures/
|
||||
└── bundle.dsse.json
|
||||
```
|
||||
|
||||
### EPSS Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"model_date": "2025-12-14",
|
||||
"row_count": 248732,
|
||||
"sha256": "abc123...",
|
||||
"source": "first.org",
|
||||
"created_at": "2025-12-14T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Importing Offline EPSS Data
|
||||
|
||||
```bash
|
||||
# Import risk bundle (includes EPSS)
|
||||
stellaops offline import --kit risk-bundle-2025-12-14.tar.zst
|
||||
|
||||
# Verify EPSS data imported
|
||||
stellaops epss status
|
||||
# Output:
|
||||
# EPSS Data Status:
|
||||
# Model Date: 2025-12-14
|
||||
# CVE Count: 248,732
|
||||
# Last Import: 2025-12-14T10:30:00Z
|
||||
```
|
||||
|
||||
## Accuracy Considerations
|
||||
|
||||
| Metric | Value | Notes |
|
||||
|--------|-------|-------|
|
||||
| EPSS Coverage | ~95% of NVD CVEs | Some very new CVEs (<24h) not yet scored |
|
||||
| Model Refresh | Daily | Scores can change day-to-day |
|
||||
| Prediction Window | 30 days | Probability of exploit in next 30 days |
|
||||
| Historical Accuracy | ~85% AUC | Based on FIRST published evaluations |
|
||||
|
||||
### Limitations
|
||||
|
||||
1. **New CVEs**: Very recent CVEs may not have EPSS scores yet
|
||||
2. **Model Lag**: EPSS model updates daily; real-world exploit activity may be faster
|
||||
3. **Zero-Days**: Pre-disclosure vulnerabilities cannot be scored
|
||||
4. **Context Blind**: EPSS doesn't consider your specific environment
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Combine Signals**: Always use EPSS alongside CVSS and KEV, not in isolation
|
||||
2. **Review High EPSS**: Manually review vulnerabilities with EPSS >= 95th percentile
|
||||
3. **Track Changes**: Monitor EPSS score changes over time for trending threats
|
||||
4. **Update Regularly**: Keep EPSS data fresh (daily in online mode, weekly for offline)
|
||||
5. **Verify High-Risk**: For critical decisions, verify EPSS data against FIRST API
|
||||
|
||||
## API Usage
|
||||
|
||||
### Query EPSS Score
|
||||
|
||||
```bash
|
||||
# Get EPSS score for a specific CVE
|
||||
stellaops epss get CVE-2024-12345
|
||||
|
||||
# Batch query
|
||||
stellaops epss batch --file cves.txt --output epss-scores.json
|
||||
```
|
||||
|
||||
### Programmatic Access
|
||||
|
||||
```csharp
|
||||
// Using IEpssSource
|
||||
var epssData = await epssSource.GetEpssAsync("CVE-2024-12345", cancellationToken);
|
||||
if (epssData is not null)
|
||||
{
|
||||
Console.WriteLine($"Score: {epssData.Score:P2}");
|
||||
Console.WriteLine($"Percentile: {epssData.Percentile:F1}th");
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### EPSS Data Not Available
|
||||
|
||||
```bash
|
||||
# Check EPSS source status
|
||||
stellaops epss status
|
||||
|
||||
# Force refresh from FIRST API
|
||||
stellaops epss refresh --force
|
||||
|
||||
# Check for specific CVE
|
||||
stellaops epss get CVE-2024-12345 --verbose
|
||||
```
|
||||
|
||||
### Stale EPSS Data
|
||||
|
||||
If EPSS data is older than 7 days:
|
||||
|
||||
```bash
|
||||
# Check staleness
|
||||
stellaops epss check-staleness
|
||||
|
||||
# Import fresh bundle
|
||||
stellaops offline import --kit latest-bundle.tar.zst
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [FIRST EPSS Model](https://www.first.org/epss/)
|
||||
- [EPSS API Documentation](https://www.first.org/epss/api)
|
||||
- [EPSS FAQ](https://www.first.org/epss/faq)
|
||||
- [StellaOps Risk Engine Architecture](../modules/risk-engine/architecture.md)
|
||||
35
docs/modules/risk-engine/guides/explainability.md
Normal file
35
docs/modules/risk-engine/guides/explainability.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Risk Explainability
|
||||
|
||||
> Source: `CONTRACT-RISK-SCORING-002` (2025-12-05). Fixtures live under `docs/modules/risk-engine/samples/explain/`; all hashes in `SHA256SUMS`. Keep outputs deterministic (frozen payloads, stable ordering).
|
||||
|
||||
## Purpose
|
||||
- Show how the scoring engine produces per-factor contributions and traces that UI/CLI/export surfaces render for auditors and operators.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: Console/CLI users, auditors, SREs.
|
||||
- In scope: explainability payload shape, field meanings, provenance, UI/CLI mapping, offline/export behavior.
|
||||
- Out of scope: formula math (see `formulas.md`), API specifics (see `api.md`).
|
||||
|
||||
## Payload Shape
|
||||
- Envelope: `job_id`, `tenant_id`, `context_id`, `profile_id`, `profile_version`, `profile_hash`, `finding_id`, `raw_score`, `normalized_score`, `severity`, `signal_values{}`, `signal_contributions{}`, optional `override_applied`, `override_reason`, `gates_triggered[]`, `scored_at`, `provenance` (job hash + fixture hashes).
|
||||
- Factor entries (from `signal_values`/`signal_contributions`): `name`, `source`, `type`, `path`, `raw_value`, `normalized_value`, `weight`, `contribution`, `provenance`.
|
||||
- UI/CLI expectations: deterministic ordering (factor type → source → timestamp), highlight top contributors, show attestation status for each factor.
|
||||
|
||||
## UI/CLI Views
|
||||
- Console: frame sample in `docs/modules/risk-engine/samples/explain/console-frame.json` shows top contributors, gate badges, and provenance hashes.
|
||||
- CLI `stella risk explain job-001`: deterministic text fixture in `docs/modules/risk-engine/samples/explain/cli-explain.txt`; `--json` mirrors `explain-trace.json`.
|
||||
- Export Center: embed explain payload + SHA256 manifest; CSV export keeps deterministic ordering.
|
||||
|
||||
## Determinism & Offline Posture
|
||||
- Example payload: `docs/modules/risk-engine/samples/explain/explain-trace.json` (hash in `SHA256SUMS`).
|
||||
- No live calls; all captures from frozen fixtures. Use exact ordering and timestamps when regenerating.
|
||||
|
||||
## Open Items
|
||||
- Add schema file once JSON schema is frozen; update references accordingly.
|
||||
|
||||
## References
|
||||
- `docs/modules/risk-engine/guides/overview.md`
|
||||
- `docs/modules/risk-engine/guides/profiles.md`
|
||||
- `docs/modules/risk-engine/guides/factors.md`
|
||||
- `docs/modules/risk-engine/guides/formulas.md`
|
||||
- `docs/modules/risk-engine/guides/api.md`
|
||||
46
docs/modules/risk-engine/guides/factors.md
Normal file
46
docs/modules/risk-engine/guides/factors.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Risk Factors
|
||||
|
||||
> Aligned to `CONTRACT-RISK-SCORING-002` (published 2025-12-05). Keep fixtures deterministic and offline-friendly.
|
||||
|
||||
## Purpose
|
||||
- Catalog supported factors (exploit likelihood, VEX state, reachability, runtime facts, fix availability, asset criticality, provenance trust, tenant overrides) and how they normalize into risk math.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: risk engineers, policy authors, platform SREs.
|
||||
- In scope: factor definitions, required/optional fields, normalization rules, TTLs, provenance expectations.
|
||||
- Out of scope: full formula math (see `formulas.md`), API wiring (see `api.md`).
|
||||
|
||||
## Factor Catalog (mirrors profile `signals[]`)
|
||||
| Factor | Required fields | Optional fields | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| CVSS / exploit likelihood | `name`, `source`, `type:"numeric"`, `path`, `transform:"normalize_10"` | `unit:"score"`, `last_seen`, `confidence` | Normalize 0–10 to 0–1; clamp and keep original in provenance. |
|
||||
| KEV flag | `name`, `source`, `type:"boolean"`, `path` | `last_seen` | Boolean boost; drives severity overrides/decisions. |
|
||||
| Reachability | `name`, `source`, `type:"numeric"`, `path` | `unit:"score"`, `guards` | May fuse static reachability + runtime observation; ordered by entrypoint/path hash. |
|
||||
| Runtime facts | `name`, `source`, `type:"categorical" or "numeric"`, `path` | `trace_id`, `span_id` | Includes host/container identity and provenance for runtime traces. |
|
||||
| Fix availability | `name`, `source`, `type`, `path` | `mitigation`, `vendor_status` | Decay older advisories; keep mitigation text intact. |
|
||||
| Asset criticality | `name`, `source`, `type`, `path` | `tenant_scope`, `owner` | Used as multiplier/guard in formulas. |
|
||||
| Provenance trust | `name`, `source`, `type:"categorical"`, `path` | `key_id`, `chain_of_custody` | Gate low-trust inputs; must carry attestation hash. |
|
||||
| Custom overrides | `name`, `source`, `type`, `path` | `override_reason`, `reviewer`, `expires_at` | Logged and expiring; surfaced in `signal_contributions`. |
|
||||
|
||||
## Normalization Rules
|
||||
- Validate against profile `signals.type` and known transforms; reject unknown fields.
|
||||
- Clamp numeric inputs to 0–1; record original value in provenance for audit.
|
||||
- TTL/decay: apply per-factor defaults (pending payload fixtures); drop expired signals deterministically.
|
||||
- Precedence: signed → unsigned; runtime → static; newer → older; when tied, lowest hash order.
|
||||
|
||||
Interim notes: follow legacy profile guidance — preserve provenance, never mutate source evidence, and keep ordering stable so explainability hashes are repeatable across UI/CLI/exports.
|
||||
|
||||
## Determinism & Ordering
|
||||
- Sort factors by `factor_type` then `source` then `timestamp_utc`; deterministic hashing for fixtures.
|
||||
- Record SHA256 for sample payloads in `docs/modules/risk-engine/samples/factors/SHA256SUMS` once provided.
|
||||
|
||||
## Open Items
|
||||
- Sample payloads per factor for fixtures + hashes.
|
||||
- TTL/decay parameters from Risk Engine Guild.
|
||||
- Provenance attestation examples (signed runtime traces, KEV ingestion evidence).
|
||||
|
||||
## References
|
||||
- `docs/modules/risk-engine/guides/overview.md`
|
||||
- `docs/modules/risk-engine/guides/profiles.md`
|
||||
- `docs/modules/risk-engine/guides/formulas.md`
|
||||
- `docs/modules/risk-engine/guides/api.md`
|
||||
62
docs/modules/risk-engine/guides/formulas.md
Normal file
62
docs/modules/risk-engine/guides/formulas.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Risk Formulas
|
||||
|
||||
> Based on `CONTRACT-RISK-SCORING-002` (2025-12-05). Keep math examples deterministic with fixed fixtures.
|
||||
|
||||
## Purpose
|
||||
- Describe how normalized factors combine into a 0–100 risk score with severity bands.
|
||||
- Capture gating, weighting, normalization, and override rules.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: risk engineers, policy authors, auditors.
|
||||
- In scope: weighting strategies, aggregation functions, severity thresholds, gating rules, tie-breakers.
|
||||
- Out of scope: full API payloads (see `api.md`), factor definitions (see `factors.md`).
|
||||
|
||||
## Formula Building Blocks
|
||||
- Weighted sum with per-factor caps; enforce max contribution per family (exploitability, reachability, runtime).
|
||||
- Base rule (contract): `raw_score = Σ(signal_value × weight)`, `normalized_score = clamp(raw_score, 0.0, 1.0)`.
|
||||
- VEX gate: if `signals.HasVexDenial`, return `0.0` immediately (mitigated finding).
|
||||
- CVSS + KEV provider: `score = clamp01((cvss/10) + (kev ? 0.2 : 0))`.
|
||||
- Guard rails: hard gates when `(exploit_likelihood >= T1) AND (reachability >= T2)` or when provenance trust below minimum.
|
||||
- Decay/time weighting: exponential decay for stale runtime/KEV signals; fresh VEX `not_affected` may down-weight exploit scores.
|
||||
- Tenant/asset overrides: additive/override blocks with expiry; always logged in explainability output.
|
||||
- Safety: divide-by-zero and null handling must be deterministic and reflected in explain trace.
|
||||
|
||||
## Severity Mapping
|
||||
- Contract levels: `critical`, `high`, `medium`, `low`, `informational` (priority 1–5).
|
||||
- Map `normalized_score` to bands per profile policy; include band rationale in explainability payload.
|
||||
|
||||
## Determinism
|
||||
- Stable ordering of factors before aggregation.
|
||||
- Use fixed precision (e.g., 4 decimals) before severity mapping; round not truncate.
|
||||
- Hash fixtures and record SHA256 for every example payload in `docs/modules/risk-engine/samples/formulas/SHA256SUMS`.
|
||||
|
||||
Interim notes: mirror legacy rule — simulation and production must share the exact evaluation codepath; no per-environment divergences. Severity buckets must be deterministic and governed by Authority scopes.
|
||||
|
||||
## Example (contract-aligned)
|
||||
```json
|
||||
{
|
||||
"finding_id": "f-123",
|
||||
"profile_id": "default-profile",
|
||||
"profile_version": "1.0.0",
|
||||
"raw_score": 0.75,
|
||||
"normalized_score": 0.85,
|
||||
"severity": "high",
|
||||
"signal_values": { "cvss": 7.5, "kev": true, "reachability": 0.9 },
|
||||
"signal_contributions": { "cvss": 0.4, "kev": 0.3, "reachability": 0.3 },
|
||||
"override_applied": "kev-boost",
|
||||
"override_reason": "Known Exploited Vulnerability",
|
||||
"scored_at": "2025-12-05T00:00:02Z"
|
||||
}
|
||||
```
|
||||
- CLI/Console screenshots pending telemetry assets (keep deterministic fixture IDs).
|
||||
|
||||
## Open Items
|
||||
- Fixtures for jobs/results and explainability traces.
|
||||
- Final per-profile severity thresholds (document once agreed).
|
||||
- UI traces for console/CLI explainability views.
|
||||
|
||||
## References
|
||||
- `docs/modules/risk-engine/guides/overview.md`
|
||||
- `docs/modules/risk-engine/guides/profiles.md`
|
||||
- `docs/modules/risk-engine/guides/factors.md`
|
||||
- `docs/modules/risk-engine/guides/api.md`
|
||||
50
docs/modules/risk-engine/guides/overview.md
Normal file
50
docs/modules/risk-engine/guides/overview.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Risk Overview
|
||||
|
||||
> Source of truth: `CONTRACT-RISK-SCORING-002` (published 2025-12-05). Keep fixtures deterministic (UTC timestamps, stable ordering, sealed sample payloads) and avoid external assets.
|
||||
|
||||
## Purpose
|
||||
- Explain the risk model at a glance: factors, formulas, scoring semantics (0–100), and severity bands.
|
||||
- Show how risk flows through StellaOps services (ingest → evaluate → explain → export) and how provenance is preserved.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: policy authors, risk engineers, auditors, and SREs consuming risk outputs.
|
||||
- In scope: concepts, glossary, lifecycle, artifacts, cross-module data flow diagrams (add after schema approval).
|
||||
- Out of scope: detailed factor math (goes to `formulas.md`), API specifics (goes to `api.md`).
|
||||
|
||||
## Core Concepts
|
||||
- **Signal → evidence → factor:** raw events (scanner, VEX, runtime) become evidence once validated; evidence is normalized into factors listed under profile `signals[]`.
|
||||
- **Profile vs. formula:** a profile bundles factor weights, thresholds, overrides, and severity mapping; formulas describe how weighted signals aggregate and when gates short-circuit.
|
||||
- **Provenance:** every input keeps its attestation/signature and source hash; explainability echoes `profile_hash`, factor hashes, and job correlation IDs.
|
||||
- **Explainability payloads:** UI/CLI show per-factor contributions (`signal_contributions`), source hashes, and rule gates; exports reuse the same envelope.
|
||||
- **Determinism:** stable ordering (factor type → source → timestamp), UTC ISO-8601 timestamps, fixed precision math, sealed fixtures.
|
||||
|
||||
Profiles use normalized factors (exploit likelihood, KEV flag, reachability, runtime evidence, fix availability, asset criticality, provenance trust) to produce 0–1 scores mapped to severity buckets. Simulation and production share the exact code path.
|
||||
|
||||
## Lifecycle
|
||||
1. **Job submit:** POST `/api/v1/risk/jobs` with `tenant_id`, `context_id`, `profile_id`, finding list; request is signed and queued.
|
||||
2. **Evidence ingestion:** scanner surface + reachability graphs, Zastava runtime signals, VEX/KEV feeds, mirror bundles (offline).
|
||||
3. **Normalization:** clamp units to 0–1, apply TTL/decay, dedupe by provenance hash, map to canonical factor catalog.
|
||||
4. **Profile evaluation:** apply weighted sum and overrides; respect gates (e.g., KEV + reachability) and Authority-imposed rules.
|
||||
5. **Severity assignment:** map `normalized_score` to severity levels (critical/high/medium/low/informational) with rationale.
|
||||
6. **Explainability & observability:** emit per-factor contribution table, provenance pointers, evaluation latency metrics; surface via `/risk/jobs/{id}` and export bundles.
|
||||
7. **Export/archival:** package explainability + profile version/hash for Findings Ledger/Export Center; mirror-friendly.
|
||||
|
||||
## Artifacts & Schemas
|
||||
- Contract: `CONTRACT-RISK-SCORING-002` (2025-12-05) — risk scoring jobs, results, and profile model.
|
||||
- Profile schema fields: `id`, `version`, `description`, optional `extends`, `signals[] {name, source, type, path, transform, unit}`, `weights{}`, `overrides{severity[], decisions[]}`, `metadata`, `provenance`.
|
||||
- Job/result fields: `job_id`, `profile_hash`, `normalized_score`, `severity`, `signal_values`, `signal_contributions`, optional overrides and timestamps.
|
||||
- Explainability envelope: reuse `signal_contributions` + `profile_hash`; store fixtures under `docs/modules/risk-engine/samples/explain/`.
|
||||
|
||||
## Determinism & Offline Posture
|
||||
- Use frozen fixture sets with SHA256 tables; keep manifests in `docs/modules/risk-engine/samples/*/SHA256SUMS`.
|
||||
- Regenerate examples via documented scripts only; no live network calls.
|
||||
- Simulation, API, UI, and export consumers must share the same deterministic ordering and precision.
|
||||
|
||||
## Open Items
|
||||
- Need real payload fixtures (jobs + explainability traces) and UI telemetry captures; placeholders remain in samples folders.
|
||||
|
||||
## References (to link once available)
|
||||
- `docs/modules/risk-engine/guides/profiles.md`
|
||||
- `docs/modules/risk-engine/guides/factors.md`
|
||||
- `docs/modules/risk-engine/guides/formulas.md`
|
||||
- `docs/modules/risk-engine/guides/api.md`
|
||||
85
docs/modules/risk-engine/guides/profiles.md
Normal file
85
docs/modules/risk-engine/guides/profiles.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Risk Profiles
|
||||
|
||||
> Contract source: `CONTRACT-RISK-SCORING-002` (published 2025-12-05). This file supersedes `docs/modules/risk-engine/guides/risk-profiles.md` once fixtures are added.
|
||||
|
||||
## Purpose
|
||||
- Define how profiles group factors, weights, thresholds, and severity bands.
|
||||
- Describe authoring, simulation, promotion, rollback, and provenance for profiles.
|
||||
|
||||
## Scope & Audience
|
||||
- Audience: policy authors, risk engineers, platform SREs.
|
||||
- Coverage: profile schema, lifecycle, governance, promotion paths, rollback, and observability hooks.
|
||||
|
||||
## Schema (from CONTRACT-RISK-SCORING-002)
|
||||
- Required: `id`, `version`, `description`, `signals[]`, `weights`, `metadata`.
|
||||
- `signals[]` fields: `name`, `source`, `type` (`numeric|boolean|categorical`), `path`, optional `transform`, optional `unit`.
|
||||
- Overrides: `overrides.severity[] { when, set }`, `overrides.decisions[] { when, action, reason }`.
|
||||
- Optional: `extends`, rollout flags, tenant overrides, `valid_from`/`valid_until`.
|
||||
- Storage rules: immutable once promoted; each change creates a new version with DSSE envelope and SHA256 manifest entry (`docs/modules/risk-engine/samples/profiles/SHA256SUMS`).
|
||||
|
||||
### Example Profile (contract snippet)
|
||||
```json
|
||||
{
|
||||
"id": "default-profile",
|
||||
"version": "1.0.0",
|
||||
"description": "Default risk profile for vulnerability prioritization",
|
||||
"extends": "base-profile",
|
||||
"signals": [
|
||||
{ "name": "cvss", "source": "nvd", "type": "numeric", "path": "/cvss/base_score", "transform": "normalize_10", "unit": "score" },
|
||||
{ "name": "kev", "source": "cisa", "type": "boolean", "path": "/kev/in_catalog" },
|
||||
{ "name": "reachability", "source": "scanner", "type": "numeric", "path": "/reachability/score" }
|
||||
],
|
||||
"weights": { "cvss": 0.4, "kev": 0.3, "reachability": 0.3 },
|
||||
"overrides": {
|
||||
"severity": [{ "when": { "kev": true }, "set": "critical" }],
|
||||
"decisions": [{ "when": { "kev": true, "reachability": { "$gt": 0.8 } }, "action": "deny", "reason": "KEV with high reachability" }]
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Severity Levels
|
||||
| Level | Value | Priority |
|
||||
| --- | --- | --- |
|
||||
| Critical | `critical` | 1 |
|
||||
| High | `high` | 2 |
|
||||
| Medium | `medium` | 3 |
|
||||
| Low | `low` | 4 |
|
||||
| Informational | `informational` | 5 |
|
||||
|
||||
## Lifecycle (outline)
|
||||
1. Authoring in Policy Studio (draft state)
|
||||
2. Simulation against fixtures (deterministic inputs)
|
||||
3. Review/approval workflow
|
||||
4. Promotion to environments (dev → staging → prod)
|
||||
5. Rollback hooks and audit trail
|
||||
|
||||
## Governance & Determinism
|
||||
- Profiles stored with DSSE/signatures; fixtures recorded in `docs/modules/risk-engine/samples/profiles/SHA256SUMS`.
|
||||
- Simulation and production share the same evaluation codepath; feature flags must be documented in `metadata.flags`.
|
||||
- Offline posture: include profiles, fixtures, and explainability bundles inside mirror packages with manifest hashes.
|
||||
|
||||
## Explainability & Observability
|
||||
- Per-factor contribution outputs (JSON) with stable ordering (factor type → source).
|
||||
- Metrics: evaluation latency (p50/p95), cache hit ratio, factor coverage %, profile hit rate, failed provenance validations.
|
||||
- Dashboards/alerts: to be filled when telemetry payloads arrive; reserve panels for gating violations and override usage.
|
||||
|
||||
## Open Items
|
||||
- Add signed fixtures (profiles + hashes) under `docs/modules/risk-engine/samples/profiles/` once payloads arrive.
|
||||
- Capture feature-flag list for registry alignment.
|
||||
- Telemetry field list for dashboards/alerts.
|
||||
- Finalize migration note when legacy `docs/modules/risk-engine/guides/risk-profiles.md` is archived.
|
||||
|
||||
## References
|
||||
- `docs/modules/risk-engine/guides/overview.md`
|
||||
- `docs/modules/risk-engine/guides/factors.md`
|
||||
- `docs/modules/risk-engine/guides/formulas.md`
|
||||
- `docs/modules/risk-engine/guides/explainability.md`
|
||||
- `docs/modules/risk-engine/guides/api.md`
|
||||
- Existing context: `docs/modules/risk-engine/guides/risk-profiles.md` (to reconcile once schema lands)
|
||||
|
||||
## Interim Notes (carried from legacy `docs/modules/risk-engine/guides/risk-profiles.md`)
|
||||
- Profiles define how evidence (CVSS/EPSS-like exploit likelihood, KEV flags, VEX status, reachability, runtime evidence, fix availability, asset criticality, provenance trust) normalizes into a 0–100 score with severity buckets.
|
||||
- Workflow highlights: author in Policy Studio → simulate with fixtures → activate in Policy Engine → explain outputs in CLI/Console → export for auditors via Export Center.
|
||||
- Governance: draft/review/approval with DSSE/signatures; rollback hooks and promotion gates enforced by Authority scopes; determinism required (same codepath for simulation and production).
|
||||
- Observability: record scoring latency, factor distribution, and profile usage; offline posture via mirror bundles with fixtures and hash manifests.
|
||||
57
docs/modules/risk-engine/guides/risk-profiles.md
Normal file
57
docs/modules/risk-engine/guides/risk-profiles.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Risk Scoring Profiles
|
||||
|
||||
> Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
|
||||
|
||||
## Overview
|
||||
|
||||
Risk Scoring Profiles define customizable formulas that convert raw evidence (CVSS, EPSS-like exploit likelihood, KEV exploited lists, VEX status, reachability, runtime evidence, fix availability, asset criticality, provenance trust) into normalized risk scores (0–100) with severity buckets. Profiles are authored in Policy Studio, simulated, versioned, and executed by the scoring engine with full explainability.
|
||||
|
||||
- **Primary components:** Policy Engine, Findings Ledger, Conseiller, Excitor, Console, Policy Studio, CLI, Export Center, Authority & Tenancy, Observability.
|
||||
- **Surfaces:** policy documents, scoring engine, factor providers, explainability artefacts, APIs, CLI, UI.
|
||||
|
||||
Aggregation-Only Contract remains in force: Conseiller and Excitor never merge or mutate source records. Risk scoring consumes linked evidence and preserves provenance for explainability.
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. **Profile authoring:** Policy Studio exposes declarative DSL to define factors, weights, thresholds, and severity buckets.
|
||||
2. **Simulation:** operators preview profiles against historical findings/SBOMs, compare with existing policies, and inspect factor breakdowns.
|
||||
3. **Activation:** Policy Engine evaluates profiles on change streams, producing scores and detailed factor contributions per finding and per asset.
|
||||
4. **Explainability:** CLI/Console display math traces, provenance IDs, and rationale for each factor. Export Center packages reports for auditors.
|
||||
5. **Versioning:** profiles carry semantic versions, promotion workflows, and rollback hooks; Authority scopes enforce who can publish or edit.
|
||||
|
||||
## Factor model
|
||||
|
||||
| Factor | Description | Typical signal source |
|
||||
| --- | --- | --- |
|
||||
| Exploit likelihood | EPSS/KEV or internal intel | Conseiller enrichment |
|
||||
| VEX status | not_affected / affected / fixed | Excitor (VEX Lens) |
|
||||
| Reachability | entrypoint closure, runtime observations | Scanner + Zastava |
|
||||
| Fix availability | patch released, vendor guidance | Conseiller, Policy Engine |
|
||||
| Asset criticality | business context, tenant overrides | Policy Studio inputs |
|
||||
| Provenance trust | signed evidence, attestation status | Attestor, Authority |
|
||||
|
||||
Factors feed into a weighted scoring engine with per-factor contribution reporting.
|
||||
|
||||
## Governance & guardrails
|
||||
|
||||
- Profiles live in Policy Studio with draft/review/approval workflows.
|
||||
- Policy Engine enforces deterministic evaluation; simulations and production runs share the same scoring code.
|
||||
- CLI parity enables automated promotion, export/import, and simulation from pipelines.
|
||||
- Observability records scoring latency, factor distribution, and profile usage.
|
||||
- Offline support: profiles, factor plugins, and explain bundles ship inside mirror bundles for air-gapped environments.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- Policy language reference and examples.
|
||||
- Simulation APIs/CLI with diff output.
|
||||
- Scoring engine implementation with explain traces and determinism checks.
|
||||
- Console visualizations (severity heatmaps, contribution waterfalls).
|
||||
- Export Center reports with risk scoring sections.
|
||||
- Observability dashboards for profile health and scoring throughput.
|
||||
|
||||
## References
|
||||
|
||||
- Policy core: `docs/modules/policy/architecture.md`
|
||||
- Findings ledger: `docs/modules/vuln-explorer/architecture.md`
|
||||
- VEX consensus: `docs/modules/vex-lens/architecture.md`
|
||||
- Offline operations: `docs/airgap/airgap-mode.md`
|
||||
8
docs/modules/risk-engine/samples/INGEST_CHECKLIST.md
Normal file
8
docs/modules/risk-engine/samples/INGEST_CHECKLIST.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Risk Samples Ingest Checklist (use when payloads arrive)
|
||||
|
||||
1) Drop payloads into the correct folder (`profiles/`, `factors/`, `explain/`, `api/`).
|
||||
2) Normalize JSON deterministically (e.g., `jq -S .`) before hashing; keep UTC timestamps.
|
||||
3) Run `sha256sum * > SHA256SUMS` in the target folder; keep file sorted.
|
||||
4) Verify hashes: `sha256sum -c SHA256SUMS`.
|
||||
5) Add a short README snippet in the sprint Execution Log noting files added and hashes updated.
|
||||
6) Keep fixtures offline-only; no external calls or redactions after hashing.
|
||||
26
docs/modules/risk-engine/samples/README.md
Normal file
26
docs/modules/risk-engine/samples/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Risk Samples (fixtures layout)
|
||||
|
||||
Use this folder for frozen, deterministic fixtures once schemas and payloads arrive.
|
||||
|
||||
Structure (proposed):
|
||||
- `profiles/` — profile JSON (DSSE-wrapped where applicable) + `SHA256SUMS`
|
||||
- `factors/` — factor input payloads grouped by source (epss/, kev/, reachability/, runtime/), each with `SHA256SUMS`
|
||||
- `explain/` — explainability outputs paired with inputs; include `SHA256SUMS`
|
||||
- `api/` — request/response examples for risk endpoints; include `SHA256SUMS`
|
||||
|
||||
Rules:
|
||||
- UTC timestamps; stable ordering of arrays/objects.
|
||||
- No live calls; fixtures only.
|
||||
- Record hashes via `sha256sum` and keep manifests alongside samples.
|
||||
|
||||
Quick receipt checklist (see `INGEST_CHECKLIST.md` for detail):
|
||||
1) Normalize JSON with `jq -S .`
|
||||
2) Update `SHA256SUMS` in the target folder
|
||||
3) Verify with `sha256sum -c`
|
||||
4) Log files + hashes in the sprint Execution Log
|
||||
|
||||
Manifests created:
|
||||
- `profiles/SHA256SUMS`
|
||||
- `factors/SHA256SUMS`
|
||||
- `explain/SHA256SUMS`
|
||||
- `api/SHA256SUMS`
|
||||
3
docs/modules/risk-engine/samples/api/README.md
Normal file
3
docs/modules/risk-engine/samples/api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use the root `INGEST_CHECKLIST.md`.
|
||||
Place request/response examples here; normalize with `jq -S .`, update `SHA256SUMS`, verify with `sha256sum -c`.
|
||||
Include required headers; redact secrets; UTC timestamps only.
|
||||
3
docs/modules/risk-engine/samples/api/SHA256SUMS
Normal file
3
docs/modules/risk-engine/samples/api/SHA256SUMS
Normal file
@@ -0,0 +1,3 @@
|
||||
fe460af2699ce335199f6e26597bab4530c6f3f476d4b1f93526175597565d10 README.md
|
||||
00f8dc4e466eb95c06545e6336d7b0866b53ac430335b7fd1b7889da13529b93 error-catalog.json
|
||||
96926cd81dfb6ff02d62d1fde5d7b2b7b5b3950e50eb651e51b8ae3042ac9506 risk-api-samples.json
|
||||
13
docs/modules/risk-engine/samples/api/error-catalog.json
Normal file
13
docs/modules/risk-engine/samples/api/error-catalog.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"errors": [
|
||||
{"code": "risk.job.not_found", "message": "Risk job not found", "http_status": 404, "remediation": "Verify job_id"},
|
||||
{"code": "risk.profile.invalid_signature", "message": "Profile DSSE signature failed", "http_status": 400, "remediation": "Re-sign profile and retry"},
|
||||
{"code": "risk.job.rate_limited", "message": "Rate limit exceeded", "http_status": 429, "remediation": "Retry after backoff", "retry_after": 5},
|
||||
{"code": "risk.tenant.scope_denied", "message": "Tenant scope not authorized", "http_status": 403, "remediation": "Provide required scope header"}
|
||||
],
|
||||
"headers": {
|
||||
"etag": "\"risk-api-sample-etag\"",
|
||||
"x-ratelimit-remaining": 99,
|
||||
"retry-after": 5
|
||||
}
|
||||
}
|
||||
61
docs/modules/risk-engine/samples/api/risk-api-samples.json
Normal file
61
docs/modules/risk-engine/samples/api/risk-api-samples.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"submit_job_request": {
|
||||
"method": "POST",
|
||||
"path": "/api/v1/risk/jobs",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"X-Stella-Tenant": "tenant-default"
|
||||
},
|
||||
"body": {
|
||||
"tenant_id": "tenant-default",
|
||||
"context_id": "ctx-001",
|
||||
"profile_id": "default-profile",
|
||||
"findings": [
|
||||
{
|
||||
"finding_id": "finding-123",
|
||||
"component_purl": "pkg:npm/lodash@4.17.20",
|
||||
"advisory_id": "CVE-2024-1234",
|
||||
"trigger": "created"
|
||||
}
|
||||
],
|
||||
"priority": "normal",
|
||||
"requested_at": "2025-12-05T00:00:00Z"
|
||||
},
|
||||
"response": {
|
||||
"status": 202,
|
||||
"body": {"job_id": "job-001", "status": "queued"}
|
||||
}
|
||||
},
|
||||
"get_job_status": {
|
||||
"method": "GET",
|
||||
"path": "/api/v1/risk/jobs/job-001",
|
||||
"response": {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"job_id": "job-001",
|
||||
"status": "completed",
|
||||
"results": [
|
||||
{
|
||||
"finding_id": "finding-123",
|
||||
"profile_id": "default-profile",
|
||||
"profile_version": "1.0.0",
|
||||
"raw_score": 0.75,
|
||||
"normalized_score": 0.85,
|
||||
"severity": "high",
|
||||
"signal_values": {"cvss": 7.5, "kev": true, "reachability": 0.9},
|
||||
"signal_contributions": {"cvss": 0.4, "kev": 0.3, "reachability": 0.3},
|
||||
"scored_at": "2025-12-05T00:00:02Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_explain": {
|
||||
"method": "GET",
|
||||
"path": "/api/v1/risk/explain/job-001",
|
||||
"response": {
|
||||
"status": 200,
|
||||
"body_ref": "../explain/explain-trace.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
docs/modules/risk-engine/samples/explain/README.md
Normal file
3
docs/modules/risk-engine/samples/explain/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use the root `INGEST_CHECKLIST.md`.
|
||||
Store explainability outputs paired with their inputs; normalize with `jq -S .`, update `SHA256SUMS`, verify with `sha256sum -c`.
|
||||
Maintain ordering and UTC timestamps; no live data.
|
||||
4
docs/modules/risk-engine/samples/explain/SHA256SUMS
Normal file
4
docs/modules/risk-engine/samples/explain/SHA256SUMS
Normal file
@@ -0,0 +1,4 @@
|
||||
fe460af2699ce335199f6e26597bab4530c6f3f476d4b1f93526175597565d10 README.md
|
||||
abcacb431d35d649a0deae81aecce9996b28304da6342a083f9616af6b1ca6a2 cli-explain.txt
|
||||
f3f1b41f5261f50f3fc104ebeeb2649cc9866d04f9634228778551e6c3364cb8 console-frame.json
|
||||
1d2e56eebf0a266f80519f073e1db532c4a4f2d7fa604ea5c05d4e208719cc7c explain-trace.json
|
||||
15
docs/modules/risk-engine/samples/explain/cli-explain.txt
Normal file
15
docs/modules/risk-engine/samples/explain/cli-explain.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
stella risk explain job-001 --tenant tenant-default
|
||||
==================================================
|
||||
Finding: finding-123
|
||||
Profile: default-profile v1.0.0 (hash sha256:profilehash)
|
||||
Score: 0.85 (HIGH)
|
||||
Gates: kev_and_reachability
|
||||
|
||||
Contributions (ordered)
|
||||
- cvss 0.40 raw=7.5 source=nvd prov=sha256:cvsshash
|
||||
- kev 0.30 raw=true source=cisa prov=sha256:kevhash
|
||||
- reachability 0.30 raw=0.9 source=scanner prov=sha256:reachhash
|
||||
|
||||
Overrides: kev-boost (Known Exploited Vulnerability)
|
||||
Provenance: job sha256:jobhash | fixtures [sha256:cvsshash, sha256:kevhash, sha256:reachhash]
|
||||
Timestamp: 2025-12-05T00:00:02Z
|
||||
22
docs/modules/risk-engine/samples/explain/console-frame.json
Normal file
22
docs/modules/risk-engine/samples/explain/console-frame.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"frame_id": "console-explain-001",
|
||||
"captured_at": "2025-12-05T00:05:00Z",
|
||||
"ui_version": "1.0.0",
|
||||
"tenant_id": "tenant-default",
|
||||
"finding_id": "finding-123",
|
||||
"profile_id": "default-profile",
|
||||
"profile_hash": "sha256:profilehash",
|
||||
"score": 0.85,
|
||||
"severity": "high",
|
||||
"gates": ["kev_and_reachability"],
|
||||
"top_contributors": [
|
||||
{"factor": "cvss", "contribution": 0.4, "raw": 7.5, "source": "nvd", "provenance": "sha256:cvsshash"},
|
||||
{"factor": "kev", "contribution": 0.3, "raw": true, "source": "cisa", "provenance": "sha256:kevhash"},
|
||||
{"factor": "reachability", "contribution": 0.3, "raw": 0.9, "source": "scanner", "provenance": "sha256:reachhash"}
|
||||
],
|
||||
"charts": {
|
||||
"donut": {"critical": 0, "high": 1, "medium": 0, "low": 0, "informational": 0},
|
||||
"stacked": [0.4, 0.3, 0.3]
|
||||
},
|
||||
"provenance": {"job_hash": "sha256:jobhash", "fixtures": ["sha256:cvsshash", "sha256:kevhash", "sha256:reachhash"]}
|
||||
}
|
||||
34
docs/modules/risk-engine/samples/explain/explain-trace.json
Normal file
34
docs/modules/risk-engine/samples/explain/explain-trace.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"job_id": "job-001",
|
||||
"tenant_id": "tenant-default",
|
||||
"context_id": "ctx-001",
|
||||
"profile_id": "default-profile",
|
||||
"profile_version": "1.0.0",
|
||||
"profile_hash": "sha256:profilehash",
|
||||
"finding_id": "finding-123",
|
||||
"raw_score": 0.75,
|
||||
"normalized_score": 0.85,
|
||||
"severity": "high",
|
||||
"signal_values": {
|
||||
"cvss": 7.5,
|
||||
"kev": true,
|
||||
"reachability": 0.9
|
||||
},
|
||||
"signal_contributions": {
|
||||
"cvss": 0.4,
|
||||
"kev": 0.3,
|
||||
"reachability": 0.3
|
||||
},
|
||||
"override_applied": "kev-boost",
|
||||
"override_reason": "Known Exploited Vulnerability",
|
||||
"gates_triggered": ["kev_and_reachability"],
|
||||
"scored_at": "2025-12-05T00:00:02Z",
|
||||
"provenance": {
|
||||
"job_hash": "sha256:jobhash",
|
||||
"fixtures": [
|
||||
"sha256:cvsshash",
|
||||
"sha256:kevhash",
|
||||
"sha256:reachhash"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
docs/modules/risk-engine/samples/factors/README.md
Normal file
3
docs/modules/risk-engine/samples/factors/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use the root `INGEST_CHECKLIST.md`.
|
||||
Drop factor payloads by source (epss/, kev/, reachability/, runtime/), normalize with `jq -S .`, update `SHA256SUMS`, verify with `sha256sum -c`.
|
||||
Keep UTC timestamps and no live data.
|
||||
2
docs/modules/risk-engine/samples/factors/SHA256SUMS
Normal file
2
docs/modules/risk-engine/samples/factors/SHA256SUMS
Normal file
@@ -0,0 +1,2 @@
|
||||
fe460af2699ce335199f6e26597bab4530c6f3f476d4b1f93526175597565d10 README.md
|
||||
13cf45be5a287a38d000aff4db266616e765fc1acdc1df9f37b2e03eb729d1d2 factors-normalized.json
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"profile_id": "default-profile",
|
||||
"context_id": "ctx-001",
|
||||
"factors": [
|
||||
{
|
||||
"name": "cvss",
|
||||
"source": "nvd",
|
||||
"type": "numeric",
|
||||
"path": "/cvss/base_score",
|
||||
"raw_value": 7.5,
|
||||
"normalized_value": 0.75,
|
||||
"weight": 0.4,
|
||||
"contribution": 0.4,
|
||||
"timestamp_utc": "2025-12-05T00:00:00Z",
|
||||
"provenance": "sha256:cvsshash"
|
||||
},
|
||||
{
|
||||
"name": "kev",
|
||||
"source": "cisa",
|
||||
"type": "boolean",
|
||||
"path": "/kev/in_catalog",
|
||||
"raw_value": true,
|
||||
"normalized_value": 1.0,
|
||||
"weight": 0.3,
|
||||
"contribution": 0.3,
|
||||
"timestamp_utc": "2025-12-05T00:00:00Z",
|
||||
"provenance": "sha256:kevhash"
|
||||
},
|
||||
{
|
||||
"name": "reachability",
|
||||
"source": "scanner",
|
||||
"type": "numeric",
|
||||
"path": "/reachability/score",
|
||||
"raw_value": 0.9,
|
||||
"normalized_value": 0.9,
|
||||
"weight": 0.3,
|
||||
"contribution": 0.3,
|
||||
"timestamp_utc": "2025-12-05T00:00:01Z",
|
||||
"provenance": "sha256:reachhash"
|
||||
}
|
||||
],
|
||||
"ordering": "factor_type->source->timestamp_utc",
|
||||
"precision": 4
|
||||
}
|
||||
8
docs/modules/risk-engine/samples/intake-log-template.md
Normal file
8
docs/modules/risk-engine/samples/intake-log-template.md
Normal file
@@ -0,0 +1,8 @@
|
||||
| Date (UTC) | Folder | Files added | SHA256SUMS updated | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2025-__-__ | profiles/ | | yes/no | source + checklist step refs |
|
||||
| 2025-__-__ | factors/ | | yes/no | source + checklist step refs |
|
||||
| 2025-__-__ | explain/ | | yes/no | source + checklist step refs |
|
||||
| 2025-__-__ | api/ | | yes/no | source + checklist step refs |
|
||||
|
||||
Instructions: copy a row per drop, fill actual date, list filenames, mark whether `SHA256SUMS` was updated, and note evidence source. Keep this file sorted by date for determinism.
|
||||
3
docs/modules/risk-engine/samples/profiles/README.md
Normal file
3
docs/modules/risk-engine/samples/profiles/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use the root `INGEST_CHECKLIST.md`.
|
||||
Place profile JSON/DSSE here, normalize with `jq -S .`, update `SHA256SUMS`, and verify with `sha256sum -c`.
|
||||
UTC timestamps only; no live data.
|
||||
2
docs/modules/risk-engine/samples/profiles/SHA256SUMS
Normal file
2
docs/modules/risk-engine/samples/profiles/SHA256SUMS
Normal file
@@ -0,0 +1,2 @@
|
||||
fe460af2699ce335199f6e26597bab4530c6f3f476d4b1f93526175597565d10 README.md
|
||||
c8242d4051232152d024dd37324b346dcf019a5e46b7b82fae8349ad802affab default-profile.json
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "default-profile",
|
||||
"version": "1.0.0",
|
||||
"description": "Default risk profile for vulnerability prioritization",
|
||||
"extends": "base-profile",
|
||||
"signals": [
|
||||
{ "name": "cvss", "source": "nvd", "type": "numeric", "path": "/cvss/base_score", "transform": "normalize_10", "unit": "score" },
|
||||
{ "name": "kev", "source": "cisa", "type": "boolean", "path": "/kev/in_catalog" },
|
||||
{ "name": "reachability", "source": "scanner", "type": "numeric", "path": "/reachability/score", "unit": "score" }
|
||||
],
|
||||
"weights": { "cvss": 0.4, "kev": 0.3, "reachability": 0.3 },
|
||||
"overrides": {
|
||||
"severity": [ { "when": { "kev": true }, "set": "critical" } ],
|
||||
"decisions": [ { "when": { "kev": true, "reachability": { "$gt": 0.8 } }, "action": "deny", "reason": "KEV with high reachability" } ]
|
||||
},
|
||||
"metadata": { "author": "docs-guild", "created_at": "2025-12-05T00:00:00Z" },
|
||||
"provenance": { "hash": "sha256:placeholder", "signed": false }
|
||||
}
|
||||
Reference in New Issue
Block a user