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). Imposed rule: Policies that alter reachability or trust weighting must run in shadow mode first with coverage fixtures; promotion to active is blocked until shadow + coverage gates pass.

This document specifies the stella-dsl@1 grammar, semantics, and guardrails used by 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, the Policy Lifecycle and Policy Runs guides for operational workflows, and the Policy Editor guide for Console authoring.


1 · Design Goals


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";
  }

  rule reachability_gate priority 20 {
    when telemetry.reachability.state == "reachable" and telemetry.reachability.score >= 0.6
    then status := "affected"
    because "Runtime/graph evidence shows reachable code path";
  }

  rule trust_penalty priority 30 {
    when signals.trust_score < 0.4 or signals.entropy_penalty > 0.2
    then severity := severity_band("critical")
    because "Low trust score or high entropy";
  }
}

High-level layout:

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


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:


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.
signalsNormalised signal dictionary: trust_score (0–1), reachability.state (`reachableunreachable
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).

Reachability evidence gate. When reachability.state == "unreachable" but reachability.evidence_ref is missing (or confidence is below the high-confidence threshold), Policy Engine downgrades the state to under_investigation to avoid false “not affected” claims.

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

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


6 · Built-ins (v1)

Function / 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).
reach_state(state)string → ReachStateNormalises reachability state strings (reachable, unreachable, unknown, under_investigation).
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


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 Shadow mode & coverage

9.5 Authoring workflow (quick checklist)

  1. Write/update policy with shadow enabled.
  2. Add/refresh coverage fixtures; run stella policy test.
  3. stella policy lint and stella policy simulate --fixtures ... with expected signals (trust_score, reachability, entropy_penalty) noted in comments.
  4. Submit with attachments: lint, simulate diff, coverage results.
  5. After approval, disable shadow and promote; retain fixtures for regression tests.

9.6 Anti-pattern (flagged by linter)

rule catch_all {
  when true
  then status := "suppressed"
  because "Suppress everything"  // ❌ Fails lint: unbounded suppression
}

10 · Validation & Tooling


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 · Uncertainty Gates (U1/U2/U3)

Uncertainty gates enforce evidence-quality thresholds before a policy is allowed to commit to a high-confidence VEX decision. When evidence is missing or signal entropy is high, a policy should downgrade the verdict to under_investigation rather than risk a false not_affected claim.

The relevant signal is signals.uncertainty.level, which the Policy Engine normalises into three tiers (see §5, Evaluation Context):

LevelMeaningTypical cause
U1Highest uncertainty; core resolution is missing.Missing symbol resolution or unresolved component identity.
U2Moderate uncertainty; coordinates are incomplete.Missing or partial purl / lockfile data.
U3Lowest gated tier; advisory corroboration is weak.Single-source or untrusted advisory.

12.1 Gating on Uncertainty Level

Hold a not_affected outcome until uncertainty is resolved:

rule gate_high_uncertainty priority 4 {
  when vex.any(status == "not_affected")
       and signals.uncertainty.level == "U1"
  then status := "under_investigation"
       annotate uncertainty_gate := "U1: missing core resolution"
  because "High uncertainty must be resolved before accepting not_affected";
}

12.2 Tier-Aware Compound Rules

Combine an uncertainty tier with a reachability state for nuanced gating — for example, allow a downgrade only when both confidence and reachability evidence are strong:

rule accept_low_uncertainty priority 6 {
  when vex.any(status == "not_affected")
       and signals.uncertainty.level == "U3"
       and signals.reachability.state == "unreachable"
       and exists(signals.reachability.evidence_ref)
  then status := vex.status
  because "Low uncertainty plus reachability evidence supports not_affected";
}

12.3 Remediation Guidance

Policies should steer authors and operators toward reducing uncertainty rather than silently accepting a weak verdict. Use annotate to surface the required next step alongside the gated verdict:

Uncertainty levelSuggested remediation
U1Upload debug symbols and resolve unknown components so identity is established.
U2Generate lockfiles and verify package coordinates (purl) so the advisory can be matched precisely.
U3Cross-reference a trusted advisory source or wait for corroboration before accepting the claim.

Restore needed. The detailed uncertainty-gate tier thresholds, the per-gate block/allow matrix, and the YAML configuration block that previously lived in this section were lost in an earlier edit and could not be reconstructed from a verifiable source. Treat the rules above as illustrative until the canonical thresholds are restored from the Policy Engine determinization configuration (see Policy architecture).


13 · Signed Override Enforcement (Sprint 20260112.004)

Signed VEX overrides provide cryptographic assurance that operator decisions (not_affected, compensating controls) are authentic and auditable. The Policy Engine exposes override signature status to DSL rules for enforcement.

13.1 Override Signal Namespace

Within predicates and actions you may reference the following override signals:

SignalTypeDescription
override.signedbooltrue when the VEX override has a valid DSSE signature.
override.rekor_verifiedbooltrue when the override signature is anchored in Rekor transparency log.
override.signing_key_idstringKey identifier used to sign the override.
override.signer_identitystringIdentity (email, OIDC subject) of the signer.
override.envelope_digeststringSHA-256 digest of the DSSE envelope.
override.rekor_log_indexint?Rekor log index if anchored; null otherwise.
override.rekor_integrated_timedatetime?Timestamp when anchored in Rekor.
override.valid_fromdatetime?Override validity window start (if specified).
override.valid_untildatetime?Override validity window end (if specified).
override.within_validity_periodbooltrue when current time is within validity window (or no window specified).
override.key_trust_levelstringTrust level: Unknown, LowTrust, OrganizationTrusted, HighlyTrusted.

13.2 Enforcement Rules

13.2.1 Require Signed Overrides

Block unsigned VEX overrides from being accepted:

rule require_signed_overrides priority 1 {
  when vex.any(status in ["not_affected", "fixed"])
       and not override.signed
  then status := "under_investigation"
       annotate override_blocked := "Unsigned override rejected"
  because "Production environments require signed VEX overrides";
}

13.2.2 Require Rekor Anchoring for Critical Assets

For critical assets, require transparency log anchoring:

rule require_rekor_for_critical priority 2 {
  when env.asset_tier == "critical"
       and vex.any(status == "not_affected")
       and override.signed
       and not override.rekor_verified
  then status := "under_investigation"
       warn message "Critical asset requires Rekor-anchored override"
  because "Critical assets require transparency log verification";
}

13.2.3 Trust Level Gating

Gate override acceptance based on signer trust level:

rule gate_by_trust_level priority 5 {
  when override.signed
       and override.key_trust_level in ["Unknown", "LowTrust"]
       and env.security_posture == "strict"
  then status := "under_investigation"
       annotate trust_gate_failed := override.signer_identity
  because "Strict posture requires OrganizationTrusted or higher";
}

13.2.4 Validity Period Enforcement

Reject expired or not-yet-valid overrides:

rule enforce_validity_period priority 3 {
  when override.signed
       and exists(override.valid_until)
       and not override.within_validity_period
  then status := "affected"
       annotate override_expired := override.valid_until
  because "VEX override has expired or is not yet valid";
}

13.3 Default Enforcement Profile

The default enforcement profile blocks unsigned overrides in production:

settings {
  require_signed_overrides = true;
  require_rekor_for_production = false;
  minimum_trust_level = "OrganizationTrusted";
  enforce_validity_period = true;
}

Override these settings in environment-specific policy packs.

13.4 Offline Mode Considerations

In sealed/offline deployments:

rule offline_safe_override priority 5 {
  when env.sealed_mode == true
       and override.signed
       and override.key_trust_level in ["OrganizationTrusted", "HighlyTrusted"]
  then status := vex.status
  because "Offline mode accepts signed overrides from trusted keys without Rekor";
}

14 · Versioning & Compatibility


15 · Compliance Checklist


Last updated: 2025-12-13 (Sprint 0401).