doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
149
examples/policies/opa/README.md
Normal file
149
examples/policies/opa/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# OPA/Rego Policy Examples for CVE Gating
|
||||
|
||||
This directory contains Open Policy Agent (OPA) Rego policies for CVE-aware release gating. These policies can be used alongside or instead of the Stella DSL for advanced policy scenarios.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install OPA
|
||||
brew install opa # macOS
|
||||
# or download from https://www.openpolicyagent.org/docs/latest/#running-opa
|
||||
|
||||
# Run all tests
|
||||
opa test . -v
|
||||
|
||||
# Evaluate a policy
|
||||
opa eval -d epss-threshold.rego -i sample-input.json "data.stellaops.gates.epss.allow"
|
||||
```
|
||||
|
||||
## Available Policies
|
||||
|
||||
| Policy | Description |
|
||||
|--------|-------------|
|
||||
| [cve-gate-base.rego](cve-gate-base.rego) | Base policy with DSSE signature and Rekor anchor verification |
|
||||
| [epss-threshold.rego](epss-threshold.rego) | EPSS exploitation probability threshold enforcement |
|
||||
| [kev-blocker.rego](kev-blocker.rego) | CISA KEV catalog blocking |
|
||||
| [reachable-cve.rego](reachable-cve.rego) | Reachability-aware CVE blocking |
|
||||
| [release-aggregate.rego](release-aggregate.rego) | Aggregate CVE count limits per release |
|
||||
|
||||
## Input Schema
|
||||
|
||||
All policies expect input conforming to `input-schema.json`. Key fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"attestation": {
|
||||
"dsse_envelope": { ... },
|
||||
"rekor_entry": { ... }
|
||||
},
|
||||
"cve_findings": [
|
||||
{
|
||||
"cve_id": "CVE-2024-1234",
|
||||
"cvss_score": 7.5,
|
||||
"epss_score": 0.42,
|
||||
"is_kev": false,
|
||||
"is_reachable": true
|
||||
}
|
||||
],
|
||||
"environment": "production",
|
||||
"config": {
|
||||
"epss_threshold": 0.6,
|
||||
"max_critical": 0,
|
||||
"max_high": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [input-schema.json](input-schema.json) for full schema documentation.
|
||||
|
||||
## Policy Composition
|
||||
|
||||
Policies can be combined using OPA's standard composition:
|
||||
|
||||
```rego
|
||||
package stellaops.gates.combined
|
||||
|
||||
import data.stellaops.gates.base
|
||||
import data.stellaops.gates.epss
|
||||
import data.stellaops.gates.kev
|
||||
import data.stellaops.gates.reachable
|
||||
|
||||
# All gates must pass
|
||||
default allow = false
|
||||
|
||||
allow {
|
||||
base.valid_attestation
|
||||
epss.allow
|
||||
kev.allow
|
||||
reachable.allow
|
||||
}
|
||||
|
||||
# Collect all denial reasons
|
||||
deny[msg] {
|
||||
not base.valid_attestation
|
||||
msg := base.deny[_]
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
not epss.allow
|
||||
msg := epss.deny[_]
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
not kev.allow
|
||||
msg := kev.deny[_]
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
not reachable.allow
|
||||
msg := reachable.deny[_]
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Stella
|
||||
|
||||
These policies can be executed via the Stella CLI:
|
||||
|
||||
```bash
|
||||
# Evaluate OPA policy against release candidate
|
||||
stella policy evaluate --engine opa --policy examples/policies/opa/epss-threshold.rego --image myapp:v1.2.3
|
||||
|
||||
# Evaluate multiple policies
|
||||
stella policy evaluate --engine opa --bundle examples/policies/opa/ --image myapp:v1.2.3
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Each policy has corresponding test files (`*_test.rego`). Run tests with:
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
opa test . -v
|
||||
|
||||
# Specific policy tests
|
||||
opa test epss-threshold.rego epss-threshold_test.rego -v
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Policy configuration is passed via `input.config`. Environment-specific overrides are supported:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"epss_threshold": 0.6,
|
||||
"environments": {
|
||||
"production": {
|
||||
"epss_threshold": 0.3
|
||||
},
|
||||
"staging": {
|
||||
"epss_threshold": 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-01-19.*
|
||||
99
examples/policies/opa/cve-gate-base.rego
Normal file
99
examples/policies/opa/cve-gate-base.rego
Normal file
@@ -0,0 +1,99 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# cve-gate-base.rego
|
||||
# Sprint: SPRINT_20260118_027_Policy_cve_release_gates
|
||||
# Task: TASK-027-08 - OPA/Rego Policy Examples
|
||||
# Description: Base policy for DSSE signature and Rekor anchor verification
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.base
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# Default deny - require explicit allow
|
||||
default valid_attestation = false
|
||||
|
||||
# Attestation is valid if DSSE envelope has valid signature from trusted key
|
||||
valid_attestation if {
|
||||
valid_dsse_envelope
|
||||
valid_signature
|
||||
valid_rekor_anchor
|
||||
}
|
||||
|
||||
# Allow without Rekor if not required
|
||||
valid_attestation if {
|
||||
valid_dsse_envelope
|
||||
valid_signature
|
||||
not config_require_rekor
|
||||
}
|
||||
|
||||
# DSSE envelope structure validation
|
||||
valid_dsse_envelope if {
|
||||
input.attestation.dsse_envelope.payloadType
|
||||
input.attestation.dsse_envelope.payload
|
||||
count(input.attestation.dsse_envelope.signatures) > 0
|
||||
}
|
||||
|
||||
# Signature validation - at least one signature from trusted key
|
||||
valid_signature if {
|
||||
some sig in input.attestation.dsse_envelope.signatures
|
||||
sig.keyid in trusted_keys
|
||||
sig.sig != ""
|
||||
}
|
||||
|
||||
# Rekor anchor validation
|
||||
valid_rekor_anchor if {
|
||||
input.attestation.rekor_entry.log_index >= 0
|
||||
input.attestation.rekor_entry.integrated_time > 0
|
||||
input.attestation.rekor_entry.inclusion_proof.root_hash != ""
|
||||
}
|
||||
|
||||
# Configuration helpers
|
||||
config_require_rekor if {
|
||||
input.config.require_rekor == true
|
||||
}
|
||||
|
||||
# Get trusted keys from input or use default
|
||||
trusted_keys := input.attestation.trusted_keys if {
|
||||
input.attestation.trusted_keys
|
||||
} else := []
|
||||
|
||||
# Denial messages
|
||||
deny[msg] if {
|
||||
not input.attestation.dsse_envelope
|
||||
msg := "Missing DSSE envelope in attestation"
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
input.attestation.dsse_envelope
|
||||
not valid_dsse_envelope
|
||||
msg := "Invalid DSSE envelope structure"
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
valid_dsse_envelope
|
||||
not valid_signature
|
||||
msg := "No valid signature from trusted key"
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
config_require_rekor
|
||||
not input.attestation.rekor_entry
|
||||
msg := "Rekor anchor required but not present"
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
config_require_rekor
|
||||
input.attestation.rekor_entry
|
||||
not valid_rekor_anchor
|
||||
msg := "Invalid Rekor inclusion proof"
|
||||
}
|
||||
|
||||
# Metadata for debugging
|
||||
attestation_info := {
|
||||
"has_dsse": valid_dsse_envelope,
|
||||
"has_valid_sig": valid_signature,
|
||||
"has_rekor": valid_rekor_anchor,
|
||||
"signature_count": count(input.attestation.dsse_envelope.signatures),
|
||||
"trusted_key_count": count(trusted_keys),
|
||||
}
|
||||
103
examples/policies/opa/cve-gate-base_test.rego
Normal file
103
examples/policies/opa/cve-gate-base_test.rego
Normal file
@@ -0,0 +1,103 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# cve-gate-base_test.rego
|
||||
# Tests for base attestation verification policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.base
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
# Test valid attestation with DSSE and Rekor
|
||||
test_valid_attestation_with_rekor if {
|
||||
valid_attestation with input as {
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJzdWJqZWN0IjpbXX0=",
|
||||
"signatures": [{"keyid": "key-1", "sig": "abc123"}]
|
||||
},
|
||||
"rekor_entry": {
|
||||
"log_index": 12345,
|
||||
"integrated_time": 1705689600,
|
||||
"inclusion_proof": {"root_hash": "abc", "tree_size": 100, "hashes": []}
|
||||
},
|
||||
"trusted_keys": ["key-1"]
|
||||
},
|
||||
"config": {"require_rekor": true}
|
||||
}
|
||||
}
|
||||
|
||||
# Test valid attestation without Rekor when not required
|
||||
test_valid_attestation_no_rekor_not_required if {
|
||||
valid_attestation with input as {
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJzdWJqZWN0IjpbXX0=",
|
||||
"signatures": [{"keyid": "key-1", "sig": "abc123"}]
|
||||
},
|
||||
"trusted_keys": ["key-1"]
|
||||
},
|
||||
"config": {"require_rekor": false}
|
||||
}
|
||||
}
|
||||
|
||||
# Test invalid - missing DSSE envelope
|
||||
test_invalid_missing_dsse if {
|
||||
not valid_attestation with input as {
|
||||
"attestation": {},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Test invalid - untrusted key
|
||||
test_invalid_untrusted_key if {
|
||||
not valid_attestation with input as {
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJzdWJqZWN0IjpbXX0=",
|
||||
"signatures": [{"keyid": "untrusted-key", "sig": "abc123"}]
|
||||
},
|
||||
"trusted_keys": ["key-1"]
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Test invalid - Rekor required but missing
|
||||
test_invalid_rekor_required_but_missing if {
|
||||
not valid_attestation with input as {
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJzdWJqZWN0IjpbXX0=",
|
||||
"signatures": [{"keyid": "key-1", "sig": "abc123"}]
|
||||
},
|
||||
"trusted_keys": ["key-1"]
|
||||
},
|
||||
"config": {"require_rekor": true}
|
||||
}
|
||||
}
|
||||
|
||||
# Test denial messages
|
||||
test_deny_missing_dsse if {
|
||||
"Missing DSSE envelope in attestation" in deny with input as {
|
||||
"attestation": {},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_no_valid_signature if {
|
||||
"No valid signature from trusted key" in deny with input as {
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJzdWJqZWN0IjpbXX0=",
|
||||
"signatures": [{"keyid": "bad-key", "sig": "abc123"}]
|
||||
},
|
||||
"trusted_keys": ["key-1"]
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
70
examples/policies/opa/epss-threshold.rego
Normal file
70
examples/policies/opa/epss-threshold.rego
Normal file
@@ -0,0 +1,70 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# epss-threshold.rego
|
||||
# Sprint: SPRINT_20260118_027_Policy_cve_release_gates
|
||||
# Task: TASK-027-08 - OPA/Rego Policy Examples
|
||||
# Description: EPSS exploitation probability threshold enforcement
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.epss
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# Default allow if no CVEs exceed threshold
|
||||
default allow = true
|
||||
|
||||
# Block if any CVE exceeds EPSS threshold
|
||||
allow = false if {
|
||||
some cve in relevant_cves
|
||||
cve.epss_score > epss_threshold
|
||||
}
|
||||
|
||||
# Get CVEs to evaluate (optionally filtered by reachability)
|
||||
relevant_cves := [cve |
|
||||
some cve in input.cve_findings
|
||||
config_only_reachable
|
||||
cve.is_reachable == true
|
||||
]
|
||||
|
||||
relevant_cves := input.cve_findings if {
|
||||
not config_only_reachable
|
||||
}
|
||||
|
||||
# Get threshold with environment override support
|
||||
epss_threshold := env_config.epss_threshold if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.epss_threshold
|
||||
} else := input.config.epss_threshold if {
|
||||
input.config.epss_threshold
|
||||
} else := 0.6 # Default threshold
|
||||
|
||||
# Configuration flags
|
||||
config_only_reachable if {
|
||||
input.config.only_reachable == true
|
||||
}
|
||||
|
||||
# Denial messages with CVE details
|
||||
deny[msg] if {
|
||||
some cve in relevant_cves
|
||||
cve.epss_score > epss_threshold
|
||||
msg := sprintf("CVE %s exceeds EPSS threshold: %.2f > %.2f", [
|
||||
cve.cve_id,
|
||||
cve.epss_score,
|
||||
epss_threshold
|
||||
])
|
||||
}
|
||||
|
||||
# Count CVEs exceeding threshold
|
||||
exceeding_cves := [cve |
|
||||
some cve in relevant_cves
|
||||
cve.epss_score > epss_threshold
|
||||
]
|
||||
|
||||
# Summary for reporting
|
||||
summary := {
|
||||
"total_cves": count(relevant_cves),
|
||||
"exceeding_count": count(exceeding_cves),
|
||||
"threshold": epss_threshold,
|
||||
"environment": input.environment,
|
||||
"exceeding_cves": [cve.cve_id | some cve in exceeding_cves],
|
||||
}
|
||||
93
examples/policies/opa/epss-threshold_test.rego
Normal file
93
examples/policies/opa/epss-threshold_test.rego
Normal file
@@ -0,0 +1,93 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# epss-threshold_test.rego
|
||||
# Tests for EPSS threshold policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.epss
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
# Test allow - all CVEs below threshold
|
||||
test_allow_below_threshold if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "epss_score": 0.3},
|
||||
{"cve_id": "CVE-2024-0002", "epss_score": 0.5}
|
||||
],
|
||||
"config": {"epss_threshold": 0.6}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - CVE above threshold
|
||||
test_deny_above_threshold if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "epss_score": 0.3},
|
||||
{"cve_id": "CVE-2024-0002", "epss_score": 0.7}
|
||||
],
|
||||
"config": {"epss_threshold": 0.6}
|
||||
}
|
||||
}
|
||||
|
||||
# Test allow - empty findings
|
||||
test_allow_empty_findings if {
|
||||
allow with input as {
|
||||
"cve_findings": [],
|
||||
"config": {"epss_threshold": 0.6}
|
||||
}
|
||||
}
|
||||
|
||||
# Test environment override
|
||||
test_environment_override if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "epss_score": 0.4}
|
||||
],
|
||||
"environment": "production",
|
||||
"config": {
|
||||
"epss_threshold": 0.6,
|
||||
"environments": {
|
||||
"production": {"epss_threshold": 0.3}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test only_reachable filter
|
||||
test_only_reachable_filters_unreachable if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "epss_score": 0.8, "is_reachable": false},
|
||||
{"cve_id": "CVE-2024-0002", "epss_score": 0.3, "is_reachable": true}
|
||||
],
|
||||
"config": {"epss_threshold": 0.6, "only_reachable": true}
|
||||
}
|
||||
}
|
||||
|
||||
# Test denial message content
|
||||
test_deny_message_content if {
|
||||
msg := deny[_] with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-1234", "epss_score": 0.72}
|
||||
],
|
||||
"config": {"epss_threshold": 0.6}
|
||||
}
|
||||
contains(msg, "CVE-2024-1234")
|
||||
contains(msg, "0.72")
|
||||
}
|
||||
|
||||
# Test summary output
|
||||
test_summary_structure if {
|
||||
s := summary with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "epss_score": 0.3},
|
||||
{"cve_id": "CVE-2024-0002", "epss_score": 0.7}
|
||||
],
|
||||
"environment": "staging",
|
||||
"config": {"epss_threshold": 0.6}
|
||||
}
|
||||
s.total_cves == 2
|
||||
s.exceeding_count == 1
|
||||
s.threshold == 0.6
|
||||
s.environment == "staging"
|
||||
}
|
||||
240
examples/policies/opa/input-schema.json
Normal file
240
examples/policies/opa/input-schema.json
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://stellaops.io/schemas/opa/policy-input.json",
|
||||
"title": "Stella OPA Policy Input Schema",
|
||||
"description": "Input schema for OPA/Rego CVE gating policies",
|
||||
"type": "object",
|
||||
"required": ["attestation", "cve_findings", "environment"],
|
||||
"properties": {
|
||||
"attestation": {
|
||||
"type": "object",
|
||||
"description": "Attestation data including DSSE envelope and Rekor entry",
|
||||
"required": ["dsse_envelope"],
|
||||
"properties": {
|
||||
"dsse_envelope": {
|
||||
"type": "object",
|
||||
"description": "DSSE envelope containing signed statement",
|
||||
"required": ["payloadType", "payload", "signatures"],
|
||||
"properties": {
|
||||
"payloadType": {
|
||||
"type": "string",
|
||||
"description": "DSSE payload type URI",
|
||||
"examples": ["application/vnd.in-toto+json"]
|
||||
},
|
||||
"payload": {
|
||||
"type": "string",
|
||||
"description": "Base64-encoded payload (in-toto statement)"
|
||||
},
|
||||
"signatures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["keyid", "sig"],
|
||||
"properties": {
|
||||
"keyid": {
|
||||
"type": "string",
|
||||
"description": "Key identifier"
|
||||
},
|
||||
"sig": {
|
||||
"type": "string",
|
||||
"description": "Base64-encoded signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rekor_entry": {
|
||||
"type": "object",
|
||||
"description": "Rekor transparency log entry (optional)",
|
||||
"properties": {
|
||||
"log_index": {
|
||||
"type": "integer",
|
||||
"description": "Rekor log index"
|
||||
},
|
||||
"log_id": {
|
||||
"type": "string",
|
||||
"description": "Rekor log ID (base64 SHA256)"
|
||||
},
|
||||
"integrated_time": {
|
||||
"type": "integer",
|
||||
"description": "Unix timestamp of log inclusion"
|
||||
},
|
||||
"inclusion_proof": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"root_hash": { "type": "string" },
|
||||
"tree_size": { "type": "integer" },
|
||||
"hashes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trusted_keys": {
|
||||
"type": "array",
|
||||
"description": "List of trusted signing key IDs",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"cve_findings": {
|
||||
"type": "array",
|
||||
"description": "CVE findings from scan results",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["cve_id"],
|
||||
"properties": {
|
||||
"cve_id": {
|
||||
"type": "string",
|
||||
"pattern": "^CVE-\\d{4}-\\d{4,}$",
|
||||
"description": "CVE identifier"
|
||||
},
|
||||
"cvss_score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"description": "CVSS v3 base score"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "unknown"],
|
||||
"description": "Severity classification"
|
||||
},
|
||||
"epss_score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "EPSS exploitation probability (0-1)"
|
||||
},
|
||||
"epss_percentile": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "EPSS percentile (0-100)"
|
||||
},
|
||||
"is_kev": {
|
||||
"type": "boolean",
|
||||
"description": "Whether CVE is in CISA KEV catalog"
|
||||
},
|
||||
"kev_due_date": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"description": "KEV remediation due date (YYYY-MM-DD)"
|
||||
},
|
||||
"is_reachable": {
|
||||
"type": "boolean",
|
||||
"description": "Whether vulnerable code is reachable"
|
||||
},
|
||||
"reachability_state": {
|
||||
"type": "string",
|
||||
"enum": ["confirmed_reachable", "runtime_observed", "statically_reachable", "not_reachable", "unknown"],
|
||||
"description": "Detailed reachability state"
|
||||
},
|
||||
"is_suppressed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether CVE is suppressed/excepted"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string",
|
||||
"description": "Affected package name"
|
||||
},
|
||||
"package_version": {
|
||||
"type": "string",
|
||||
"description": "Affected package version"
|
||||
},
|
||||
"fix_available": {
|
||||
"type": "boolean",
|
||||
"description": "Whether a fix is available"
|
||||
},
|
||||
"fixed_version": {
|
||||
"type": "string",
|
||||
"description": "Version containing the fix"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"baseline_cve_findings": {
|
||||
"type": "array",
|
||||
"description": "CVE findings from baseline release (for delta comparison)",
|
||||
"items": { "$ref": "#/properties/cve_findings/items" }
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Target deployment environment",
|
||||
"examples": ["development", "staging", "production"]
|
||||
},
|
||||
"release": {
|
||||
"type": "object",
|
||||
"description": "Release metadata",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"image_digest": { "type": "string" },
|
||||
"baseline_digest": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"description": "Policy configuration",
|
||||
"properties": {
|
||||
"epss_threshold": {
|
||||
"type": "number",
|
||||
"description": "EPSS score threshold (0-1)"
|
||||
},
|
||||
"epss_percentile_threshold": {
|
||||
"type": "number",
|
||||
"description": "EPSS percentile threshold (0-100)"
|
||||
},
|
||||
"severity_threshold": {
|
||||
"type": "number",
|
||||
"description": "CVSS severity threshold"
|
||||
},
|
||||
"max_critical": {
|
||||
"type": "integer",
|
||||
"description": "Maximum allowed critical CVEs"
|
||||
},
|
||||
"max_high": {
|
||||
"type": "integer",
|
||||
"description": "Maximum allowed high CVEs"
|
||||
},
|
||||
"max_medium": {
|
||||
"type": "integer",
|
||||
"description": "Maximum allowed medium CVEs"
|
||||
},
|
||||
"max_low": {
|
||||
"type": "integer",
|
||||
"description": "Maximum allowed low CVEs"
|
||||
},
|
||||
"max_total": {
|
||||
"type": "integer",
|
||||
"description": "Maximum total CVEs"
|
||||
},
|
||||
"require_rekor": {
|
||||
"type": "boolean",
|
||||
"description": "Require Rekor anchor for attestations"
|
||||
},
|
||||
"count_suppressed": {
|
||||
"type": "boolean",
|
||||
"description": "Include suppressed CVEs in counts"
|
||||
},
|
||||
"only_reachable": {
|
||||
"type": "boolean",
|
||||
"description": "Only evaluate reachable CVEs"
|
||||
},
|
||||
"environments": {
|
||||
"type": "object",
|
||||
"description": "Per-environment configuration overrides",
|
||||
"additionalProperties": { "$ref": "#/properties/config" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"current_time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Current evaluation timestamp (ISO 8601)"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
examples/policies/opa/kev-blocker.rego
Normal file
78
examples/policies/opa/kev-blocker.rego
Normal file
@@ -0,0 +1,78 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# kev-blocker.rego
|
||||
# Sprint: SPRINT_20260118_027_Policy_cve_release_gates
|
||||
# Task: TASK-027-08 - OPA/Rego Policy Examples
|
||||
# Description: CISA KEV catalog blocking policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.kev
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# Default allow if no KEV CVEs found
|
||||
default allow = true
|
||||
|
||||
# Block if any CVE is in KEV catalog
|
||||
allow = false if {
|
||||
some cve in relevant_cves
|
||||
cve.is_kev == true
|
||||
}
|
||||
|
||||
# Get CVEs to evaluate (optionally filtered by reachability)
|
||||
relevant_cves := [cve |
|
||||
some cve in input.cve_findings
|
||||
config_only_reachable
|
||||
cve.is_reachable == true
|
||||
]
|
||||
|
||||
relevant_cves := input.cve_findings if {
|
||||
not config_only_reachable
|
||||
}
|
||||
|
||||
# Configuration flags
|
||||
config_only_reachable if {
|
||||
input.config.only_reachable == true
|
||||
}
|
||||
|
||||
# Denial messages with KEV details
|
||||
deny[msg] if {
|
||||
some cve in relevant_cves
|
||||
cve.is_kev == true
|
||||
not cve.kev_due_date
|
||||
msg := sprintf("CVE %s is in CISA KEV catalog (actively exploited)", [cve.cve_id])
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
some cve in relevant_cves
|
||||
cve.is_kev == true
|
||||
cve.kev_due_date
|
||||
msg := sprintf("CVE %s is in CISA KEV catalog, due date: %s", [
|
||||
cve.cve_id,
|
||||
cve.kev_due_date
|
||||
])
|
||||
}
|
||||
|
||||
# Collect all KEV CVEs
|
||||
kev_cves := [cve |
|
||||
some cve in relevant_cves
|
||||
cve.is_kev == true
|
||||
]
|
||||
|
||||
# Check for overdue KEV CVEs (if current_time provided)
|
||||
overdue_kev_cves := [cve |
|
||||
some cve in kev_cves
|
||||
cve.kev_due_date
|
||||
input.current_time
|
||||
time.parse_rfc3339_ns(cve.kev_due_date) < time.parse_rfc3339_ns(input.current_time)
|
||||
]
|
||||
|
||||
# Summary for reporting
|
||||
summary := {
|
||||
"total_cves": count(relevant_cves),
|
||||
"kev_count": count(kev_cves),
|
||||
"overdue_count": count(overdue_kev_cves),
|
||||
"kev_cves": [{"cve_id": cve.cve_id, "due_date": cve.kev_due_date} |
|
||||
some cve in kev_cves
|
||||
],
|
||||
}
|
||||
86
examples/policies/opa/kev-blocker_test.rego
Normal file
86
examples/policies/opa/kev-blocker_test.rego
Normal file
@@ -0,0 +1,86 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# kev-blocker_test.rego
|
||||
# Tests for KEV blocker policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.kev
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
# Test allow - no KEV CVEs
|
||||
test_allow_no_kev if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "is_kev": false},
|
||||
{"cve_id": "CVE-2024-0002", "is_kev": false}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - KEV CVE present
|
||||
test_deny_kev_present if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "is_kev": false},
|
||||
{"cve_id": "CVE-2024-0002", "is_kev": true}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Test allow - empty findings
|
||||
test_allow_empty_findings if {
|
||||
allow with input as {
|
||||
"cve_findings": [],
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Test only_reachable filters unreachable KEV
|
||||
test_only_reachable_filters_unreachable_kev if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "is_kev": true, "is_reachable": false}
|
||||
],
|
||||
"config": {"only_reachable": true}
|
||||
}
|
||||
}
|
||||
|
||||
# Test denial message includes due date
|
||||
test_deny_message_with_due_date if {
|
||||
msg := deny[_] with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-1234", "is_kev": true, "kev_due_date": "2024-02-15"}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
contains(msg, "CVE-2024-1234")
|
||||
contains(msg, "2024-02-15")
|
||||
}
|
||||
|
||||
# Test denial message without due date
|
||||
test_deny_message_without_due_date if {
|
||||
msg := deny[_] with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-5678", "is_kev": true}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
contains(msg, "CVE-2024-5678")
|
||||
contains(msg, "actively exploited")
|
||||
}
|
||||
|
||||
# Test summary structure
|
||||
test_summary_structure if {
|
||||
s := summary with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "is_kev": false},
|
||||
{"cve_id": "CVE-2024-0002", "is_kev": true, "kev_due_date": "2024-02-15"},
|
||||
{"cve_id": "CVE-2024-0003", "is_kev": true}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
s.total_cves == 3
|
||||
s.kev_count == 2
|
||||
}
|
||||
81
examples/policies/opa/reachable-cve.rego
Normal file
81
examples/policies/opa/reachable-cve.rego
Normal file
@@ -0,0 +1,81 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# reachable-cve.rego
|
||||
# Sprint: SPRINT_20260118_027_Policy_cve_release_gates
|
||||
# Task: TASK-027-08 - OPA/Rego Policy Examples
|
||||
# Description: Reachability-aware CVE blocking policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.reachable
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# Default allow if no reachable high-severity CVEs
|
||||
default allow = true
|
||||
|
||||
# Block if any reachable CVE exceeds severity threshold
|
||||
allow = false if {
|
||||
some cve in input.cve_findings
|
||||
is_blocking_reachable(cve)
|
||||
cve.cvss_score >= severity_threshold
|
||||
}
|
||||
|
||||
# Determine if CVE's reachability state is blocking
|
||||
is_blocking_reachable(cve) if {
|
||||
cve.is_reachable == true
|
||||
}
|
||||
|
||||
is_blocking_reachable(cve) if {
|
||||
cve.reachability_state in blocking_states
|
||||
}
|
||||
|
||||
# Reachability states that trigger blocking
|
||||
blocking_states := {"confirmed_reachable", "runtime_observed", "statically_reachable"}
|
||||
|
||||
# Non-blocking reachability states
|
||||
non_blocking_states := {"not_reachable", "unknown"}
|
||||
|
||||
# Get severity threshold with environment override support
|
||||
severity_threshold := env_config.severity_threshold if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.severity_threshold
|
||||
} else := input.config.severity_threshold if {
|
||||
input.config.severity_threshold
|
||||
} else := 7.0 # Default threshold (High severity)
|
||||
|
||||
# Denial messages with reachability details
|
||||
deny[msg] if {
|
||||
some cve in input.cve_findings
|
||||
is_blocking_reachable(cve)
|
||||
cve.cvss_score >= severity_threshold
|
||||
msg := sprintf("Reachable CVE %s exceeds severity threshold: CVSS %.1f >= %.1f (state: %s)", [
|
||||
cve.cve_id,
|
||||
cve.cvss_score,
|
||||
severity_threshold,
|
||||
object.get(cve, "reachability_state", "reachable")
|
||||
])
|
||||
}
|
||||
|
||||
# Collect blocking CVEs
|
||||
blocking_cves := [cve |
|
||||
some cve in input.cve_findings
|
||||
is_blocking_reachable(cve)
|
||||
cve.cvss_score >= severity_threshold
|
||||
]
|
||||
|
||||
# Collect allowed unreachable CVEs (for reporting)
|
||||
allowed_unreachable := [cve |
|
||||
some cve in input.cve_findings
|
||||
cve.cvss_score >= severity_threshold
|
||||
not is_blocking_reachable(cve)
|
||||
]
|
||||
|
||||
# Summary for reporting
|
||||
summary := {
|
||||
"total_cves": count(input.cve_findings),
|
||||
"reachable_high_severity": count(blocking_cves),
|
||||
"unreachable_high_severity": count(allowed_unreachable),
|
||||
"severity_threshold": severity_threshold,
|
||||
"blocking_cves": [cve.cve_id | some cve in blocking_cves],
|
||||
"allowed_unreachable": [cve.cve_id | some cve in allowed_unreachable],
|
||||
}
|
||||
101
examples/policies/opa/reachable-cve_test.rego
Normal file
101
examples/policies/opa/reachable-cve_test.rego
Normal file
@@ -0,0 +1,101 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# reachable-cve_test.rego
|
||||
# Tests for reachability-aware CVE policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.reachable
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
# Test allow - high severity but not reachable
|
||||
test_allow_high_not_reachable if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.0, "is_reachable": false}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test allow - reachable but below threshold
|
||||
test_allow_reachable_below_threshold if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 5.0, "is_reachable": true}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - reachable and above threshold
|
||||
test_deny_reachable_above_threshold if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 8.5, "is_reachable": true}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - confirmed_reachable state
|
||||
test_deny_confirmed_reachable_state if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 8.5, "reachability_state": "confirmed_reachable"}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test allow - not_reachable state
|
||||
test_allow_not_reachable_state if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5, "reachability_state": "not_reachable"}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test environment threshold override
|
||||
test_environment_threshold_override if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 5.0, "is_reachable": true}
|
||||
],
|
||||
"environment": "production",
|
||||
"config": {
|
||||
"severity_threshold": 7.0,
|
||||
"environments": {
|
||||
"production": {"severity_threshold": 4.0}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test denial message content
|
||||
test_deny_message_content if {
|
||||
msg := deny[_] with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-1234", "cvss_score": 8.1, "is_reachable": true}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
contains(msg, "CVE-2024-1234")
|
||||
contains(msg, "8.1")
|
||||
}
|
||||
|
||||
# Test summary structure
|
||||
test_summary_structure if {
|
||||
s := summary with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.0, "is_reachable": true},
|
||||
{"cve_id": "CVE-2024-0002", "cvss_score": 8.0, "is_reachable": false},
|
||||
{"cve_id": "CVE-2024-0003", "cvss_score": 5.0, "is_reachable": true}
|
||||
],
|
||||
"config": {"severity_threshold": 7.0}
|
||||
}
|
||||
s.total_cves == 3
|
||||
s.reachable_high_severity == 1
|
||||
s.unreachable_high_severity == 1
|
||||
}
|
||||
192
examples/policies/opa/release-aggregate.rego
Normal file
192
examples/policies/opa/release-aggregate.rego
Normal file
@@ -0,0 +1,192 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# release-aggregate.rego
|
||||
# Sprint: SPRINT_20260118_027_Policy_cve_release_gates
|
||||
# Task: TASK-027-08 - OPA/Rego Policy Examples
|
||||
# Description: Aggregate CVE count limits per release
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.aggregate
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# Default allow if all counts within limits
|
||||
default allow = true
|
||||
|
||||
# Block if any severity count exceeds limit
|
||||
allow = false if {
|
||||
counts.critical > max_critical
|
||||
}
|
||||
|
||||
allow = false if {
|
||||
counts.high > max_high
|
||||
}
|
||||
|
||||
allow = false if {
|
||||
counts.medium > max_medium
|
||||
}
|
||||
|
||||
allow = false if {
|
||||
max_low != null
|
||||
counts.low > max_low
|
||||
}
|
||||
|
||||
allow = false if {
|
||||
max_total != null
|
||||
counts.total > max_total
|
||||
}
|
||||
|
||||
# Get CVEs to count (optionally filtered)
|
||||
counted_cves := [cve |
|
||||
some cve in input.cve_findings
|
||||
should_count(cve)
|
||||
]
|
||||
|
||||
# Determine if CVE should be counted
|
||||
should_count(cve) if {
|
||||
not config_only_reachable
|
||||
not config_exclude_suppressed
|
||||
}
|
||||
|
||||
should_count(cve) if {
|
||||
config_only_reachable
|
||||
cve.is_reachable == true
|
||||
not config_exclude_suppressed
|
||||
}
|
||||
|
||||
should_count(cve) if {
|
||||
not config_only_reachable
|
||||
config_exclude_suppressed
|
||||
not cve.is_suppressed
|
||||
}
|
||||
|
||||
should_count(cve) if {
|
||||
config_only_reachable
|
||||
cve.is_reachable == true
|
||||
config_exclude_suppressed
|
||||
not cve.is_suppressed
|
||||
}
|
||||
|
||||
# Classify severity from CVSS score
|
||||
severity(cve) := "critical" if {
|
||||
cve.cvss_score >= 9.0
|
||||
}
|
||||
|
||||
severity(cve) := "high" if {
|
||||
cve.cvss_score >= 7.0
|
||||
cve.cvss_score < 9.0
|
||||
}
|
||||
|
||||
severity(cve) := "medium" if {
|
||||
cve.cvss_score >= 4.0
|
||||
cve.cvss_score < 7.0
|
||||
}
|
||||
|
||||
severity(cve) := "low" if {
|
||||
cve.cvss_score >= 0.1
|
||||
cve.cvss_score < 4.0
|
||||
}
|
||||
|
||||
severity(cve) := "unknown" if {
|
||||
not cve.cvss_score
|
||||
}
|
||||
|
||||
# Count CVEs by severity
|
||||
counts := {
|
||||
"critical": count([c | some c in counted_cves; severity(c) == "critical"]),
|
||||
"high": count([c | some c in counted_cves; severity(c) == "high"]),
|
||||
"medium": count([c | some c in counted_cves; severity(c) == "medium"]),
|
||||
"low": count([c | some c in counted_cves; severity(c) == "low"]),
|
||||
"unknown": count([c | some c in counted_cves; severity(c) == "unknown"]),
|
||||
"total": count(counted_cves),
|
||||
}
|
||||
|
||||
# Get limits with environment override support
|
||||
max_critical := env_config.max_critical if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.max_critical != null
|
||||
} else := input.config.max_critical if {
|
||||
input.config.max_critical != null
|
||||
} else := 0 # Default: no critical allowed
|
||||
|
||||
max_high := env_config.max_high if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.max_high != null
|
||||
} else := input.config.max_high if {
|
||||
input.config.max_high != null
|
||||
} else := 3 # Default: max 3 high
|
||||
|
||||
max_medium := env_config.max_medium if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.max_medium != null
|
||||
} else := input.config.max_medium if {
|
||||
input.config.max_medium != null
|
||||
} else := 20 # Default: max 20 medium
|
||||
|
||||
max_low := env_config.max_low if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.max_low != null
|
||||
} else := input.config.max_low if {
|
||||
input.config.max_low != null
|
||||
} else := null # Default: unlimited
|
||||
|
||||
max_total := env_config.max_total if {
|
||||
env_config := input.config.environments[input.environment]
|
||||
env_config.max_total != null
|
||||
} else := input.config.max_total if {
|
||||
input.config.max_total != null
|
||||
} else := null # Default: unlimited
|
||||
|
||||
# Configuration flags
|
||||
config_only_reachable if {
|
||||
input.config.only_reachable == true
|
||||
}
|
||||
|
||||
config_exclude_suppressed if {
|
||||
input.config.count_suppressed == false
|
||||
}
|
||||
|
||||
config_exclude_suppressed if {
|
||||
not input.config.count_suppressed
|
||||
}
|
||||
|
||||
# Denial messages
|
||||
deny[msg] if {
|
||||
counts.critical > max_critical
|
||||
msg := sprintf("Critical CVE count exceeds limit: %d > %d", [counts.critical, max_critical])
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
counts.high > max_high
|
||||
msg := sprintf("High CVE count exceeds limit: %d > %d", [counts.high, max_high])
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
counts.medium > max_medium
|
||||
msg := sprintf("Medium CVE count exceeds limit: %d > %d", [counts.medium, max_medium])
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
max_low != null
|
||||
counts.low > max_low
|
||||
msg := sprintf("Low CVE count exceeds limit: %d > %d", [counts.low, max_low])
|
||||
}
|
||||
|
||||
deny[msg] if {
|
||||
max_total != null
|
||||
counts.total > max_total
|
||||
msg := sprintf("Total CVE count exceeds limit: %d > %d", [counts.total, max_total])
|
||||
}
|
||||
|
||||
# Summary for reporting
|
||||
summary := {
|
||||
"counts": counts,
|
||||
"limits": {
|
||||
"max_critical": max_critical,
|
||||
"max_high": max_high,
|
||||
"max_medium": max_medium,
|
||||
"max_low": max_low,
|
||||
"max_total": max_total,
|
||||
},
|
||||
"environment": input.environment,
|
||||
}
|
||||
137
examples/policies/opa/release-aggregate_test.rego
Normal file
137
examples/policies/opa/release-aggregate_test.rego
Normal file
@@ -0,0 +1,137 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# release-aggregate_test.rego
|
||||
# Tests for aggregate CVE limits policy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
package stellaops.gates.aggregate
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
# Test allow - within all limits
|
||||
test_allow_within_limits if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 8.0},
|
||||
{"cve_id": "CVE-2024-0002", "cvss_score": 7.5},
|
||||
{"cve_id": "CVE-2024-0003", "cvss_score": 5.0}
|
||||
],
|
||||
"config": {"max_critical": 0, "max_high": 3, "max_medium": 20}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - critical exceeds limit
|
||||
test_deny_critical_exceeds if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5}
|
||||
],
|
||||
"config": {"max_critical": 0}
|
||||
}
|
||||
}
|
||||
|
||||
# Test deny - high exceeds limit
|
||||
test_deny_high_exceeds if {
|
||||
not allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 8.0},
|
||||
{"cve_id": "CVE-2024-0002", "cvss_score": 7.5},
|
||||
{"cve_id": "CVE-2024-0003", "cvss_score": 8.5},
|
||||
{"cve_id": "CVE-2024-0004", "cvss_score": 7.0}
|
||||
],
|
||||
"config": {"max_high": 3}
|
||||
}
|
||||
}
|
||||
|
||||
# Test allow - empty findings
|
||||
test_allow_empty_findings if {
|
||||
allow with input as {
|
||||
"cve_findings": [],
|
||||
"config": {"max_critical": 0, "max_high": 3}
|
||||
}
|
||||
}
|
||||
|
||||
# Test only_reachable filter
|
||||
test_only_reachable_filters if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5, "is_reachable": false}
|
||||
],
|
||||
"config": {"max_critical": 0, "only_reachable": true}
|
||||
}
|
||||
}
|
||||
|
||||
# Test exclude suppressed
|
||||
test_exclude_suppressed if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5, "is_suppressed": true}
|
||||
],
|
||||
"config": {"max_critical": 0, "count_suppressed": false}
|
||||
}
|
||||
}
|
||||
|
||||
# Test environment override
|
||||
test_environment_override if {
|
||||
allow with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5}
|
||||
],
|
||||
"environment": "staging",
|
||||
"config": {
|
||||
"max_critical": 0,
|
||||
"environments": {
|
||||
"staging": {"max_critical": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test severity classification
|
||||
test_severity_classification if {
|
||||
c := counts with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-001", "cvss_score": 9.5},
|
||||
{"cve_id": "CVE-002", "cvss_score": 8.0},
|
||||
{"cve_id": "CVE-003", "cvss_score": 7.0},
|
||||
{"cve_id": "CVE-004", "cvss_score": 5.0},
|
||||
{"cve_id": "CVE-005", "cvss_score": 3.0},
|
||||
{"cve_id": "CVE-006"}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
c.critical == 1
|
||||
c.high == 2
|
||||
c.medium == 1
|
||||
c.low == 1
|
||||
c.unknown == 1
|
||||
c.total == 6
|
||||
}
|
||||
|
||||
# Test denial message content
|
||||
test_deny_message_critical if {
|
||||
msg := deny[_] with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 9.5}
|
||||
],
|
||||
"config": {"max_critical": 0}
|
||||
}
|
||||
contains(msg, "Critical")
|
||||
contains(msg, "1 > 0")
|
||||
}
|
||||
|
||||
# Test summary structure
|
||||
test_summary_structure if {
|
||||
s := summary with input as {
|
||||
"cve_findings": [
|
||||
{"cve_id": "CVE-2024-0001", "cvss_score": 8.0},
|
||||
{"cve_id": "CVE-2024-0002", "cvss_score": 5.0}
|
||||
],
|
||||
"environment": "production",
|
||||
"config": {"max_high": 3, "max_medium": 20}
|
||||
}
|
||||
s.counts.high == 1
|
||||
s.counts.medium == 1
|
||||
s.limits.max_high == 3
|
||||
s.limits.max_medium == 20
|
||||
s.environment == "production"
|
||||
}
|
||||
130
examples/policies/opa/sample-input.json
Normal file
130
examples/policies/opa/sample-input.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"attestation": {
|
||||
"dsse_envelope": {
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoibXlhcHA6djEuMi4zIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImFiYzEyMyJ9fV19",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "stella-release-key-001",
|
||||
"sig": "MEUCIQDcJT8...signature..."
|
||||
}
|
||||
]
|
||||
},
|
||||
"rekor_entry": {
|
||||
"log_index": 12345678,
|
||||
"log_id": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=",
|
||||
"integrated_time": 1705689600,
|
||||
"inclusion_proof": {
|
||||
"root_hash": "abc123def456...",
|
||||
"tree_size": 98765432,
|
||||
"hashes": ["hash1", "hash2", "hash3"]
|
||||
}
|
||||
},
|
||||
"trusted_keys": ["stella-release-key-001", "stella-release-key-002"]
|
||||
},
|
||||
"cve_findings": [
|
||||
{
|
||||
"cve_id": "CVE-2024-1234",
|
||||
"cvss_score": 9.1,
|
||||
"severity": "critical",
|
||||
"epss_score": 0.72,
|
||||
"epss_percentile": 95,
|
||||
"is_kev": false,
|
||||
"is_reachable": true,
|
||||
"reachability_state": "confirmed_reachable",
|
||||
"is_suppressed": false,
|
||||
"package_name": "vulnerable-lib",
|
||||
"package_version": "1.2.3",
|
||||
"fix_available": true,
|
||||
"fixed_version": "1.2.4"
|
||||
},
|
||||
{
|
||||
"cve_id": "CVE-2024-5678",
|
||||
"cvss_score": 7.5,
|
||||
"severity": "high",
|
||||
"epss_score": 0.42,
|
||||
"epss_percentile": 78,
|
||||
"is_kev": false,
|
||||
"is_reachable": false,
|
||||
"reachability_state": "not_reachable",
|
||||
"is_suppressed": false,
|
||||
"package_name": "another-lib",
|
||||
"package_version": "2.0.0",
|
||||
"fix_available": false
|
||||
},
|
||||
{
|
||||
"cve_id": "CVE-2024-9012",
|
||||
"cvss_score": 5.3,
|
||||
"severity": "medium",
|
||||
"epss_score": 0.15,
|
||||
"epss_percentile": 45,
|
||||
"is_kev": false,
|
||||
"is_reachable": true,
|
||||
"reachability_state": "statically_reachable",
|
||||
"is_suppressed": false,
|
||||
"package_name": "common-util",
|
||||
"package_version": "3.1.0"
|
||||
},
|
||||
{
|
||||
"cve_id": "CVE-2023-44487",
|
||||
"cvss_score": 7.5,
|
||||
"severity": "high",
|
||||
"epss_score": 0.89,
|
||||
"epss_percentile": 99,
|
||||
"is_kev": true,
|
||||
"kev_due_date": "2024-02-15",
|
||||
"is_reachable": true,
|
||||
"reachability_state": "runtime_observed",
|
||||
"is_suppressed": true,
|
||||
"package_name": "http2-lib",
|
||||
"package_version": "1.0.0"
|
||||
}
|
||||
],
|
||||
"baseline_cve_findings": [
|
||||
{
|
||||
"cve_id": "CVE-2024-5678",
|
||||
"cvss_score": 7.5
|
||||
},
|
||||
{
|
||||
"cve_id": "CVE-2024-0001",
|
||||
"cvss_score": 6.0
|
||||
}
|
||||
],
|
||||
"environment": "production",
|
||||
"release": {
|
||||
"id": "rel-2024-01-19-001",
|
||||
"version": "1.2.3",
|
||||
"image_digest": "sha256:abc123...",
|
||||
"baseline_digest": "sha256:def456..."
|
||||
},
|
||||
"config": {
|
||||
"epss_threshold": 0.6,
|
||||
"severity_threshold": 7.0,
|
||||
"max_critical": 0,
|
||||
"max_high": 3,
|
||||
"max_medium": 20,
|
||||
"require_rekor": true,
|
||||
"count_suppressed": false,
|
||||
"only_reachable": false,
|
||||
"environments": {
|
||||
"production": {
|
||||
"epss_threshold": 0.3,
|
||||
"severity_threshold": 7.0,
|
||||
"max_critical": 0,
|
||||
"max_high": 0,
|
||||
"only_reachable": true
|
||||
},
|
||||
"staging": {
|
||||
"epss_threshold": 0.7,
|
||||
"max_critical": 1,
|
||||
"max_high": 5
|
||||
},
|
||||
"development": {
|
||||
"epss_threshold": 0.9,
|
||||
"max_critical": null,
|
||||
"max_high": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"current_time": "2024-01-19T12:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user