Policy DSL Reference (Rego / OPA Authoring Surface)
Author custom org-specific rules in Rego against the chainsaw.policy entrypoint: the input fields a rule can read, the decision shape it returns, the supported actions, and which inputs are populated at which enforcement surface.
Overview
Chainsaw has two policy surfaces. The dashboard / YAML conditions:
surface (covered in
How to Configure Chainsaw with YAML Configuration Files)
is the simpler, built-in one — pick a condition, pick an operator,
pick a value. The Policy DSL is the second surface: a
Rego /
OPA rule set you author as code, sign, and load into the server. It
exists for the long tail of org-specific rules that don’t map cleanly
onto a single built-in condition — multi-field logic, surface-scoped
tightening, and rules that key on caller identity or coordinate
patterns.
This page is the authoring reference: the entrypoint, the input fields a rule can read, the decision it returns, and which inputs are populated at which surface.
Prerequisites
- Admin access to the Chainsaw server and its filesystem (the bundle
loads from
CHAINSAW_POLICY_BUNDLE) - Familiarity with Rego
- Signing set up before you enforce in production — see Signed Policy Bundles
The entrypoint
Every rule contributes to a single decision query:
data.chainsaw.policy.decision
Your module declares package chainsaw.policy and adds one or more
partial decision rules. The engine collects every decision object
that fires for a request and reduces them to the strictest action.
package chainsaw.policy
decision contains {
"action": "block",
"rule_id": "my-rule",
"message": "human-readable reason",
"exception_eligible": true,
} if {
# conditions that read input.* fields
input.isKnownMalicious == true
}
The decision object
Each decision your rule emits is an object with these keys:
| Key | Type | Meaning |
|---|---|---|
action | string | One of block, quarantine, monitor, allow, notify_owner. Defaults to block if omitted. |
rule_id | string | Stable identifier recorded on the audit row. (ruleId is also accepted.) |
message | string | Human-readable reason. May sprintf in input.* field values. |
exception_eligible | bool | Whether operators can grant a per-package waiver for this violation. (exceptionEligible also accepted.) |
The actions and their strictness ranking (strictest first):
block— refuse the request.quarantine— at the proxy, the artifact may be cached but withheld; at the publish surface there is no stored row to withhold yet, so a quarantine verdict refuses the upload outright (quarantine-at-publish is block-not-log).monitor— record what would have happened without enforcing. Only escalates an otherwise-allowdecision.allow— explicit permit.notify_owner— a side-effect action that adds an ownership routing intent without gating the install. It ranks at the same level asallow(it carries no enforcement weight on its own). See How to Route Violations to Owners.
monitor verdict cannot
downgrade a native block. And if your Rego throws a runtime error,
the DSL path fails open (the request is not blocked by the broken
rule) and the error is logged. A buggy custom rule must never become a
denial-of-service on the data plane.Input fields
The request is projected onto a single JSON document (input) before
your rule runs. Every surface produces the same shape; missing fields
stay zero-valued (false, 0, "", or omitted). The field names
below are the contract — they are lowerCamelCase so you don’t have to
remember Go conventions.
Surface and identity
| Field | Type | Notes |
|---|---|---|
input.surface | string | "pr", "proxy", "publish", "promote", "deploy", or "runtime". Scope a rule with input.surface == "publish". |
input.repository | string | Repository (proxy) name. |
input.ecosystem | string | Package format, lowercased (npm, pypi, cargo, …). |
input.package | string | Package coordinate / name. |
input.version | string | Package version. |
Caller / network identity
| Field | Type | Notes |
|---|---|---|
input.clientId | string | Calling client identity. |
input.clientGroups | array | Groups the client belongs to. |
input.requestingIp | string | Source IP. |
input.requestingCountry | string | Resolved country code. |
Package metadata
| Field | Type | Notes |
|---|---|---|
input.isInternal | bool | Internal package. |
input.releaseDate | string | RFC3339, or empty. |
input.licenseSpdx | string | Declared SPDX expression. |
input.licenseTags | array | Classified license tags. |
Vulnerability
| Field | Type | Notes |
|---|---|---|
input.isVulnerable | bool | Has at least one known vulnerability. |
input.cvss | number | CVSS score. |
input.epss | number | EPSS probability (0–1). |
input.cves | array | CVE identifiers. |
Supply-chain integrity
| Field | Type | Notes |
|---|---|---|
input.hasProvenance | bool | Provenance attestation verified. |
input.provenanceStatus | string | verified / missing / unavailable / failed. |
input.isSuspectedTyposquat | bool | Flagged by typosquat detection. |
input.isKnownMalicious | bool | In the OpenSSF malware database. |
input.trustScore | number | Composite trust score (0–100). Absent when no score was computed — guard with input.trustScore before comparing, or the rule is simply undefined for unscored packages. A score of 0 is a real score (malware sentinel / checksum mismatch) and IS present. |
input.publisherChanged | bool | Publisher set changed vs prior version. |
Attestation (SLSA / Sigstore)
| Field | Type | Notes |
|---|---|---|
input.slsaLevel | number | 0 when no verified attestation, 1–4 otherwise. |
input.attestationBuilderId | string | OIDC subject of the build. |
input.attestationIssuer | string | OIDC issuer URL. |
input.attestationSourceRepo | string | Canonicalised source repo URL. |
input.attestationTransparencyLog | string | Rekor entry URL. |
input.attestationCacheStale | bool | Served from stale Sigstore cache. |
Install-script and behavior signals
| Field | Type | Notes |
|---|---|---|
input.hasInstallScript | bool | Lifecycle script declared. |
input.installScriptFetchesRemote | bool | Install script fetches a remote payload. |
input.importTimeExecution | bool | Runs code at import/install time (PyPI). |
input.maliciousIOC | bool | Embeds an exfil sink / stealer string. |
Version and Unicode anomalies
| Field | Type | Notes |
|---|---|---|
input.versionAnomaly | bool | At least one version-sequence flag fired. |
input.versionAnomalyFlags | array | e.g. semver_regression, major_skip, timestamp_regression, registry_withdrawn. |
input.hasHiddenUnicode | bool | Hidden Unicode runes found. |
input.hiddenUnicodeKinds | array | e.g. zero_width, bidi_override, tag. |
input.publishVelocity24h | number | Versions published by the publisher set in trailing 24h. |
Manifest hygiene and maintainer signals
| Field | Type | Notes |
|---|---|---|
input.deprecatedByMaintainer | bool | |
input.shrinkwrapPresent | bool | |
input.manifestConfusion | bool | |
input.gitDependency | bool | |
input.httpTarballDependency | bool | |
input.wildcardDependencyRange | bool | |
input.badDependencySemver | bool | |
input.trivialPackage | bool | |
input.tooManyFiles | bool | |
input.nonExistentAuthor | bool | |
input.firstTimeCollaborator | bool | Three-state: omitted when unknown. |
input.suspiciousRepoStars | bool | |
input.maintainerAccountAgeDays | number | Age of the youngest maintainer account. |
Capability scanners (npm; gated)
| Field | Type | Notes |
|---|---|---|
input.usesEval | bool | |
input.networkAccess | bool | |
input.shellAccess | bool | |
input.filesystemAccess | bool | |
input.envVarAccess | bool | |
input.nativeBinaryPresent | bool | |
input.highEntropyStrings | bool | |
input.urlStrings | bool | |
input.minifiedCode | bool |
AI-artifact signals
| Field | Type | Notes |
|---|---|---|
input.artifactSubtype | string | |
input.dangerousPickle | bool | |
input.unsafeSerializationFormat | bool | |
input.modelCardInjection | bool | |
input.agentToolDangerousCapability | bool | |
input.mcpServerDeclared | bool | |
input.promptTemplateInjection | bool |
Checksum
| Field | Type | Notes |
|---|---|---|
input.checksumUnavailable | bool |
true for ecosystems where Chainsaw produces it. If you write a rule
keyed on input.hasInstallScript and apply it to a Maven proxy (where
there is no lifecycle-script concept), the clause simply stays false
and the rule is inert. Your server reports the per-ecosystem support grid:
run chainsaw policy preflight,
which reads the same matrix the dashboard warns from. Per-ecosystem totals
are published in
docs/ecosystems.md.Which inputs are available at which surface
The input shape is identical at every surface, but the intelligence-derived signal fields are only populated when the intelligence providers have run for that request.
| Surface | input.surface | Signal fields populated? |
|---|---|---|
| Registry proxy fetch | proxy | Yes — the full intelligence fan-out runs on the request path. |
| GitHub-Actions PR check | pr | Coordinate + the signals the PR scanner computes. No tarball-side signals it didn’t scan. |
| K8s admission webhook | deploy | Coordinate / identity, plus cached intelligence where available. |
| Package-manager install hook | runtime | Coordinate / identity; some signals arrive only after a tarball-side scan. |
| Env→env promotion gate | promote | Coordinate / identity. |
| Pre-publish gate | publish | Sparse. A bounded, synchronous Tier-1 pre-run hydrates coordinate-keyed signals (isVulnerable, isKnownMalicious, isSuspectedTyposquat, cves); byte-requiring Tier-2 signals are not available pre-upload. |
input.hasHiddenUnicode) will not fire reliably at upload time.Worked examples
The examples below are adapted from the demo bundle that ships in the
repo at
policies/chainsaw_policy.rego
— use that file as your starting point.
Example 1 — young maintainer + install script
The canonical demo rule: block packages that declare an install script and have a maintainer account younger than 90 days. Authored once, enforced at every surface where both signals are populated.
package chainsaw.policy
decision contains {
"action": "block",
"rule_id": "young-maintainer-with-install-script",
"message": sprintf("youngest maintainer is %d days old; package declares install script", [input.maintainerAccountAgeDays]),
"exception_eligible": true,
} if {
input.maintainerAccountAgeDays > 0
input.maintainerAccountAgeDays <= 90
input.hasInstallScript == true
}
Example 2 — harder verdict on remote-fetching install scripts
Same shape, no exception, when the install script fetches a remote payload — that’s the exfil signature, not just a build step.
decision contains {
"action": "block",
"rule_id": "young-maintainer-with-remote-fetching-install-script",
"message": "young maintainer + install script that fetches remote payload",
"exception_eligible": false,
} if {
input.maintainerAccountAgeDays > 0
input.maintainerAccountAgeDays <= 90
input.installScriptFetchesRemote == true
}
Example 3 — surface-scoped tightening
React to input.surface. At runtime install, flag young maintainers in
monitor mode even when the install-script signal is missing (some
ecosystems only get it after a tarball-side scan).
decision contains {
"action": "monitor",
"rule_id": "runtime-young-maintainer",
"message": "young maintainer at install time — flagging for review",
"exception_eligible": true,
} if {
input.surface == "runtime"
input.maintainerAccountAgeDays > 0
input.maintainerAccountAgeDays <= 30
}
Audit trail
Every decision the DSL contributes is stamped with the bundle’s
content digest, so a verdict is reproducible against a known rule-set
version. Each decision your rule emits surfaces in the audit trail as
the rule_id and message you set, and that bundle digest is carried
into the audit trail — see
Signed Policy Bundles.
Next Steps
- Signed Policy Bundles — author, sign, verify-at-load, and promote your bundle
- YAML Conditions vs the Rego DSL — which surface to use, and how they relate
- How to Manage Policy Precedence and Exception Workflows — how DSL verdicts merge with native policy