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 for architectural context and the upcoming lifecycle/run guides for operational workflows.


1 · Design Goals

  • Deterministic: Same policy + same inputs ⇒ identical findings on every machine.
  • Declarative: No arbitrary loops, network calls, or clock access.
  • Explainable: Every decision records the rule, inputs, and rationale in the explain trace.
  • Lean authoring: Common precedence, severity, and suppression patterns are first-class.
  • Offline-friendly: Grammar and built-ins avoid cloud dependencies, run the same in sealed deployments.
  • Reachability-aware: Policies can consume reachability lattice states (ReachState) and evidence scores to drive VEX gates (not_affected, under_investigation, affected).

2 · Document Structure

Policy packs ship one or more .stella files. Each file contains exactly one policy block:

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:

SectionPurpose
metadataOptional descriptive fields surfaced in Console/CLI.
importsReserved for future reuse (not yet implemented in @1).
profile blocksDeclarative scoring modifiers (severity, trust, reachability).
rule blocksWhen/then logic applied to each (component, advisory, vex[]) tuple.
settingsOptional 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)

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:

NamespaceFieldsDescription
sbompurl, name, version, licenses, layerDigest, tags, usedByEntrypointComponent metadata from Scanner.
advisoryid, source, aliases, severity, cvss, publishedAt, modifiedAt, content.rawCanonical Concelier advisory view.
vexstatus, justification, statementId, timestamp, scopeCurrent VEX statement when iterating; aggregator helpers available.
vex.any(...), vex.all(...), vex.count(...)Functions operating over all matching statements.
runpolicyId, policyVersion, tenant, timestampMetadata for explain annotations.
envArbitrary key/value pairs injected per run (e.g., environment, runtime).
telemetryOptional reachability signals. Example fields: telemetry.reachability.state, telemetry.reachability.score, telemetry.reachability.policyVersion. Missing fields evaluate to unknown.
secretfindings, bundle, helper predicatesPopulated when the Secrets Analyzer runs. Exposes masked leak findings and bundle metadata for policy decisions.
profile.<name>Values computed inside profile blocks (maps, scalars).

Secrets namespace. When StellaOps.Scanner.Analyzers.Secrets is enabled the Policy Engine receives masked findings (secret.findings[*]) plus bundle metadata (secret.bundle.id, secret.bundle.version). Policies should rely on the helper predicates listed below rather than reading raw arrays to preserve determinism and future compatibility.

Missing fields evaluate to null, which is falsey in boolean context and propagates through comparisons unless explicitly checked.


6 · Built-ins (v1)

Function / PropertySignatureDescription
normalize_cvss(advisory)Advisory → SeverityScalarParses advisory.content.raw for CVSS data; falls back to policy maps.
cvss(score, vector)double × string → SeverityScalarConstructs a severity object manually.
severity_band(value)string → SeverityBandNormalises strings like "critical", "medium".
risk_score(base, modifiers...)VariadicMultiplies numeric modifiers (severity × trust × reachability).
vex.any(predicate)(Statement → bool) → booltrue if any statement satisfies predicate.
vex.all(predicate)(Statement → bool) → booltrue if all statements satisfy predicate.
vex.latest()→ StatementLexicographically newest statement.
advisory.has_tag(tag)string → boolChecks advisory metadata tags.
advisory.matches(pattern)string → boolGlob match against advisory identifiers.
sbom.has_tag(tag)string → boolUses SBOM inventory tags (usage vs inventory).
sbom.any_component(predicate)(Component → bool) → boolIterates SBOM components, exposing component plus language scopes (e.g., ruby).
exists(expression)→ booltrue when value is non-null/empty.
coalesce(a, b, ...)→ valueFirst non-null argument.
days_between(dateA, dateB)→ intAbsolute day difference (UTC).
percent_of(part, whole)→ doubleFractions for scoring adjustments.
lowercase(text)string → stringNormalises casing deterministically (InvariantCulture).
secret.hasFinding(ruleId?, severity?, confidence?)→ boolTrue if any secret leak finding matches optional filters.
secret.match.count(ruleId?)→ intCount of findings, optionally scoped to a rule ID.
secret.bundle.version(required)string → boolEnsures the active secret rule bundle version ≥ required (semantic compare).
secret.mask.applied→ boolIndicates whether masking succeeded for all surfaced payloads.
secret.path.allowlist(patterns)list<string> → boolTrue when all findings fall within allowed path patterns (useful for waivers).

All built-ins are pure; if inputs are null the result is null unless otherwise noted.


6.1 · Ruby Component Scope

Inside sbom.any_component(...), Ruby gems surface a ruby scope with the following helpers:

HelperSignatureDescription
ruby.group(name)string → boolMatches Bundler group membership (development, test, etc.).
ruby.groups()→ set<string>Returns all groups for the active component.
ruby.declared_only()→ booltrue when no vendor cache artefacts were observed for the gem.
ruby.source(kind?)string? → boolReturns the raw source when called without args, or matches provenance kinds (registry, git, path, vendor-cache).
ruby.capability(name)string → boolChecks capability flags emitted by the analyzer (exec, net, scheduler, scheduler.activejob, etc.).
ruby.capability_any(names)set<string> → booltrue when any capability in the set is present.

Scheduler capability sub-types use dot notation (ruby.capability("scheduler.sidekiq")) and inherit from the broad scheduler capability.


7 · Rule Semantics

  1. Ordering: Rules execute in ascending priority. When priorities tie, lexical order defines precedence.
  2. Short-circuit: Once a rule sets status, subsequent rules only execute if they use combine. Use this sparingly to avoid ambiguity.
  3. Actions:
    • status := <string> – Allowed values: affected, not_affected, fixed, suppressed, under_investigation, escalated.
    • severity := <SeverityScalar> – Either from normalize_cvss, cvss, or numeric map; ensures normalized and score.
    • ignore until <ISO-8601> – Temporarily treats finding as suppressed until timestamp; recorded in explain trace.
    • warn message "<text>" – Adds warn verdict and deducts warnPenalty.
    • escalate to severity_band("critical") when condition – Forces verdict severity upward when condition true.
    • requireVex { vendors = ["VendorX"], justifications = ["component_not_present"] } – Fails evaluation if matching VEX evidence absent.
    • annotate reason := "text" – Adds free-form key/value pairs to explain payload.
  4. Because clause: Mandatory for actions changing status or severity; captured verbatim in explain traces.

8 · Scoping Helpers

  • Maps: Use profile severity { map vendor_weight { ... } } to declare additive factors. Retrieve with profile.severity.vendor_weight["GHSA"].
  • Environment overrides: env profiles allow conditional adjustments based on runtime metadata.
  • Tenancy: run.tenant ensures policies remain tenant-aware; avoid hardcoding single-tenant IDs.
  • Default values: Use settings { default_status = "affected"; } to override built-in defaults.

9 · Examples

9.1 Baseline Severity Normalisation

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

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

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)

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-patternRiskMitigation
Catch-all suppress/ignore without scopeMasks all findingsLinter blocks rules with when true unless priority > 1000 and justification includes remediation plan.
Comparing strings with inconsistent casingMissed matchesWrap comparisons in lowercase(value) to align casing or normalise metadata during ingest.
Referencing telemetry without fallbackNull propagationWrap access in exists(telemetry.reachability).
Hardcoding tenant IDsBreaks multi-tenantPrefer env.tenantTag or metadata-sourced predicates.
Duplicated rule namesExplain trace ambiguityCompiler 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-11-05 (Sprint 21).