docs consolidation and others
This commit is contained in:
@@ -1,363 +0,0 @@
|
||||
# Compare Workflow User Guide
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2025-12-22
|
||||
|
||||
## Overview
|
||||
|
||||
The Compare workflow in StellaOps enables you to analyze what changed between two container image versions from a security perspective. Instead of reviewing entire vulnerability lists, you focus on **material risk changes** — the delta that matters for security decisions.
|
||||
|
||||
## When to Use Compare
|
||||
|
||||
Use the Compare view when you need to:
|
||||
|
||||
- **Evaluate a new release** before deploying to production
|
||||
- **Understand risk delta** between current and previous versions
|
||||
- **Investigate policy gate failures** to see what caused blocking
|
||||
- **Review security posture changes** after dependency updates
|
||||
- **Audit compliance** by verifying what changed and why
|
||||
|
||||
## Accessing the Compare View
|
||||
|
||||
### From Release Details
|
||||
|
||||
1. Navigate to **Releases** → Select a release
|
||||
2. Click the **Compare** button in the release header
|
||||
3. The system automatically selects the recommended baseline
|
||||
|
||||
### From Build/Artifact
|
||||
|
||||
1. Navigate to **Scans** → Select a scan
|
||||
2. Click **Compare with baseline**
|
||||
3. Select a baseline from the recommended options
|
||||
|
||||
### Direct URL
|
||||
|
||||
```
|
||||
/compare/{currentDigest}/{baselineDigest}
|
||||
```
|
||||
|
||||
## Understanding the Interface
|
||||
|
||||
### Baseline Selector
|
||||
|
||||
At the top of the Compare view, you'll see the baseline selector:
|
||||
|
||||
```
|
||||
Comparing: [myapp:v2.1.0] → [Select baseline ▼]
|
||||
├── Last Green Build (Recommended)
|
||||
├── Previous Release (v2.0.0)
|
||||
├── Main Branch
|
||||
└── Custom...
|
||||
```
|
||||
|
||||
**Baseline Options:**
|
||||
- **Last Green Build**: Most recent version that passed all security gates
|
||||
- **Previous Release**: The version tagged before the current one
|
||||
- **Main Branch**: Latest scan from the main/master branch
|
||||
- **Custom**: Manually select any previous scan
|
||||
|
||||
### Baseline Rationale
|
||||
|
||||
Below the selector, you'll see why this baseline was chosen:
|
||||
|
||||
> "Selected last prod release with Allowed verdict under policy P-2024-001."
|
||||
|
||||
This helps auditors understand the comparison context.
|
||||
|
||||
### Delta Summary Strip
|
||||
|
||||
The summary strip shows high-level counts:
|
||||
|
||||
```
|
||||
┌────────────┬─────────────┬──────────────┬──────────────┬─────────────┐
|
||||
│ +5 added │ -3 removed │ ~2 changed │ Policy: v1.2 │ Feed: 2h ago│
|
||||
└────────────┴─────────────┴──────────────┴──────────────┴─────────────┘
|
||||
```
|
||||
|
||||
### Three-Pane Layout
|
||||
|
||||
#### Left Pane: Categories
|
||||
|
||||
Categories organize changes by type:
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| **SBOM Changes** | Component additions, removals, version changes |
|
||||
| **Reachability** | Functions becoming reachable/unreachable |
|
||||
| **VEX Status** | Vulnerability status changes |
|
||||
| **Policy** | Policy rule trigger changes |
|
||||
| **Findings** | New or resolved vulnerabilities |
|
||||
| **Unknowns** | New gaps in analysis |
|
||||
|
||||
Click a category to filter the items pane.
|
||||
|
||||
#### Middle Pane: Items
|
||||
|
||||
Shows individual changes sorted by risk priority:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ⊕ CVE-2024-1234 · lodash@4.17.20 · +reachable [HIGH] │
|
||||
│ ⊕ CVE-2024-5678 · requests@2.28.0 · +KEV [CRITICAL]│
|
||||
│ ⊖ CVE-2024-9999 · urllib3@1.26.0 · -reachable [MEDIUM] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Change Types:**
|
||||
- ⊕ Added (green): New in current version
|
||||
- ⊖ Removed (red): Gone from current version
|
||||
- ↔ Changed (yellow): Status or value changed
|
||||
|
||||
#### Right Pane: Proof/Evidence
|
||||
|
||||
When you select an item, the proof pane shows:
|
||||
|
||||
1. **Witness Path**: Call path from entrypoint to vulnerable function
|
||||
2. **VEX Merge**: How multiple VEX sources were combined
|
||||
3. **Policy Rule**: Which rule triggered and why
|
||||
4. **Envelope Hashes**: Cryptographic evidence for verification
|
||||
|
||||
### Trust Indicators
|
||||
|
||||
The trust indicators bar shows:
|
||||
|
||||
| Indicator | Description |
|
||||
|-----------|-------------|
|
||||
| **Det. Hash** | Determinism hash for reproducibility |
|
||||
| **Policy** | Policy version used for evaluation |
|
||||
| **Feed** | Vulnerability feed snapshot timestamp |
|
||||
| **Signature** | DSSE signature verification status |
|
||||
|
||||
**Warning States:**
|
||||
- ⚠️ **Stale Feed**: Vulnerability data > 24h old
|
||||
- ⚠️ **Policy Drift**: Policy changed between scans
|
||||
- 🔴 **Signature Invalid**: Verification failed
|
||||
|
||||
### Actionables Panel
|
||||
|
||||
The "What to do next" section provides prioritized recommendations:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ What to do next: │
|
||||
│ 1. [CRITICAL] Upgrade lodash → 4.17.21 [Apply] │
|
||||
│ 2. [HIGH] Add VEX statement for urllib3 [Apply] │
|
||||
│ 3. [MEDIUM] Investigate new reachable path [Investigate] │
|
||||
│ 4. [LOW] Resolve unknown: missing SBOM [Investigate] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### 1. Pre-Release Security Review
|
||||
|
||||
**Goal**: Verify a release is safe to deploy
|
||||
|
||||
1. Open the release in the UI
|
||||
2. Click **Compare** (defaults to last green baseline)
|
||||
3. Review the delta summary:
|
||||
- New critical/high vulnerabilities?
|
||||
- Reachability increases?
|
||||
- Policy violations?
|
||||
4. Examine each critical item:
|
||||
- Check witness paths
|
||||
- Review VEX status
|
||||
5. Apply actionables or approve release
|
||||
|
||||
### 2. Investigating a Blocked Release
|
||||
|
||||
**Goal**: Understand why a release was blocked
|
||||
|
||||
1. Open the blocked release
|
||||
2. Look at the **Verdict** chip: `BLOCKED`
|
||||
3. Click **Compare** to see what changed
|
||||
4. Filter to **Policy** category
|
||||
5. Select blocking rules to see:
|
||||
- Which policy rule fired
|
||||
- Evidence that triggered it
|
||||
- Remediation options
|
||||
|
||||
### 3. Dependency Update Impact
|
||||
|
||||
**Goal**: Assess security impact of dependency updates
|
||||
|
||||
1. Compare current branch to main
|
||||
2. Filter to **SBOM Changes**
|
||||
3. Review component version changes:
|
||||
- Check if upgrades fix vulnerabilities
|
||||
- Check if new vulnerabilities introduced
|
||||
4. Filter to **Findings** for net impact
|
||||
|
||||
### 4. Auditor Verification
|
||||
|
||||
**Goal**: Verify security claims are accurate
|
||||
|
||||
1. Open the Compare view
|
||||
2. Check **Trust Indicators**:
|
||||
- Signature valid?
|
||||
- Feed current?
|
||||
- Policy version expected?
|
||||
3. Click **Copy Replay Command**
|
||||
4. Run replay locally to verify determinism
|
||||
5. Download **Evidence Pack** for records
|
||||
|
||||
## Understanding Evidence
|
||||
|
||||
### Witness Paths
|
||||
|
||||
Witness paths show how vulnerable code is reachable:
|
||||
|
||||
```
|
||||
main() [entrypoint]
|
||||
↓
|
||||
parseConfig()
|
||||
↓
|
||||
loadJson()
|
||||
↓
|
||||
yaml.load() [sink - CVE-2024-1234]
|
||||
|
||||
Confidence: CONFIRMED
|
||||
Gates: input_validation, sandboxing
|
||||
```
|
||||
|
||||
**Confidence Tiers:**
|
||||
- **CONFIRMED**: Call path verified through multiple sources
|
||||
- **LIKELY**: High-confidence static analysis
|
||||
- **PRESENT**: Function exists but reachability uncertain
|
||||
|
||||
### VEX Merge Explanation
|
||||
|
||||
When multiple VEX sources exist, the merge shows how they combined:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ VEX Status: NOT_AFFECTED │
|
||||
│ Strategy: priority │
|
||||
│ │
|
||||
│ Sources: │
|
||||
│ ★ [vendor] RedHat RHSA-2024:1234 - not_affected P1 │
|
||||
│ [distro] Ubuntu USN-5678-1 - affected P2 │
|
||||
│ [internal] Team assessment - not_affected P3 │
|
||||
│ │
|
||||
│ Resolution: Vendor claim takes priority │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Determinism Verification
|
||||
|
||||
To verify a comparison is reproducible:
|
||||
|
||||
1. Copy the replay command from Trust Indicators
|
||||
2. Run locally:
|
||||
```bash
|
||||
stellaops smart-diff replay \
|
||||
--base sha256:abc123... \
|
||||
--target sha256:def456... \
|
||||
--feed-snapshot sha256:feed789... \
|
||||
--policy sha256:policy012...
|
||||
```
|
||||
3. Compare the determinism hash
|
||||
|
||||
## Role-Based Views
|
||||
|
||||
### Developer View (Default)
|
||||
|
||||
Focus: What do I need to fix?
|
||||
|
||||
- **Default Tab**: Actionables
|
||||
- **Visible**: Upgrade suggestions, witness paths
|
||||
- **Hidden**: Detailed attestations, policy internals
|
||||
|
||||
### Security View
|
||||
|
||||
Focus: Are the security claims valid?
|
||||
|
||||
- **Default Tab**: Claims/VEX
|
||||
- **Visible**: VEX merge, policy reasoning, claim sources
|
||||
- **Hidden**: Low-level attestation details
|
||||
|
||||
### Audit View
|
||||
|
||||
Focus: Can I verify these claims?
|
||||
|
||||
- **Default Tab**: Attestations
|
||||
- **Visible**: Signatures, replay commands, evidence pack
|
||||
- **Hidden**: Actionables (read-only mode)
|
||||
|
||||
## Exporting Reports
|
||||
|
||||
### JSON Export
|
||||
|
||||
Click **Export → JSON** to download:
|
||||
- Full delta with all items
|
||||
- Evidence references
|
||||
- Trust indicators
|
||||
- Actionables
|
||||
|
||||
### PDF Export
|
||||
|
||||
Click **Export → PDF** for a formatted report including:
|
||||
- Executive summary
|
||||
- Delta breakdown by category
|
||||
- Critical findings
|
||||
- Remediation recommendations
|
||||
|
||||
### SARIF Export
|
||||
|
||||
Click **Export → SARIF** for CI/CD integration:
|
||||
- SARIF 2.1.0 format
|
||||
- Compatible with GitHub Security, Azure DevOps
|
||||
- Includes rule IDs for automation
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` | Move between panes |
|
||||
| `↑/↓` | Navigate items |
|
||||
| `Enter` | Select/expand item |
|
||||
| `Esc` | Close expanded detail |
|
||||
| `C` | Copy replay command |
|
||||
| `E` | Export menu |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No baseline available"
|
||||
|
||||
The system couldn't find a suitable baseline because:
|
||||
- This is the first scan of this image
|
||||
- No previous scans passed policy gates
|
||||
|
||||
**Solution**: Use "Custom" to manually select any previous scan.
|
||||
|
||||
### "Stale feed warning"
|
||||
|
||||
The vulnerability feed is more than 24 hours old.
|
||||
|
||||
**Impact**: New CVEs may not be reflected in the comparison.
|
||||
|
||||
**Solution**:
|
||||
1. Trigger a feed refresh
|
||||
2. Re-run comparison after refresh
|
||||
|
||||
### "Signature verification failed"
|
||||
|
||||
The DSSE envelope signature couldn't be verified.
|
||||
|
||||
**Causes**:
|
||||
- Key rotation occurred
|
||||
- Attestation was modified
|
||||
- Network issue fetching public key
|
||||
|
||||
**Solution**:
|
||||
1. Check if keys were recently rotated
|
||||
2. Try offline verification with local key
|
||||
3. Contact security team if persistent
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Smart-Diff CLI Reference](../cli/smart-diff-cli.md)
|
||||
- [Smart-Diff UI Architecture](../modules/web/smart-diff-ui-architecture.md)
|
||||
- [SARIF Integration Guide](../ci/sarif-integration.md)
|
||||
- [Deterministic Replay Specification](../replay/DETERMINISTIC_REPLAY.md)
|
||||
@@ -1,817 +0,0 @@
|
||||
# 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
|
||||
@@ -1,290 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,310 +0,0 @@
|
||||
# Identity Constraints for Keyless Verification
|
||||
|
||||
## Overview
|
||||
|
||||
Keyless signing binds cryptographic signatures to OIDC identities. When verifying signatures, you must specify which identities are trusted. This document covers identity constraint patterns for all supported CI/CD platforms.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Certificate Identity
|
||||
|
||||
The certificate identity is the subject claim from the OIDC token, embedded in the Fulcio certificate. It identifies:
|
||||
|
||||
- **Who** created the signature (repository, branch, workflow)
|
||||
- **When** the signature was created (within the certificate validity window)
|
||||
- **Where** the signing happened (CI platform, environment)
|
||||
|
||||
### OIDC Issuer
|
||||
|
||||
The OIDC issuer is the URL of the identity provider that issued the token. Each CI platform has its own issuer:
|
||||
|
||||
| Platform | Issuer URL |
|
||||
|----------|------------|
|
||||
| GitHub Actions | `https://token.actions.githubusercontent.com` |
|
||||
| GitLab CI (SaaS) | `https://gitlab.com` |
|
||||
| GitLab CI (Self-hosted) | `https://your-gitlab-instance.com` |
|
||||
| Gitea | `https://your-gitea-instance.com` |
|
||||
|
||||
### Verification Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Verification Process │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Extract certificate from attestation │
|
||||
│ └─▶ Contains: subject, issuer, SAN, validity period │
|
||||
│ │
|
||||
│ 2. Validate certificate chain │
|
||||
│ └─▶ Chains to trusted Fulcio root │
|
||||
│ │
|
||||
│ 3. Check OIDC issuer │
|
||||
│ └─▶ Must match --certificate-oidc-issuer │
|
||||
│ │
|
||||
│ 4. Check certificate identity │
|
||||
│ └─▶ Subject must match --certificate-identity pattern │
|
||||
│ │
|
||||
│ 5. Verify Rekor inclusion (if required) │
|
||||
│ └─▶ Signature logged during certificate validity │
|
||||
│ │
|
||||
│ 6. Verify signature │
|
||||
│ └─▶ Signature valid for artifact digest │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Platform-Specific Patterns
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
GitHub Actions OIDC tokens include rich context about the workflow execution.
|
||||
|
||||
#### Token Claims
|
||||
|
||||
| Claim | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| `sub` | Subject (identity) | `repo:org/repo:ref:refs/heads/main` |
|
||||
| `repository` | Full repository name | `org/repo` |
|
||||
| `repository_owner` | Organization/user | `org` |
|
||||
| `ref` | Git ref | `refs/heads/main` |
|
||||
| `ref_type` | Ref type | `branch` or `tag` |
|
||||
| `job_workflow_ref` | Workflow file | `.github/workflows/release.yml@refs/heads/main` |
|
||||
| `environment` | Deployment environment | `production` |
|
||||
|
||||
#### Identity Patterns
|
||||
|
||||
| Constraint | Pattern | Example |
|
||||
|------------|---------|---------|
|
||||
| Any ref | `repo:<owner>/<repo>:.*` | `repo:stellaops/scanner:.*` |
|
||||
| Main branch | `repo:<owner>/<repo>:ref:refs/heads/main` | `repo:stellaops/scanner:ref:refs/heads/main` |
|
||||
| Any branch | `repo:<owner>/<repo>:ref:refs/heads/.*` | `repo:stellaops/scanner:ref:refs/heads/.*` |
|
||||
| Version tags | `repo:<owner>/<repo>:ref:refs/tags/v.*` | `repo:stellaops/scanner:ref:refs/tags/v.*` |
|
||||
| Environment | `repo:<owner>/<repo>:environment:<env>` | `repo:stellaops/scanner:environment:production` |
|
||||
| Workflow | (use SAN) | N/A |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Accept only main branch
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "repo:stellaops/scanner:ref:refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
|
||||
# Accept main or release branches
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "repo:stellaops/scanner:ref:refs/heads/(main|release/.*)" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
|
||||
# Accept any version tag
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "repo:stellaops/scanner:ref:refs/tags/v[0-9]+\.[0-9]+\.[0-9]+.*" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
|
||||
# Accept production environment only
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "repo:stellaops/scanner:environment:production" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
GitLab CI provides OIDC tokens with project and pipeline context.
|
||||
|
||||
#### Token Claims
|
||||
|
||||
| Claim | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| `sub` | Subject | `project_path:group/project:ref_type:branch:ref:main` |
|
||||
| `project_path` | Full project path | `stellaops/scanner` |
|
||||
| `namespace_path` | Namespace | `stellaops` |
|
||||
| `ref` | Git ref | `main` |
|
||||
| `ref_type` | Ref type | `branch` or `tag` |
|
||||
| `ref_protected` | Protected ref | `true` or `false` |
|
||||
| `environment` | Environment name | `production` |
|
||||
| `pipeline_source` | Trigger source | `push`, `web`, `schedule` |
|
||||
|
||||
#### Identity Patterns
|
||||
|
||||
| Constraint | Pattern | Example |
|
||||
|------------|---------|---------|
|
||||
| Any ref | `project_path:<group>/<project>:.*` | `project_path:stellaops/scanner:.*` |
|
||||
| Main branch | `project_path:<group>/<project>:ref_type:branch:ref:main` | Full pattern |
|
||||
| Protected refs | `project_path:<group>/<project>:ref_protected:true` | Full pattern |
|
||||
| Tags | `project_path:<group>/<project>:ref_type:tag:ref:.*` | Full pattern |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Accept main branch only
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "project_path:stellaops/scanner:ref_type:branch:ref:main" \
|
||||
--certificate-oidc-issuer "https://gitlab.com"
|
||||
|
||||
# Accept any protected ref
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "project_path:stellaops/scanner:ref_protected:true.*" \
|
||||
--certificate-oidc-issuer "https://gitlab.com"
|
||||
|
||||
# Self-hosted GitLab
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "project_path:mygroup/myproject:.*" \
|
||||
--certificate-oidc-issuer "https://gitlab.internal.example.com"
|
||||
```
|
||||
|
||||
### Gitea
|
||||
|
||||
Gitea OIDC tokens follow a similar pattern to GitHub Actions.
|
||||
|
||||
#### Token Claims
|
||||
|
||||
| Claim | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| `sub` | Subject | `org/repo:ref:refs/heads/main` |
|
||||
| `repository` | Repository path | `org/repo` |
|
||||
| `ref` | Git ref | `refs/heads/main` |
|
||||
|
||||
#### Identity Patterns
|
||||
|
||||
| Constraint | Pattern | Example |
|
||||
|------------|---------|---------|
|
||||
| Any ref | `<org>/<repo>:.*` | `stellaops/scanner:.*` |
|
||||
| Main branch | `<org>/<repo>:ref:refs/heads/main` | `stellaops/scanner:ref:refs/heads/main` |
|
||||
| Tags | `<org>/<repo>:ref:refs/tags/.*` | `stellaops/scanner:ref:refs/tags/.*` |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Accept main branch
|
||||
stella attest verify \
|
||||
--artifact sha256:abc123... \
|
||||
--certificate-identity "stella-ops.org/git.stella-ops.org:ref:refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://git.stella-ops.org"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security Recommendations
|
||||
|
||||
1. **Always Constrain to Repository**
|
||||
|
||||
Never accept wildcards that could match any repository:
|
||||
|
||||
```bash
|
||||
# BAD - accepts any repository
|
||||
--certificate-identity "repo:.*"
|
||||
|
||||
# GOOD - specific repository
|
||||
--certificate-identity "repo:stellaops/scanner:.*"
|
||||
```
|
||||
|
||||
2. **Prefer Branch/Tag Constraints for Production**
|
||||
|
||||
```bash
|
||||
# Better - only main branch
|
||||
--certificate-identity "repo:stellaops/scanner:ref:refs/heads/main"
|
||||
|
||||
# Even better - only signed tags
|
||||
--certificate-identity "repo:stellaops/scanner:ref:refs/tags/v.*"
|
||||
```
|
||||
|
||||
3. **Use Environment Constraints When Available**
|
||||
|
||||
```bash
|
||||
# Most specific - production environment only
|
||||
--certificate-identity "repo:stellaops/scanner:environment:production"
|
||||
```
|
||||
|
||||
4. **Always Require Rekor Proofs**
|
||||
|
||||
```bash
|
||||
# Always include --require-rekor for production
|
||||
stella attest verify \
|
||||
--artifact sha256:... \
|
||||
--certificate-identity "..." \
|
||||
--certificate-oidc-issuer "..." \
|
||||
--require-rekor
|
||||
```
|
||||
|
||||
5. **Pin Trusted Issuers**
|
||||
|
||||
Only trust expected OIDC issuers. Never accept `.*` for issuer.
|
||||
|
||||
### Common Patterns
|
||||
|
||||
#### Multi-Environment Trust
|
||||
|
||||
```yaml
|
||||
# GitHub Actions - Different constraints per environment
|
||||
staging:
|
||||
identity: "repo:myorg/myrepo:ref:refs/heads/.*"
|
||||
|
||||
production:
|
||||
identity: "repo:myorg/myrepo:ref:refs/(heads/main|tags/v.*)"
|
||||
```
|
||||
|
||||
#### Cross-Repository Trust
|
||||
|
||||
```bash
|
||||
# Trust signatures from multiple repositories
|
||||
stella attest verify \
|
||||
--artifact sha256:... \
|
||||
--certificate-identity "repo:myorg/(repo1|repo2|repo3):ref:refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
#### Organization-Wide Trust
|
||||
|
||||
```bash
|
||||
# Trust any repository in organization (use with caution)
|
||||
stella attest verify \
|
||||
--artifact sha256:... \
|
||||
--certificate-identity "repo:myorg/.*:ref:refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| `identity mismatch` | Pattern doesn't match certificate subject | Check ref format (refs/heads/ vs branch name) |
|
||||
| `issuer mismatch` | Wrong OIDC issuer URL | Use correct issuer for platform |
|
||||
| `certificate expired` | Signing cert expired, no Rekor proof | Ensure `--require-rekor` and Rekor was used at signing |
|
||||
| `no attestations found` | Attestation not attached to artifact | Verify attestation was pushed to registry |
|
||||
|
||||
### Debugging Identity Patterns
|
||||
|
||||
```bash
|
||||
# Inspect certificate to see actual identity
|
||||
stella attest inspect \
|
||||
--artifact sha256:... \
|
||||
--show-cert
|
||||
|
||||
# Expected output:
|
||||
# Certificate Subject: repo:stellaops/scanner:ref:refs/heads/main
|
||||
# Certificate Issuer: https://token.actions.githubusercontent.com
|
||||
# Certificate SAN: https://github.com/stellaops/scanner/.github/workflows/release.yml@refs/heads/main
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
```bash
|
||||
# Test pattern matching locally
|
||||
echo "repo:myorg/myrepo:ref:refs/heads/main" | \
|
||||
grep -E "repo:myorg/myrepo:ref:refs/heads/(main|develop)"
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Keyless Signing Guide](../modules/signer/guides/keyless-signing.md)
|
||||
- [GitHub Actions Templates](../../.github/workflows/examples/)
|
||||
- [GitLab CI Templates](../../deploy/gitlab/examples/)
|
||||
- [Sigstore Documentation](https://docs.sigstore.dev/)
|
||||
@@ -1,247 +0,0 @@
|
||||
# Keyless Signing Quick Start
|
||||
|
||||
Get keyless signing working in your CI/CD pipeline in under 5 minutes.
|
||||
|
||||
## Overview
|
||||
|
||||
Keyless signing uses your CI platform's OIDC identity to sign artifacts without managing private keys. The signature is bound to your repository, branch, and workflow identity.
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────┐ ┌───────────────┐
|
||||
│ CI Platform │────▶│ Fulcio │────▶│ Signed Artifact│
|
||||
│ OIDC Token │ │ Sigstore│ │ + Rekor Entry │
|
||||
└─────────────┘ └─────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
## GitHub Actions (Fastest)
|
||||
|
||||
### Step 1: Add the workflow
|
||||
|
||||
Create `.github/workflows/sign.yml`:
|
||||
|
||||
```yaml
|
||||
name: Build and Sign
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-and-sign:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build container
|
||||
run: |
|
||||
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
|
||||
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
|
||||
- name: Install StellaOps CLI
|
||||
run: curl -sL https://get.stella-ops.org/cli | sh
|
||||
|
||||
- name: Get OIDC Token
|
||||
id: oidc
|
||||
run: |
|
||||
TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \
|
||||
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
|
||||
| jq -r '.value')
|
||||
echo "::add-mask::${TOKEN}"
|
||||
echo "token=${TOKEN}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sign container
|
||||
env:
|
||||
STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
|
||||
run: |
|
||||
DIGEST=$(docker inspect ghcr.io/${{ github.repository }}:${{ github.sha }} \
|
||||
--format='{{index .RepoDigests 0}}' | cut -d@ -f2)
|
||||
stella attest sign --keyless --artifact "$DIGEST"
|
||||
```
|
||||
|
||||
### Step 2: Push and verify
|
||||
|
||||
```bash
|
||||
git add .github/workflows/sign.yml
|
||||
git commit -m "Add keyless signing"
|
||||
git push
|
||||
```
|
||||
|
||||
Check Actions tab - your container is now signed!
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI (5 minutes)
|
||||
|
||||
### Step 1: Update `.gitlab-ci.yml`
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- sign
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
- echo "ARTIFACT_DIGEST=$(docker inspect $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --format='{{index .RepoDigests 0}}' | cut -d@ -f2)" >> build.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: build.env
|
||||
|
||||
sign:
|
||||
stage: sign
|
||||
image: stella-ops/cli:latest
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
needs:
|
||||
- build
|
||||
script:
|
||||
- stella attest sign --keyless --artifact "$ARTIFACT_DIGEST"
|
||||
only:
|
||||
- main
|
||||
```
|
||||
|
||||
### Step 2: Push
|
||||
|
||||
```bash
|
||||
git add .gitlab-ci.yml
|
||||
git commit -m "Add keyless signing"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Gate
|
||||
|
||||
Add verification before deployment:
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
needs: [build-and-sign]
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Verify before deploy
|
||||
run: |
|
||||
stella attest verify \
|
||||
--artifact "${{ needs.build-and-sign.outputs.digest }}" \
|
||||
--certificate-identity "repo:${{ github.repository }}:ref:refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
--require-rekor
|
||||
|
||||
- name: Deploy
|
||||
run: kubectl set image deployment/app app=$IMAGE
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
stage: deploy
|
||||
environment: production
|
||||
needs:
|
||||
- sign
|
||||
script:
|
||||
- |
|
||||
stella attest verify \
|
||||
--artifact "$ARTIFACT_DIGEST" \
|
||||
--certificate-identity "project_path:$CI_PROJECT_PATH:ref_type:branch:ref:main" \
|
||||
--certificate-oidc-issuer "https://gitlab.com" \
|
||||
--require-rekor
|
||||
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
only:
|
||||
- main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Identity Patterns Cheat Sheet
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
| Pattern | Example |
|
||||
|---------|---------|
|
||||
| Any branch | `repo:org/repo:.*` |
|
||||
| Main only | `repo:org/repo:ref:refs/heads/main` |
|
||||
| Tags only | `repo:org/repo:ref:refs/tags/v.*` |
|
||||
| Environment | `repo:org/repo:environment:production` |
|
||||
|
||||
**OIDC Issuer:** `https://token.actions.githubusercontent.com`
|
||||
|
||||
### GitLab CI
|
||||
|
||||
| Pattern | Example |
|
||||
|---------|---------|
|
||||
| Any ref | `project_path:group/project:.*` |
|
||||
| Main only | `project_path:group/project:ref_type:branch:ref:main` |
|
||||
| Tags only | `project_path:group/project:ref_type:tag:.*` |
|
||||
| Protected | `project_path:group/project:ref_protected:true` |
|
||||
|
||||
**OIDC Issuer:** `https://gitlab.com` (or self-hosted URL)
|
||||
|
||||
---
|
||||
|
||||
## Using Reusable Workflows
|
||||
|
||||
For cleaner pipelines, use StellaOps reusable workflows:
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
sign:
|
||||
uses: stella-ops/workflows/.github/workflows/stellaops-sign.yml@v1
|
||||
with:
|
||||
artifact-digest: sha256:abc123...
|
||||
artifact-type: image
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
verify:
|
||||
needs: [sign]
|
||||
uses: stella-ops/workflows/.github/workflows/stellaops-verify.yml@v1
|
||||
with:
|
||||
artifact-digest: sha256:abc123...
|
||||
certificate-identity: "repo:${{ github.repository }}:ref:refs/heads/main"
|
||||
certificate-oidc-issuer: "https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- project: 'stella-ops/templates'
|
||||
file: '.gitlab-ci-stellaops.yml'
|
||||
|
||||
sign-container:
|
||||
extends: .stellaops-sign
|
||||
variables:
|
||||
ARTIFACT_DIGEST: sha256:abc123...
|
||||
ARTIFACT_TYPE: image
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
- [Identity Constraints Guide](./identity-constraints.md) - Secure verification patterns
|
||||
- [Troubleshooting Guide](./keyless-signing-troubleshooting.md) - Common issues and fixes
|
||||
- [Offline Verification](../airgap/offline-verification.md) - Air-gapped environments
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Documentation: https://docs.stella-ops.org/
|
||||
- Issues: https://github.com/stella-ops/stellaops/issues
|
||||
- Slack: https://stellaops.slack.com/
|
||||
@@ -1,399 +0,0 @@
|
||||
# Keyless Signing Troubleshooting Guide
|
||||
|
||||
This guide covers common issues when integrating StellaOps keyless signing into CI/CD pipelines.
|
||||
|
||||
## Common Errors
|
||||
|
||||
### OIDC Token Acquisition Failures
|
||||
|
||||
#### Error: "Unable to get OIDC token"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL
|
||||
```
|
||||
|
||||
**Cause:** The workflow doesn't have `id-token: write` permission.
|
||||
|
||||
**Solution:**
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
# GitLab CI
|
||||
job:
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
```
|
||||
|
||||
#### Error: "Token audience mismatch"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Token audience 'api://default' does not match expected 'sigstore'
|
||||
```
|
||||
|
||||
**Cause:** OIDC token was requested with wrong audience.
|
||||
|
||||
**Solution:**
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \
|
||||
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}")
|
||||
|
||||
# GitLab CI
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore # Must be 'sigstore' for Fulcio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Fulcio Certificate Errors
|
||||
|
||||
#### Error: "Failed to get certificate from Fulcio"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: error getting certificate from Fulcio: 401 Unauthorized
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. OIDC token expired (tokens are short-lived, typically 5-10 minutes)
|
||||
2. Fulcio doesn't recognize the OIDC issuer
|
||||
3. Network connectivity issues to Fulcio
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Token expiry:** Request token immediately before signing:
|
||||
```yaml
|
||||
- name: Get OIDC Token
|
||||
id: oidc
|
||||
run: |
|
||||
# Get fresh token right before signing
|
||||
OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \
|
||||
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
|
||||
| jq -r '.value')
|
||||
echo "token=${OIDC_TOKEN}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sign (immediately after)
|
||||
env:
|
||||
STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
|
||||
run: stella attest sign --keyless --artifact "$DIGEST"
|
||||
```
|
||||
|
||||
2. **Unknown issuer:** Ensure your CI platform is supported:
|
||||
- GitHub Actions: `https://token.actions.githubusercontent.com`
|
||||
- GitLab.com: `https://gitlab.com`
|
||||
- Self-hosted GitLab: Must be configured in Fulcio
|
||||
|
||||
3. **Network issues:** Check connectivity:
|
||||
```bash
|
||||
curl -v https://fulcio.sigstore.dev/api/v2/signingCert
|
||||
```
|
||||
|
||||
#### Error: "Certificate identity not found in token"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: no matching subject or SAN found in OIDC token
|
||||
```
|
||||
|
||||
**Cause:** Token claims don't include expected identity fields.
|
||||
|
||||
**Solution:** Verify token contents:
|
||||
```bash
|
||||
# Decode and inspect token (don't do this in production logs)
|
||||
echo $OIDC_TOKEN | cut -d. -f2 | base64 -d | jq .
|
||||
```
|
||||
|
||||
Expected claims for GitHub Actions:
|
||||
```json
|
||||
{
|
||||
"sub": "repo:org/repo:ref:refs/heads/main",
|
||||
"iss": "https://token.actions.githubusercontent.com",
|
||||
"repository": "org/repo",
|
||||
"ref": "refs/heads/main"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rekor Transparency Log Errors
|
||||
|
||||
#### Error: "Failed to upload to Rekor"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: error uploading entry to Rekor: 500 Internal Server Error
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
1. Rekor service temporarily unavailable
|
||||
2. Entry too large
|
||||
3. Network issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Retry with backoff:**
|
||||
```yaml
|
||||
- name: Sign with retry
|
||||
run: |
|
||||
for i in 1 2 3; do
|
||||
stella attest sign --keyless --artifact "$DIGEST" && break
|
||||
echo "Attempt $i failed, retrying in 30s..."
|
||||
sleep 30
|
||||
done
|
||||
```
|
||||
|
||||
2. **Check Rekor status:** https://status.sigstore.dev/
|
||||
|
||||
3. **Use offline bundle (air-gapped):**
|
||||
```bash
|
||||
stella attest sign --keyless --artifact "$DIGEST" --offline-bundle
|
||||
```
|
||||
|
||||
#### Error: "Rekor entry not found"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: entry not found in transparency log
|
||||
```
|
||||
|
||||
**Cause:** Verification requiring Rekor but entry wasn't logged (offline signing).
|
||||
|
||||
**Solution:** Either:
|
||||
- Sign with Rekor enabled (default)
|
||||
- Verify without Rekor requirement:
|
||||
```bash
|
||||
stella attest verify --artifact "$DIGEST" --skip-rekor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Verification Failures
|
||||
|
||||
#### Error: "Certificate identity mismatch"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: certificate identity 'repo:org/repo:ref:refs/heads/feature'
|
||||
does not match expected 'repo:org/repo:ref:refs/heads/main'
|
||||
```
|
||||
|
||||
**Cause:** Artifact was signed from a different branch/ref than expected.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use regex for flexibility:**
|
||||
```bash
|
||||
stella attest verify \
|
||||
--artifact "$DIGEST" \
|
||||
--certificate-identity "repo:org/repo:.*" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
||||
```
|
||||
|
||||
2. **Verify expected signing context:**
|
||||
```bash
|
||||
# Check what identity was actually used
|
||||
stella attest inspect --artifact "$DIGEST" --show-identity
|
||||
```
|
||||
|
||||
#### Error: "Certificate OIDC issuer mismatch"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: certificate issuer 'https://gitlab.com'
|
||||
does not match expected 'https://token.actions.githubusercontent.com'
|
||||
```
|
||||
|
||||
**Cause:** Artifact was signed by a different CI platform.
|
||||
|
||||
**Solution:** Update verification to accept correct issuer:
|
||||
```bash
|
||||
# For GitLab-signed artifacts
|
||||
stella attest verify \
|
||||
--artifact "$DIGEST" \
|
||||
--certificate-identity "project_path:org/repo:.*" \
|
||||
--certificate-oidc-issuer "https://gitlab.com"
|
||||
```
|
||||
|
||||
#### Error: "Signature expired"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: certificate validity period has expired
|
||||
```
|
||||
|
||||
**Cause:** Fulcio certificates are short-lived (10 minutes). Verification after expiry requires Rekor proof.
|
||||
|
||||
**Solution:** Ensure Rekor verification is enabled:
|
||||
```bash
|
||||
stella attest verify \
|
||||
--artifact "$DIGEST" \
|
||||
--require-rekor \
|
||||
--certificate-identity "..." \
|
||||
--certificate-oidc-issuer "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Platform-Specific Issues
|
||||
|
||||
#### GitHub Actions: "Resource not accessible by integration"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Resource not accessible by integration
|
||||
```
|
||||
|
||||
**Cause:** GitHub App or token lacks required permissions.
|
||||
|
||||
**Solution:** Ensure workflow has correct permissions:
|
||||
```yaml
|
||||
permissions:
|
||||
id-token: write # For OIDC token
|
||||
contents: read # For checkout
|
||||
packages: write # If pushing to GHCR
|
||||
attestations: write # For GitHub attestations
|
||||
```
|
||||
|
||||
#### GitLab CI: "id_tokens not available"
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: STELLAOPS_OIDC_TOKEN variable not set
|
||||
```
|
||||
|
||||
**Cause:** GitLab version doesn't support `id_tokens` or feature is disabled.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check GitLab version (requires 15.7+)
|
||||
2. Enable CI/CD OIDC in project settings:
|
||||
- Settings > CI/CD > Token Access
|
||||
- Enable "Allow CI job tokens from the following projects"
|
||||
|
||||
3. Use service account as fallback:
|
||||
```yaml
|
||||
sign:
|
||||
script:
|
||||
- |
|
||||
if [ -z "$STELLAOPS_OIDC_TOKEN" ]; then
|
||||
# Fallback to service account
|
||||
stella attest sign --key "$SIGNING_KEY" --artifact "$DIGEST"
|
||||
else
|
||||
stella attest sign --keyless --artifact "$DIGEST"
|
||||
fi
|
||||
```
|
||||
|
||||
#### Gitea: OIDC Token Format
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: Invalid OIDC token format
|
||||
```
|
||||
|
||||
**Cause:** Gitea Actions uses different token acquisition method.
|
||||
|
||||
**Solution:**
|
||||
```yaml
|
||||
- name: Get OIDC Token
|
||||
run: |
|
||||
# Gitea provides token directly in environment
|
||||
if [ -n "$ACTIONS_ID_TOKEN" ]; then
|
||||
echo "token=$ACTIONS_ID_TOKEN" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "::error::OIDC token not available"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Network and Connectivity
|
||||
|
||||
#### Error: "Connection refused" to Sigstore services
|
||||
|
||||
**Symptoms:**
|
||||
```
|
||||
Error: dial tcp: connection refused
|
||||
```
|
||||
|
||||
**Cause:** Firewall blocking outbound connections.
|
||||
|
||||
**Required endpoints:**
|
||||
| Service | URL | Purpose |
|
||||
|---------|-----|---------|
|
||||
| Fulcio | `https://fulcio.sigstore.dev` | Certificate issuance |
|
||||
| Rekor | `https://rekor.sigstore.dev` | Transparency log |
|
||||
| TUF | `https://tuf-repo-cdn.sigstore.dev` | Trust root |
|
||||
| OIDC | CI platform URL | Token validation |
|
||||
|
||||
**Solution:** Allow outbound HTTPS to these endpoints, or use self-hosted Sigstore.
|
||||
|
||||
#### Proxy Configuration
|
||||
|
||||
```yaml
|
||||
- name: Sign with proxy
|
||||
env:
|
||||
HTTPS_PROXY: http://proxy.internal:8080
|
||||
NO_PROXY: internal.corp.com
|
||||
STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
|
||||
run: stella attest sign --keyless --artifact "$DIGEST"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Commands
|
||||
|
||||
### Inspect OIDC Token
|
||||
```bash
|
||||
# Decode token payload (never log in production)
|
||||
echo $OIDC_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .
|
||||
```
|
||||
|
||||
### Verify Fulcio Connectivity
|
||||
```bash
|
||||
curl -v https://fulcio.sigstore.dev/api/v2/configuration
|
||||
```
|
||||
|
||||
### Check Rekor Entry
|
||||
```bash
|
||||
# Search by artifact hash
|
||||
rekor-cli search --sha "sha256:abc123..."
|
||||
|
||||
# Get entry details
|
||||
rekor-cli get --uuid "24296fb24b8ad77a..."
|
||||
```
|
||||
|
||||
### Inspect Attestation
|
||||
```bash
|
||||
stella attest inspect \
|
||||
--artifact "$DIGEST" \
|
||||
--show-certificate \
|
||||
--show-rekor-entry
|
||||
```
|
||||
|
||||
### Verbose Signing
|
||||
```bash
|
||||
STELLAOPS_LOG_LEVEL=debug stella attest sign --keyless --artifact "$DIGEST"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. **Check service status:** https://status.sigstore.dev/
|
||||
2. **StellaOps documentation:** https://docs.stella-ops.org/
|
||||
3. **Sigstore documentation:** https://docs.sigstore.dev/
|
||||
4. **File an issue:** https://github.com/stella-ops/stellaops/issues
|
||||
|
||||
When reporting issues, include:
|
||||
- CI platform and version
|
||||
- StellaOps CLI version (`stella --version`)
|
||||
- Sanitized error output (remove tokens/secrets)
|
||||
- Relevant workflow configuration
|
||||
@@ -1,223 +0,0 @@
|
||||
# VexTrustGate Rollout Guide
|
||||
|
||||
This guide describes the phased rollout procedure for the VexTrustGate policy feature, which enforces VEX signature verification trust thresholds.
|
||||
|
||||
## Overview
|
||||
|
||||
VexTrustGate adds a new policy gate that:
|
||||
1. Validates VEX signature verification trust scores
|
||||
2. Enforces per-environment thresholds (production stricter than staging/dev)
|
||||
3. Blocks or warns on status transitions when trust is insufficient
|
||||
4. Contributes to confidence scoring via VexTrustConfidenceFactorProvider
|
||||
|
||||
## Gate Order
|
||||
|
||||
VexTrustGate is positioned in the policy gate chain at **order 250**:
|
||||
- **100**: EvidenceCompleteness
|
||||
- **200**: LatticeState
|
||||
- **250**: VexTrust ← NEW
|
||||
- **300**: UncertaintyTier
|
||||
- **400**: Confidence
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. VEX signature verification pipeline active (SPRINT_1227_0004_0001)
|
||||
2. IssuerDirectory populated with trusted VEX sources
|
||||
3. Excititor properly populating VexTrustStatus in API responses
|
||||
|
||||
## Rollout Phases
|
||||
|
||||
### Phase 1: Feature Flag Deployment
|
||||
|
||||
Deploy with gate disabled to establish baseline:
|
||||
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Enabled: false # Gate off initially
|
||||
```
|
||||
|
||||
**Duration**: 1-2 days
|
||||
**Monitoring**: Verify deployment health, no regression in existing gates.
|
||||
|
||||
### Phase 2: Shadow Mode (Warn Everywhere)
|
||||
|
||||
Enable gate in warn-only mode across all environments:
|
||||
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Enabled: true
|
||||
Thresholds:
|
||||
production:
|
||||
MinCompositeScore: 0.80
|
||||
RequireIssuerVerified: true
|
||||
FailureAction: Warn # Changed from Block
|
||||
staging:
|
||||
MinCompositeScore: 0.60
|
||||
RequireIssuerVerified: true
|
||||
FailureAction: Warn
|
||||
development:
|
||||
MinCompositeScore: 0.40
|
||||
RequireIssuerVerified: false
|
||||
FailureAction: Warn
|
||||
MissingTrustBehavior: Warn
|
||||
```
|
||||
|
||||
**Duration**: 1-2 weeks
|
||||
**Monitoring**:
|
||||
- Review `stellaops.policy.vex_trust_gate.decisions.total` metrics
|
||||
- Analyze warn events to understand threshold impact
|
||||
- Collect feedback from operators on false positives
|
||||
|
||||
### Phase 3: Threshold Tuning
|
||||
|
||||
Based on Phase 2 data, adjust thresholds:
|
||||
|
||||
1. **Review decision breakdown by reason**:
|
||||
- `composite_score`: May need to lower threshold
|
||||
- `issuer_verified`: Check IssuerDirectory completeness
|
||||
- `freshness`: Consider expanding acceptable states
|
||||
|
||||
2. **Tenant-specific adjustments** (if needed):
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
TenantOverrides:
|
||||
tenant-with-internal-vex:
|
||||
production:
|
||||
MinCompositeScore: 0.70 # Lower for self-signed internal VEX
|
||||
high-security-tenant:
|
||||
production:
|
||||
MinCompositeScore: 0.90 # Higher for regulated workloads
|
||||
```
|
||||
|
||||
**Duration**: 1 week
|
||||
**Outcome**: Validated threshold configuration
|
||||
|
||||
### Phase 4: Production Enforcement
|
||||
|
||||
Enable blocking in production only:
|
||||
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Enabled: true
|
||||
Thresholds:
|
||||
production:
|
||||
MinCompositeScore: 0.80
|
||||
RequireIssuerVerified: true
|
||||
MinAccuracyRate: 0.85
|
||||
AcceptableFreshness:
|
||||
- fresh
|
||||
FailureAction: Block # Now enforcing
|
||||
staging:
|
||||
FailureAction: Warn # Still warn only
|
||||
development:
|
||||
FailureAction: Warn
|
||||
```
|
||||
|
||||
**Duration**: Ongoing with monitoring
|
||||
**Rollback**: Set `FailureAction: Warn` or `Enabled: false` if issues arise.
|
||||
|
||||
### Phase 5: Full Rollout
|
||||
|
||||
After production stabilization, optionally enable blocking in staging:
|
||||
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Thresholds:
|
||||
staging:
|
||||
MinCompositeScore: 0.60
|
||||
RequireIssuerVerified: true
|
||||
FailureAction: Block # Optional stricter staging
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Key Metrics
|
||||
|
||||
| Metric | Description | Alert Threshold |
|
||||
|--------|-------------|-----------------|
|
||||
| `stellaops.policy.vex_trust_gate.evaluations.total` | Total evaluations | Baseline variance |
|
||||
| `stellaops.policy.vex_trust_gate.decisions.total{decision="block"}` | Block decisions | Sudden spike |
|
||||
| `stellaops.policy.vex_trust_gate.trust_score` | Score distribution | Mean < 0.50 |
|
||||
| `stellaops.policy.vex_trust_gate.evaluation_duration_ms` | Latency | p99 > 100ms |
|
||||
|
||||
### Trace Spans
|
||||
|
||||
- `VexTrustGate.EvaluateAsync`
|
||||
- Attributes: `environment`, `trust_score`, `decision`, `issuer_id`
|
||||
|
||||
### Audit Trail
|
||||
|
||||
PolicyAuditEntity now includes VEX trust fields:
|
||||
- `VexTrustScore`: Composite score at decision time
|
||||
- `VexTrustTier`: Tier classification
|
||||
- `VexSignatureVerified`: Whether signature was verified
|
||||
- `VexIssuerId`/`VexIssuerName`: Issuer info
|
||||
- `VexTrustGateResult`: Gate decision
|
||||
- `VexTrustGateReason`: Reason code
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
### Immediate Disable
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Enabled: false
|
||||
```
|
||||
|
||||
### Switch to Warn Mode
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
Thresholds:
|
||||
production:
|
||||
FailureAction: Warn
|
||||
staging:
|
||||
FailureAction: Warn
|
||||
development:
|
||||
FailureAction: Warn
|
||||
```
|
||||
|
||||
### Per-Tenant Disable
|
||||
```yaml
|
||||
PolicyGates:
|
||||
VexTrust:
|
||||
TenantOverrides:
|
||||
affected-tenant:
|
||||
production:
|
||||
MinCompositeScore: 0.01 # Effectively bypass
|
||||
RequireIssuerVerified: false
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Symptom | Likely Cause | Resolution |
|
||||
|---------|--------------|------------|
|
||||
| All VEX blocked | Missing IssuerDirectory entries | Populate directory with trusted issuers |
|
||||
| High false positive rate | Threshold too strict | Lower `MinCompositeScore` |
|
||||
| "missing_vex_trust_data" warnings | Verification pipeline not running | Check Excititor logs |
|
||||
| Inconsistent decisions | Stale trust cache | Verify cache TTL settings |
|
||||
|
||||
### Debug Logging
|
||||
|
||||
Enable debug logging for gate:
|
||||
```yaml
|
||||
Logging:
|
||||
LogLevel:
|
||||
StellaOps.Policy.Engine.Gates.VexTrustGate: Debug
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- Sprint: `SPRINT_1227_0004_0003`
|
||||
- Component: `StellaOps.Policy.Engine.Gates`
|
||||
- Files:
|
||||
- `src/Policy/StellaOps.Policy.Engine/Gates/VexTrustGate.cs`
|
||||
- `src/Policy/StellaOps.Policy.Engine/Gates/VexTrustGateOptions.cs`
|
||||
- `etc/policy-gates.yaml.sample`
|
||||
Reference in New Issue
Block a user