diff --git a/policy-reference/rego_policy.mdx b/policy-reference/rego_policy.mdx index e01a5cc..0d73d5c 100644 --- a/policy-reference/rego_policy.mdx +++ b/policy-reference/rego_policy.mdx @@ -3,7 +3,7 @@ title: "Rego Policy" description: "Reference for Rego policy files used with kosli evaluate trail and kosli evaluate trails." --- -A Rego policy defines the rules Kosli evaluates trail data against. You pass a `.rego` file to [`kosli evaluate trail`](/client_reference/kosli_evaluate_trail) or [`kosli evaluate trails`](/client_reference/kosli_evaluate_trails) via the `--policy` flag. Kosli has a built-in Rego evaluator — no OPA installation required. +A Rego policy defines the rules Kosli evaluates trail data against. You pass a `.rego` file to [`kosli evaluate trail`](/client_reference/kosli_evaluate_trail) or [`kosli evaluate trails`](/client_reference/kosli_evaluate_trails) via the `--policy` flag. Kosli includes a built-in Rego evaluator with no OPA installation required. ## Policy contract @@ -14,19 +14,21 @@ These rules are Kosli-specific conventions, not OPA built-ins. Kosli queries `da - Must evaluate to a boolean. Kosli exits with code `0` when `true`, code `1` when `false`. Typically defined as: + Must evaluate to a boolean. Kosli exits with code `0` when `true`, code `1` when `false`. + + Always define `allow` with a fail-safe default and drive it through a positive assertion, not through the absence of violations. See [Safe policy design](#safe-policy-design). ```rego - default allow = false + default allow := false - allow if { - count(violations) == 0 - } + allow if trail_is_compliant(input.trail) ``` - Optional but recommended. A set of human-readable strings describing why the policy failed. Kosli displays these when `allow` is `false`. Each message should identify the offending resource and the reason. + Optional but recommended. A set of human-readable strings explaining why the policy denied. Kosli displays these when `allow` is `false`. Each message should identify the offending resource and the reason. + + Violations are diagnostics only. They must not drive the `allow` decision. See [Safe policy design](#safe-policy-design). ```rego violations contains msg if { @@ -36,11 +38,86 @@ These rules are Kosli-specific conventions, not OPA built-ins. Kosli queries `da ``` +## Safe policy design + +Three rules prevent a policy from incorrectly reporting a non-compliant trail as compliant. + +### Rule 1: use a fail-safe default + +Always start with `default allow := false`. A trail must be explicitly approved rather than allowed by the absence of evidence against it. + +Use parameter aliases at the top of the policy file rather than hardcoding threshold values. If a required param is absent from the params file, any rule that references its alias will fail to evaluate, and `allow` will correctly remain `false`. + +```rego +max_days_by_severity := data.params.max_days_by_severity +max_ignore_expiry_days := data.params.max_ignore_expiry_days +``` + +See [Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) for a detailed walkthrough. + +### Rule 2: drive `allow` through positive assertions + +Drive the `allow` decision through a condition that must be true for the trail to be compliant. Do not write: + +```rego +# Unsafe: allow depends on the absence of violations +allow if { + count(violations) == 0 +} +``` + +When a `violations` rule body encounters an undefined reference, such as a missing param or an absent attestation field, OPA silently skips that rule body and adds no message to the set. The set is then empty, `count(violations) == 0` evaluates to `true`, and `allow` fires even though the policy never verified compliance. This produces a false-positive compliant result. + +The safe pattern makes compliance explicit: + +```rego +# Safe: allow fires only when trail_is_compliant is positively true +allow if trail_is_compliant(input.trail) +``` + +If any field referenced inside `trail_is_compliant` is undefined, the rule body fails to evaluate and `allow` remains `false`. + +See [Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) for a detailed walkthrough. + +### Rule 3: violations are diagnostics only + +In a `violations` rule, an undefined reference causes the rule body to fail silently: no message is added. This is the safe failure mode for diagnostics. Violations explain a denial determined by the `allow` rule and must not determine it themselves. + +See [Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) for a detailed walkthrough. + +## Params + +Policies can read external configuration via the `--params` flag. Params are available in the policy as `data.params.*`. This separates policy logic from the thresholds it enforces, so one `.rego` file can cover multiple environments with different params files. + +```shell +# Inline JSON +kosli evaluate trail "$TRAIL_NAME" \ + --policy my-policy.rego \ + --params '{"max_high": 0}' \ + --org "$ORG" \ + --flow "$FLOW" + +# JSON file +kosli evaluate trail "$TRAIL_NAME" \ + --policy my-policy.rego \ + --params @rego.params.prod.json \ + --org "$ORG" \ + --flow "$FLOW" +``` + +Alias params at the top of the policy file so that missing values cause rules to fail rather than silently proceeding: + +```rego +max_high := data.params.max_high +``` + +If `max_high` is absent, `max_high` is undefined and any rule that references it fails to evaluate, leaving `allow` at its `false` default. + ## Input data The data structure passed to the policy as `input` depends on which command you use. -### `kosli evaluate trail` — single trail +### Single trail (`kosli evaluate trail`) The policy receives `input.trail`, a single trail object. @@ -65,18 +142,18 @@ The policy receives `input.trail`, a single trail object. - Map of attestation name → attestation status object. Each object contains the attestation's data, including type-specific fields enriched via `--attestations`. For example, a `pull-request` attestation includes a `pull_requests` array, each with an `approvers` array and a `url` string. + Map of attestation name to attestation status object. Each object contains the attestation's data, including type-specific fields enriched via `--attestations`. For example, a `pull-request` attestation includes a `pull_requests` array, each with an `approvers` array and a `url` string. - Map of artifact name → artifact status object. Each artifact has its own `attestations_statuses` map with the same structure as above. + Map of artifact name to artifact status object. Each artifact has its own `attestations_statuses` map with the same structure as above. -### `kosli evaluate trails` — multiple trails +### Multiple trails (`kosli evaluate trails`) The policy receives `input.trails`, an array of trail objects with the same structure as `input.trail` above. @@ -97,6 +174,23 @@ kosli evaluate trail "$TRAIL_NAME" \ ``` +## Local testing + +Use [`kosli evaluate input`](/client_reference/kosli_evaluate_input) to test a policy against captured trail data without making live Kosli API calls: + +```shell +# Capture trail data once +kosli evaluate trail "$TRAIL_NAME" \ + --policy allow-all.rego \ + --show-input --output json | jq '.input' > trail-data.json + +# Iterate on the policy locally +kosli evaluate input \ + --input-file trail-data.json \ + --policy my-policy.rego \ + --params '{"max_high": 0}' +``` + ## Exit codes | Code | Meaning | @@ -110,50 +204,77 @@ Exit code `1` is used for both denial and failure. To distinguish between them i ### Check pull request approvals across multiple trails +Allows only when every trail in `input.trails` has at least one pull request with at least one approver. The attestation name is read from params so the same policy works across orgs that use different naming conventions. + ```rego package policy import rego.v1 -default allow = false +pr_attestation_name := data.params.pr_attestation_name + +default allow := false + +trail_has_approved_pr(trail) if { + some pr in trail.compliance_status.attestations_statuses[pr_attestation_name].pull_requests + count(pr.approvers) > 0 +} + +allow if { + every trail in input.trails { + trail_has_approved_pr(trail) + } +} violations contains msg if { some trail in input.trails - some pr in trail.compliance_status.attestations_statuses["pull-request"].pull_requests + some pr in trail.compliance_status.attestations_statuses[pr_attestation_name].pull_requests count(pr.approvers) == 0 msg := sprintf("trail '%v': pull-request %v has no approvers", [trail.name, pr.url]) } - -allow if { - count(violations) == 0 -} ``` ### Check Snyk scan results on a single trail +Allows only when every artifact in the trail has a Snyk scan where the high-severity vulnerability count does not exceed `max_high`. Both the attestation name and the threshold are read from params. + ```rego package policy import rego.v1 -default allow = false +snyk_attestation_name := data.params.snyk_attestation_name +max_high := data.params.max_high + +default allow := false + +artifact_within_threshold(artifact) if { + snyk := artifact.attestations_statuses[snyk_attestation_name] + every result in snyk.processed_snyk_results.results { + result.high_count <= max_high + } +} + +trail_is_compliant(trail) if { + every name, artifact in trail.compliance_status.artifacts_statuses { + artifact_within_threshold(artifact) + } +} + +allow if trail_is_compliant(input.trail) violations contains msg if { some name, artifact in input.trail.compliance_status.artifacts_statuses - snyk := artifact.attestations_statuses["snyk-container-scan"] + snyk := artifact.attestations_statuses[snyk_attestation_name] some result in snyk.processed_snyk_results.results - result.high_count > 0 - msg := sprintf("artifact '%v': snyk scan found %d high severity vulnerabilities", [name, result.high_count]) -} - -allow if { - count(violations) == 0 + result.high_count > max_high + msg := sprintf("artifact '%v': snyk scan found %d high severity vulnerabilities (limit: %d)", [name, result.high_count, max_high]) } ``` ## Further reading -- [Rego Style Guide](https://docs.styra.com/opa/rego-style-guide) — naming, rule structure, and test conventions -- [OPA Annotations](https://www.openpolicyagent.org/docs/latest/annotations/) — including `entrypoint: true` for use with `opa build` +- [Rego Style Guide](https://docs.styra.com/opa/rego-style-guide): naming, rule structure, and test conventions +- [OPA Annotations](https://www.openpolicyagent.org/docs/latest/annotations/): including `entrypoint: true` for use with `opa build` - [OPA Best Practices](https://www.openpolicyagent.org/docs/latest/best-practices/) - [Tutorial: Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa)