Restructure solution layout by module
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -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 (Sprint 20). | ||||
|  | ||||
| This document specifies the `stella-dsl@1` grammar, semantics, and guardrails used by Stella Ops 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 (Sprint 20). | ||||
|  | ||||
| This document specifies the `stella-dsl@1` grammar, semantics, and guardrails used by Stella Ops 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).* | ||||
|   | ||||
| @@ -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. | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -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 Kit’s `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 Kit’s `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. | ||||
|   | ||||
| @@ -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 Stella Ops, 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 (<24 h 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 tenant’s 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 <24 h 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 24 h. | | ||||
| | **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 Stella Ops, 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 (<24 h 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 tenant’s 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 <24 h 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 24 h. | | ||||
| | **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).* | ||||
|   | ||||
| @@ -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 Stella Ops, 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 `< 5 s` 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 ≤ 5 min 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 Stella Ops, 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 `< 5 s` 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 ≤ 5 min 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).* | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user