Skip to main content
Version: main-dev

Rules and criteria

Rules are the evaluation heart of Regis. A rule is the policy decision your playbook makes: it binds a reusable criterion to concrete options, a severity level, and a tier. Criteria are shipped by analyzers; rules are written by you in a playbook. Their results feed into the overall score.

The four-layer model

Regis separates what an analyzer detects from the policy you enforce through four layers. Read the chain from the bottom up: evidence is aggregated into measurements, measurements are evaluated by conditions, and conditions are bound into decisions.

LayerWhat it isWho owns it
findingA raw detection of a problem (a CVE on a package, a leaked secret). Evidence for drill-down.Analyzer
metricAn aggregate measurement an analyzer exposes (critical_count, has_sbom, score). Lives under results.<analyzer>.<metric>. This is what criteria evaluate.Analyzer
criterionA reusable, parameterized condition shipped by an analyzer (for example, the cve-count criterion). Policy-neutral: it carries a JSON Logic condition plus open params.Analyzer
ruleThe policy decision: a criterion bound to concrete options, a severity level, and a tier. This is what you write in a playbook.You
info

SBOM components are inventory, not findings — having a package is not, by itself, a problem. See Analyzers.

Criteria vs. rules

A criterion is policy-neutral. The cve-count criterion knows how to count vulnerabilities at a given severity and compare the count against a threshold, but it does not decide which severity matters or how many are too many. Those are policy choices.

A rule makes those choices. You build a rule by binding a criterion to:

  • concrete options (the values its params take — for example, level: critical, max_count: 0);
  • a severity level (critical, high, warning…) that drives scoring;
  • the tier it contributes to.

The same criterion can back many rules. A single cve-count criterion can be bound once to forbid all critical CVEs and again to tolerate up to ten high-severity CVEs.

How rules are evaluated

When regis runs an analysis, the rules engine:

  1. Builds the criterion catalogue — the reusable, parameterized conditions shipped by every analyzer that participated in the run, plus the core registry-domain-whitelist. The catalogue is not evaluated on its own.
  2. Resolves the playbook's declared rules against that catalogue. The evaluated set is exactly what the playbook declares: there is no implicit inheritance of analyzer defaults.
  3. Evaluates each active rule's condition against the analysis report using JSON Logic.
  4. Interpolates pass/fail messages with live report values.
  5. Produces a scored rules report.

The criterion catalogue

Each analyzer ships reusable criterion templates (e.g. cve:cve-count, oci:max-size). These templates are a catalogue you bind from a playbook — they are never evaluated unless a playbook declares them. To evaluate one, add a rule under spec.rules that binds the criterion:

tip

For the full list of standard criteria, their parameters, and condition details, see the Rules Reference.

You can inspect the catalogue at any time from the CLI:

# List all criteria in the catalogue (table)
regis rules list

# Show the full definition of a specific criterion
regis rules show cve-count

Writing rules in a playbook

Add a rules list under spec in your playbook.yaml to declare, bind, or customize rules.

Binding a catalogue criterion

Reference a criterion from the catalogue by its provider and criterion key. Set the level, options, or messages you want to enforce:

spec:
rules:
# Demote critical CVE rule to a warning
- provider: cve
criterion: fix-available
level: warning
messages:
fail: "${results.cve.fixed_count} patchable vulnerabilities found — please fix soon."

# Disable a rule entirely
- provider: oci
criterion: platforms-count
enable: false

# Restrict to your private registry only
- provider: core
criterion: registry-domain-whitelist
options:
domains: ["my-private-registry.example.com"]
note

The criterion: key replaces the older rule: key, which referenced the same thing. rule: still works as a deprecated alias and emits a warning. To migrate existing playbooks automatically, run regis playbook migrate — see the migration guide.

Binding a criterion several times

Many criteria are designed to be bound multiple times with different options. Use provider + criterion + options to create a named rule. Give each binding its own slug:

spec:
rules:
# Block any critical CVEs
- provider: cve
criterion: cve-count
slug: cve-critical
options:
level: critical
max_count: 0

# Allow up to 10 high-severity CVEs
- provider: cve
criterion: cve-count
slug: cve-high-tolerance
options:
level: high
max_count: 10

When no slug is provided, the engine generates one automatically from the criterion name and the level option (for example, cve-count.critical).

note

Criteria such as cve/cve-count, hadolint/severity-count, or dockle/severity-count are multi-purpose. Rather than shipping one hard-coded rule per severity, a single criterion can be bound as many times as you need.

