Restructure solution layout by module
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
root
2025-10-28 15:10:40 +02:00
parent 4e3e575db5
commit 68da90a11a
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -1,294 +1,294 @@
# Stella Policy DSL (`stella-dsl@1`)
> **Audience:** Policy authors, reviewers, and tooling engineers building lint/compile flows for the Policy Engine v2 rollout (Sprint20).
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.
---
## 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";
}
}
```
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.
---
## 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; missing fields evaluate to `unknown`. |
| `profile.<name>` | Values computed inside profile blocks (maps, scalars). |
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). |
| `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). |
| `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). |
All built-ins are pure; if inputs are null the result is null unless otherwise noted.
---
## 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 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·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.
---
## 13·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-10-26 (Sprint 20).*
# Stella Policy DSL (`stella-dsl@1`)
> **Audience:** Policy authors, reviewers, and tooling engineers building lint/compile flows for the Policy Engine v2 rollout (Sprint20).
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.
---
## 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";
}
}
```
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.
---
## 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; missing fields evaluate to `unknown`. |
| `profile.<name>` | Values computed inside profile blocks (maps, scalars). |
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). |
| `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). |
| `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). |
All built-ins are pure; if inputs are null the result is null unless otherwise noted.
---
## 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 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·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.
---
## 13·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-10-26 (Sprint 20).*

View File

@@ -32,7 +32,7 @@ Effects are validated at bind time (`PolicyBinder`), while instances are ingeste
| `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/StellaOps.Policy.Tests/PolicyBinderTests.cs:33`). Routing templates additionally declare `authorityRouteId` and `requireMfa` flags for governance routing.
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.
---
@@ -68,7 +68,7 @@ Only one exception effect is applied per finding. Evaluation proceeds as follows
- `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/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:209` for tie-break coverage.
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.
---
@@ -81,7 +81,7 @@ These rules guarantee deterministic selection even when multiple waivers overlap
| `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/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:130` & `src/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:169`).
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`).
---
@@ -132,9 +132,9 @@ Example verdict excerpt (JSON):
## 8·Testing References
- `src/StellaOps.Policy.Tests/PolicyBinderTests.cs:33` Validates schema rules for defining effects, routing templates, and downgrade guardrails.
- `src/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:130` Covers suppression, downgrade, and metadata propagation.
- `src/StellaOps.Policy.Engine.Tests/PolicyEvaluatorTests.cs:209` Confirms specificity ordering and metadata forwarding for competing exceptions.
- `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.
---

View File

@@ -1,124 +1,124 @@
# 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.
## 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/24_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.
# 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.
## 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/24_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

@@ -1,238 +1,238 @@
# 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.
---
## 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.
```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.
- 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).
- 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.
- 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 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.6 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`, `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.
---
## 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/observability/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. |
| **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`). |
| **Activation health** | Approve 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.
- [ ] **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-10-26 (Sprint 20).*
# 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.
---
## 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.
```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.
- 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).
- 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.
- 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 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.6 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`, `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.
---
## 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/observability/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. |
| **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`). |
| **Activation health** | Approve 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.
- [ ] **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-10-26 (Sprint 20).*

View File

