This commit is contained in:
StellaOps Bot
2026-01-06 21:03:06 +02:00
841 changed files with 15706 additions and 68106 deletions

View File

@@ -20,8 +20,8 @@ The service operates strictly downstream of the **Aggregation-Only Contract (AOC
- Materialise effective findings (`effective_finding_{policyId}`) with append-only history and produce explain traces.
- Emit CVSS v4.0 receipts with canonical hashing and policy replay/backfill rules; store tenant-scoped receipts with RBAC; export receipts deterministically (UTC/fonts/order) and flag v3.1→v4.0 conversions (see Sprint 0190 CVSS-GAPS-190-014 / `docs/modules/policy/cvss-v4.md`).
- Emit per-finding OpenVEX decisions anchored to reachability evidence, forward them to Signer/Attestor for DSSE/Rekor, and publish the resulting artifacts for bench/verification consumers.
- Consume reachability lattice decisions (`ReachDecision`, `docs/reachability/lattice.md`) to drive confidence-based VEX gates (not_affected / under_investigation / affected) and record the policy hash used for each decision.
- Honor **hybrid reachability attestations**: graph-level DSSE is required input; when edge-bundle DSSEs exist, prefer their per-edge provenance for quarantine, dispute, and high-risk decisions. Quarantined edges (revoked in bundles or listed in Unknowns registry) must be excluded before VEX emission. See [`docs/reachability/hybrid-attestation.md`](../../reachability/hybrid-attestation.md) for verification runbooks and offline replay steps.
- Consume reachability lattice decisions (`ReachDecision`, `docs/modules/reach-graph/guides/lattice.md`) to drive confidence-based VEX gates (not_affected / under_investigation / affected) and record the policy hash used for each decision.
- Honor **hybrid reachability attestations**: graph-level DSSE is required input; when edge-bundle DSSEs exist, prefer their per-edge provenance for quarantine, dispute, and high-risk decisions. Quarantined edges (revoked in bundles or listed in Unknowns registry) must be excluded before VEX emission. See [`docs/modules/reach-graph/guides/hybrid-attestation.md`](../reach-graph/guides/hybrid-attestation.md) for verification runbooks and offline replay steps.
- Enforce **shadow + coverage gates** for new/changed policies: shadow runs record findings without enforcement; promotion blocked until shadow and coverage fixtures pass (see lifecycle/runtime docs). CLI/Console enforce attachment of lint/simulate/coverage evidence.
- Operate incrementally: react to change streams (advisory/vex/SBOM deltas) with ≤5min SLA.
- Provide simulations with diff summaries for UI/CLI workflows without modifying state.

View File

@@ -0,0 +1,31 @@
{
"id": "ulid-01J000REACH000000000000000",
"tenant": "urn:tenant:demo",
"subject": "service://demo-api",
"signal_type": "reachability",
"source": "signals",
"evidence": [
{
"kind": "runtime",
"uri": "cas://runtime-facts/123",
"digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
},
{
"kind": "attestation",
"uri": "cas://attestations/abc",
"digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
}
],
"provenance": {
"pipeline": "build:reachability-001",
"inputs": [
"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
],
"signer": "sigkey:runtime",
"transparency": {
"rekor_uuid": "a1b2c3d4",
"skip_reason": null
}
},
"created": "2025-11-19T12:00:00Z"
}

View File

@@ -0,0 +1,26 @@
{
"id": "ulid-01J00000000000000000000000",
"tenant": "urn:tenant:00000000-0000-0000-0000-000000000000",
"subject": "purl:pkg:maven/org.example/app@1.2.3",
"signal_type": "reachability",
"source": "signals",
"confidence": 0.92,
"evidence": [
{
"kind": "linkset",
"uri": "cas://linksets/advisory-ghsa-1234",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"scope": "tenant:default"
}
],
"provenance": {
"pipeline": "git:abcd1234",
"inputs": ["sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
"signer": "sigkey:policy",
"transparency": {
"rekor_uuid": null,
"skip_reason": "offline"
}
},
"created": "2025-11-19T00:00:00Z"
}

View File

@@ -0,0 +1,50 @@
# Policy API Reference (runtime endpoints)
> **Imposed rule:** Policy API calls must include tenant context and operate on frozen inputs; mutating endpoints require Authority scopes and audit events.
## Base
`/api/v1/policies`
## Endpoints
- `GET /policies` list policies (with filters: tenant, status, name, tags); paginated.
- `GET /policies/{id}` fetch metadata and versions.
- `GET /policies/{id}/versions/{v}` fetch IR, hash, status, shadow flag, attestation refs.
- `POST /policies/{id}/simulate` run simulate; body: `{ inputs: { sbom_digest, advisory_snapshot, vex_set, reachability_hash, signals_digest }, settings: { shadow: bool } }`. Returns `runId`, findings, explain summary; full explain via run endpoint.
- `POST /policies/{id}/run` full run with frozen cursors; same body as simulate plus `mode` (`full|incremental`).
- `GET /policy-runs/{runId}` returns findings, explain trace refs, hashes, shadow flag, status.
- `POST /policies/{id}/submit` attach lint/simulate/coverage artefacts; transitions to `submitted`.
- `POST /policies/{id}/approve` requires `policy:approve`; records approval note.
- `POST /policies/{id}/publish` requires `policy:publish`; body includes `reason`, `ticket`, `sign=true|false`; returns attestation ref.
- `POST /policies/{id}/activate` requires `policy:activate`; activates version.
- `POST /policies/{id}/archive` archive version; reason required.
## Headers
- `X-Stella-Tenant` (required)
- `X-Stella-Shadow` (optional; simulate)
- `If-None-Match` (IR cache)
## Auth & scopes
- Read: `policy:read`
- Simulate: `policy:simulate`
- Submit: `policy:author`
- Approve: `policy:approve`
- Publish/Promote: `policy:publish`/`policy:promote`
- Activate/Run: `policy:operate`
## Errors (Problem+JSON)
- `policy_inputs_unfrozen` (409) missing cursors.
- `policy_ir_hash_mismatch` (409) IR hash differs from attested value.
- `policy_shadow_required` (412) shadow gate not satisfied.
- `policy_attestation_required` (412) publish without attestation metadata.
- Standard auth/tenant errors.
## Pagination & determinism
- `limit`/`cursor`; stable ordering by `policyId` then `version`.
- All list endpoints return `ETag` and `Content-SHA256` headers.
## Offline
- API supports `file://` bundle handler when running in sealed mode; simulate/run accept `bundle` path instead of remote cursors.
## Observability
- Metrics: `policy_api_requests_total{endpoint,status}`, `policy_simulate_latency_seconds`, `policy_run_latency_seconds`.
- Logs: include `policyId`, `version`, `runId`, `tenant`, `shadow`, `cursors` hashes.

View File

@@ -0,0 +1,112 @@
# Advisory AI Assistant Parameters
_Primary audience: platform operators & policy authors • Updated: 2025-11-24_
This note centralises the tunable knobs that control Advisory AIs planner, retrieval stack, inference clients, and guardrails. All options live under the `AdvisoryAI` configuration section and can be set via `appsettings.*` files or environment variables using ASP.NET Cores double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.).
**Policy/version pin** — For Sprint 0111, use the policy bundle hash shipped on 2025-11-19 (same drop as `CLI-VULN-29-001` / `CLI-VEX-30-001`). Set `AdvisoryAI:PolicyVersion` or `ADVISORYAI__POLICYVERSION=2025.11.19` in deployments; include the hash in DSSE metadata for Offline Kits.
| Area | Key(s) | Environment variable | Default | Notes |
| --- | --- | --- | --- | --- |
| Inference mode | `AdvisoryAI:Inference:Mode` | `ADVISORYAI__INFERENCE__MODE` | `Local` | `Local` runs the deterministic pipeline only; `Remote` posts sanitized prompts to `Remote.BaseAddress`. |
| Remote base URI | `AdvisoryAI:Inference:Remote:BaseAddress` | `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS` | — | Required when `Mode=Remote`. HTTPS strongly recommended. |
| Remote API key | `AdvisoryAI:Inference:Remote:ApiKey` | `ADVISORYAI__INFERENCE__REMOTE__APIKEY` | — | Injected as `Authorization: Bearer <key>` when present. |
| Remote timeout | `AdvisoryAI:Inference:Remote:TimeoutSeconds` | `ADVISORYAI__INFERENCE__REMOTE__TIMEOUTSECONDS` | `30` | Failing requests fall back to the sanitized prompt with `inference.fallback_reason=remote_timeout`. |
| Guardrail prompt cap | `AdvisoryAI:Guardrails:MaxPromptLength` | `ADVISORYAI__GUARDRAILS__MAXPROMPTLENGTH` | `16000` | Prompts longer than the cap are blocked with `prompt_too_long`. |
| Guardrail citations | `AdvisoryAI:Guardrails:RequireCitations` | `ADVISORYAI__GUARDRAILS__REQUIRECITATIONS` | `true` | When `true`, at least one citation must accompany every prompt. |
| Guardrail phrase seeds | `AdvisoryAI:Guardrails:BlockedPhrases[]`<br>`AdvisoryAI:Guardrails:BlockedPhraseFile` | `ADVISORYAI__GUARDRAILS__BLOCKEDPHRASES__0`<br>`ADVISORYAI__GUARDRAILS__BLOCKEDPHRASEFILE` | See defaults below | File paths are resolved relative to the content root; phrases are merged, de-duped, and lower-cased. |
| Plan cache TTL | `AdvisoryAI:PlanCache:DefaultTimeToLive`* | `ADVISORYAI__PLANCACHE__DEFAULTTIMETOLIVE` | `00:10:00` | Controls how long cached plans are reused. (`CleanupInterval` defaults to `00:05:00`). |
| Queue capacity | `AdvisoryAI:Queue:Capacity` | `ADVISORYAI__QUEUE__CAPACITY` | `1024` | Upper bound on in-memory tasks when using the default queue. |
| Queue wait interval | `AdvisoryAI:Queue:DequeueWaitInterval` | `ADVISORYAI__QUEUE__DEQUEUEWAITINTERVAL` | `00:00:01` | Back-off between queue polls when empty. |
> \* The plan-cache section is bound via `AddOptions<AdvisoryPlanCacheOptions>()`; override by adding an `AdvisoryAI__PlanCache` block to the host configuration.
---
## 1. Inference knobs & “temperature”
Advisory AI supports two inference modes:
- **Local (default)** The orchestrator emits deterministic prompts and the worker returns the sanitized prompt verbatim. This mode is offline-friendly and does **not** call any external LLMs. There is no stochastic “temperature” here—the pipeline is purely rule-based.
- **Remote** Sanitized prompts, citations, and metadata are POSTed to `Remote.BaseAddress + Remote.Endpoint` (default `/v1/inference`). Remote providers control sampling temperature on their side. StellaOps treats remote responses deterministically: we record the providers `modelId`, token usage, and any metadata they return. If your remote tier exposes a temperature knob, set it there; Advisory AI simply forwards the prompt.
### Remote inference quick sample
```json
{
"AdvisoryAI": {
"Inference": {
"Mode": "Remote",
"Remote": {
"BaseAddress": "https://inference.internal",
"Endpoint": "/v1/inference",
"ApiKey": "${ADVISORYAI_REMOTE_KEY}",
"TimeoutSeconds": 45
}
}
}
}
```
## 2. Guardrail configuration
| Setting | Default | Explanation |
| --- | --- | --- |
| `MaxPromptLength` | 16000 chars | Upper bound enforced after redaction. Increase cautiously—remote providers typically cap prompts at 32k tokens. |
| `RequireCitations` | `true` | Forces each prompt to include at least one citation. Disable only when testing synthetic prompts. |
| `BlockedPhrases[]` | `ignore previous instructions`, `disregard earlier instructions`, `you are now the system`, `override the system prompt`, `please jailbreak` | Inline list merged with the optional file. Comparisons are case-insensitive. |
| `BlockedPhraseFile` | — | Points to a newline-delimited list. Relative paths resolve against the content root (`AdvisoryAI.Hosting` sticks to AppContext base). |
Violations surface in the response metadata (`guardrail.violations[*]`) and increment `advisory_ai_guardrail_blocks_total`. Console consumes the same payload for its ribbon state.
## 3. Retrieval & ranking weights (per-task)
Each task type (Summary, Conflict, Remediation) inherits the defaults below. Override any value via `AdvisoryAI:Tasks:<TaskType>:<Property>`.
| Task | `StructuredMaxChunks` | `VectorTopK` | `VectorQueries` (default) | `SbomMaxTimelineEntries` | `SbomMaxDependencyPaths` | `IncludeBlastRadius` |
| --- | --- | --- | --- | --- | --- | --- |
| Summary | 25 | 5 | `Summarize key facts`, `What is impacted?` | 10 | 20 | ✔ |
| Conflict | 30 | 6 | `Highlight conflicting statements`, `Where do sources disagree?` | 8 | 15 | ✖ |
| Remediation | 35 | 6 | `Provide remediation steps`, `Outline mitigations and fixes` | 12 | 25 | ✔ |
These knobs act as weighting levers: lower `VectorTopK` emphasises deterministic evidence; higher values favor breadth. `StructuredMaxChunks` bounds how many CSAF/OSV/VEX chunks reach the prompt, keeping token budgets predictable.
## 4. Token budgets
`AdvisoryTaskBudget` holds `PromptTokens` and `CompletionTokens` per task. Defaults:
| Task | Prompt tokens | Completion tokens |
| --- | --- | --- |
| Summary | 2048 | 512 |
| Conflict | 2048 | 512 |
| Remediation | 2048 | 640 |
Overwrite via `AdvisoryAI:Tasks:Summary:Budget:PromptTokens`, etc. The worker records actual consumption in the response metadata (`inference.prompt_tokens`, `inference.completion_tokens`).
## 5. Cache TTLs & queue directories
- **Plan cache TTLs** In-memory and file-system caches honour `AdvisoryAI:PlanCache:DefaultTimeToLive` (default 10 minutes) and `CleanupInterval` (default 5 minutes). Shorten the TTL to reduce stale plans or increase it to favour offline reuse. Both values accept ISO 8601 or `hh:mm:ss` time spans.
- **Queue & storage paths** `AdvisoryAI:Queue:DirectoryPath`, `AdvisoryAI:Storage:PlanCacheDirectory`, and `AdvisoryAI:Storage:OutputDirectory` default to `data/advisory-ai/{queue,plans,outputs}` under the content root; override these when mounting RWX volumes in sovereign clusters.
- **Output TTLs** Output artefacts inherit the host file-system retention policies. Combine `DefaultTimeToLive` with a cron or systemd timer to prune `outputs/` periodically when operating in remote-inference-heavy environments.
### Example: raised TTL & custom queue path
```json
{
"AdvisoryAI": {
"PlanCache": {
"DefaultTimeToLive": "00:20:00",
"CleanupInterval": "00:05:00"
},
"Queue": {
"DirectoryPath": "/var/lib/advisory-ai/queue"
}
}
}
```
## 6. Operational notes
- Updating **guardrail phrases** triggers only on host reload. When distributing blocked-phrase files via Offline Kits, keep filenames stable and version them through Git so QA can diff changes.
- **Temperature / sampling** remains a remote-provider concern. StellaOps records the providers `modelId` and exposes fallback metadata so policy authors can audit when sanitized prompts were returned instead of model output.
- Always track changes in `docs/implplan/SPRINT_0111_0001_0001_advisoryai.md` (task `DOCS-AIAI-31-006`) when promoting this document so the guild can trace which parameters were added per sprint.

View File

@@ -0,0 +1,40 @@
# POLICY-AUTH-SIGNALS-LIB-115 · Shared P/A/S contracts (draft v0.1)
Purpose: deliver shared models/schemas for Policy, Authority, and Signals so Concelier/Excititor consumers can bind without merge logic.
## Core models (C#-friendly, JSON schema inline)
- `PolicyAuthSignal`:
- `id` (string, required) — stable identifier (ULID preferred)
- `tenant` (string, required)
- `subject` (string, required) — e.g., `purl`, `sbom://`, `service://`
- `signal_type` (string, enum: `reachability`, `attestation`, `risk`, `vex`)
- `source` (string, required) — producer service
- `confidence` (float?, optional)
- `evidence` (array of `EvidenceRef`)
- `created` (string, UTC ISO-8601, required)
- `EvidenceRef`:
- `kind` (string, enum: `linkset`, `runtime`, `attestation`, `bundle`)
- `uri` (string, required) — CAS or storage pointer
- `digest` (string, sha256, required)
- `scope` (string) — tenant/scopes
- `Provenance`:
- `pipeline` (string) — build id
- `inputs` (array<string>) — hashes of inputs
- `signer` (string)
- `transparency` (object: `rekor_uuid` or `skip_reason`)
## JSON schema stub (add-only)
See `schemas/policy-auth-signals-lib-115.json` (to be emitted with the NuGet package).
## Package plan
- Project: `StellaOps.Policy.AuthSignals` (net10.0)
- Deliverables: models, JSON schema, sample fixtures, `PolicyAuthSignalJsonContext` for source generators, deterministic serialization.
- Publish target: `local-nugets/` (version `0.1.0-alpha+draft`), then promote after guild ratification.
## Fixtures (to include in package)
- `fixtures/policy-auth-signal-sample.json`
- `fixtures/policy-auth-signal-reachability.json`
- Schema: `schemas/policy-auth-signals-lib-115.json`
## Status
- NuGet package `StellaOps.Policy.AuthSignals` 0.1.0-alpha built and placed in `local-nugets/` (sha256: `8ab5aa6c0daf5e56e1355d4d6bcaf110a8bc28b28a5ee1970864bcd4b6ba6750`). Awaiting guild ratification to promote beyond alpha.

View File

@@ -0,0 +1,389 @@
# Stella Policy DSL (`stella-dsl@1`)
> **Audience:** Policy authors, reviewers, and tooling engineers building lint/compile flows for the Policy Engine v2 rollout (Sprint20).
> **Imposed rule:** Policies that alter reachability or trust weighting must run in shadow mode first with coverage fixtures; promotion to active is blocked until shadow + coverage gates pass.
This document specifies the `stella-dsl@1` grammar, semantics, and guardrails used by StellaOps to transform SBOM facts, Concelier advisories, and Excititor VEX statements into effective findings. Use it with the [Policy Engine Overview](overview.md) for architectural context and the upcoming lifecycle/run guides for operational workflows.
---
## 1·Design Goals
- **Deterministic:** Same policy + same inputs ⇒ identical findings on every machine.
- **Declarative:** No arbitrary loops, network calls, or clock access.
- **Explainable:** Every decision records the rule, inputs, and rationale in the explain trace.
- **Lean authoring:** Common precedence, severity, and suppression patterns are first-class.
- **Offline-friendly:** Grammar and built-ins avoid cloud dependencies, run the same in sealed deployments.
- **Reachability-aware:** Policies can consume reachability lattice states (`ReachState`) and evidence scores to drive VEX gates (`not_affected`, `under_investigation`, `affected`).
- **Signal-first:** Trust, reachability, entropy, and uncertainty signals are first-class so explain traces stay reproducible.
---
## 2·Document Structure
Policy packs ship one or more `.stella` files. Each file contains exactly one `policy` block:
```dsl
policy "Default Org Policy" syntax "stella-dsl@1" {
metadata {
description = "Baseline severity + VEX precedence"
tags = ["baseline","vex"]
}
profile severity {
map vendor_weight {
source "GHSA" => +0.5
source "OSV" => +0.0
source "VendorX" => -0.2
}
env exposure_adjustments {
if env.runtime == "serverless" then -0.5
if env.exposure == "internal-only" then -1.0
}
}
rule vex_precedence priority 10 {
when vex.any(status in ["not_affected","fixed"])
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
because "Strong vendor justification prevails";
}
rule reachability_gate priority 20 {
when telemetry.reachability.state == "reachable" and telemetry.reachability.score >= 0.6
then status := "affected"
because "Runtime/graph evidence shows reachable code path";
}
rule trust_penalty priority 30 {
when signals.trust_score < 0.4 or signals.entropy_penalty > 0.2
then severity := severity_band("critical")
because "Low trust score or high entropy";
}
}
```
High-level layout:
| Section | Purpose |
|---------|---------|
| `metadata` | Optional descriptive fields surfaced in Console/CLI. |
| `imports` | Reserved for future reuse (not yet implemented in `@1`). |
| `profile` blocks | Declarative scoring modifiers (`severity`, `trust`, `reachability`). |
| `rule` blocks | When/then logic applied to each `(component, advisory, vex[])` tuple. |
| `settings` | Optional evaluation toggles (sampling, default status overrides). |
---
## 3·Lexical Rules
- **Case sensitivity:** Keywords are lowercase; identifiers are case-sensitive.
- **Whitespace:** Space, tab, newline act as separators. Indentation is cosmetic.
- **Comments:** `// inline` and `/* block */` are ignored.
- **Literals:**
- Strings use double quotes (`"text"`); escape with `\"`, `\n`, `\t`.
- Numbers are decimal; suffix `%` allowed for percentage weights (`-2.5%` becomes `-0.025`).
- Booleans: `true`, `false`.
- Lists: `[1, 2, 3]`, `["a","b"]`.
- **Identifiers:** Start with letter or underscore, continue with letters, digits, `_`.
- **Operators:** `=`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not in`, `and`, `or`, `not`, `:=`.
---
## 4·Grammar (EBNF)
```ebnf
policy = "policy", string, "syntax", string, "{", policy-body, "}" ;
policy-body = { metadata | profile | settings | rule | helper } ;
metadata = "metadata", "{", { meta-entry }, "}" ;
meta-entry = identifier, "=", (string | list) ;
profile = "profile", identifier, "{", { profile-item }, "}" ;
profile-item= map | env-map | scalar ;
map = "map", identifier, "{", { "source", string, "=>", number, ";" }, "}" ;
env-map = "env", identifier, "{", { "if", expression, "then", number, ";" }, "}" ;
scalar = identifier, "=", (number | string | list), ";" ;
settings = "settings", "{", { setting-entry }, "}" ;
setting-entry = identifier, "=", (number | string | boolean), ";" ;
rule = "rule", identifier, [ "priority", integer ], "{",
"when", predicate,
{ "and", predicate },
"then", { action },
[ "else", { action } ],
[ "because", string ],
"}" ;
predicate = expression ;
expression = term, { ("and" | "or"), term } ;
term = ["not"], factor ;
factor = comparison | membership | function-call | literal | identifier | "(" expression ")" ;
comparison = value, comparator, value ;
membership = value, ("in" | "not in"), list ;
value = identifier | literal | function-call | field-access ;
field-access= identifier, { ".", identifier | "[" literal "]" } ;
function-call = identifier, "(", [ arg-list ], ")" ;
arg-list = expression, { ",", expression } ;
literal = string | number | boolean | list ;
action = assignment | ignore | escalate | require | warn | defer | annotate ;
assignment = target, ":=", expression, ";" ;
target = identifier, { ".", identifier } ;
ignore = "ignore", [ "until", expression ], [ "because", string ], ";" ;
escalate = "escalate", [ "to", expression ], [ "when", expression ], ";" ;
require = "requireVex", "{", require-fields, "}", ";" ;
warn = "warn", [ "message", string ], ";" ;
defer = "defer", [ "until", expression ], ";" ;
annotate = "annotate", identifier, ":=", expression, ";" ;
```
Notes:
- `helper` is reserved for shared calculcations (not yet implemented in `@1`).
- `else` branch executes only if `when` predicates evaluate truthy **and** no prior rule earlier in priority handled the tuple.
- Semicolons inside rule bodies are optional when each clause is on its own line; the compiler emits canonical semicolons in IR.
- `settings.shadow = true` enables shadow-mode evaluation (findings recorded but not enforced). Promotion gates require at least one shadow run with coverage fixtures.
---
## 5·Evaluation Context
Within predicates and actions you may reference the following namespaces:
| Namespace | Fields | Description |
|-----------|--------|-------------|
| `sbom` | `purl`, `name`, `version`, `licenses`, `layerDigest`, `tags`, `usedByEntrypoint` | Component metadata from Scanner. |
| `advisory` | `id`, `source`, `aliases`, `severity`, `cvss`, `publishedAt`, `modifiedAt`, `content.raw` | Canonical Concelier advisory view. |
| `vex` | `status`, `justification`, `statementId`, `timestamp`, `scope` | Current VEX statement when iterating; aggregator helpers available. |
| `vex.any(...)`, `vex.all(...)`, `vex.count(...)` | Functions operating over all matching statements. |
| `run` | `policyId`, `policyVersion`, `tenant`, `timestamp` | Metadata for explain annotations. |
| `env` | Arbitrary key/value pairs injected per run (e.g., `environment`, `runtime`). |
| `telemetry` | Optional reachability signals. Example fields: `telemetry.reachability.state`, `telemetry.reachability.score`, `telemetry.reachability.policyVersion`. Missing fields evaluate to `unknown`. |
| `signals` | Normalised signal dictionary: `trust_score` (01), `reachability.state` (`reachable|unreachable|unknown|under_investigation`), `reachability.score` (01), `reachability.confidence` (01), `reachability.evidence_ref` (string), `entropy_penalty` (00.3), `uncertainty.level` (`U1``U3`), `runtime_hits` (bool). |
| `secret` | `findings`, `bundle`, helper predicates | Populated when the Secrets Analyzer runs. Exposes masked leak findings and bundle metadata for policy decisions. |
| `profile.<name>` | Values computed inside profile blocks (maps, scalars). |
> **Reachability evidence gate.** When `reachability.state == "unreachable"` but `reachability.evidence_ref` is missing (or confidence is below the high-confidence threshold), Policy Engine downgrades the state to `under_investigation` to avoid false "not affected" claims.
>
> **Secrets namespace.** When `StellaOps.Scanner.Analyzers.Secrets` is enabled the Policy Engine receives masked findings (`secret.findings[*]`) plus bundle metadata (`secret.bundle.id`, `secret.bundle.version`). Policies should rely on the helper predicates listed below rather than reading raw arrays to preserve determinism and future compatibility.
Missing fields evaluate to `null`, which is falsey in boolean context and propagates through comparisons unless explicitly checked.
---
## 6·Built-ins (v1)
| Function / Property | Signature | Description |
|---------------------|-----------|-------------|
| `normalize_cvss(advisory)` | `Advisory → SeverityScalar` | Parses `advisory.content.raw` for CVSS data; falls back to policy maps. |
| `cvss(score, vector)` | `double × string → SeverityScalar` | Constructs a severity object manually. |
| `severity_band(value)` | `string → SeverityBand` | Normalises strings like `"critical"`, `"medium"`. |
| `risk_score(base, modifiers...)` | Variadic | Multiplies numeric modifiers (severity × trust × reachability). |
| `reach_state(state)` | `string → ReachState` | Normalises reachability state strings (`reachable`, `unreachable`, `unknown`, `under_investigation`). |
| `vex.any(predicate)` | `(Statement → bool) → bool` | `true` if any statement satisfies predicate. |
| `vex.all(predicate)` | `(Statement → bool) → bool` | `true` if all statements satisfy predicate. |
| `vex.latest()` | `→ Statement` | Lexicographically newest statement. |
| `advisory.has_tag(tag)` | `string → bool` | Checks advisory metadata tags. |
| `advisory.matches(pattern)` | `string → bool` | Glob match against advisory identifiers. |
| `sbom.has_tag(tag)` | `string → bool` | Uses SBOM inventory tags (usage vs inventory). |
| `sbom.any_component(predicate)` | `(Component → bool) → bool` | Iterates SBOM components, exposing `component` plus language scopes (e.g., `ruby`). |
| `exists(expression)` | `→ bool` | `true` when value is non-null/empty. |
| `coalesce(a, b, ...)` | `→ value` | First non-null argument. |
| `days_between(dateA, dateB)` | `→ int` | Absolute day difference (UTC). |
| `percent_of(part, whole)` | `→ double` | Fractions for scoring adjustments. |
| `lowercase(text)` | `string → string` | Normalises casing deterministically (InvariantCulture). |
| `secret.hasFinding(ruleId?, severity?, confidence?)` | `→ bool` | True if any secret leak finding matches optional filters. |
| `secret.match.count(ruleId?)` | `→ int` | Count of findings, optionally scoped to a rule ID. |
| `secret.bundle.version(required)` | `string → bool` | Ensures the active secret rule bundle version ≥ required (semantic compare). |
| `secret.mask.applied` | `→ bool` | Indicates whether masking succeeded for all surfaced payloads. |
| `secret.path.allowlist(patterns)` | `list<string> → bool` | True when all findings fall within allowed path patterns (useful for waivers). |
All built-ins are pure; if inputs are null the result is null unless otherwise noted.
---
### 6.1·Ruby Component Scope
Inside `sbom.any_component(...)`, Ruby gems surface a `ruby` scope with the following helpers:
| Helper | Signature | Description |
|--------|-----------|-------------|
| `ruby.group(name)` | `string → bool` | Matches Bundler group membership (`development`, `test`, etc.). |
| `ruby.groups()` | `→ set<string>` | Returns all groups for the active component. |
| `ruby.declared_only()` | `→ bool` | `true` when no vendor cache artefacts were observed for the gem. |
| `ruby.source(kind?)` | `string? → bool` | Returns the raw source when called without args, or matches provenance kinds (`registry`, `git`, `path`, `vendor-cache`). |
| `ruby.capability(name)` | `string → bool` | Checks capability flags emitted by the analyzer (`exec`, `net`, `scheduler`, `scheduler.activejob`, etc.). |
| `ruby.capability_any(names)` | `set<string> → bool` | `true` when any capability in the set is present. |
Scheduler capability sub-types use dot notation (`ruby.capability("scheduler.sidekiq")`) and inherit from the broad `scheduler` capability.
---
## 7·Rule Semantics
1. **Ordering:** Rules execute in ascending `priority`. When priorities tie, lexical order defines precedence.
2. **Short-circuit:** Once a rule sets `status`, subsequent rules only execute if they use `combine`. Use this sparingly to avoid ambiguity.
3. **Actions:**
- `status := <string>` Allowed values: `affected`, `not_affected`, `fixed`, `suppressed`, `under_investigation`, `escalated`.
- `severity := <SeverityScalar>` Either from `normalize_cvss`, `cvss`, or numeric map; ensures `normalized` and `score`.
- `ignore until <ISO-8601>` Temporarily treats finding as suppressed until timestamp; recorded in explain trace.
- `warn message "<text>"` Adds warn verdict and deducts `warnPenalty`.
- `escalate to severity_band("critical") when condition` Forces verdict severity upward when condition true.
- `requireVex { vendors = ["VendorX"], justifications = ["component_not_present"] }` Fails evaluation if matching VEX evidence absent.
- `annotate reason := "text"` Adds free-form key/value pairs to explain payload.
4. **Because clause:** Mandatory for actions changing status or severity; captured verbatim in explain traces.
---
## 8·Scoping Helpers
- **Maps:** Use `profile severity { map vendor_weight { ... } }` to declare additive factors. Retrieve with `profile.severity.vendor_weight["GHSA"]`.
- **Environment overrides:** `env` profiles allow conditional adjustments based on runtime metadata.
- **Tenancy:** `run.tenant` ensures policies remain tenant-aware; avoid hardcoding single-tenant IDs.
- **Default values:** Use `settings { default_status = "affected"; }` to override built-in defaults.
---
## 9·Examples
### 9.1 Baseline Severity Normalisation
```dsl
rule advisory_normalization {
when advisory.source in ["GHSA","OSV"]
then severity := normalize_cvss(advisory)
because "Align vendor severity to CVSS baseline";
}
```
### 9.2 VEX Override with Quiet Mode
```dsl
rule vex_strong_claim priority 5 {
when vex.any(status == "not_affected")
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
annotate winning_statement := vex.latest().statementId
warn message "VEX override applied"
because "Strong VEX justification";
}
```
### 9.3 Environment-Specific Escalation
```dsl
rule internet_exposed_guard {
when env.exposure == "internet"
and severity.normalized >= "High"
then escalate to severity_band("Critical")
because "Internet-exposed assets require critical posture";
}
```
### 9.4 Shadow mode & coverage
- Enable `settings { shadow = true; }` for new policies or major changes. Findings are recorded but not enforced.
- Provide coverage fixtures under `tests/policy/<policyId>/cases/*.json`; run `stella policy test` locally and in CI. Coverage results must be attached on submission.
- Promotion to active is blocked until shadow runs + coverage gates pass (see lifecycle §3).
### 9.5 Authoring workflow (quick checklist)
1. Write/update policy with shadow enabled.
2. Add/refresh coverage fixtures; run `stella policy test`.
3. `stella policy lint` and `stella policy simulate --fixtures ...` with expected signals (trust_score, reachability, entropy_penalty) noted in comments.
4. Submit with attachments: lint, simulate diff, coverage results.
5. After approval, disable shadow and promote; retain fixtures for regression tests.
### 9.4 Anti-pattern (flagged by linter)
```dsl
rule catch_all {
when true
then status := "suppressed"
because "Suppress everything" // ❌ Fails lint: unbounded suppression
}
```
---
## 10·Validation & Tooling
- `stella policy lint` ensures:
- Grammar compliance and canonical formatting.
- Static determinism guard (no forbidden namespaces).
- Anti-pattern detection (e.g., unconditional suppression, missing `because`).
- `stella policy compile` emits IR (`.stella.ir.json`) and SHA-256 digest used in `policy_runs`.
- CI pipelines (see `DEVOPS-POLICY-20-001`) compile sample packs and fail on lint violations.
- Simulation harnesses (`stella policy simulate`) highlight provided/queried fields so policy authors affirm assumptions before promotion.
---
## 11·Anti-patterns & Mitigations
| Anti-pattern | Risk | Mitigation |
|--------------|------|------------|
| Catch-all suppress/ignore without scope | Masks all findings | Linter blocks rules with `when true` unless `priority` > 1000 and justification includes remediation plan. |
| Comparing strings with inconsistent casing | Missed matches | Wrap comparisons in `lowercase(value)` to align casing or normalise metadata during ingest. |
| Referencing `telemetry` without fallback | Null propagation | Wrap access in `exists(telemetry.reachability)`. |
| Hardcoding tenant IDs | Breaks multi-tenant | Prefer `env.tenantTag` or metadata-sourced predicates. |
| Duplicated rule names | Explain trace ambiguity | Compiler enforces unique `rule` identifiers within a policy. |
---
## 12 · Uncertainty Gates (U1/U2/U3)
Uncertainty gates enforce evidence-quality thresholds before allowing high-confidence VEX decisions. When entropy is too high or evidence is missing, policies should downgrade to \ rather than risk false negatives.
### 12.1 Gate Types
| Gate | Tier Threshold | Blocks | Allows | Remediation |
|------|---------------|--------|--------|-------------|
| \ | T1 (\) | \ | \, \ | Upload symbols, resolve unknowns |
| \ | T2 (\) | \ (warns) | \ with review flag | Populate lockfiles, fix purl resolution |
| \ | T3 (\) | None (advisory only) | All with caveat | Corroborate advisory, add trusted source |
### 12.2 Uncertainty Gate Rules
### 12.3 Tier-Aware Compound Rules
Combine uncertainty tiers with reachability states for nuanced gating:
### 12.4 Remediation Actions
Policy rules should guide users toward reducing uncertainty:
| Uncertainty State | Remediation Action | Policy Annotation |
|-------------------|-------------------|-------------------|
| \ (MissingSymbolResolution) | Upload debug symbols, run \ | \ |
| \ (MissingPurl) | Generate lockfiles, verify package coordinates | \ |
| \ (UntrustedAdvisory) | Cross-reference trusted sources, wait for corroboration | \ |
| \ (Unknown) | Run initial analysis, enable probes | \ |
### 12.5 YAML Configuration for Gate Thresholds
The Policy Engine reads uncertainty gate thresholds from configuration:
---
## 13 · Versioning & Compatibility
- `syntax "stella-dsl@1"` is mandatory.
- Future revisions (`@2`, …) will be additive; existing packs continue to compile with their declared version.
- The compiler canonicalises documents (sorted keys, normalised whitespace) before hashing to ensure reproducibility.
---
## 14·Compliance Checklist
- [ ] **Grammar validated:** Policy compiles with `stella policy lint` and matches `syntax "stella-dsl@1"`.
- [ ] **Deterministic constructs only:** No use of forbidden namespaces (`DateTime.Now`, `Guid.NewGuid`, external services).
- [ ] **Rationales present:** Every status/severity change includes a `because` clause or `annotate` entry.
- [ ] **Scoped suppressions:** Rules that ignore/suppress findings reference explicit components, vendors, or VEX justifications.
- [ ] **Explain fields verified:** `annotate` keys align with Console/CLI expectations (documented in upcoming lifecycle guide).
- [ ] **Offline parity tested:** Policy pack simulated in sealed mode (`--sealed`) to confirm absence of network dependencies.
---
*Last updated: 2025-12-13 (Sprint 0401).*

View File

@@ -0,0 +1,49 @@
# Policy Editor Guide
> **Imposed rule:** Edits must run lint, simulate, and shadow+coverage gates before promotion; UI enforces attachment of results on submission.
This guide walks through the Console Policy Editor: authoring, validation, simulation, approvals, and offline workflow.
## 1. Workspace
- **Left rail:** policy list, versions, status (draft/submitted/approved/active/archived), shadow flag badge.
- **Editor pane:** YAML/SPL with schema validation, syntax highlighting, auto-format; shows IR hash after successful lint.
- **Metadata panel:** description, tags, AOC indicator, attestation status.
- **Attachments panel:** lint report, simulate diff, coverage results; mandatory before submission.
## 2. Validation
- Live lint via compiler service; blocks save on fatal errors.
- Schema assist: hover shows field descriptions; unknown fields flagged as warnings.
- Determinism check: twin-run diff runs on save; failures block submission.
## 3. Simulation
- Quick simulate: select fixtures (SBOM/VEX bundles) → runs in shadow mode; results shown inline with deltas vs previous version.
- Batch simulate: enqueue via orchestrator; results stored as attachments; required freshness <24h for submission.
## 4. Submission & approvals
- Submit requires: lint OK, simulate attachment, coverage results, shadow enabled.
- Reviewers comment inline; blocking comments must be resolved before approval.
- Approvers must enter reason/ticket; Authority enforces two-person rule when configured.
## 5. Promotion & activation
- Publish & sign: produces DSSE attestation over IR hash + approval metadata; Rekor mirror when online.
- Activate: selects approved version; records input cursors; triggers run if requested.
- Rollback: pick prior approved version; requires reason.
## 6. Offline workflow
- Load policy pack + attachments from Offline Kit; editor runs local lint/simulate with sealed inputs.
- Submit/approve offline records events locally; sync to Authority when reconnected.
## 7. Shortcuts & a11y
- Keyboard: `Ctrl+S` save, `Ctrl+Shift+L` lint, `Ctrl+Shift+R` simulate.
- Screen reader labels on editor, results table, and buttons; focus order follows workflow.
## 8. Troubleshooting
- Lint failures: open Problems tab; fix schema/unknown fields.
- Simulate stale: rerun quick simulate; ensure fixtures match policy inputs.
- Attestation mismatch: regenerate IR (auto) and retry publish; check Authority scopes.
## References
- `docs/modules/policy/guides/dsl.md`
- `docs/modules/policy/guides/spl-v1.md`
- `docs/modules/policy/guides/lifecycle.md`
- `docs/modules/policy/guides/runtime.md`

View File

@@ -0,0 +1,56 @@
# Effective Severity Selection
Last updated: 2025-11-25 (Docs Tasks Md.V)
## Goal
Provide a deterministic, explainable way to pick the *effective* severity for a vulnerability or VEX observation when multiple signals exist (CVSS, KEV, exploit intel, VEX status, asset criticality, policy overrides).
## Inputs
- **Base scores**: CVSS v3/v4 (base + temporal) and ecosystem-native severities (npm/yarn, PyPI, Maven). Missing scores are treated as `null`.
- **Exploitability**: KEV flag, EPSS probability, in-the-wild sightings, vendor exploit flags.
- **VEX status**: `not_affected`, `affected`, `fixed`, `under_investigation`, `unknown` (per OpenVEX/CSAF/CycloneDX-VEX).
- **Context signals**: asset criticality, exposure surface (`internet`, `intranet`, `airgap`), runtime enablement flag, workload type.
- **Policy overrides**: tenant/org rules (allow lists, waiver ids, force-upgrade requirements, SLA class).
## Algorithm (deterministic)
1) **Normalize** all scores to a 010 float with 3-decimal rounding; store provenance for each signal.
2) **VEX gate**
- `not_affected` → effective severity `None` (score 0). Short-circuit unless override `force_evaluate=true`.
- `fixed` → keep score but add mitigation note.
3) **Exploit boost**: if `KEV=true` or `EPSS >= 0.7`, set `exploit_boost = +1.0` (cap at 10). Record reason.
4) **Exposure clamp**
- `airgap` → max score 7.0 unless override `allow_airgap_breakglass`.
- `intranet` → no cap; `internet` → no cap.
5) **Criticality weight**: multiply by asset criticality weight (default 1.0; high=1.2, medium=1.0, low=0.8). Clamp 010.
6) **Policy override**: apply explicit tenant rules (force severity, waive to `None`, or constrain max). Overrides always log the applied rule id.
7) **Bucket** into severity bands (stable mapping):
| Score range (inclusive) | Band |
| --- | --- |
| 9.010.0 | Critical |
| 7.08.9 | High |
| 4.06.9 | Medium |
| 0.13.9 | Low |
| 0 | None |
All arithmetic uses `decimal` and rounds only when persisted or returned (3 decimals) to stay replayable.
## Examples
- CVSS 7.5 + KEV + internet + high criticality → base 7.5 → +1.0 exploit → *before clamp* 8.5 → ×1.2 = 10.2 → clamp 10.0 → **Critical**.
- CVSS 5.0, `not_affected` VEX → short-circuit to **None** (score 0) with rationale `vex:not_affected`.
- No CVSS, EPSS 0.2, exposure `airgap` → default score 0, band **None**; remains deterministic.
## Observability
- Emit `stellaops.policy.effective_severity` histogram (010) with tags `tenant`, `source`, `vex_status`, `kev`, `epss_bucket`, `criticality`, `override_id`.
- Log structured event `severity.selected` containing input signals, applied steps, final score/band.
- Traces: span attribute `severity.score` and `severity.band`; link to upstream ingest span (`traceparent` propagated).
## Determinism & Offline posture
- No live network lookups during evaluation; KEV/EPSS/VEX feeds must be preloaded from frozen bundles.
- Sorting of tied severities: break ties by subject id (lexicographic) then source priority (`vex` > `kev` > `cvss` > `ecosystem`).
- All timestamps are UTC ISO-8601; caches keyed by `(tenant, subject, advisory)`.
## Contract for consumers
- API and CLI MUST return both the raw inputs and the chosen band/score so auditors can replay decisions.
- Downstream UIs should surface the rationale chain (steps 26) and any overrides applied.
- Waivers must reference the override id that changed the severity.

View File

@@ -0,0 +1,152 @@
# Policy Exception Effects
> **Audience:** Policy authors, reviewers, operators, and governance owners.
> **Scope:** How exception definitions are authored, resolved, and surfaced by the Policy Engine during evaluation, including precedence rules, metadata flow, and simulation/diff behaviour.
Exception effects let teams codify governed waivers without compromising determinism. This guide explains the artefacts involved, how the evaluator selects a single winning exception, and where downstream consumers observe the applied override.
---
## 1·Exception Building Blocks
| Artefact | Description |
|----------|-------------|
| **Exception Effect** | Declared inside a policy pack (`exceptions.effects`). Defines the override behaviour plus governance metadata. See effect fields in §2. |
| **Routing Template** | Optional mapping (`exceptions.routingTemplates`) used by Authority to route approvals/MFA. Effects reference templates by id. |
| **Exception Instance** | Stored outside the policy pack (Authority/API). Captures who requested the waiver, scope filters, metadata, and creation time. |
Effects are validated at bind time (`PolicyBinder`), while instances are ingested alongside policy evaluation inputs. Both are normalized to case-insensitive identifiers to avoid duplicate conflicts.
---
## 2·Effect Fields
| Field | Required | Purpose | Notes |
|-------|----------|---------|-------|
| `id` | ✅ | Stable identifier (`[A-Za-z0-9-_]+`). | Must be unique per policy pack. |
| `name` | — | Friendly label for consoles/reports. | Forwarded to verdict metadata if present. |
| `effect` | ✅ | Behaviour enum: `suppress`, `defer`, `downgrade`, `requireControl`. | Case-insensitive. |
| `downgradeSeverity` | ⚠️ | Target severity for `downgrade`. | Must map to DSL severities (`high`, `medium`, etc.). Validation enforced in `PolicyBinder` (`policy.exceptions.effect.downgrade.missingSeverity`). |
| `requiredControlId` | ⚠️ | Control catalogue key for `requireControl`. | Required when effect is `requireControl`. |
| `routingTemplate` | — | Connects to an Authority approval flow. | CLI/Console resolve to `authorityRouteId`. |
| `maxDurationDays` | — | Soft limit for temporary waivers. | Must be > 0 when provided. |
| `description` | — | Rich-text rationale. | Displayed in approvals centre (optional). |
Authoring invalid combinations returns structured errors with JSON paths, preventing packs from compiling (see `src/Policy/__Tests/StellaOps.Policy.Tests/PolicyBinderTests.cs:33`). Routing templates additionally declare `authorityRouteId` and `requireMfa` flags for governance routing.
---
## 3·Exception Instances & Scope
Instances are resolved from Authority or API collections and injected into the evaluation context (`PolicyEvaluationExceptions`). Each instance contains:
| Field | Source | Usage |
|-------|--------|-------|
| `id` | Authority storage | Propagated to annotations and `appliedException.exceptionId`. |
| `effectId` | Links to pack-defined effect | Must resolve to a known effect; otherwise ignored. |
| `scope.ruleNames` | Optional list | Limits to specific rule identifiers. |
| `scope.severities` | Optional list (`severity.normalized`) | Normalized against the evaluators severity string. |
| `scope.sources` | Optional advisory sources (`GHSA`, `NVD`, …) | Compared against the advisory context. |
| `scope.tags` | Optional SBOM tags | Matched using `sbom.has_tag(...)`. |
| `createdAt` | RFC3339 UTC timestamp | Used as tie-breaker when specificity scores match. |
| `metadata` | Arbitrary key/value bag | Copied to verdict annotations (`exception.meta.*`). |
Scopes are case-insensitive and trimmed. Empty scopes behave as global waivers but still require routing and metadata supplied by Authority workflows.
---
## 4·Resolution & Specificity
Only one exception effect is applied per finding. Evaluation proceeds as follows:
1. Filter instances whose `effectId` resolves to a known effect.
2. Discard instances whose scope does not match the candidate finding (rule name, severity, advisory source, SBOM tags).
3. Score remaining instances for **specificity**:
- `ruleNames``1000 + (count × 25)`
- `severities``500 + (count × 10)`
- `sources``250 + (count × 10)`
- `tags``100 + (count × 5)`
4. Highest score wins. Ties fall back to the newest `createdAt`, then lexical `id` (stable sorting).
These rules guarantee deterministic selection even when multiple waivers overlap. See `src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:209` for tie-break coverage.
---
## 5·Effect Behaviours
| Effect | Status impact | Severity impact | Warnings / metadata |
|--------|---------------|-----------------|---------------------|
| `suppress` | Forces status `suppressed`. | No change. | `exception.status=suppressed`. |
| `defer` | Forces status `deferred`. | No change. | `exception.status=deferred`. |
| `downgrade` | No change. | Sets severity to configured `downgradeSeverity`. | `exception.severity` annotation. |
| `requireControl` | No change. | No change. | Adds warning `Exception '<id>' requires control '<requiredControlId>'`. Annotation `exception.requiredControl`. |
All effects stamp shared annotations: `exception.id`, `exception.effectId`, `exception.effectType`, optional `exception.effectName`, optional `exception.routingTemplate`, plus `exception.maxDurationDays`. Instance metadata is surfaced both in annotations (`exception.meta.<key>`) and the structured `AppliedException.Metadata` payload for downstream APIs. Behaviour is validated by unit tests (`src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:130` & `src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:169`).
---
## 6·Explain, Simulation & Outputs
- **Explain traces / CLI simulate** Verdict payloads include `appliedException` capturing original vs applied status/severity, enabling diff visualisation in Console and CLI previews.
- **Annotations** Deterministic keys make it trivial for exports or alerting pipelines to flag waived findings.
- **Warnings** `requireControl` adds runtime warnings so operators can enforce completion of compensating controls.
- **Routing** When `routingTemplate` is populated, verdict metadata includes `routingTemplate`, allowing UI surfaces to deep-link into the approvals centre.
Example verdict excerpt (JSON):
```json
{
"status": "suppressed",
"severity": "Critical",
"annotations": {
"exception.id": "exc-001",
"exception.effectId": "suppress-critical",
"exception.effectType": "Suppress",
"exception.status": "suppressed",
"exception.meta.requestedBy": "alice"
},
"appliedException": {
"exceptionId": "exc-001",
"effectId": "suppress-critical",
"effectType": "Suppress",
"originalStatus": "blocked",
"appliedStatus": "suppressed",
"metadata": {
"effectName": "Rule Critical Suppress",
"requestedBy": "alice"
}
}
}
```
---
## 7·Operational Notes
- **Authoring** Policy packs must ship effect definitions before Authority can issue instances. CLI validation (`stella policy lint`) fails if required fields are missing.
- **Approvals & MFA** Effects referencing routing templates inherit `requireMfa` rules from `exceptions.routingTemplates`. When a template requires MFA, Authority will refuse to mint tokens containing `exceptions:approve` unless the authenticating identity provider exposes MFA capability; the failure is logged as `authority.password.grant` with `reason="Exception approval scope requires an MFA-capable identity provider."` Review `/docs/security/authority-scopes.md` for scope/role assignments and `/docs/AUTHORITY.md` for configuration samples.
- **Presence in exports** Even when an exception suppresses a finding, explain traces and effective findings retain the applied exception metadata for audit parity.
- **Determinism** Specificity scoring plus tie-breakers ensure repeatable outcomes across runs, supporting sealed/offline replay.
---
## 8·Testing References
- `src/Policy/__Tests/StellaOps.Policy.Tests/PolicyBinderTests.cs:33` Validates schema rules for defining effects, routing templates, and downgrade guardrails.
- `src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:130` Covers suppression, downgrade, and metadata propagation.
- `src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:209` Confirms specificity ordering and metadata forwarding for competing exceptions.
---
## 9·Compliance Checklist
- [ ] **Effect catalogue maintained:** Each policy pack documents available effects and routing templates for auditors.
- [ ] **Authority alignment:** Approval routes in Authority mirror `routingTemplate` definitions and enforce MFA where required.
- [ ] **Explain coverage:** Console/CLI surfaces display `appliedException` details and `exception.*` annotations for every waived verdict.
- [ ] **Simulation parity:** `stella policy simulate` outputs include exception metadata, ensuring PR/CI reviews catch unintended waivers.
- [ ] **Audit retention:** Effective findings history retains `appliedException` payloads so exception lifecycle reviews remain replayable.
- [ ] **Tests locked:** Binder and evaluator tests covering exception paths remain green before publishing documentation updates.
---
*Last updated: 2025-10-27 (Sprint 25).*

View File

@@ -0,0 +1,96 @@
# Policy Engine FAQ
Answers to questions that Support, Ops, and Policy Guild teams receive most frequently. Pair this FAQ with the [Policy Lifecycle](../policy/lifecycle.md), [Runs](../policy/runs.md), and [CLI guide](../modules/cli/guides/policy.md) for deeper explanations.
---
## Authoring & DSL
**Q:** *Lint succeeds locally, but submit still fails with `ERR_POL_001`. Why?*
**A:** The CLI requires lint & compile artefacts newer than 24hours. Re-run `stella policy lint` and `stella policy compile` before submitting; ensure you upload the latest diff files with `--attach`.
**Q:** *How do I layer tenant-specific overrides on top of the baseline policy?*
**A:** Keep the baseline in `tenant-global`. For tenant overrides, create a policy referencing the baseline via CLI (`stella policy new --from baseline@<version>`), then adjust rules. Activation is per tenant.
**Q:** *Can I import YAML/Rego policies from earlier releases?*
**A:** No direct import. Use the migration script (`stella policy migrate legacy.yaml`) which outputs `stella-dsl@1` skeletons. Review manually before submission.
---
## Simulation & Determinism
**Q:** *Simulation shows huge differences even though I only tweaked metadata. What did I miss?*
**A:** Check if your simulation used the same SBOM set/env as previous runs. CLI default uses golden fixtures; UI can store custom presets. Large diffs may also indicate Concelier updates; compare advisory cursors in the Simulation tab.
**Q:** *How do we guard against non-deterministic behaviour?*
**A:** CI runs `policy simulate` twice with identical inputs and compares outputs (`DEVOPS-POLICY-20-003`). Any difference fails the pipeline. Locally you can use `stella policy run replay` to verify determinism.
**Q:** *What happens if the determinism guard (`ERR_POL_004`) triggers?*
**A:** Policy Engine halts the run, raises `policy.run.failed` with code `ERR_POL_004`, and switches to incident mode (100% sampling). Review recent code changes; often caused by new helpers that call `DateTime.Now` or non-allowlisted HTTP clients.
---
## VEX & Suppressions
**Q:** *A vendor marked a CVE `not_affected` but the policy still blocks. Why?*
**A:** Check the required justifications. Baseline policy only accepts `component_not_present` and `vulnerable_code_not_present`. Other statuses need explicit rules. Use `stella findings explain` to see which VEX statement was considered.
**Q:** *Can we quiet a finding indefinitely?*
**A:** Avoid indefinite quiets. Policy DSL requires an `until` timestamp. If the use case is permanent, move the rule into baseline logic with strong justification and documentation.
**Q:** *How do we detect overuse of suppressions?*
**A:** Observability exports `policy_suppressions_total` and CLI `stella policy stats`. Review weekly; Support flags tenants whose suppressions grow faster than remediation tickets.
---
## Runs & Operations
**Q:** *Incremental runs are backlogged. What should we check first?*
**A:** Inspect `policy_run_queue_depth` and `policy_delta_backlog_age_seconds` dashboards. If queue depth high, scale worker replicas or investigate upstream change storms (Concelier/Excititor). Use `stella policy run list --status failed` for recent errors.
**Q:** *Full runs take longer than 30 min. Is that a breach?*
**A:** Goal is ≤ 30 min, but large tenants may exceed temporarily. Ensure PostgreSQL indexes are current and that worker nodes meet sizing (4 vCPU). Consider sharding runs by SBOM group.
**Q:** *How do I replay a run for audit evidence?*
**A:** `stella policy run replay <runId> --output replay.tgz` produces a sealed bundle. Upload to evidence locker or attach to incident tickets.
---
## Approvals & Governance
**Q:** *Can authors approve their own policies?*
**A:** No. Authority denies approval if `approved_by == submitted_by`. Assign at least two reviewers (one security, one product).
**Q:** *What scopes do bots need for CI pipelines?*
**A:** Typically `policy:read`, `policy:simulate`, `policy:runs`. Only grant `policy:run` if the pipeline should trigger runs. Never give CI tokens `policy:approve`.
**Q:** *How do we manage policies in air-gapped deployments?*
**A:** Use `stella policy bundle export --sealed` on a connected site, transfer via approved media, then `stella policy bundle import` inside the enclave. Enable `--sealed` flag in CLI/UI to block accidental outbound calls.
---
## Troubleshooting
**Q:** *API calls return `403` despite valid token.*
**A:** Verify scope includes the specific operation (`policy:activate` vs `policy:run`). Check tenant header matches token tenant. Inspect Authority logs for denial reason (`policy_scope_denied_total` metric).
**Q:** *`stella policy run` exits with code `30`.*
**A:** Network/transport error. Check connectivity to Policy Engine endpoint, TLS configuration, and CLI proxy settings.
**Q:** *Explain drawer shows no VEX data.*
**A:** Either no VEX statement matched or the tenant lacks `findings:read` scope. If VEX should exist, confirm Excititor ingestion and policy joiners (see Observability dashboards).
---
## Compliance Checklist
- [ ] FAQ linked from Console help menu and CLI `stella policy help`.
- [ ] Entries reviewed quarterly by Policy & Support Guilds.
- [ ] Answers cross-reference lifecycle, runs, observability, and governance docs.
- [ ] Incident/Escalation contact details kept current in Support playbooks.
- [ ] FAQ translated for supported locales (if applicable).
---
*Last updated: 2025-10-26 (Sprint 20).*

View File

@@ -0,0 +1,138 @@
# Policy Gateway
> **Delivery scope:** `StellaOps.Policy.Gateway` minimal API service fronting Policy Engine pack CRUD + activation endpoints for UI/CLI clients. Sender-constrained with DPoP and tenant headers, suitable for online and Offline Kit deployments.
## 1 · Responsibilities
- Proxy policy pack CRUD and activation requests to Policy Engine while enforcing scope policies (`policy:read`, `policy:author`, `policy:review`, `policy:operate`, `policy:activate`).
- Normalise responses (DTO + `ProblemDetails`) so Console, CLI, and automation receive consistent payloads.
- Guard activation actions with structured logging and metrics so approvals are auditable.
- Support dual auth modes:
- Forwarded caller tokens (Console/CLI) with DPoP proofs + `X-Stella-Tenant` header.
- Gateway client credentials (DPoP) for service automation or Offline Kit flows when no caller token is present.
## 2 · Endpoints
| Route | Method | Description | Required scope(s) |
|-------|--------|-------------|-------------------|
| `/api/policy/packs` | `GET` | List policy packs and revisions for the active tenant. | `policy:read` |
| `/api/policy/packs` | `POST` | Create a policy pack shell or upsert display metadata. | `policy:author` |
| `/api/policy/packs/{packId}/revisions` | `POST` | Create or update a policy revision (draft/approved). | `policy:author` |
| `/api/policy/packs/{packId}/revisions/{version}:activate` | `POST` | Activate a revision, enforcing single/two-person approvals. | `policy:operate`, `policy:activate` |
### Response shapes
- Successful responses return camel-case DTOs matching `PolicyPackDto`, `PolicyRevisionDto`, or `PolicyRevisionActivationDto` as described in the Policy Engine API doc (`/docs/api/policy.md`).
- Errors always return RFC 7807 `ProblemDetails` with deterministic fields (`title`, `detail`, `status`). Missing caller credentials now surface `401` with `"Upstream authorization missing"` detail.
### Dual-control activation
- **Config-driven.** Set `PolicyEngine.activation.forceTwoPersonApproval=true` when every activation must collect two distinct `policy:activate` approvals. When false, operators can opt into dual-control per revision (`requiresTwoPersonApproval: true`).
- **Defaults.** `PolicyEngine.activation.defaultRequiresTwoPersonApproval` feeds the default when callers omit the checkbox/flag.
- **Statuses.** First approval on a dual-control revision returns `202 pending_second_approval`; duplicate actors get `400 duplicate_approval`; the second distinct approver receives the usual `200 activated`.
- **Audit trail.** With `PolicyEngine.activation.emitAuditLogs` on, Policy Engine emits structured `policy.activation.*` scopes (pack id, revision, tenant, approver IDs, comments) so the gateway metrics/ELK dashboards can show who approved what.
#### Activation configuration wiring
- **Helm ConfigMap.** `deploy/helm/stellaops/values*.yaml` now include a `policy-engine-activation` ConfigMap. The chart automatically injects it via `envFrom` into both the Policy Engine and Policy Gateway pods, so overriding the ConfigMap data updates the services with no manifest edits.
- **Type safety.** Quote ConfigMap values (e.g., `"true"`, `"false"`) because Kubernetes ConfigMaps carry string data. This mirrors the defaults checked into the repo and keeps `helm template` deterministic.
- **File-based overrides (optional).** The Policy Engine host already probes `/config/policy-engine/activation.yaml`, `../etc/policy-engine.activation.yaml`, and ambient `policy-engine.activation.yaml` files beside the binary. Mounting the ConfigMap as a file at `/config/policy-engine/activation.yaml` works immediately if/when we add a volume.
- **Offline/Compose.** Compose/offline bundles can continue exporting `STELLAOPS_POLICY_ENGINE__ACTIVATION__*` variables directly; the ConfigMap wiring simply mirrors those keys for Kubernetes clusters.
## 3 · Authentication & headers
| Header | Source | Notes |
|--------|--------|-------|
| `Authorization` | Forwarded caller token *or* gateway client credentials. | Caller tokens must include tenant scope; gateway tokens default to `DPoP` scheme. |
| `DPoP` | Caller or gateway. | Required when Authority mandates proof-of-possession (default). Generated per request; gateway keeps ES256/ES384 key material under `etc/policy-gateway-dpop.pem`. |
| `X-Stella-Tenant` | Caller | Tenant isolation header. Forwarded unchanged; gateway automation omits it. |
Gateway client credentials are configured in `policy-gateway.yaml`:
```yaml
policyEngine:
baseAddress: "https://policy-engine.internal"
audience: "api://policy-engine"
clientCredentials:
enabled: true
clientId: "policy-gateway"
clientSecret: "<secret>"
scopes:
- policy:read
- policy:author
- policy:review
- policy:operate
- policy:activate
dpop:
enabled: true
keyPath: "../etc/policy-gateway-dpop.pem"
algorithm: "ES256"
```
> 🔐 **DPoP key** store the private key alongside Offline Kit secrets; rotate it whenever the gateway identity or Authority configuration changes.
## 4 · Metrics & logging
All activation calls emit:
- `policy_gateway_activation_requests_total{outcome,source}` counter labelled with `outcome` (`activated`, `pending_second_approval`, `already_active`, `bad_request`, `not_found`, `unauthorized`, `forbidden`, `error`) and `source` (`caller`, `service`).
- `policy_gateway_activation_latency_ms{outcome,source}` histogram measuring proxy latency.
Structured logs (category `StellaOps.Policy.Gateway.Activation`) include `PackId`, `Version`, `Outcome`, `Source`, and upstream status code for audit trails.
## 5 · Sample `curl` workflows
Assuming you already obtained a DPoP-bound access token (`$TOKEN`) for tenant `acme`:
```bash
# Generate a DPoP proof for GET via the CLI helper
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs \
--htm GET \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme"
# Draft a new revision
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions \
--htm POST \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \
-H "Content-Type: application/json" \
-d '{"version":5,"requiresTwoPersonApproval":true,"initialStatus":"Draft"}'
# Activate revision 5 (returns 202 when awaiting the second approver)
DPoP_PROOF=$(stella auth dpop proof \
--htu https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
--htm POST \
--token "$TOKEN")
curl -sS https://gateway.example.com/api/policy/packs/policy.core/revisions/5:activate \
-H "Authorization: DPoP $TOKEN" \
-H "DPoP: $DPoP_PROOF" \
-H "X-Stella-Tenant: acme" \
-H "Content-Type: application/json" \
-d '{"comment":"Rollout baseline"}'
```
For air-gapped environments, bundle `policy-gateway.yaml` and the DPoP key in the Offline Kit (see `/docs/OFFLINE_KIT.md` §5.7).
> **DPoP proof helper:** Use `stella auth dpop proof` to mint sender-constrained proofs locally. The command accepts `--htu`, `--htm`, and `--token` arguments and emits a ready-to-use header value. Teams maintaining alternate tooling (for example, `scripts/make-dpop.sh`) can substitute it as long as the inputs and output match the CLI behaviour.
## 6 · Offline Kit guidance
- Include `policy-gateway.yaml.sample` and the resolved runtime config in the Offline Kits `config/` tree.
- Place the DPoP private key under `secrets/policy-gateway-dpop.pem` with restricted permissions; document rotation steps in the manifest.
- When building verification scripts, use the gateway endpoints above instead of hitting Policy Engine directly. The Offline Kit validator now expects `policy_gateway_activation_requests_total` metrics in the Prometheus snapshot.
## 7 · Change log
- **2025-10-27 Sprint 18.5**: Initial gateway bootstrap + activation metrics + DPoP client credentials.

View File

@@ -0,0 +1,51 @@
# Policy Governance
> **Imposed rule:** Publish/Promote actions require reason + ticket metadata and DSSE attestation; two-person approval is recommended and enforced where configured by Authority.
This guide defines roles, scopes, approvals, signing, and exception handling for Stella policies.
## 1. Roles & scopes
- Author: `policy:author`, `policy:simulate`
- Reviewer: `policy:review`, `policy:simulate`
- Approver: `policy:approve`, `policy:audit`
- Operator: `policy:operate`, `policy:activate`, `policy:run`
- Publisher: `policy:publish`, `policy:promote`
- Auditor: `policy:audit`
Authority policy can map org roles to scopes; two-person rule can be enabled per tenant for publish/promote.
## 2. Approval workflow
1) Author drafts with shadow + coverage fixtures; runs lint/simulate/test.
2) Submit with attachments (lint, simulate, coverage, reason/ticket optional at this stage).
3) Reviewers comment/resolve; approver checks gates (shadow, coverage, determinism).
4) Publisher runs `stella policy publish --reason --ticket --sign`; attestation stored and optionally mirrored to Rekor.
5) Operator activates version; audit events recorded.
## 3. Signing & attestation
- DSSE payload includes IR hash, policyId/version, reason, ticket, approvals, shadow/coverage evidence refs.
- Rekor mirror when online; offline deployments store bundle + checkpoint for later replay.
- Evidence Locker stores DSSE + run inputs/outputs for audit.
## 4. Exceptions & waivers
- Use SPL rules with explicit scope and `because` rationale; no perpetual suppressions.
- Waivers must include expiration and owner; DSSE attested if exported.
- AOC: Aggregation-Only Contract requires waiver scope to avoid cross-tenant data; UI/CLI enforce tenant scoping.
## 5. Compliance checklist
- [ ] Two-person rule enforced (Authority config) for publish/promote.
- [ ] Reason and ticket captured on publish; stored in attestation metadata.
- [ ] Shadow + coverage gates passed and attached.
- [ ] IR hash recorded; attestation verified before activation.
- [ ] Waivers have expiry, owner, `because`, and scope.
- [ ] Offline replay path documented for the policy pack.
## 6. Audit & observability
- Timeline events: `policy.submitted`, `policy.approved`, `policy.published`, `policy.promoted`, `policy.activated`, `policy.archived`.
- Metrics: `policy_publish_total`, `policy_promote_total`, `policy_attestation_verify_failures`, `policy_shadow_runs_total`.
- Logs: include `policyId`, `version`, `attestation_ref`, `reason`, `ticket`, `shadow`.
## References
- `docs/modules/policy/guides/overview.md`
- `docs/modules/policy/guides/lifecycle.md`
- `docs/modules/policy/guides/spl-v1.md`
- `docs/modules/policy/guides/runtime.md`

View File

@@ -0,0 +1,299 @@
# Policy Lifecycle & Approvals
> **Audience:** Policy authors, reviewers, security approvers, release engineers.
> **Scope:** End-to-end flow for `stella-dsl@1` policies from draft through archival, including CLI/Console touch-points, Authority scopes, audit artefacts, and offline considerations.
This guide explains how a policy progresses through StellaOps, which roles are involved, and the artefacts produced at every step. Pair it with the [Policy Engine Overview](overview.md), [DSL reference](dsl.md), and upcoming run documentation to ensure consistent authoring and rollout.
> **Imposed rule:** New or significantly changed policies must run in **shadow mode** with coverage fixtures before activation. Promotions are blocked until shadow + coverage gates pass.
---
## 1·Protocol Summary
- Policies are **immutable versions** attached to a stable `policy_id`.
- Lifecycle states: `draft → submitted → approved → active → archived`.
- Every transition requires explicit Authority scopes and produces structured events + storage artefacts (`policies`, `policy_runs`, audit log collections).
- Simulation and CI gating happen **before** approvals can be granted.
- Activation triggers (runs, bundle exports, CLI `promote`) operate on the **latest approved** version per tenant.
- Shadow mode runs capture findings without enforcement; shadow exit requires coverage + twin-run determinism checks.
```mermaid
stateDiagram-v2
[*] --> Draft
Draft --> Draft: edit/save (policy:author)
Draft --> Submitted: submit(reviewers) (policy:author)
Submitted --> Draft: requestChanges (policy:review)
Submitted --> Approved: approve (policy:approve)
Approved --> Active: activate/run (policy:operate)
Active --> Archived: archive (policy:operate)
Approved --> Archived: superseded/explicit archive
Archived --> [*]
```
---
## 2·Roles & Authority Scopes
| Role (suggested) | Required scopes | Responsibilities |
|------------------|-----------------|------------------|
| **Policy Author** | `policy:author`, `policy:simulate`, `findings:read` | Draft DSL, run local/CI simulations, submit for review. |
| **Policy Reviewer** | `policy:review`, `policy:simulate`, `findings:read` | Comment on submissions, demand additional simulations, request changes. |
| **Policy Approver** | `policy:approve`, `policy:audit`, `findings:read` | Grant final approval, ensure sign-off evidence captured. |
| **Policy Operator** | `policy:operate`, `policy:run`, `policy:activate`, `findings:read` | Trigger full/incremental runs, monitor results, roll back to previous version. |
| **Policy Auditor** | `policy:audit`, `findings:read` | Review past versions, verify attestations, respond to compliance requests. |
| **Policy Engine Service** | `effective:write`, `findings:read` | Materialise effective findings during runs; no approval capabilities. |
> Scopes are issued by Authority (`AUTH-POLICY-20-001`). Tenants may map organisational roles (e.g., `secops.approver`) to these scopes via issuer policy.
---
## 3·Lifecycle Stages in Detail
### 3.1 Draft
- **Who:** Authors (`policy:author`).
- **Tools:** Console editor, `stella policy edit`, policy DSL files.
- **Actions:**
- Author DSL leveraging [stella-dsl@1](dsl.md).
- Run `stella policy lint` and `stella policy simulate --sbom <fixtures>` locally.
- Add/refresh coverage fixtures under `tests/policy/<policyId>/cases/*.json`; run `stella policy test`.
- Keep `settings.shadow = true` until coverage + shadow gates pass.
- Attach rationale metadata (`metadata.description`, tags).
- **Artefacts:**
- `policies` document with `status=draft`, `version=n`, `provenance.created_by`.
- Local IR cache (`.stella.ir.json`) generated by CLI compile.
- **Guards:**
- Draft versions never run in production.
- CI must lint drafts before allowing submission PRs (see `DEVOPS-POLICY-20-001`).
### 3.2 Submission
- **Who:** Authors (`policy:author`).
- **Tools:** Console “Submit for review” button, `stella policy submit <policyId> --reviewers ...`.
- **Actions:**
- Provide review notes and required simulations (CLI uploads attachments).
- Attach coverage results (shadow mode + `stella policy test`).
- Choose reviewer groups; Authority records them in submission metadata.
- **Artefacts:**
- Policy document transitions to `status=submitted`, capturing `submitted_by`, `submitted_at`, reviewer list, simulation digest references.
- Audit event `policy.submitted` (Authority timeline / Notifier integration).
- **Guards:**
- Submission blocked unless latest lint + compile succeed (<24h freshness).
- Must reference at least one simulation artefact (CLI enforces via `--attach`).
### 3.3 Review (Submitted)
- **Who:** Reviewers (`policy:review`), optionally authors responding.
- **Tools:** Console review pane (line comments, overall verdict), `stella policy review`.
- **Actions:**
- Inspect DSL diff vs previous approved version.
- Run additional `simulate` jobs (UI button or CLI).
- Request changes policy returns to `draft` with comment log.
- **Artefacts:**
- Comments stored in `policy_reviews` collection with timestamps, resolved flag.
- Additional simulation run records appended to submission metadata.
- **Guards:**
- Approval cannot proceed until all blocking comments resolved.
- Required reviewers (Authority rule) must vote before approver sees Approve button.
### 3.4 Approval
- **Who:** Approvers (`policy:approve`).
- **Tools:** Console Approve”, CLI `stella policy approve <id> --version n --note "rationale"`.
- **Actions:**
- Confirm compliance checks (see §6) all green.
- Verify shadow gate + coverage suite passed in CI.
- Provide approval note (mandatory string captured in audit trail).
- **Artefacts:**
- Policy `status=approved`, `approved_by`, `approved_at`, `approval_note`.
- Audit event `policy.approved` plus optional Notifier broadcast.
- Immutable approval record stored in `policy_history`.
- **Guards:**
- Approver cannot be same identity as author (enforced by Authority config).
- Approver must attest to successful simulation diff review (`--attach diff.json`).
### 3.5 Signing & Publication
- **Who:** Operators with fresh-auth (`policy:publish`, `policy:promote`) and approval backing.
- **Tools:** Console Publish & Sign wizard, CLI `stella policy publish`, `stella policy promote`.
- **Actions:**
- Execute `stella policy publish <id> --version n --reason "<why>" --ticket SEC-123 --sign` to produce a DSSE attestation capturing IR digest + approval metadata.
- Provide required metadata headers (`policy_reason`, `policy_ticket`, `policy_digest`), enforced by Authority; CLI flags map to headers automatically.
- Promote the signed version to targeted environments (`stella policy promote <id> --version n --environment stage`).
- **Artefacts:**
- DSSE payload stored in `policy_attestations`, containing SHA-256 digest, signer, reason, ticket, promoted environment.
- Audit events `policy.published`, `policy.promoted` including metadata snapshot and attestation reference.
- **Guards:**
- Publish requires a fresh-auth window (<5 minutes) and interactive identity (client-credentials tokens are rejected).
- Metadata headers must be present; missing values return `policy_attestation_metadata_missing`.
- Signing key rotation enforced via Authority JWKS; CLI refuses to publish if attestation verification fails.
### 3.6 Activation & Runs
- **Who:** Operators (`policy:operate`, `policy:run`, `policy:activate`).
- **Tools:** Console Promote to active”, CLI `stella policy activate <id> --version n`, `stella policy run`.
- **Actions:**
- Mark approved version as tenants active policy.
- Trigger full run or rely on orchestrator for incremental runs.
- Monitor results via Console dashboards or CLI run logs.
- **Artefacts:**
- `policy_runs` entries with `mode=full|incremental`, `policy_version=n`.
- Effective findings collections updated; explain traces stored.
- Activation event `policy.activated` with `runId`.
- **Guards:**
- Activation blocked if previous full run <24h old failed or is pending.
- Selection of SBOM/advisory snapshots uses consistent cursors recorded for reproducibility.
### 3.7 Archival / Rollback
- **Who:** Approvers or Operators with `policy:archive`.
- **Tools:** Console menu, CLI `stella policy archive <id> --version n --reason`.
- **Actions:**
- Retire policies superseded by newer versions or revert to older approved version (`stella policy activate <id> --version n-1`).
- Export archived version for audit bundles (Offline Kit integration).
- **Artefacts:**
- Policy `status=archived`, `archived_by`, `archived_at`, reason.
- Audit event `policy.archived`.
- Exported DSSE-signed policy pack stored if requested.
- **Guards:**
- Archival cannot proceed while runs using that version are in-flight.
- Rollback requires documented incident reference.
---
## 4·Tooling Touchpoints
| Stage | Console | CLI | API |
|-------|---------|-----|-----|
| Draft | Inline linting, simulation panel | `stella policy lint`, `edit`, `test`, `simulate` | `POST /policies`, `PUT /policies/{id}/versions/{v}` |
| Submit | Submit modal (attach simulations) | `stella policy submit` | `POST /policies/{id}/submit` |
| Review | Comment threads, diff viewer | `stella policy review --approve/--request-changes` | `POST /policies/{id}/reviews` |
| Approve | Approve dialog | `stella policy approve` | `POST /policies/{id}/approve` |
| Activate | Promote button, run scheduler | `stella policy activate`, `run`, `simulate` | `POST /policies/{id}/run`, `POST /policies/{id}/activate` |
| Archive | Archive / rollback menu | `stella policy archive` | `POST /policies/{id}/archive` |
All CLI commands emit structured JSON by default; use `--format table` for human review.
### 4.1 · CLI Command Reference
#### `stella policy edit <file>`
Open a policy DSL file in your configured editor (`$EDITOR` or `$VISUAL`), validate after editing, and optionally commit with SemVer metadata.
**Options:**
- `-c, --commit` - Commit changes after successful validation
- `-V, --version <semver>` - SemVer version for commit metadata (e.g., `1.2.0`)
- `-m, --message <msg>` - Custom commit message (auto-generated if not provided)
- `--no-validate` - Skip validation after editing (not recommended)
**Example:**
```bash
# Edit and commit with version metadata
stella policy edit policies/my-policy.dsl --commit --version 1.2.0
```
#### `stella policy test <file>`
Run coverage test fixtures against a policy DSL file to validate rule behavior.
**Options:**
- `-d, --fixtures <dir>` - Path to fixtures directory (defaults to `tests/policy/<policy-name>/cases`)
- `--filter <pattern>` - Run only fixtures matching this pattern
- `-f, --format <fmt>` - Output format: `table` (default) or `json`
- `-o, --output <file>` - Write test results to a file
- `--fail-fast` - Stop on first test failure
**Example:**
```bash
stella policy test policies/vuln-policy.dsl --filter critical
```
---
## 5·Audit & Observability
- **Storage:**
- `policies` retains all versions with provenance metadata.
- `policy_reviews` stores reviewer comments, timestamps, attachments.
- `policy_history` summarises transitions (state, actor, note, diff digest).
- `policy_runs` retains input cursors and determinism hash per run.
- **Events:**
- `policy.submitted`, `policy.review.requested`, `policy.approved`, `policy.activated`, `policy.archived`, `policy.rollback`.
- Routed to Notifier + Timeline Indexer; offline deployments log to local event store.
- **Logs & metrics:**
- Policy Engine logs include `policyId`, `policyVersion`, `runId`, `approvalNote`.
- Observability dashboards (see forthcoming `/docs/modules/telemetry/guides/policy.md`) highlight pending approvals, run SLA, VEX overrides.
- **Reproducibility:**
- Each state transition stores IR checksum and simulation diff digests, enabling offline audit replay.
---
## 6 · Compliance Gates
| Gate | Stage | Enforced by | Requirement |
|------|-------|-------------|-------------|
| **DSL lint** | Draft Submit | CLI/CI | `stella policy lint` successful within 24h. |
| **Simulation evidence** | Submit | CLI/Console | Attach diff from `stella policy simulate` covering baseline SBOM set. |
| **Shadow run** | Submit Approve | Policy Engine / CI | Shadow mode enabled (`settings.shadow=true`) with findings recorded; must execute once per change. |
| **Coverage suite** | Submit Approve | CI (`stella policy test`) | Coverage fixtures present and passing; artefact attached to submission. |
| **Reviewer quorum** | Submit Approve | Authority | Minimum approver/reviewer count configurable per tenant. |
| **Determinism CI** | Approve | DevOps job | Twin run diff passes (`DEVOPS-POLICY-20-003`). |
| **Attestation metadata** | Approve Publish | Authority / CLI | `policy:publish` executed with reason & ticket metadata; DSSE attestation verified. |
| **Activation health** | Publish/Promote Activate | Policy Engine | Last run status succeeded; orchestrator queue healthy. |
| **Export validation** | Archive | Offline Kit | DSSE-signed policy pack generated for long-term retention. |
Failure of any gate emits a `policy.lifecycle.violation` event and blocks transition until resolved.
---
## 7 · Offline / Air-Gap Considerations
- Offline Kit bundles include:
- Approved policy packs (`.policy.bundle` + DSSE signatures).
- Submission/approval audit logs.
- Simulation diff JSON for reproducibility.
- Air-gapped sites operate with the same lifecycle:
- Approvals happen locally; Authority runs in enclave.
- Rollout requires manual import of policy packs from connected environment via signed bundles.
- `stella policy simulate --sealed` ensures no outbound calls; required before approval in sealed mode.
---
## 8 · Incident Response & Rollback
- Incident mode (triggered via `policy incident activate`) forces:
- Immediate incremental run to evaluate mitigation policies.
- Expanded trace retention for affected runs.
- Automatic snapshot of currently active policies for evidence locker.
- Rollback path:
1. `stella policy activate <id> --version <previous>` with incident note.
2. Orchestrator schedules full run to ensure findings align.
3. Archive problematic version with reason referencing incident ticket.
- Post-incident review must confirm new version passes gates before re-activation.
---
## 9 · CI/CD Integration (Reference)
- **Pre-merge:** run lint + simulation jobs against golden SBOM fixtures.
- **Post-merge (main):** compile, compute IR checksum, stage for Offline Kit.
- **Nightly:** determinism replay, `policy simulate` diff drift alerts, backlog of pending approvals.
- **Notifications:** Slack/Email via Notifier when submissions await review > SLA or approvals succeed.
---
## 10 · Compliance Checklist
- [ ] **Role mapping validated:** Authority issuer config maps organisational roles to required `policy:*` scopes (per tenant).
- [ ] **Submission evidence attached:** Latest simulation diff and lint artefacts linked to submission.
- [ ] **Reviewer quorum met:** All required reviewers approved or acknowledged; no unresolved blocking comments.
- [ ] **Approval note logged:** Approver justification recorded in audit trail alongside IR checksum.
- [ ] **Publish attestation signed:** `stella policy publish` executed by interactive operator, metadata (`policy_reason`, `policy_ticket`, `policy_digest`) present, DSSE attestation stored.
- [ ] **Promotion recorded:** Target environment promoted via CLI/Console with audit event linking to attestation.
- [ ] **Activation guard passed:** Latest run status success, orchestrator queue healthy, determinism job green.
- [ ] **Archive bundles produced:** When archiving, DSSE-signed policy pack exported and stored for offline retention.
- [ ] **Offline parity proven:** For sealed deployments, `--sealed` simulations executed and logged before approval.
---
*Last updated: 2025-11-27 (Sprint 401).*

View File

@@ -0,0 +1,54 @@
# Policy System Overview
> **Imposed rule:** Policies that change reachability or trust weighting must enter shadow mode first and ship coverage fixtures; promotion is blocked until shadow + coverage gates pass (see `docs/modules/policy/guides/lifecycle.md`).
This overview orients authors, reviewers, and operators to the Stella Policy system: the SPL language, lifecycle, evidence inputs, and how policies are enforced online and in air-gapped sites.
## 1. What the Policy System Does
- Combines SBOM facts, advisories (Concelier), VEX claims (Excititor), reachability signals (Graphs + runtime), trust/entropy signals, and operator metadata to produce deterministic findings.
- Produces explainable outputs: every verdict carries rule, rationale (`because`), inputs, and evidence hashes.
- Works online or offline: policies, inputs, and outputs are content-addressed and can be replayed with no network.
## 2. Layers
- **SPL (Stella Policy Language):** declarative rules (`stella-dsl@1`) with profiles, maps, and rule blocks; no loops or network calls.
- **Compiler:** canonicalises SPL, emits IR + hash; used by CLI, Console, and CI. Canonical hashes feed attestation and replay.
- **Engine:** evaluates IR against SBOM/VEX/reachability signals; outputs effective findings and explains every rule fire.
- **Attestation:** optional DSSE over policy IR and approval metadata; Rekor mirror when online.
- **Distribution:** policy packs are versioned, tenant-scoped, and promoted via Authority scopes; Offline Kit includes packs + attestations.
## 3. Inputs & Signals
- SBOM inventory/usage (Scanner), advisories (Concelier), VEX (Excititor), reachability graphs/runtime (Signals), trust/entropy/uncertainty scores, secret-leak findings, environment metadata, and tenant policy defaults.
- Signals dictionary (normalised): `trust_score`, `reachability.state/score`, `entropy_penalty`, `uncertainty.level`, `runtime_hits`.
- All inputs must be content-addressed; missing fields evaluate to `unknown`/null and must be handled explicitly.
## 4. Lifecycle (summary)
1. Draft in SPL with shadow mode on and coverage fixtures (`stella policy test`).
2. Submit with lint/simulate + coverage artefacts attached.
3. Review/approve with Authority scopes; determinism and shadow gates enforced in CI.
4. Publish/attest (DSSE + optional Rekor); promote to environments; activate runs.
5. Archive or roll back with audit trail preserved.
## 5. Governance & Roles
- Scopes: `policy:author`, `policy:review`, `policy:approve`, `policy:operate`, `policy:publish`, `policy:activate`, `policy:audit`.
- Two-person rule recommended for publish/promote; enforced by Authority per tenant.
- AOC: Aggregation-Only Contract applies to regulated tenants—UI/CLI must respect AOC flags on policies and evidence.
## 6. Review Checklist (fast path)
- Lint + simulate outputs attached and fresh (<24h).
- Shadow mode enabled; coverage fixtures passing; twin-run determinism check green.
- `because` present on every status/severity change; suppressions scoped.
- Inputs handled explicitly when `unknown` (reachability/runtime missing).
- Attestation metadata ready (reason, ticket, IR hash) if publish is requested.
- AOC impact noted; air-gap replay steps documented if applicable.
## 7. Air-gap / Offline Notes
- Policy packs, attestations, and coverage fixtures ship in Offline Kits; no live feed calls allowed during evaluation.
- CLI `stella policy simulate --sealed` enforces no-network; policy runs must use frozen SBOM/advisory/VEX bundles and reachability graphs.
- Attestations and hashes recorded in Evidence Locker; Timeline events emitted on publish/activate.
## 8. Key References
- `docs/modules/policy/guides/dsl.md` (language)
- `docs/modules/policy/guides/lifecycle.md` (process, gates)
- `docs/modules/policy/guides/architecture.md` (engine internals)
- `docs/modules/policy/implementation_plan.md`
- `docs/modules/policy/guides/governance.md` (once published)

View File

@@ -0,0 +1,187 @@
# Policy Runs & Orchestration
> **Audience:** Policy Engine operators, Scheduler team, DevOps, and tooling engineers planning CI integrations.
> **Scope:** Run modes (`full`, `incremental`, `simulate`), orchestration pipeline, cursor management, replay/determinism guarantees, monitoring, and recovery procedures.
Policies only generate value when they execute deterministically against current SBOM, advisory, and VEX inputs. This guide explains how runs are triggered, how the orchestrator scopes work, and what artefacts you should expect at each stage.
---
## 1·Run Modes at a Glance
| Mode | Trigger sources | Scope | Persistence | Primary use |
|------|-----------------|-------|-------------|-------------|
| **Full** | Manual CLI (`stella policy run`), Console “Run now”, scheduled nightly job | Entire tenant (all registered SBOMs) | Writes `effective_finding_{policyId}` and `policy_runs` record | Baseline after policy approval, quarterly attestation, post-incident rechecks |
| **Incremental** | Change streams (Concelier advisories, Excititor VEX, SBOM imports), orchestrator cron | Only affected `(sbom, advisory)` tuples | Writes diffs to effective findings and run record | Continuous upkeep meeting ≤5min SLA from input change |
| **Simulate** | Console review workspace, CLI (`stella policy simulate`), CI pipeline | Selected SBOM sample set (provided or golden set) | No materialisation; captures diff summary + explain traces | Authoring validation, regression safeguards, sealed-mode rehearsals |
All modes record their status in `policy_runs` with deterministic metadata:
```json
{
"_id": "run:P-7:2025-10-26T14:05:11Z:3f9a",
"policy_id": "P-7",
"policy_version": 4,
"mode": "incremental",
"status": "succeeded", // queued | running | succeeded | failed | canceled | replay_pending
"inputs": {
"sbom_set": ["sbom:S-42","sbom:S-318"],
"advisory_cursor": "2025-10-26T13:59:00Z",
"vex_cursor": "2025-10-26T13:58:30Z",
"env": {"exposure":"internet"}
},
"stats": {
"components": 1742,
"rules_fired": 68023,
"findings_written": 4321,
"vex_overrides": 210
},
"determinism_hash": "sha256:…",
"started_at": "2025-10-26T14:05:11Z",
"finished_at": "2025-10-26T14:06:01Z",
"tenant": "default"
}
```
> **Schemas & samples:** see `src/Scheduler/__Libraries/StellaOps.Scheduler.Models/docs/SCHED-MODELS-20-001-POLICY-RUNS.md` and the fixtures in `samples/api/scheduler/policy-*.json` (including `policy-simulation-status.json`) for canonical payloads consumed by CLI/UI/worker integrations. Cloned simulations append `metadata.retry-of=<sourceRunId>` so operators can trace retries without losing provenance.
---
## 2·Pipeline Overview
```mermaid
sequenceDiagram
autonumber
participant Trigger as Trigger (CLI / Console / Change Stream)
participant Orchestrator as Policy Orchestrator
participant Queue as Scheduler Queue (PostgreSQL/NATS)
participant Engine as Policy Engine Workers
participant Concelier as Concelier Service
participant Excititor as Excititor Service
participant SBOM as SBOM Service
participant Store as PostgreSQL (policy_runs & effective_finding_*)
participant Observability as Metrics/Events
Trigger->>Orchestrator: Run request (mode, scope, env)
Orchestrator->>Queue: Enqueue PolicyRunRequest (idempotent key)
Queue->>Engine: Lease job (fairness window)
Engine->>Concelier: Fetch advisories + linksets (cursor-aware)
Engine->>Excititor: Fetch VEX statements (cursor-aware)
Engine->>SBOM: Fetch SBOM segments / BOM-Index
Engine->>Engine: Evaluate policy (deterministic batches)
Engine->>Store: Upsert effective findings + append history
Engine->>Store: Persist policy_runs record + determinism hash
Engine->>Observability: Emit metrics, traces, rule-hit logs
Engine->>Orchestrator: Ack completion / failure
Orchestrator->>Trigger: Notify (webhook, CLI, Console update)
```
- **Trigger** CLI, Console, or automated change stream publishes a `PolicyRunRequest`.
- **Orchestrator** Runs inside `StellaOps.Policy.Engine` worker host; applies fairness (tenant + policy quotas) and idempotency using run keys.
- **Queue** Backed by PostgreSQL + optional NATS for fan-out; supports leases and replay on crash.
- **Engine** Stateless worker executing the deterministic evaluator.
- **Store** PostgreSQL tables: `policy_runs`, `effective_finding_{policyId}`, `policy_run_events` (append-only history), optional object storage for explain traces.
- **Observability** Prometheus metrics (`policy_run_seconds`, `policy_simulation_queue_depth`, `policy_simulation_latency_seconds`), OTLP traces, structured logs.
---
## 3·Input Scoping & Cursors
### 3.1 Advisory & VEX Cursors
- Each run records the latest Concelier change stream timestamp (`advisory_cursor`) and Excititor timestamp (`vex_cursor`).
- Incremental runs receive change batches `(feedId, lastOffset)`; orchestrator deduplicates using `change_digest`.
- Full runs set cursors to “current read time”, effectively resetting incremental baseline.
### 3.2 SBOM Selection
- Full runs enumerate all SBOM records declared active for the tenant.
- Incremental runs derive SBOM set by intersecting advisory/VEX changes with BOM-Index lookups (component → SBOM mapping).
- Simulations accept explicit SBOM list; if omitted, CLI uses `etc/policy/golden-sboms.json`.
### 3.3 Environment Metadata
- `env` block (free-form key/values) allows scenario-specific evaluation (e.g., `env.exposure=internet`).
- Stored verbatim in `policy_runs.inputs.env` for replay; orchestrator hashes environment data to avoid cache collisions.
---
## 4·Execution Semantics
1. **Preparation:** Worker loads compiled IR for target policy version (cached by digest).
2. **Batching:** Candidate tuples are grouped by SBOM, then by advisory to maintain deterministic order; page size defaults to 1024 tuples.
3. **Evaluation:** Rules execute with first-match semantics; results captured as `PolicyVerdict`.
4. **Materialisation:**
- Upserts into `effective_finding_{policyId}` using `{policyId, sbomId, findingKey}`.
- Previous versions stored in `effective_finding_{policyId}_history`.
5. **Explain storage:** Full explain trees stored in blob store when `captureExplain=true`; incremental runs keep sampled traces (configurable).
6. **Completion:** Worker writes final status, stats, determinism hash (combination of policy digest + ordered input digests), and emits `policy.run.completed` event.
---
## 5·Retry, Replay & Determinism
- **Retries:** Failures (network, validation) mark run `status=failed` and enqueue retry with exponential backoff capped at 3 attempts. Manual re-run via CLI resets counters.
- **Replay:**
- Use `policy_runs` record to assemble input snapshot (policy version, cursors, env).
- Fetch associated SBOM/advisory/VEX data via `stella policy replay --run <id>` which rehydrates data into a sealed bundle.
- Determinism hash mismatches between replay and recorded run indicate drift; CI job `DEVOPS-POLICY-20-003` compares successive runs to guard this.
- **Cancellation:** Manual `stella policy run cancel <runId>` or orchestrator TTL triggers `status=canceled`; partial changes roll back via history append (no destructive delete).
---
## 6·Trigger Sources & Scheduling
| Source | Description | SLAs |
|--------|-------------|------|
| **Nightly full run** | Default schedule per tenant; ensures baseline alignment. | Finish before 07:00 UTC |
| **Change stream** | Concelier (`advisory_raw`), Excititor (`vex_raw`), SBOM imports emit `policy.trigger.delta` events. | Start within 60s; complete within 5min |
| **Manual CLI/Console** | Operators run ad-hoc evaluations. | No SLA; warns if warm path > target |
| **CI** | `stella policy simulate` runs in pipelines referencing golden SBOMs. | Must complete under 10min to avoid pipeline timeout |
The orchestrator enforces max concurrency per tenant (`maxActiveRuns`), queue depth alarms, and fairness (round-robin per policy).
---
## 7·Monitoring & Alerts
- **Metrics:** `policy_run_seconds`, `policy_run_queue_depth`, `policy_run_failures_total`, `policy_run_incremental_backlog`, `policy_rules_fired_total`.
- **Dashboards:** Highlight pending approvals, incremental backlog age, top failing policies, VEX override ratios (tie-in with `/docs/modules/telemetry/guides/policy.md` once published).
- **Alerts:**
- Incremental backlog > 3 cycles.
- Determinism hash mismatch.
- Failure rate > 5% over rolling hour.
- Run duration > SLA (full > 30min, incremental > 5min).
---
## 8·Failure Handling & Rollback
- **Soft failures:** Worker retries; after final failure, orchestrator emits `policy.run.failed` with diagnostics and recommended actions (e.g., missing SBOM segment).
- **Hard failures:** Schema mismatch, determinism guard violation (`ERR_POL_004`) blocks further runs until resolved.
- **Rollback:** Operators can activate previous policy version (see [Lifecycle guide](lifecycle.md)) and schedule full run to restore prior state.
---
## 9·Offline / Sealed Mode
- Change streams originate from offline bundle imports; orchestrator processes delta manifests.
- Runs execute with `sealed=true`, blocking any external lookups; `policy_runs.inputs.env.sealed` set for auditing.
- Explain traces annotate cached data usage to prompt bundle refresh.
- Offline Kit exports include latest `policy_runs` snapshot and determinism hashes for evidence lockers.
---
## 10·Compliance Checklist
- [ ] **Run schemas validated:** `PolicyRunRequest` / `PolicyRunStatus` DTOs from Scheduler Models (`SCHED-MODELS-20-001`) serialise deterministically; schema samples up to date.
- [ ] **Cursor integrity:** Incremental runs persist advisory & VEX cursors; replay verifies identical input digests.
- [ ] **Queue fairness configured:** Tenant-level concurrency limits and lease timeouts applied; no starvation of lower-volume policies.
- [ ] **Determinism guard active:** CI replay job (`DEVOPS-POLICY-20-003`) green; determinism hash recorded on each run.
- [ ] **Observability wired:** Metrics exported, alerts configured, and run events flowing to Notifier/Timeline.
- [ ] **Offline tested:** `stella policy run --sealed` executed in air-gapped environment; explain traces flag cached evidence usage.
- [ ] **Recovery plan rehearsed:** Failure and rollback drill documented; incident checklist aligned with Lifecycle guide.
---
*Last updated: 2025-10-26 (Sprint 20).*

View File

@@ -0,0 +1,65 @@
# Policy Runtime & Evaluation
> **Imposed rule:** Runtime evaluations must use frozen inputs (SBOM, advisories, VEX, reachability, signals) and emit explain traces plus DSSE/attestation metadata; no live feed calls during evaluation.
This document describes how SPL policies are compiled, cached, and executed, and how results are surfaced via APIs, CLI, UI, and observability.
## 1. Components
- **Compiler**: converts SPL (`stella-dsl@1`) into canonical IR JSON, hashes it, and validates lint/coverage. Produces IR cache used by Engine.
- **Engine**: deterministic evaluator that consumes IR + inputs (SBOM, advisory, VEX, signals) and emits findings + explain traces.
- **Caches**:
- IR cache keyed by `policyId`/`version`/IR hash.
- Input cursors (SBOM/advisory/VEX snapshots, reachability graphs) to guarantee replay.
- Explain trace cache for recently queried runs (TTL, tenant-scoped).
- **Attestation**: optional DSSE over IR hash + approval metadata; Rekor mirror when online; stored alongside run outputs in Evidence Locker.
## 2. Execution flow
1. Resolve active policy version for tenant (or specified version for simulate).
2. Load IR from cache; verify hash matches attested value if provided.
3. Fetch frozen inputs via cursors: SBOM digest, advisory snapshot id, VEX set, reachability graph hash, signals bundle.
4. Evaluate rules in priority order; record explain entries (rule, because, inputs, signals).
5. Persist findings, explain traces, and run metadata (`runId`, `policyVersion`, hashes) to storage.
6. Emit events: `policy.run.started`, `policy.run.completed`, `policy.run.failed`; optionally `policy.run.shadow` when settings.shadow=true.
## 3. Caching & determinism
- IR cache warmed at publish; invalidated on new policy version.
- Input cursors are mandatory; if missing, run is blocked (returns `inputs_unfrozen`).
- Explain trace storage keeps deterministic ordering; capped by tenant quotas.
- Shadow mode runs record findings but mark `enforced=false`; promotion blocked until shadow+coverage gates pass.
## 4. APIs & CLI
- API: `POST /policies/{id}/simulate`, `POST /policies/{id}/run`, `GET /policy-runs/{runId}` (findings + explain), `GET /policies/{id}/versions/{v}` (IR, hash, attestation refs).
- CLI: `stella policy simulate`, `stella policy run`, `stella policy explain <runId> --format json|table`, `stella policy export --run <runId> --offline`.
- Headers: `X-Stella-Tenant`, `X-Stella-Shadow` (optional), `If-None-Match` for IR cache revalidation.
## 5. Observability & SLOs
- Metrics: `policy_runs_total{status}`, `policy_run_duration_seconds`, `policy_explain_cache_hits`, `policy_inputs_unfrozen_total`, `policy_shadow_runs_total`.
- Logs include `policyId`, `version`, `runId`, `tenant`, `shadow`, `input_cursor` hashes.
- Traces: span per run with events for rule evaluation batches; attributes include counts of rules fired and unknowns encountered.
- SLOs (suggested):
- p95 policy run latency < 2s for simulate, < 10s for full run.
- Error budget: <0.5% failed runs per rolling 7d.
- Explain cache hit rate >80% for repeated queries.
## 6. Failure modes & handling
- **Inputs unfrozen**: return 409 with required cursors; emit `policy.inputs_unfrozen` event.
- **Hash mismatch**: IR hash differs from attested; block run and emit `policy.ir_hash_mismatch` alert.
- **Unknown signals**: if required signals missing, downgrade to `unknown` and optionally set `status=under_investigation`; flag in explain trace.
- **Exceeded quotas**: explain storage or run count caps → 429 with `Retry-After`; run not executed.
## 7. Offline / air-gap
- All inputs fetched from Offline Kit bundles; no network during evaluate.
- CLI `stella policy run --sealed --bundle <path>` loads IR, inputs, and signals from bundle; writes outputs + attestation-ready manifest.
- Runs produce DSSE-ready payloads (`policy.run@1`) that can be signed later when connectivity is restored.
## 8. Data model (high level)
- `policy_runs`: `runId`, `policyId`, `version`, `tenant`, `shadow`, `input_cursors`, `ir_hash`, `attestation_ref`, `started_at`, `completed_at`, `status`, `stats` (rules fired, explains, unknowns), `storage_refs` (findings, explains).
- `policy_findings`: flattened findings with references to explain entries.
- `policy_explains`: rule-level explain traces with inputs, signals, because text.
## 9. References
- `docs/modules/policy/guides/dsl.md`
- `docs/modules/policy/guides/lifecycle.md`
- `docs/modules/policy/guides/architecture.md`
- `docs/modules/policy/guides/overview.md`
- `docs/modules/reach-graph/guides/DELIVERY_GUIDE.md`

View File

@@ -0,0 +1,291 @@
# Score Policy YAML Format
**Sprint:** SPRINT_3402_0001_0001
**Status:** Complete
## Overview
StellaOps uses a YAML-based configuration for deterministic vulnerability scoring. The score policy defines how different factors contribute to the final vulnerability score, ensuring reproducible and auditable results.
## Schema Version
Current version: `score.v1`
## File Location
By default, score policies are loaded from:
- `etc/score-policy.yaml` (production)
- `etc/score-policy.yaml.sample` (reference template)
Override via environment variable: `STELLAOPS_SCORE_POLICY_PATH`
## Basic Structure
```yaml
# Required fields
policyVersion: score.v1
policyId: unique-policy-identifier
# Optional metadata
policyName: "My Organization's Scoring Policy"
description: "Custom scoring weights for our security posture"
# Weight distribution (must sum to 10000 basis points = 100%)
weightsBps:
baseSeverity: 2500 # 25% - CVSS base score contribution
reachability: 2500 # 25% - Code reachability analysis
evidence: 2500 # 25% - KEV, EPSS, exploit evidence
provenance: 2500 # 25% - Supply chain trust signals
```
## Weight Configuration
Weights are specified in **basis points (bps)** where 10000 bps = 100%. This avoids floating-point precision issues and ensures weights always sum to exactly 100%.
### Example: Reachability-Heavy Profile
```yaml
policyVersion: score.v1
policyId: reachability-focused
weightsBps:
baseSeverity: 2000 # 20%
reachability: 4000 # 40% - Heavy emphasis on reachability
evidence: 2000 # 20%
provenance: 2000 # 20%
```
### Example: Evidence-Heavy Profile
```yaml
policyVersion: score.v1
policyId: evidence-focused
weightsBps:
baseSeverity: 2000 # 20%
reachability: 2000 # 20%
evidence: 4000 # 40% - Heavy emphasis on KEV/EPSS
provenance: 2000 # 20%
```
## Reachability Configuration
Fine-tune how reachability analysis affects scores:
```yaml
reachabilityConfig:
reachableMultiplier: 1.5 # Boost for reachable code paths
unreachableMultiplier: 0.3 # Reduction for unreachable code
unknownMultiplier: 1.0 # Default when analysis unavailable
```
### Multiplier Bounds
- Minimum: 0.0
- Maximum: 2.0 (configurable)
- Default for unknown: 1.0 (no adjustment)
## Evidence Configuration
Configure how exploit evidence affects scoring:
```yaml
evidenceConfig:
kevWeight: 1.5 # Boost for KEV-listed vulnerabilities
epssThreshold: 0.5 # EPSS score threshold for high-risk
epssWeight: 1.2 # Weight multiplier for high EPSS
```
### KEV Integration
Known Exploited Vulnerabilities (KEV) from CISA are automatically boosted:
- `kevWeight: 1.5` means 50% score increase for KEV-listed CVEs
- Setting `kevWeight: 1.0` disables KEV boost
### EPSS Integration
Exploit Prediction Scoring System (EPSS) provides probability-based risk:
- `epssThreshold`: Minimum EPSS for applying the weight
- `epssWeight`: Multiplier applied when EPSS exceeds threshold
## Provenance Configuration
Configure how supply chain trust signals affect scoring:
```yaml
provenanceConfig:
signedBonus: 0.1 # 10% reduction for signed artifacts
rekorVerifiedBonus: 0.2 # 20% reduction for Rekor-verified
unsignedPenalty: -0.1 # 10% increase for unsigned artifacts
```
### Trust Signals
| Signal | Effect | Use Case |
|--------|--------|----------|
| `signedBonus` | Score reduction | Artifact has valid signature |
| `rekorVerifiedBonus` | Score reduction | Signature in transparency log |
| `unsignedPenalty` | Score increase | No signature present |
## Score Overrides
Override scoring for specific CVEs or patterns:
```yaml
overrides:
# Exact CVE match
- id: log4shell-critical
match:
cvePattern: "CVE-2021-44228"
action:
setScore: 10.0
reason: "Known critical RCE in production"
# Pattern match
- id: log4j-family
match:
cvePattern: "CVE-2021-442.*"
action:
multiplyScore: 1.2
reason: "Log4j family vulnerabilities"
# Severity-based
- id: low-severity-suppress
match:
severityEquals: "LOW"
action:
multiplyScore: 0.5
reason: "Reduce noise from low-severity findings"
# Combined conditions
- id: unreachable-medium
match:
severityEquals: "MEDIUM"
reachabilityEquals: "UNREACHABLE"
action:
multiplyScore: 0.3
reason: "Medium + unreachable = low priority"
```
### Override Actions
| Action | Description | Example |
|--------|-------------|---------|
| `setScore` | Force specific score | `setScore: 10.0` |
| `multiplyScore` | Apply multiplier | `multiplyScore: 0.5` |
| `addScore` | Add/subtract value | `addScore: -2.0` |
### Match Conditions
| Condition | Description | Example |
|-----------|-------------|---------|
| `cvePattern` | Regex match on CVE ID | `"CVE-2021-.*"` |
| `severityEquals` | Exact severity match | `"HIGH"`, `"CRITICAL"` |
| `reachabilityEquals` | Reachability state | `"REACHABLE"`, `"UNREACHABLE"`, `"UNKNOWN"` |
| `packagePattern` | Package name regex | `"log4j.*"` |
## Complete Example
```yaml
policyVersion: score.v1
policyId: production-v2024.12
policyName: "Production Security Policy"
description: |
Balanced scoring policy with emphasis on exploitability
and reachability for production workloads.
weightsBps:
baseSeverity: 2000
reachability: 3000
evidence: 3000
provenance: 2000
reachabilityConfig:
reachableMultiplier: 1.5
unreachableMultiplier: 0.4
unknownMultiplier: 1.0
evidenceConfig:
kevWeight: 1.5
epssThreshold: 0.3
epssWeight: 1.3
provenanceConfig:
signedBonus: 0.1
rekorVerifiedBonus: 0.15
unsignedPenalty: -0.05
overrides:
- id: critical-rce
match:
cvePattern: "CVE-2021-44228|CVE-2022-22965"
action:
setScore: 10.0
reason: "Known critical RCE vulnerabilities"
- id: unreachable-low
match:
severityEquals: "LOW"
reachabilityEquals: "UNREACHABLE"
action:
multiplyScore: 0.2
reason: "Minimal risk: low severity + unreachable"
```
## Validation
Policies are validated against JSON Schema on load:
1. **Schema validation**: Structure and types
2. **Weight sum check**: `weightsBps` must sum to 10000
3. **Range checks**: Multipliers within bounds
4. **Override validation**: Valid patterns and actions
### Programmatic Validation
```csharp
var validator = new ScorePolicyValidator();
var result = validator.Validate(policy);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine(error);
}
}
```
## Determinism
For reproducible scoring:
1. **Policy Digest**: Each policy has a content-addressed digest
2. **Replay Manifest**: Digest is recorded in scan manifests
3. **Audit Trail**: Policy version tracked with every scan
### Digest Format
```
sha256:abc123def456...
```
The digest is computed from canonical JSON serialization of the policy, ensuring identical policies always produce identical digests.
## Migration
### From Hardcoded Weights
1. Export current weights to YAML format
2. Validate with `stellaops policy validate score.yaml`
3. Deploy to `etc/score-policy.yaml`
4. Restart services to load new policy
### Version Upgrades
Future schema versions (e.g., `score.v2`) will include migration guides and backward compatibility notes.
## Related Documentation
- [Architecture Overview](../ARCHITECTURE_OVERVIEW.md)
- [Determinism Technical Reference](../product-advisories/14-Dec-2025%20-%20Determinism%20and%20Reproducibility%20Technical%20Reference.md)
- [Policy Engine Architecture](../modules/policy/architecture.md)

View File

@@ -0,0 +1,192 @@
# Scoring Profiles
**Sprint:** SPRINT_3407_0001_0001
**Task:** PROF-3407-014
**Last Updated:** 2025-12-16
## Overview
StellaOps supports multiple scoring profiles to accommodate different customer needs, from simple transparent scoring to advanced entropy-based analysis. Scoring profiles determine how vulnerability findings are evaluated and scored.
## Available Profiles
### Simple Profile
The Simple profile uses a transparent 4-factor basis-points weighted formula:
```
riskScore = (wB × B + wR × R + wE × E + wP × P) / 10000
```
Where:
- **B** (Base Severity): CVSS score × 10 (0-100 range)
- **R** (Reachability): Hop-based score with gate multipliers
- **E** (Evidence): Evidence points × freshness multiplier
- **P** (Provenance): Level-based score (unsigned to reproducible)
- **wB, wR, wE, wP**: Weight basis points (must sum to 10000)
**Default weights:**
| Factor | Weight (bps) | Percentage |
|--------|-------------|------------|
| Base Severity | 1000 | 10% |
| Reachability | 4500 | 45% |
| Evidence | 3000 | 30% |
| Provenance | 1500 | 15% |
**Use cases:**
- Organizations requiring audit-friendly, explainable scoring
- Compliance scenarios requiring transparent formulas
- Initial deployments before advanced analysis is available
### Advanced Profile (Default)
The Advanced profile extends Simple with:
- **CVSS version adjustment**: Scores weighted by CVSS version (4.0 > 3.1 > 3.0 > 2.0)
- **KEV boost**: +20 points for Known Exploited Vulnerabilities
- **Uncertainty penalty**: Deductions for missing data (reachability, evidence, provenance, CVSS version)
- **Semantic category multipliers**: Entry points and API endpoints scored higher than internal services
- **Multi-evidence overlap bonus**: 10% bonus per additional evidence type
- **Advanced score passthrough**: Uses pre-computed advanced scores when available
**Use cases:**
- Production deployments with full telemetry
- Organizations with mature security programs
- Scenarios requiring nuanced risk differentiation
### Custom Profile (Enterprise)
The Custom profile allows fully user-defined scoring via Rego policies. Requires:
- Valid Rego policy path
- Policy Engine license with Custom Scoring feature
## Configuration
### Score Policy YAML
Add the `scoringProfile` field to your score policy:
```yaml
policyVersion: score.v1
scoringProfile: simple # Options: simple, advanced, custom
weightsBps:
baseSeverity: 1000
reachability: 4500
evidence: 3000
provenance: 1500
# ... rest of policy configuration
```
### Tenant Override
Tenants can override the default profile via the Scoring Profile Service:
```csharp
// Set profile for a tenant
scoringProfileService.SetProfileForTenant("tenant-id", new ScoringProfileConfig
{
Profile = ScoringProfile.Simple
});
// Remove override (revert to default)
scoringProfileService.RemoveProfileForTenant("tenant-id");
```
## API Integration
### Scoring with Default Profile
```csharp
var result = await profileAwareScoringService.ScoreAsync(input);
// Uses tenant's configured profile
```
### Scoring with Explicit Profile
```csharp
var result = await profileAwareScoringService.ScoreWithProfileAsync(
input,
ScoringProfile.Simple);
```
### Profile Comparison
```csharp
var comparison = await profileAwareScoringService.CompareProfilesAsync(input);
// Returns scores from all profiles for analysis
```
## Audit Trail
All scoring results include profile identification:
```json
{
"finding_id": "CVE-2024-12345-pkg-1.0.0",
"scoring_profile": "simple",
"profile_version": "simple-v1",
"raw_score": 65,
"final_score": 65,
"severity": "medium",
"signal_values": {
"baseSeverity": 75,
"reachability": 70,
"evidence": 45,
"provenance": 60
},
"signal_contributions": {
"baseSeverity": 7.5,
"reachability": 31.5,
"evidence": 13.5,
"provenance": 9.0
},
"explain": [
{ "factor": "baseSeverity", "value": 75, "reason": "CVSS 7.5 (v3.1) with version adjustment" },
{ "factor": "evidence", "value": 45, "reason": "45 evidence points, 14 days old (90% freshness)" },
{ "factor": "provenance", "value": 60, "reason": "Provenance level: SignedWithSbom" },
{ "factor": "reachability", "value": 70, "reason": "2 hops from call graph" }
]
}
```
## Migration Guide
### From Legacy Scoring
1. **Audit current scores**: Export current scores for baseline comparison
2. **Enable Simple profile**: Start with Simple for predictable behavior
3. **Compare profiles**: Use `CompareProfilesAsync` to understand differences
4. **Gradual rollout**: Move to Advanced when confidence is established
### Profile Switching Best Practices
- **Test in staging first**: Validate score distribution before production
- **Monitor severity distribution**: Watch for unexpected shifts
- **Document changes**: Record profile changes in policy lifecycle
- **Use replay**: Re-score historical findings to validate behavior
## Determinism
Both Simple and Advanced profiles are fully deterministic:
- **Explicit time**: All calculations use `AsOf` timestamp
- **Integer math**: Basis-point arithmetic avoids floating-point drift
- **Stable ordering**: Explanations sorted alphabetically by factor
- **Input digests**: Track input hashes for replay validation
## Performance
| Profile | Typical Latency | Memory |
|---------|----------------|--------|
| Simple | < 1ms | Minimal |
| Advanced | < 5ms | Minimal |
| Custom | Varies | Depends on Rego complexity |
## Related Documentation
- [Score Policy YAML](./score-policy-yaml.md)
- [Signals Weighting](./signals-weighting.md)
- [VEX Trust Model](./vex-trust-model.md)
- [Policy Overview](./overview.md)

View File

@@ -0,0 +1,15 @@
# Signals Weighting (outline)
## Pending Inputs
- See sprint SPRINT_0309_0001_0009_docs_tasks_md_ix action tracker; inputs due 2025-12-09..12 from owning guilds.
## Determinism Checklist
- [ ] Hash any inbound assets/payloads; place sums alongside artifacts (e.g., SHA256SUMS in this folder).
- [ ] Keep examples offline-friendly and deterministic (fixed seeds, pinned versions, stable ordering).
- [ ] Note source/approver for any provided captures or schemas.
## Sections to fill (once inputs arrive)
- SPL predicate patterns and weighting strategy.
- Default weights and configurable knobs.
- Examples (policy snippets/recipes) with deterministic ordering.
- Hashes for any example bundles or fixtures.

View File

@@ -0,0 +1,116 @@
# Stella Policy Language (SPL) v1
> **Status:** Draft (2025-11)
> **Imposed rule:** SPL packs must pass lint, simulate, shadow, and coverage gates before activation; IR hashes must be attested when published.
This document defines the SPL v1 language: syntax, semantics, JSON schema, and examples used by the Policy Engine.
## 1. Syntax summary
- File-level directive: `policy "<name>" syntax "stella-dsl@1" { ... }`
- Blocks: `metadata`, `profile <name> {}`, `settings {}`, `rule <name> [priority n] { when ... then ... because "..." }`
- No loops, no network/clock access; pure, deterministic evaluation.
## 2. JSON Schema (canonical IR)
```jsonc
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Stella Policy Language v1",
"type": "object",
"required": ["policyId", "syntax", "rules"],
"properties": {
"policyId": {"type": "string"},
"syntax": {"const": "stella-dsl@1"},
"metadata": {"type": "object"},
"settings": {
"type": "object",
"properties": {
"shadow": {"type": "boolean"},
"default_status": {"type": "string"}
}
},
"profiles": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"maps": {"type": "object"},
"env": {"type": "object"},
"scalars": {"type": "object"}
}
}
},
"rules": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "when", "then"],
"properties": {
"name": {"type": "string"},
"priority": {"type": "integer", "minimum": 0},
"when": {"type": "object"},
"then": {"type": "array"},
"else": {"type": "array"},
"because": {"type": "string"}
}
}
}
}
}
```
Notes:
- The compiler emits canonical IR JSON sorted by keys; hashing uses this canonical form.
- `when` and actions are expressed as AST nodes; see engine schema for exact shape.
## 3. Built-in functions (v1)
- `normalize_cvss`, `cvss`, `severity_band`, `risk_score`, `reach_state`, `exists`, `coalesce`, `percent_of`, `lowercase`.
- VEX helpers: `vex.any`, `vex.all`, `vex.latest`.
- Secrets helpers: `secret.hasFinding`, `secret.match.count`, `secret.bundle.version`, `secret.mask.applied`, `secret.path.allowlist`.
- Signals: access via `signals.trust_score`, `signals.reachability.state/score`, `signals.entropy_penalty`, `signals.uncertainty.level`, `signals.runtime_hits`.
## 4. Data namespaces
- `sbom`, `advisory`, `vex`, `run`, `env`, `telemetry`, `signals`, `secret`, `profile.*`.
- Missing fields evaluate to `null/unknown`; comparisons must handle `unknown` explicitly.
## 5. Examples
### 5.1 Reachability-aware gate
```dsl
rule reachability_gate priority 20 {
when signals.reachability.state == "reachable" and signals.reachability.score >= 0.6
then status := "affected"
because "Runtime/graph evidence shows reachable code path";
}
```
### 5.2 Trust/entropy penalty
```dsl
rule trust_entropy_penalty priority 30 {
when signals.trust_score < 0.4 or signals.entropy_penalty > 0.2
then severity := severity_band("critical")
because "Low trust score or high entropy";
}
```
### 5.3 Shadow mode on
```dsl
settings {
shadow = true
}
```
## 6. Authoring workflow (quick)
1. Write/update SPL with shadow enabled; add coverage fixtures.
2. Run `stella policy lint`, `stella policy simulate`, and `stella policy test`.
3. Attach artefacts to submission; ensure determinism twin-run passes in CI.
4. Publish with DSSE attestation (IR hash + metadata) and promote to environments.
## 7. Compatibility
- SPL v1 aligns with `stella-dsl@1` grammar. Future SPL versions will be additive; declare `syntax` explicitly.
## 8. References
- `docs/modules/policy/guides/dsl.md`
- `docs/modules/policy/guides/lifecycle.md`
- `docs/modules/policy/guides/architecture.md`
- `docs/modules/policy/guides/overview.md`

View File

@@ -0,0 +1,389 @@
# Starter Policy Pack Guide
This guide covers the StellaOps starter policy pack (`starter-day1`), a production-ready policy designed for organizations beginning their software supply chain security journey.
## Quick Start
Install and activate the starter policy in under 5 minutes:
```bash
# Install the starter policy pack
stellaops policy install starter-day1
# Validate a policy file
stellaops policy validate policies/starter-day1.yaml
# Simulate policy against a scan
stellaops policy simulate --policy policies/starter-day1.yaml --scan <scan-id>
```
## Overview
The starter policy implements a sensible security posture:
| What | Action | Rationale |
|------|--------|-----------|
| Reachable HIGH/CRITICAL vulnerabilities | **Block** | Highest risk - exploitable code paths |
| Reachable MEDIUM vulnerabilities | **Warn** | Moderate risk - review recommended |
| Unreachable vulnerabilities | **Allow** (logged) | Low risk - no exploitable path |
| VEX-covered vulnerabilities | **Allow** | Evidence-based exception |
| Unknowns > 5% of packages | **Block** | Quality gate for metadata coverage |
| Unsigned SBOM (production) | **Block** | Integrity requirement |
| Unsigned verdict (production) | **Block** | Compliance attestation |
## Rule-by-Rule Explanation
### Rule 1: Block Reachable HIGH/CRITICAL
```yaml
- name: block-reachable-high-critical
description: "Block deployments with reachable HIGH or CRITICAL vulnerabilities"
match:
severity:
- CRITICAL
- HIGH
reachability: reachable
unless:
vexStatus: not_affected
vexJustification:
- vulnerable_code_not_present
- vulnerable_code_cannot_be_controlled_by_adversary
- inline_mitigations_already_exist
action: block
message: "Reachable {severity} vulnerability {cve} must be remediated or have VEX justification"
```
**What it does**: Blocks any deployment containing HIGH or CRITICAL severity vulnerabilities where the vulnerable code is reachable (can be executed).
**VEX bypass**: If a VEX statement marks the vulnerability as `not_affected` with proper justification, the block is bypassed. Valid justifications:
- `vulnerable_code_not_present` - The vulnerable function isn't compiled in
- `vulnerable_code_cannot_be_controlled_by_adversary` - Input cannot reach vulnerable code
- `inline_mitigations_already_exist` - Application-level mitigations in place
### Rule 2: Warn on Reachable MEDIUM
```yaml
- name: warn-reachable-medium
description: "Warn on reachable MEDIUM severity vulnerabilities"
match:
severity: MEDIUM
reachability: reachable
unless:
vexStatus: not_affected
action: warn
message: "Reachable MEDIUM vulnerability {cve} should be reviewed"
```
**What it does**: Generates a warning (but allows deployment) for MEDIUM severity reachable vulnerabilities.
**When to upgrade**: If your security posture matures, consider changing `action: warn` to `action: block`.
### Rule 3: Allow Unreachable
```yaml
- name: ignore-unreachable
description: "Allow unreachable vulnerabilities but log for awareness"
match:
reachability: unreachable
action: allow
log: true
message: "Vulnerability {cve} is unreachable - allowing"
```
**What it does**: Allows vulnerabilities where static analysis proves the code cannot be reached.
**Logging**: Even allowed findings are logged for audit purposes.
### Rule 4: Unknowns Budget
```yaml
- name: fail-on-unknowns
description: "Block if too many packages have unknown metadata"
type: aggregate
match:
unknownsRatio:
gt: ${settings.unknownsThreshold}
action: block
message: "Unknown packages exceed threshold ({unknownsRatio}% > {threshold}%)"
```
**What it does**: Blocks deployment if more than 5% of packages lack sufficient metadata for analysis.
**Why**: High unknowns ratio indicates poor SBOM quality or unsupported package ecosystems.
### Rules 5-6: Signed Artifacts (Production)
```yaml
- name: require-signed-sbom-prod
match:
environment: production
require:
signedSbom: true
action: block
message: "Production deployment requires signed SBOM"
- name: require-signed-verdict-prod
match:
environment: production
require:
signedVerdict: true
action: block
message: "Production deployment requires signed verdict"
```
**What they do**: Require cryptographically signed SBOM and verdict attestations for production environments.
**Why**: Ensures integrity and non-repudiation of security assessments.
### Rule 7: Default Allow
```yaml
- name: default-allow
description: "Allow everything not matched by above rules"
match:
always: true
action: allow
```
**What it does**: Explicit default action for any finding not matched by prior rules.
## Environment Overrides
The starter policy supports environment-specific overrides:
### Development (`overrides/development.yaml`)
```yaml
apiVersion: policy.stellaops.io/v1
kind: PolicyOverride
metadata:
name: starter-day1-dev
parent: starter-day1
environment: development
spec:
settings:
defaultAction: warn # Never block in dev
unknownsThreshold: 0.20 # Allow more unknowns (20%)
ruleOverrides:
- name: block-reachable-high-critical
action: warn # Downgrade to warn
- name: require-signed-sbom-prod
enabled: false # Disable signing requirements
- name: require-signed-verdict-prod
enabled: false
```
### Staging (`overrides/staging.yaml`)
```yaml
apiVersion: policy.stellaops.io/v1
kind: PolicyOverride
metadata:
name: starter-day1-staging
parent: starter-day1
environment: staging
spec:
settings:
unknownsThreshold: 0.10 # 10% unknowns budget
ruleOverrides:
- name: require-signed-sbom-prod
enabled: false # No signing in staging
```
### Production (Default)
Production uses the base policy with all rules enabled.
## Customization Guide
### Adjusting Severity Thresholds
To block MEDIUM vulnerabilities too:
```yaml
ruleOverrides:
- name: warn-reachable-medium
action: block
```
### Changing Unknowns Budget
To allow 10% unknowns:
```yaml
settings:
unknownsThreshold: 0.10
```
### Adding Custom Rules
Insert rules before `default-allow`:
```yaml
rules:
# ... existing rules ...
# Custom: Block specific CVE
- name: block-specific-cve
match:
cve: CVE-2024-1234
action: block
message: "CVE-2024-1234 is explicitly blocked by policy"
- name: default-allow
match:
always: true
action: allow
```
### Source Allowlists
Trust only specific VEX sources:
```yaml
settings:
vexSources:
allowlist:
- redhat-csaf
- canonical-usn
- vendor-security
requireTrustScore: 0.7
```
## Troubleshooting
### "Reachable HIGH vulnerability must be remediated"
**Cause**: A HIGH/CRITICAL vulnerability exists in code that can execute.
**Solutions**:
1. Update the affected package to a fixed version
2. Create a VEX statement if the vulnerability doesn't apply:
```bash
stellaops vex create --cve CVE-2024-1234 --status not_affected \
--justification vulnerable_code_not_present
```
3. Apply a temporary exception (with approval):
```yaml
exceptions:
- cve: CVE-2024-1234
expires: 2025-02-01
approver: security-team
reason: "Patch scheduled for next sprint"
```
### "Unknown packages exceed threshold"
**Cause**: Too many packages lack metadata for vulnerability analysis.
**Solutions**:
1. Ensure SBOMs are complete with package versions
2. Check for unsupported package ecosystems
3. Temporarily increase threshold in non-production:
```yaml
settings:
unknownsThreshold: 0.15
```
### "Production deployment requires signed SBOM"
**Cause**: Attempting production deployment without signed SBOM.
**Solutions**:
1. Sign SBOM during build:
```bash
stellaops sbom sign --sbom sbom.json --key-id <key>
```
2. Verify signing is configured in CI/CD pipeline
### Rule Not Matching as Expected
Use simulation mode to debug:
```bash
stellaops policy simulate --policy policy.yaml --scan <scan-id> --verbose
```
Check:
- Rule order (first match wins)
- Severity casing (use uppercase: HIGH, CRITICAL)
- Reachability values (reachable, unreachable, unknown)
## Migration Path to Custom Policies
### Step 1: Start with Starter Policy
Deploy `starter-day1` as-is to establish baseline.
### Step 2: Monitor and Tune
Review policy decisions for 2-4 weeks:
- Which rules trigger most?
- Which findings need exceptions?
- What patterns emerge?
### Step 3: Create Overlays
Add overlays for organization-specific needs:
```yaml
apiVersion: policy.stellaops.io/v1
kind: PolicyOverride
metadata:
name: acme-corp-policy
parent: starter-day1
spec:
ruleOverrides:
- name: block-reachable-high-critical
unless:
# Add company-specific VEX sources
vexSource:
- acme-security-team
```
### Step 4: Graduate to Custom Policy
When ready, fork the starter policy and customize fully:
```bash
# Export starter policy as starting point
stellaops policy pull --from registry.stellaops.io/policies/starter-day1:1.0.0 \
--output ./my-custom-policy/
# Modify as needed
vim ./my-custom-policy/base.yaml
# Validate changes
stellaops policy validate ./my-custom-policy/
# Push to your registry
stellaops policy push --policy ./my-custom-policy/base.yaml \
--to myregistry.example.com/policies/acme-policy:1.0.0 --sign
```
## CLI Reference
| Command | Description |
|---------|-------------|
| `stellaops policy validate <path>` | Validate policy against schema |
| `stellaops policy install <name>` | Install policy pack |
| `stellaops policy list-packs` | List available policy packs |
| `stellaops policy simulate --policy <path> --scan <id>` | Simulate policy evaluation |
| `stellaops policy push --policy <path> --to <ref>` | Push to OCI registry |
| `stellaops policy pull --from <ref>` | Pull from OCI registry |
| `stellaops policy export-bundle --policy <path> --output <file>` | Create offline bundle |
| `stellaops policy import-bundle --bundle <file>` | Import offline bundle |
## See Also
- [Policy DSL Reference](dsl.md)
- [VEX Trust Model](vex-trust-model.md)
- [Policy Governance](governance.md)
- [Scoring Profiles](scoring-profiles.md)

View File

@@ -0,0 +1,41 @@
# Policy UI Integration for Graph/Vuln
Status: Draft (2025-11-26) — aligns with POLICY-ENGINE-30-001..003 and Graph API overlays.
## Goals
- Explain how UI surfaces (Console, Vuln Explorer) consume policy/VEX overlays from Graph.
- Clarify cache usage, simulator contracts, and explain traces.
## Data sources
- Policy overlays (`policy.overlay.v1`) produced by Policy Engine (POLICY-ENGINE-30-001).
- VEX overlays (`openvex.v1`) from Concelier/Excititor pipelines.
- Graph API emits overlays per node (see `docs/api/graph.md`) with deterministic IDs and optional `explainTrace` sampling.
## Cache rules
- UI should respect overlay cache TTL (510 minutes). Cache key: tenant + nodeId + overlay kind.
- On cache miss, fallback to Graph API which will populate cache; avoid fan-out calls per tile.
- When policy overlay contract version changes, invalidate cache via version tag (e.g., `policy.overlay.v1``v2`).
## Requests
- Graph API: `includeOverlays=true` on `/graph/query` or `/graph/paths` to receive overlay payloads inline.
- Budget: ensure `budget.tiles` leaves room for overlays; UI may need to request higher budgets when overlays are critical to UX.
- Simulator: when running policy simulator, attach `X-Stella-Simulator: true` header (once enabled) to route to simulator instance; cache should be bypassed for simulator runs.
## UI rendering guidance
- Show policy status badge (e.g., `warn`, `deny`, `allow`) with ruleId and severity.
- If `explainTrace` present, render as expandable list; only one sampled node per query may include trace.
- VEX overlays: render status (`not_affected`, `affected`) and justification; show issued timestamp and source.
- Overlay provenance: display `overlayId`, version, and source engine version if present.
## Error handling
- If Graph returns `GRAPH_BUDGET_EXCEEDED`, prompt user to reduce scope or increase budgets; do not silently drop overlays.
- On overlay cache miss + upstream failure, surface a non-blocking warning and proceed with node data.
## Events & notifications
- Subscribe to `policy.overlay.updated` (future) or re-poll every 10 minutes to refresh overlays in UI.
- When VEX status changes, UI should refresh impacted nodes/edges and reflect new status badges.
## References
- Policy overlay contract: `docs/modules/policy/prep/2025-11-22-policy-engine-30-001-prep.md`
- Graph API overlays: `docs/api/graph.md`, `docs/modules/graph/architecture-index.md`
- Concelier/Excititor overlays: `docs/modules/excititor/vex_observations.md`

View File

@@ -0,0 +1,535 @@
# Policy Verdict Attestations
> **Status:** Implementation in Progress (SPRINT_3000_0100_0001)
> **Predicate URI:** `https://stellaops.dev/predicates/policy-verdict@v1`
> **Schema:** [`docs/schemas/stellaops-policy-verdict.v1.schema.json`](../schemas/stellaops-policy-verdict.v1.schema.json)
---
## Overview
**Verdict Attestations** provide cryptographically-bound proof that a policy evaluation verdict (passed/warned/blocked/quieted) was issued for a specific finding at a specific time with specific evidence. Every policy run produces signed verdict attestations wrapped in DSSE envelopes, enabling:
- **Trust & Integrity:** Cryptographic proof verdicts haven't been tampered with
- **Audit & Compliance:** Standalone artifacts for incident review, regulatory compliance
- **Automation:** Downstream tools can verify verdict authenticity before acting
- **Transparency:** Optional anchoring in Rekor transparency log
---
## Architecture
### Flow
```
Policy Engine
↓ (evaluates finding)
PolicyExplainTrace
↓ (mapped to)
VerdictPredicate (canonical JSON)
↓ (sent to)
Attestor Service
↓ (signs with DSSE)
Verdict Attestation (signed envelope)
↓ (stored in)
Evidence Locker (PostgreSQL + object store)
↓ (optionally anchored in)
Rekor Transparency Log
```
### Components
| Component | Responsibility | Location |
|-----------|---------------|----------|
| **PolicyExplainTrace** | Policy evaluation result | `StellaOps.Scheduler.Models` |
| **VerdictPredicateBuilder** | Maps trace → predicate | `StellaOps.Policy.Engine.Attestation` |
| **VerdictAttestationService** | Sends attestation requests | `StellaOps.Policy.Engine.Attestation` |
| **VerdictAttestationHandler** | Receives, validates, signs | `StellaOps.Attestor` |
| **Evidence Locker** | Stores and indexes verdicts | `StellaOps.EvidenceLocker` |
---
## Predicate Schema
See [`stellaops-policy-verdict.v1.schema.json`](../schemas/stellaops-policy-verdict.v1.schema.json) for the complete JSON schema.
### Example Predicate
```json
{
"_type": "https://stellaops.dev/predicates/policy-verdict@v1",
"tenantId": "tenant-alpha",
"policyId": "P-7",
"policyVersion": 4,
"runId": "run:P-7:20251223T140500Z:1b2c3d4e",
"findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21",
"evaluatedAt": "2025-12-23T14:06:01+00:00",
"verdict": {
"status": "blocked",
"severity": "critical",
"score": 19.5,
"rationale": "CVE-2025-12345 exploitable via lodash.template with confirmed reachability"
},
"ruleChain": [
{
"ruleId": "rule-allow-known",
"action": "allow",
"decision": "skipped"
},
{
"ruleId": "rule-block-critical",
"action": "block",
"decision": "matched",
"score": 19.5
}
],
"evidence": [
{
"type": "advisory",
"reference": "CVE-2025-12345",
"source": "nvd",
"status": "affected",
"digest": "sha256:abc123...",
"weight": 1.0
},
{
"type": "vex",
"reference": "vex:ghsa-2025-0001",
"source": "vendor",
"status": "not_affected",
"digest": "sha256:def456...",
"weight": 0.5,
"metadata": {}
}
],
"vexImpacts": [
{
"statementId": "vex:ghsa-2025-0001",
"provider": "vendor",
"status": "not_affected",
"accepted": false,
"justification": "VEX statement contradicts confirmed reachability analysis"
}
],
"reachability": {
"status": "confirmed",
"paths": [
{
"entrypoint": "GET /api/users",
"sink": "lodash.template",
"confidence": "high",
"digest": "sha256:path123..."
}
]
},
"metadata": {
"componentPurl": "pkg:npm/lodash@4.17.21",
"sbomId": "sbom:S-42",
"traceId": "01HE0BJX5S4T9YCN6ZT0",
"determinismHash": "sha256:..."
}
}
```
### DSSE Envelope
The predicate is wrapped in a DSSE envelope:
```json
{
"payload": "<base64(canonicalJson(predicate))>",
"payloadType": "application/vnd.stellaops.policy-verdict+json",
"signatures": [
{
"keyid": "sha256:keypair123...",
"sig": "<base64(signature)>"
}
]
}
```
---
## API Reference
### Create Verdict Attestation (Internal)
**Note:** This is an internal API called by the Policy Engine, not exposed externally.
```http
POST /internal/api/v1/attestations/verdict
Content-Type: application/json
```
**Request:**
```json
{
"predicateType": "https://stellaops.dev/predicates/policy-verdict@v1",
"predicate": "{...}",
"subject": [
{
"name": "finding:sbom:S-42/pkg:npm/lodash@4.17.21",
"digest": {
"sha256": "abc123..."
}
}
]
}
```
**Response:**
```json
{
"verdictId": "verdict:run:P-7:20251223T140500Z:finding-42",
"attestationUri": "/api/v1/verdicts/verdict:run:P-7:20251223T140500Z:finding-42",
"rekorLogIndex": 12345678
}
```
### Retrieve Verdict Attestation
```http
GET /api/v1/verdicts/{verdictId}
```
**Response:**
```json
{
"verdictId": "verdict:run:P-7:20251223T140500Z:finding-42",
"tenantId": "tenant-alpha",
"policyRunId": "run:P-7:20251223T140500Z:1b2c3d4e",
"findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21",
"verdictStatus": "blocked",
"evaluatedAt": "2025-12-23T14:06:01+00:00",
"envelope": {
"payload": "<base64>",
"payloadType": "application/vnd.stellaops.policy-verdict+json",
"signatures": [...]
},
"rekorLogIndex": 12345678,
"createdAt": "2025-12-23T14:06:05+00:00"
}
```
### List Verdicts for Policy Run
```http
GET /api/v1/runs/{runId}/verdicts?status=blocked&limit=50
```
**Query Parameters:**
- `status`: Filter by verdict status (passed/warned/blocked/quieted)
- `severity`: Filter by severity (critical/high/medium/low)
- `limit`: Maximum results (default 50, max 200)
- `offset`: Pagination offset
**Response:**
```json
{
"verdicts": [
{
"verdictId": "verdict:run:P-7:20251223T140500Z:finding-42",
"findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21",
"verdictStatus": "blocked",
"severity": "critical",
"evaluatedAt": "2025-12-23T14:06:01+00:00"
}
],
"pagination": {
"total": 234,
"limit": 50,
"offset": 0
}
}
```
### Verify Verdict Signature
```http
POST /api/v1/verdicts/{verdictId}/verify
```
**Response:**
```json
{
"verdictId": "verdict:run:P-7:20251223T140500Z:finding-42",
"signatureValid": true,
"verifiedAt": "2025-12-23T15:00:00+00:00",
"verifications": [
{
"keyId": "sha256:keypair123...",
"algorithm": "ed25519",
"valid": true
}
],
"rekorVerification": {
"logIndex": 12345678,
"inclusionProofValid": true,
"verifiedAt": "2025-12-23T15:00:01+00:00"
}
}
```
---
## CLI Usage
### Retrieve Verdict
```bash
stella verdict get verdict:run:P-7:20251223T140500Z:finding-42
# Output:
# Verdict: blocked (critical, score 19.5)
# Finding: finding:sbom:S-42/pkg:npm/lodash@4.17.21
# Evaluated: 2025-12-23T14:06:01+00:00
# Signature: ✓ Verified (ed25519)
# Rekor: ✓ Anchored (log index 12345678)
```
### Verify Verdict Signature (Offline)
```bash
stella verdict verify verdict-12345.json --public-key ./pubkey.pem
# Output:
# ✓ Signature valid
# ✓ Predicate schema valid
# ✓ Determinism hash matches
```
### List Verdicts for Run
```bash
stella verdict list --run run:P-7:20251223T140500Z:1b2c3d4e --status blocked
# Output:
# 234 verdicts found (showing 50)
#
# verdict:...:finding-42 | blocked | critical | CVE-2025-12345 | lodash@4.17.21
# verdict:...:finding-78 | blocked | high | CVE-2025-99999 | express@4.18.0
# ...
```
### Download Verdict Envelope
```bash
stella verdict download verdict:run:P-7:20251223T140500Z:finding-42 --output ./verdict.json
```
---
## Implementation Guide
### Policy Engine Integration
The Policy Engine's `PolicyRunExecutionService` calls `VerdictAttestationService` after evaluating each finding:
```csharp
public class PolicyRunExecutionService
{
private readonly VerdictAttestationService _verdictAttestationService;
public async Task<PolicyRunStatus> ExecuteAsync(PolicyRunRequest request)
{
// ... policy evaluation logic
foreach (var trace in explainTraces)
{
// Emit verdict attestation
if (_options.VerdictAttestationsEnabled)
{
var verdictId = await _verdictAttestationService.AttestVerdictAsync(trace);
_logger.LogInformation("Verdict attestation created: {VerdictId}", verdictId);
}
}
// ... continue
}
}
```
### Attestor Handler
The Attestor receives attestation requests via internal API:
```csharp
public class VerdictAttestationHandler
{
public async Task<AttestationResult> HandleAsync(AttestationRequest request)
{
// 1. Validate predicate schema
await _schemaValidator.ValidateAsync(request.Predicate, request.PredicateType);
// 2. Create DSSE envelope
var envelope = await _dsseService.SignAsync(request);
// 3. Store in Evidence Locker
var verdictId = await _evidenceLocker.StoreVerdictAsync(envelope);
// 4. Optional: Anchor in Rekor
long? rekorLogIndex = null;
if (_options.RekorEnabled)
{
rekorLogIndex = await _rekorClient.UploadAsync(envelope);
}
return new AttestationResult
{
VerdictId = verdictId,
RekorLogIndex = rekorLogIndex
};
}
}
```
### Evidence Locker Storage
Verdicts are stored in PostgreSQL with full-text index and object store reference:
```sql
INSERT INTO verdict_attestations (
verdict_id,
tenant_id,
run_id,
policy_id,
policy_version,
finding_id,
verdict_status,
evaluated_at,
envelope,
predicate_digest,
rekor_log_index
) VALUES (
'verdict:run:P-7:20251223T140500Z:finding-42',
'tenant-alpha',
'run:P-7:20251223T140500Z:1b2c3d4e',
'P-7',
4,
'finding:sbom:S-42/pkg:npm/lodash@4.17.21',
'blocked',
'2025-12-23T14:06:01+00:00',
'{"payload": "...", "signatures": [...]}',
'sha256:abc123...',
12345678
);
```
---
## Determinism
Verdict attestations are **deterministic** when evaluated with identical inputs:
1. **Input Normalization:** Policy inputs (SBOMs, advisories, VEX, environment) are canonicalized
2. **Canonical JSON:** Predicates serialize with lexicographic key ordering
3. **Stable Sorting:** Evidence, rule chains, and arrays are sorted deterministically
4. **Hash Computation:** `determinismHash` = SHA256(sorted digests of all evidence)
### Determinism Validation
```bash
# Compute determinism hash for policy run
stella policy determinism-hash run:P-7:20251223T140500Z:1b2c3d4e
# Compare with original run's hash
# If hashes match → deterministic
# If hashes differ → identify divergence source
```
---
## Offline Support
Verdict attestations support **air-gapped deployments**:
- **Signature Verification:** Uses bundled public keys, no network required
- **Rekor Optional:** Transparency log anchoring can be disabled
- **Bundle Export:** Verdicts included in evidence packs for offline transfer
---
## Performance Considerations
### Volume
Large policy runs can produce **millions of verdicts**:
- **Batch Signing:** Group verdicts into batches, sign batch manifest
- **Async Processing:** Attestation pipeline runs asynchronously from policy evaluation
- **Compression:** Verdict envelopes use compact JSON and gzip compression
### Storage
- **Hot Storage:** Recent verdicts (< 30 days) in PostgreSQL
- **Cold Storage:** Older verdicts archived to object store
- **Indexing:** Indexes on `run_id`, `finding_id`, `tenant_id`, `evaluated_at`
---
## Security
### Signing Keys
- **Key Management:** Keys stored in KMS or CryptoPro (GOST support)
- **Key Rotation:** Verdicts include `keyId`, support multi-signature
- **Offline Keys:** Support for offline signing ceremonies (air-gapped)
### Access Control
- **RBAC:** `policy:verdict:read` scope required for API access
- **Tenant Isolation:** Verdicts scoped by `tenantId`, cross-tenant queries blocked
- **Audit Trail:** All verdict retrievals logged with actor, timestamp
---
## Troubleshooting
### Verdict Attestation Failed
**Symptom:** Policy run completes but no verdicts created
**Causes:**
1. Attestor service unavailable Check health endpoint
2. Schema validation failure Check predicate structure
3. Signing key unavailable Verify KMS connectivity
**Resolution:**
```bash
# Check attestor health
curl http://attestor:8080/health
# Verify predicate schema
stella schema validate policy-verdict.json --schema stellaops-policy-verdict.v1.schema.json
# Retry attestation
stella policy rerun run:P-7:20251223T140500Z:1b2c3d4e --attestations-only
```
### Signature Verification Failed
**Symptom:** `POST /api/v1/verdicts/{id}/verify` returns `signatureValid: false`
**Causes:**
1. Public key mismatch Verify correct key for `keyId`
2. Payload tampering Compare digest with original
3. Clock skew Check timestamp validity window
**Resolution:**
```bash
# Verify with correct public key
stella verdict verify verdict.json --public-key ./correct-pubkey.pem
# Check payload digest
stella digest compute verdict.json
```
---
## References
- [DSSE Specification](https://github.com/secure-systems-lab/dsse)
- [Rekor Transparency Log](https://docs.sigstore.dev/rekor/overview/)
- [Policy Engine Architecture](../modules/policy/architecture.md)
- [Attestor Architecture](../modules/attestor/architecture.md)
- [Evidence Locker Architecture](../modules/evidence-locker/architecture.md)

View File

@@ -0,0 +1,53 @@
# VEX Trust Model
This document explains how policy evaluation uses VEX evidence and issuer trust to produce deterministic, explainable outcomes. It focuses on *policy posture* rather than API details.
## Inputs to Trust
Policy decisions about VEX rely on evidence produced by VEX ingestion and correlation layers:
- **Raw observations** and provenance (issuer, timestamps, signatures, content hashes)
- **Normalized tuples** (status, justification, scope) emitted by VEX ingestion
- **Linksets** that correlate multiple sources without merging them
- **Consensus view** (when enabled) that summarizes the current effective status and conflicts
- **Issuer registry / directory** that defines trust tiers and verification rules per tenant
See `docs/VEX_CONSENSUS_GUIDE.md` for the conceptual model and `docs/modules/excititor/architecture.md` + `docs/modules/vex-lens/architecture.md` for implementation details.
## Principles
- **Provenance-first:** VEX is stored and referenced as evidence; policy never “rewrites” upstream statements.
- **Conflicts are first-class:** disagreements remain visible; policy chooses a posture for gating and explainability.
- **Deterministic evaluation:** given the same evidence set and policy pack, evaluation must produce identical results (ordering, timestamps, digests).
## Trust Signals Used by Policy
Typical signals include:
- **Issuer identity:** stable provider IDs and (when present) cryptographic identity assertions.
- **Verification status:** signature validity, certificate chain, transparency proof availability (offline-compatible).
- **Trust tier / weight:** tenant-configured tiering (e.g., vendor/distro/internal) and weighting used for precedence.
- **Freshness / staleness:** evidence age relative to time anchors; stale evidence can be down-weighted or gated.
- **Scope fit:** whether the VEX statements scope matches the evaluated artifact/environment.
## Policy Knobs (Posture)
Common policy knobs are expressed as rules rather than hard-coded behavior:
- Require cryptographic verification for certain tiers (or for all `not_affected` gating).
- Define how to treat conflicts (e.g., block on high-tier disagreement, or downgrade to `under_investigation`).
- Set staleness budgets for offline snapshots and how they affect acceptance of `not_affected`.
- Define which justifications are acceptable for suppressing a finding (and whether reachability evidence is required).
## Simulation (What-if Analysis)
Policy simulation is used to preview how changes in VEX trust settings or exception/waiver objects would affect outcomes before promotion. See:
- `docs/api/gateway/policy-exceptions.md` (simulation contract)
- `docs/POLICY_TEMPLATES.md` (policy packs and examples)
## References
- `docs/VEX_CONSENSUS_GUIDE.md`
- `docs/modules/excititor/architecture.md`
- `docs/modules/vex-lens/architecture.md`

View File

@@ -0,0 +1,16 @@
# Policy Examples
Sample `stella-dsl@1` policies illustrating common deployment personas. Each example includes commentary, CLI usage hints, and a compliance checklist.
| Example | Description |
|---------|-------------|
| [Baseline](baseline.md) | Balanced production defaults (block critical, respect strong VEX). |
| [Serverless](serverless.md) | Aggressive blocking for serverless workloads (no High+, pinned base images). |
| [Internal Only](internal-only.md) | Lenient policy for internal/dev environments with KEV safeguards. |
Policy source files (`*.stella`) live alongside the documentation so you can copy/paste or use `stella policy new --from file://...`.
---
*Last updated: 2025-10-26.*

View File

@@ -0,0 +1,92 @@
# Baseline Policy Example (`baseline.stella`)
This sample policy provides a balanced default for production workloads: block critical findings, require strong VEX justifications to suppress advisories, and warn on deprecated runtimes. Use it as a starting point for tenants that want guardrails without excessive noise.
```dsl
policy "Baseline Production Policy" syntax "stella-dsl@1" {
metadata {
description = "Block critical, escalate high, enforce VEX justifications."
tags = ["baseline","production"]
}
profile severity {
map vendor_weight {
source "GHSA" => +0.5
source "OSV" => +0.0
source "VendorX" => -0.2
}
env exposure_adjustments {
if env.exposure == "internet" then +0.5
if env.runtime == "legacy" then +0.3
}
}
rule block_critical priority 5 {
when severity.normalized >= "Critical"
then status := "blocked"
because "Critical severity must be remediated before deploy."
}
rule escalate_high_internet {
when severity.normalized == "High"
and env.exposure == "internet"
then escalate to severity_band("Critical")
because "High severity on internet-exposed asset escalates to critical."
}
rule require_vex_justification {
when vex.any(status in ["not_affected","fixed"])
and vex.justification in ["component_not_present","vulnerable_code_not_present"]
then status := vex.status
annotate winning_statement := vex.latest().statementId
because "Respect strong vendor VEX claims."
}
rule alert_warn_eol_runtime priority 1 {
when severity.normalized <= "Medium"
and sbom.has_tag("runtime:eol")
then warn message "Runtime marked as EOL; upgrade recommended."
because "Deprecated runtime should be upgraded."
}
rule block_ruby_dev priority 4 {
when sbom.any_component(ruby.group("development") and ruby.declared_only())
then status := "blocked"
because "Development-only Ruby gems without install evidence cannot ship."
}
rule warn_ruby_git_sources {
when sbom.any_component(ruby.source("git"))
then warn message "Git-sourced Ruby gem present; review required."
because "Git-sourced Ruby dependencies require explicit review."
}
}
```
## Commentary
- **Severity profile** tightens vendor weights and applies exposure modifiers so internet-facing/high severity pairs escalate automatically.
- **VEX rule** only honours strong justifications, preventing weaker claims from hiding issues.
- **Warnings first** The `alert_warn_eol_runtime` rule name ensures it sorts before the require-VEX rule, keeping alerts visible without flipping to `RequiresVex`.
- **Ruby supply-chain guardrails** enforce Bundler groups and provenance: development-only gems without install evidence are blocked and git-sourced gems trigger review warnings.
- Works well as shared `tenant-global` baseline; use tenant overrides for stricter tolerant environments.
## Try it out
```bash
stella policy new --policy-id P-baseline --template blank --open
stella policy lint examples/policies/baseline.stella
stella policy simulate P-baseline --candidate 1 --sbom sbom:sample-prod
```
## Compliance checklist
- [ ] Policy compiled via `stella policy lint` without diagnostics.
- [ ] Simulation diff reviewed against golden SBOM set.
- [ ] Approval note documents rationale before promoting to production.
- [ ] EOL runtime tags kept up to date in SBOM metadata.
- [ ] VEX vendor allow-list reviewed quarterly.
---
*Last updated: 2025-11-10.*

View File

@@ -0,0 +1,72 @@
# Internal-Only Policy Example (`internal-only.stella`)
A relaxed profile for internal services and development environments: allow Medium severities with warnings, rely on VEX more heavily, but still block KEV/actively exploited advisories.
```dsl
policy "Internal Only Policy" syntax "stella-dsl@1" {
metadata {
description = "Lenient policy for internal / dev tenants."
tags = ["internal","dev"]
}
profile severity {
env exposure_adjustments {
if env.exposure == "internal" then -0.4
if env.stage == "dev" then -0.6
}
}
rule block_kev priority 1 {
when advisory.has_tag("kev")
then status := "blocked"
because "Known exploited vulnerabilities must be remediated."
}
rule allow_medium_with_warning {
when severity.normalized == "Medium"
and env.exposure == "internal"
then warn message "Medium severity permitted in internal environments."
because "Allow Medium findings with warning for internal workloads."
}
rule accept_vendor_vex {
when vex.any(status in ["not_affected","fixed"])
then status := vex.status
annotate justification := vex.latest().justification
because "Trust vendor VEX statements for internal scope."
}
rule quiet_low_priority {
when severity.normalized <= "Low"
then ignore until "2026-01-01T00:00:00Z"
because "Quiet low severity until next annual remediation sweep."
}
}
```
## Commentary
- Suitable for staging/dev tenants with lower blast radius.
- KEV advisories override lenient behaviour to maintain minimum security bar.
- Warnings ensure Medium findings stay visible in dashboards and CLI outputs.
- Quiet rule enforces planned clean-up date; update before expiry.
## Try it out
```bash
stella policy lint examples/policies/internal-only.stella
stella policy simulate P-internal --candidate 1 \
--sbom sbom:internal-service --env exposure=internal --env stage=dev
```
## Compliance checklist
- [ ] Tenant classified as internal-only with documented risk acceptance.
- [ ] KEV feed synced (Concelier) and tags confirmed before relying on rule.
- [ ] Quiet expiry tracked; remediation backlog updated prior to deadline.
- [ ] Developers informed that warnings still affect quality score.
- [ ] Policy not used for production or internet-exposed services.
---
*Last updated: 2025-10-26.*

View File

@@ -0,0 +1,72 @@
# Serverless Policy Example (`serverless.stella`)
Optimised for short-lived serverless workloads: focus on runtime integrity, disallow vulnerable layers entirely, and permit temporary suppressions only with strict justification windows.
```dsl
policy "Serverless Tight Policy" syntax "stella-dsl@1" {
metadata {
description = "Aggressive blocking for serverless runtimes."
tags = ["serverless","prod","strict"]
}
profile severity {
env runtime_overrides {
if env.runtime == "serverless" then +0.7
if env.runtime == "batch" then +0.2
}
}
rule block_any_high {
when severity.normalized >= "High"
then status := "blocked"
because "Serverless workloads block High+ severities."
}
rule forbid_unpinned_base {
when sbom.has_tag("image:latest-tag")
then status := "blocked"
because "Base image must be pinned (no :latest)."
}
rule zero_tolerance_vex {
when vex.any(status == "not_affected")
then requireVex { vendors = ["VendorX","VendorY"], justifications = ["component_not_present"] }
because "Allow not_affected only from trusted vendors with strongest justification."
}
rule temporary_quiet {
when env.deployment == "canary"
and severity.normalized == "Medium"
then ignore until coalesce(env.quietUntil, "2025-12-31T00:00:00Z")
because "Allow short canary quiet window while fix rolls out."
}
}
```
## Commentary
- Designed for serverless tenants where redeploy cost is low and failing fast is preferred.
- `forbid_unpinned_base` enforces supply-chain best practices.
- `temporary_quiet` ensures quiet windows expire automatically; require deployments to set `env.quietUntil`.
- Intended to be layered on top of baseline (override per tenant) or used standalone for serverless-only accounts.
## Try it out
```bash
stella policy lint examples/policies/serverless.stella
stella policy simulate P-serverless --candidate 1 \
--sbom sbom:lambda-hello --env runtime=serverless --env deployment=canary
```
## Compliance checklist
- [ ] Quiet window expirations tracked and documented.
- [ ] Trusted VEX vendor list reviewed quarterly.
- [ ] Deployment pipeline enforces pinned base images before approval.
- [ ] Canary deployments monitored for recurrence before ignoring Medium severity.
- [ ] Serverless teams acknowledge runbook for blocked deployments.
---
*Last updated: 2025-10-26.*

View File

@@ -0,0 +1,56 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://stellaops.local/schemas/policy-auth-signals-lib-115.json",
"title": "PolicyAuthSignal",
"type": "object",
"additionalProperties": false,
"required": ["id", "tenant", "subject", "signal_type", "source", "created", "evidence"],
"properties": {
"id": {"type": "string"},
"tenant": {"type": "string"},
"subject": {"type": "string"},
"signal_type": {"type": "string", "enum": ["reachability", "attestation", "risk", "vex"]},
"source": {"type": "string"},
"confidence": {"type": "number"},
"evidence": {
"type": "array",
"items": {"$ref": "#/$defs/EvidenceRef"},
"minItems": 1
},
"provenance": {"$ref": "#/$defs/Provenance"},
"created": {
"type": "string",
"format": "date-time"
}
},
"$defs": {
"EvidenceRef": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "uri", "digest"],
"properties": {
"kind": {"type": "string", "enum": ["linkset", "runtime", "attestation", "bundle"]},
"uri": {"type": "string"},
"digest": {"type": "string"},
"scope": {"type": "string"}
}
},
"Provenance": {
"type": "object",
"additionalProperties": false,
"properties": {
"pipeline": {"type": "string"},
"inputs": {"type": "array", "items": {"type": "string"}},
"signer": {"type": "string"},
"transparency": {
"type": "object",
"additionalProperties": false,
"properties": {
"rekor_uuid": {"type": ["string", "null"]},
"skip_reason": {"type": ["string", "null"]}
}
}
}
}
}
}