Adding a fully custom rule

You can define completely new rules with arbitrary JSON Logic conditions instead of binding a shipped criterion. Metrics are read from the results.* namespace:

spec:
rules:
- slug: company-label-required
description: Image must carry the company owner label.
level: critical
tags: [compliance]
condition:
"in":
[
"my-company.owner",
{ "keys": [{ "var": "results.oci.platforms.0.labels" }] },
]
messages:
pass: "Company label is present."
fail: "Missing 'my-company.owner' label."

Referencing meta in rules

Values passed via regis analyze --meta <key>=<value> are exposed to rules under the metadata.* namespace (the canonical path). The key's dotted notation becomes a nested structure:

regis analyze nginx:latest \
--meta ci.platform=github \
--meta ci.job.url=https://github.com/org/repo/actions/runs/42

is addressed in a rule via {"var": "metadata.ci.platform"} and {"var": "metadata.ci.job.url"}.

Optional namespace

Meta is user-provided: a missing metadata.* key resolves to null without marking the rule incomplete (unlike a missing results.*, which means "an analyzer did not run"). You can therefore test for the presence of a meta value reliably:

spec:
rules:
- slug: ci-job-url-required
description: A CI job URL must be provided and well-formed.
level: warning
tags: [provenance]
condition:
and:
- { "is_set": [{ "var": "metadata.ci.job.url" }] }
- { "is_url": [{ "var": "metadata.ci.job.url" }] }
messages:
pass: "CI job URL is present and valid."
fail: "Provide a valid --meta ci.job.url."

Since all --meta values are strings, the is_true / is_false helpers interpret boolean flags:

condition: { "is_true": [{ "var": "metadata.gate.enabled" }] }

Well-known fields

Regis recognizes these standard fields (validated against schemas/meta/well-known.schema.json); any other field is accepted as-is:

FieldTypeNotes
metadata.ci.platformenumgithub or gitlab.
metadata.ci.job.idstringCI job identifier.
metadata.ci.job.urlstring (uri)CI run URL.

Rule evaluation mechanics

JSON Logic conditions

Rule conditions are expressed as JSON Logic objects. The evaluation context exposes the full, flattened analysis report. Analyzer metrics are always under the results.* namespace (for example, results.cve.critical_count).

Regis adds several custom operators on top of the standard set:

OperatorDescription
intersectstrue if any element of list a is present in list b.
contains_alltrue if all elements of list b are present in list a.
subsettrue if all elements of list a are also in list b.
keysReturns the keys of a dictionary.
getGets a value from a dictionary by a computed key.
env_containstrue if any string in b is a substring of any string in a.
is_truetrue if the value is a truthy string (true/1/yes/on, case-insensitive) or boolean true.
is_falsetrue if the value is a falsy string (false/0/no/off) or boolean false.
is_urltrue if the value is a well-formed http/https URL.
is_emptytrue if the value is null, empty, or whitespace-only.
is_settrue if the value is present and non-empty (complement of is_empty).
matchestrue if the string value matches a regex: {"matches": [{"var": "..."}, "^pattern$"]}.

The bound criterion's options are accessible under criterion.params.* (for example, {"var": "criterion.params.max_count"}).

note

The legacy rule.params.* namespace still resolves during the deprecation window but is deprecated in favor of criterion.params.*.

String interpolation

Pass and fail messages support ${path.to.var} interpolation against the same evaluation context. Use results.* for metrics and criterion.params.* for the bound criterion's options:

messages:
pass: "Image is ${results.freshness.age_days} days old — within the ${criterion.params.max_days}-day limit."
fail: "Image is ${results.freshness.age_days} days old (limit: ${criterion.params.max_days})."

Incomplete rules

If the evaluation context is missing data that a condition accesses (for example, an analyzer did not run), the rule is marked incomplete rather than failed. This prevents false negatives when an analyzer is simply not part of the current run.

Evaluating rules from the CLI

# Evaluate a report against the default playbook rules
regis rules evaluate report.json

# Use a custom playbook
regis rules evaluate report.json --rules playbook.yaml

# Export results as JSON
regis rules evaluate report.json -o rules_report.json

Blocking CI/CD pipelines

Use --fail to exit with a non-zero code when rules breach a given severity threshold:

# Fail the pipeline if any CRITICAL rule is breached
regis rules evaluate report.json --fail

# Fail on WARNING or above
regis rules evaluate report.json --fail --fail-level warning