@@ -1,173 +1,173 @@
# Policy Engine Overview
> **Goal:** Evaluate organisation policies deterministically against scanner SBOMs, Concelier advisories, and Excititor VEX evidence, then publish effective findings that downstream services can trust.
This document introduces the v2 Policy Engine: how the service fits into StellaOps, the artefacts it produces, the contracts it honours, and the guardrails that keep policy decisions reproducible across air-gapped and connected deployments.
---
## 1·Role in the Platform
- **Purpose:** Compose policy verdicts by reconciling SBOM inventory, advisory metadata, VEX statements, and organisation rules.
- **Form factor:** Dedicated `.NET 10` Minimal API host (`StellaOps.Policy.Engine`) plus worker orchestration. Policies are defined in `stella-dsl@1` packs compiled to an intermediate representation (IR) with a stable SHA-256 digest.
- **Tenancy:** All workloads run under Authority-enforced scopes (`policy:*`, `findings:read`, `effective:write`). Only the Policy Engine identity may materialise effective findings collections.
- **Consumption:** Findings ledger, Console, CLI, and Notify read the published `effective_finding_{policyId}` materialisations and policy run ledger (`policy_runs`).
- **Offline parity:** Bundled policies import/export alongside advisories and VEX. In sealed mode the engine degrades gracefully, annotating explanations whenever cached signals replace live lookups.
---
## 2·High-Level Architecture
```mermaid
flowchart LR
subgraph Inputs
A[Scanner SBOMs<br/>Inventory & Usage]
B[Concelier Advisories<br/>Canonical linksets]
C[Excititor VEX<br/>Consensus status]
D[Policy Packs<br/>stella-dsl@1]
end
subgraph PolicyEngine["StellaOps.Policy.Engine"]
P1[DSL Compiler<br/>IR + Digest]
P2[Joiners<br/>SBOM ↔ Advisory ↔ VEX]
P3[Deterministic Evaluator<br/>Rule hits + scoring]
P4[Materialisers<br/>effective findings]
P5[Run Orchestrator<br/>Full & incremental]
end
subgraph Outputs
O1[Effective Findings Collections]
O2[Explain Traces<br/>Rule hit lineage]
O3[Metrics & Traces<br/>policy_run_seconds,<br/>rules_fired_total]
O4[Simulation/Preview Feeds<br/>CLI & Studio]
end
A --> P2
B --> P2
C --> P2
D --> P1 --> P3
P2 --> P3 --> P4 --> O1
P3 --> O2
P5 --> P3
P3 --> O3
P3 --> O4
```
---
## 3·Core Concepts
| Concept | Description |
|---------|-------------|
| **Policy Pack** | Versioned bundle of DSL documents, metadata, and checksum manifest. Packs import/export via CLI and Offline Kit bundles. |
| **Policy Digest** | SHA-256 of the canonical IR; used for caching, explain trace attribution, and audit proofs. |
| **Effective Findings** | Append-only Mongo collections (`effective_finding_{policyId}`) storing the latest verdict per finding, plus history sidecars. |
| **Policy Run** | Execution record persisted in `policy_runs` capturing inputs, run mode, timings, and determinism hash. |
| **Explain Trace** | Structured tree showing rule matches, data provenance, and scoring components for UI/CLI explain features. |
| **Simulation** | Dry-run evaluation that compares a candidate pack against the active pack and produces verdict diffs without persisting results. |
| **Incident Mode** | Elevated sampling/trace capture toggled automatically when SLOs breach; emits events for Notifier and Timeline Indexer. |
---
## 4·Inputs & Pre-processing
### 4.1 SBOM Inventory
- **Source:** Scanner.WebService publishes inventory/usage SBOMs plus BOM-Index (roaring bitmap) metadata.
- **Consumption:** Policy joiners use the index to expand candidate components quickly, keeping evaluation under the `<5s` warm path budget.
- **Schema:** CycloneDX Protobuf + JSON views; Policy Engine reads canonical projections via shared SBOM adapters.
### 4.2 Advisory Corpus
- **Source:** Concelier exports canonical advisories with deterministic identifiers, linksets, and equivalence tables.
- **Contract:** Policy Engine only consumes raw `content.raw`, `identifiers`, and `linkset` fields per Aggregation-Only Contract (AOC); derived precedence remains a policy concern.
### 4.3 VEX Evidence
- **Source:** Excititor consensus service resolves OpenVEX / CSAF statements, preserving conflicts.
- **Usage:** Policy rules can require specific VEX vendors or justification codes; evaluator records when cached evidence substitutes for live statements (sealed mode).
### 4.4 Policy Packs
- Authored in Policy Studio or CLI, validated against the `stella-dsl@1` schema.
- Compiler performs canonicalisation (ordering, defaulting) before emitting IR and digest.
- Packs bundle scoring profiles, allowlist metadata, and optional reachability weighting tables.
---
## 5·Evaluation Flow
1. **Run selection** Orchestrator accepts `full`, `incremental`, or `simulate` jobs. Incremental runs listen to change streams from Concelier, Excititor, and SBOM imports to scope re-evaluation.
2. **Input staging** Candidates fetched in deterministic batches; identity graph from Concelier strengthens PURL lookups.
3. **Rule execution** Evaluator walks rules in lexical order (first-match wins). Actions available: `block`, `ignore`, `warn`, `defer`, `escalate`, `requireVex`, each supporting quieting semantics where permitted.
4. **Scoring** `PolicyScoringConfig` applies severity, trust, reachability weights plus penalties (`warnPenalty`, `ignorePenalty`, `quietPenalty`).
5. **Verdict and explain** Engine constructs `PolicyVerdict` records with inputs, quiet flags, unknown confidence bands, and provenance markers; explain trees capture rule lineage.
6. **Materialisation** Effective findings collections are upserted append-only, stamped with run identifier, policy digest, and tenant.
7. **Publishing** Completed run writes to `policy_runs`, emits metrics (`policy_run_seconds`, `rules_fired_total`, `vex_overrides_total`), and raises events for Console/Notify subscribers.
---
## 6·Run Modes
| Mode | Trigger | Scope | Persistence | Typical Use |
|------|---------|-------|-------------|-------------|
| **Full** | Manual CLI (`stella policy run`), scheduled nightly, or emergency rebaseline | Entire tenant | Writes effective findings and run record | After policy publish or major advisory/VEX import |
| **Incremental** | Change-stream queue driven by Concelier/Excititor/SBOM deltas | Only affected artefacts | Writes effective findings and run record | Continuous upkeep; ensures SLA ≤5min from source change |
| **Simulate** | CLI/Studio preview, CI pipelines | Candidate subset (diff against baseline) | No materialisation; produces explain & diff payloads | Policy authoring, CI regression suites |
All modes are cancellation-aware and checkpoint progress for replay in case of deployment restarts.
---
## 7·Outputs & Integrations
- **APIs** Minimal API exposes policy CRUD, run orchestration, explain fetches, and cursor-based listing of effective findings (see `/docs/api/policy.md` once published).
- **CLI** `stella policy simulate/run/show` commands surface JSON verdicts, exit codes, and diff summaries suitable for CI gating.
- **Console / Policy Studio** UI reads explain traces, policy metadata, approval workflow status, and simulation diffs to guide reviewers.
- **Findings Ledger** Effective findings feed downstream export, Notify, and risk scoring jobs.
- **Air-gap bundles** Offline Kit includes policy packs, scoring configs, and explain indexes; export commands generate DSSE-signed bundles for transfer.
---
## 8·Determinism & Guardrails
- **Deterministic inputs** All joins rely on canonical linksets and equivalence tables; batches are sorted, and random/wall-clock APIs are blocked by static analysis plus runtime guards (`ERR_POL_004`).
- **Stable outputs** Canonical JSON serializers sort keys; digests recorded in run metadata enable reproducible diffs across machines.
- **Idempotent writes** Materialisers upsert using `{policyId, findingId, tenant}` keys and retain prior versions with append-only history.
- **Sandboxing** Policy evaluation executes in-process with timeouts; restart-only plug-ins guarantee no runtime DLL injection.
- **Compliance proof** Every run stores digest of inputs (policy, SBOM batch, advisory snapshot) so auditors can replay decisions offline.
---
## 9·Security, Tenancy & Offline Notes
- **Authority scopes:** Gateway enforces `policy:read`, `policy:write`, `policy:simulate`, `policy:runs`, `findings:read`, `effective:write`. Service identities must present DPoP-bound tokens.
- **Tenant isolation:** Collections partition by tenant identifier; cross-tenant queries require explicit admin scopes and return audit warnings.
- **Sealed mode:** In air-gapped deployments the engine surfaces `sealed=true` hints in explain traces, warning about cached EPSS/KEV data and suggesting bundle refreshes (see `docs/airgap/EPIC_16_AIRGAP_MODE.md` §3.7).
- **Observability:** Structured logs carry correlation IDs matching orchestrator job IDs; metrics integrate with OpenTelemetry exporters; sampled rule-hit logs redact policy secrets.
- **Incident response:** Incident mode can be forced via API, boosting trace retention and notifying Notifier through `policy.incident.activated` events.
---
## 10·Working with Policy Packs
1. **Author** in Policy Studio or edit DSL files locally. Validate with `stella policy lint`.
2. **Simulate** against golden SBOM fixtures (`stella policy simulate --sbom fixtures/*.json`). Inspect explain traces for unexpected overrides.
3. **Publish** via API or CLI; Authority enforces review/approval workflows (`draft → review → approve → rollout`).
4. **Monitor** the subsequent incremental runs; if determinism diff fails in CI, roll back pack while investigating digests.
5. **Bundle** packs for offline sites with `stella policy bundle export` and distribute via Offline Kit.
---
## 11·Compliance Checklist
- [ ] **Scopes enforced:** Confirm gateway policy requires `policy:*` and `effective:write` scopes for all mutating endpoints.
- [ ] **Determinism guard active:** Static analyzer blocks clock/RNG usage; CI determinism job diffing repeated runs passes.
- [ ] **Materialisation audit:** Effective findings collections use append-only writers and retain history per policy run.
- [ ] **Explain availability:** UI/CLI expose explain traces for every verdict; sealed-mode warnings display when cached evidence is used.
- [ ] **Offline parity:** Policy bundles (import/export) tested in sealed environment; air-gap degradations documented for operators.
- [ ] **Observability wired:** Metrics (`policy_run_seconds`, `rules_fired_total`, `vex_overrides_total`) and sampled rule hit logs emit to the shared telemetry pipeline with correlation IDs.
- [ ] **Documentation synced:** API (`/docs/api/policy.md`), DSL grammar (`/docs/policy/dsl.md`), lifecycle (`/docs/policy/lifecycle.md`), and run modes (`/docs/policy/runs.md`) cross-link back to this overview.
---
*Last updated: 2025-10-26 (Sprint 20).*
# Policy Engine Overview
> **Goal:** Evaluate organisation policies deterministically against scanner SBOMs, Concelier advisories, and Excititor VEX evidence, then publish effective findings that downstream services can trust.
This document introduces the v2 Policy Engine: how the service fits into StellaOps, the artefacts it produces, the contracts it honours, and the guardrails that keep policy decisions reproducible across air-gapped and connected deployments.
---
## 1·Role in the Platform
- **Purpose:** Compose policy verdicts by reconciling SBOM inventory, advisory metadata, VEX statements, and organisation rules.
- **Form factor:** Dedicated `.NET 10` Minimal API host (`StellaOps.Policy.Engine`) plus worker orchestration. Policies are defined in `stella-dsl@1` packs compiled to an intermediate representation (IR) with a stable SHA-256 digest.
- **Tenancy:** All workloads run under Authority-enforced scopes (`policy:*`, `findings:read`, `effective:write`). Only the Policy Engine identity may materialise effective findings collections.
- **Consumption:** Findings ledger, Console, CLI, and Notify read the published `effective_finding_{policyId}` materialisations and policy run ledger (`policy_runs`).
- **Offline parity:** Bundled policies import/export alongside advisories and VEX. In sealed mode the engine degrades gracefully, annotating explanations whenever cached signals replace live lookups.
---
## 2·High-Level Architecture
```mermaid
flowchart LR
subgraph Inputs
A[Scanner SBOMs<br/>Inventory & Usage]
B[Concelier Advisories<br/>Canonical linksets]
C[Excititor VEX<br/>Consensus status]
D[Policy Packs<br/>stella-dsl@1]
end
subgraph PolicyEngine["StellaOps.Policy.Engine"]
P1[DSL Compiler<br/>IR + Digest]
P2[Joiners<br/>SBOM ↔ Advisory ↔ VEX]
P3[Deterministic Evaluator<br/>Rule hits + scoring]
P4[Materialisers<br/>effective findings]
P5[Run Orchestrator<br/>Full & incremental]
end
subgraph Outputs
O1[Effective Findings Collections]
O2[Explain Traces<br/>Rule hit lineage]
O3[Metrics & Traces<br/>policy_run_seconds,<br/>rules_fired_total]
O4[Simulation/Preview Feeds<br/>CLI & Studio]
end
A --> P2
B --> P2
C --> P2
D --> P1 --> P3
P2 --> P3 --> P4 --> O1
P3 --> O2
P5 --> P3
P3 --> O3
P3 --> O4
```
---
## 3·Core Concepts
| Concept | Description |
|---------|-------------|
| **Policy Pack** | Versioned bundle of DSL documents, metadata, and checksum manifest. Packs import/export via CLI and Offline Kit bundles. |
| **Policy Digest** | SHA-256 of the canonical IR; used for caching, explain trace attribution, and audit proofs. |
| **Effective Findings** | Append-only Mongo collections (`effective_finding_{policyId}`) storing the latest verdict per finding, plus history sidecars. |
| **Policy Run** | Execution record persisted in `policy_runs` capturing inputs, run mode, timings, and determinism hash. |
| **Explain Trace** | Structured tree showing rule matches, data provenance, and scoring components for UI/CLI explain features. |
| **Simulation** | Dry-run evaluation that compares a candidate pack against the active pack and produces verdict diffs without persisting results. |
| **Incident Mode** | Elevated sampling/trace capture toggled automatically when SLOs breach; emits events for Notifier and Timeline Indexer. |
---
## 4·Inputs & Pre-processing
### 4.1 SBOM Inventory
- **Source:** Scanner.WebService publishes inventory/usage SBOMs plus BOM-Index (roaring bitmap) metadata.
- **Consumption:** Policy joiners use the index to expand candidate components quickly, keeping evaluation under the `<5s` warm path budget.
- **Schema:** CycloneDX Protobuf + JSON views; Policy Engine reads canonical projections via shared SBOM adapters.
### 4.2 Advisory Corpus
- **Source:** Concelier exports canonical advisories with deterministic identifiers, linksets, and equivalence tables.
- **Contract:** Policy Engine only consumes raw `content.raw`, `identifiers`, and `linkset` fields per Aggregation-Only Contract (AOC); derived precedence remains a policy concern.
### 4.3 VEX Evidence
- **Source:** Excititor consensus service resolves OpenVEX / CSAF statements, preserving conflicts.
- **Usage:** Policy rules can require specific VEX vendors or justification codes; evaluator records when cached evidence substitutes for live statements (sealed mode).
### 4.4 Policy Packs
- Authored in Policy Studio or CLI, validated against the `stella-dsl@1` schema.
- Compiler performs canonicalisation (ordering, defaulting) before emitting IR and digest.
- Packs bundle scoring profiles, allowlist metadata, and optional reachability weighting tables.
---
## 5·Evaluation Flow
1. **Run selection** Orchestrator accepts `full`, `incremental`, or `simulate` jobs. Incremental runs listen to change streams from Concelier, Excititor, and SBOM imports to scope re-evaluation.
2. **Input staging** Candidates fetched in deterministic batches; identity graph from Concelier strengthens PURL lookups.
3. **Rule execution** Evaluator walks rules in lexical order (first-match wins). Actions available: `block`, `ignore`, `warn`, `defer`, `escalate`, `requireVex`, each supporting quieting semantics where permitted.
4. **Scoring** `PolicyScoringConfig` applies severity, trust, reachability weights plus penalties (`warnPenalty`, `ignorePenalty`, `quietPenalty`).
5. **Verdict and explain** Engine constructs `PolicyVerdict` records with inputs, quiet flags, unknown confidence bands, and provenance markers; explain trees capture rule lineage.
6. **Materialisation** Effective findings collections are upserted append-only, stamped with run identifier, policy digest, and tenant.
7. **Publishing** Completed run writes to `policy_runs`, emits metrics (`policy_run_seconds`, `rules_fired_total`, `vex_overrides_total`), and raises events for Console/Notify subscribers.
---
## 6·Run Modes
| Mode | Trigger | Scope | Persistence | Typical Use |
|------|---------|-------|-------------|-------------|
| **Full** | Manual CLI (`stella policy run`), scheduled nightly, or emergency rebaseline | Entire tenant | Writes effective findings and run record | After policy publish or major advisory/VEX import |
| **Incremental** | Change-stream queue driven by Concelier/Excititor/SBOM deltas | Only affected artefacts | Writes effective findings and run record | Continuous upkeep; ensures SLA ≤5min from source change |
| **Simulate** | CLI/Studio preview, CI pipelines | Candidate subset (diff against baseline) | No materialisation; produces explain & diff payloads | Policy authoring, CI regression suites |
All modes are cancellation-aware and checkpoint progress for replay in case of deployment restarts.
---
## 7·Outputs & Integrations
- **APIs** Minimal API exposes policy CRUD, run orchestration, explain fetches, and cursor-based listing of effective findings (see `/docs/api/policy.md` once published).
- **CLI** `stella policy simulate/run/show` commands surface JSON verdicts, exit codes, and diff summaries suitable for CI gating.
- **Console / Policy Studio** UI reads explain traces, policy metadata, approval workflow status, and simulation diffs to guide reviewers.
- **Findings Ledger** Effective findings feed downstream export, Notify, and risk scoring jobs.
- **Air-gap bundles** Offline Kit includes policy packs, scoring configs, and explain indexes; export commands generate DSSE-signed bundles for transfer.
---
## 8·Determinism & Guardrails
- **Deterministic inputs** All joins rely on canonical linksets and equivalence tables; batches are sorted, and random/wall-clock APIs are blocked by static analysis plus runtime guards (`ERR_POL_004`).
- **Stable outputs** Canonical JSON serializers sort keys; digests recorded in run metadata enable reproducible diffs across machines.
- **Idempotent writes** Materialisers upsert using `{policyId, findingId, tenant}` keys and retain prior versions with append-only history.
- **Sandboxing** Policy evaluation executes in-process with timeouts; restart-only plug-ins guarantee no runtime DLL injection.
- **Compliance proof** Every run stores digest of inputs (policy, SBOM batch, advisory snapshot) so auditors can replay decisions offline.
---
## 9·Security, Tenancy & Offline Notes
- **Authority scopes:** Gateway enforces `policy:read`, `policy:write`, `policy:simulate`, `policy:runs`, `findings:read`, `effective:write`. Service identities must present DPoP-bound tokens.
- **Tenant isolation:** Collections partition by tenant identifier; cross-tenant queries require explicit admin scopes and return audit warnings.
- **Sealed mode:** In air-gapped deployments the engine surfaces `sealed=true` hints in explain traces, warning about cached EPSS/KEV data and suggesting bundle refreshes (see `docs/airgap/EPIC_16_AIRGAP_MODE.md` §3.7).
- **Observability:** Structured logs carry correlation IDs matching orchestrator job IDs; metrics integrate with OpenTelemetry exporters; sampled rule-hit logs redact policy secrets.
- **Incident response:** Incident mode can be forced via API, boosting trace retention and notifying Notifier through `policy.incident.activated` events.
---
## 10·Working with Policy Packs
1. **Author** in Policy Studio or edit DSL files locally. Validate with `stella policy lint`.
2. **Simulate** against golden SBOM fixtures (`stella policy simulate --sbom fixtures/*.json`). Inspect explain traces for unexpected overrides.
3. **Publish** via API or CLI; Authority enforces review/approval workflows (`draft → review → approve → rollout`).
4. **Monitor** the subsequent incremental runs; if determinism diff fails in CI, roll back pack while investigating digests.
5. **Bundle** packs for offline sites with `stella policy bundle export` and distribute via Offline Kit.
---
## 11·Compliance Checklist
- [ ] **Scopes enforced:** Confirm gateway policy requires `policy:*` and `effective:write` scopes for all mutating endpoints.
- [ ] **Determinism guard active:** Static analyzer blocks clock/RNG usage; CI determinism job diffing repeated runs passes.
- [ ] **Materialisation audit:** Effective findings collections use append-only writers and retain history per policy run.
- [ ] **Explain availability:** UI/CLI expose explain traces for every verdict; sealed-mode warnings display when cached evidence is used.
- [ ] **Offline parity:** Policy bundles (import/export) tested in sealed environment; air-gap degradations documented for operators.
- [ ] **Observability wired:** Metrics (`policy_run_seconds`, `rules_fired_total`, `vex_overrides_total`) and sampled rule hit logs emit to the shared telemetry pipeline with correlation IDs.
- [ ] **Documentation synced:** API (`/docs/api/policy.md`), DSL grammar (`/docs/policy/dsl.md`), lifecycle (`/docs/policy/lifecycle.md`), and run modes (`/docs/policy/runs.md`) cross-link back to this overview.
---
*Last updated: 2025-10-26 (Sprint 20).*

View File

@@ -43,7 +43,7 @@ All modes record their status in `policy_runs` with deterministic metadata:
}
```
> **Schemas & samples:** see `src/StellaOps.Scheduler.Models/docs/SCHED-MODELS-20-001-POLICY-RUNS.md` and the fixtures in `samples/api/scheduler/policy-*.json` for canonical payloads consumed by CLI/UI/worker integrations.
> **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` for canonical payloads consumed by CLI/UI/worker integrations.
---