From 85b964bc82167631283ac637738760f776a7ef8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Wed, 18 Mar 2026 11:06:05 +0100 Subject: [PATCH 1/2] docs: add rego eval --- docs.json | 13 +- languages/rego.json | 286 +++++++++++++++++++++++++ tutorials/evaluate_trails_with_opa.mdx | 279 ++++++++++++++++++++++++ 3 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 languages/rego.json create mode 100644 tutorials/evaluate_trails_with_opa.mdx diff --git a/docs.json b/docs.json index 759e19b..7c6c8e5 100644 --- a/docs.json +++ b/docs.json @@ -21,7 +21,12 @@ "styling": { "eyebrows": "breadcrumbs", "codeblocks": { - "theme": "dracula" + "theme": "dracula", + "languages": { + "custom": [ + "/languages/rego.json" + ] + } } }, "background": { @@ -135,6 +140,12 @@ "pages": [ "tutorials/unauthorized_iac_changes" ] + }, + { + "group": "Evaluation", + "pages": [ + "tutorials/evaluate_trails_with_opa" + ] } ] }, diff --git a/languages/rego.json b/languages/rego.json new file mode 100644 index 0000000..01d5824 --- /dev/null +++ b/languages/rego.json @@ -0,0 +1,286 @@ +{ + "fileTypes": [ + "rego" + ], + "name": "rego", + "patterns": [ + { + "include": "#comment" + }, + { + "include": "#keyword" + }, + { + "include": "#comparison-operators" + }, + { + "include": "#assignment-operators" + }, + { + "include": "#term" + } + ], + "repository": { + "call": { + "captures": { + "1": { + "name": "support.function.any-method.rego" + } + }, + "match": "([a-zA-Z_][a-zA-Z0-9_]*)\\(", + "name": "meta.function-call.rego" + }, + "comment": { + "patterns": [ + { + "match": "(#)\\s*(METADATA)\\s*$\\n?", + "captures": { + "1": { + "name": "punctuation.definition.comment.rego" + }, + "2": { + "name": "strong" + } + }, + "name": "comment.line.number-sign.rego" + }, + { + "match": "(#)\\s*(scope|title|description|related_resources|authors|organizations|schemas|entrypoint|custom):.*$\\n?", + "captures": { + "1": { + "name": "punctuation.definition.comment.rego" + }, + "2": { + "name": "strong" + } + }, + "name": "comment.line.number-sign.rego" + }, + { + "captures": { + "1": { + "name": "punctuation.definition.comment.rego" + } + }, + "match": "(#).*$\\n?", + "name": "comment.line.number-sign.rego" + } + ] + }, + "constant": { + "match": "\\b(?:true|false|null)\\b", + "name": "constant.language.rego" + }, + "root-document": { + "match": "(?|<|<\\=|>\\=|\\+|-|\\*|%|/|\\||&", + "name": "keyword.operator.comparison.rego" + }, + "assignment-operators": { + "match": ":\\=|\\=", + "name": "keyword.operator.assignment.rego" + }, + "interpolated-string-double": { + "begin": "(\\$)(\")", + "beginCaptures": { + "1": { + "name": "punctuation.definition.template-expression.begin.rego" + }, + "2": { + "name": "punctuation.definition.string.begin.rego" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.rego" + } + }, + "name": "string.template.rego", + "patterns": [ + { + "include": "#interpolation-expression" + }, + { + "include": "#string-escape" + }, + { + "include": "#interpolation-escape" + }, + { + "include": "#string-escape-invalid" + } + ] + }, + "interpolated-string-raw": { + "begin": "(\\$)(`)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.template-expression.begin.rego" + }, + "2": { + "name": "punctuation.definition.string.begin.rego" + } + }, + "end": "`", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.rego" + } + }, + "name": "string.template.rego", + "patterns": [ + { + "include": "#interpolation-expression" + } + ] + }, + "interpolation-expression": { + "begin": "(?[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). This is useful for enforcing rules like "every artifact must have an approved pull request" or "all security scans must pass" — and for gating deployments in CI/CD pipelines based on those rules. + +In this tutorial, we'll write a policy that checks whether pull requests on a trail have been approved, then evaluate it against real trails in public Kosli orgs. + + + + + +To follow this tutorial, you need to: + +* [Install Kosli CLI](/getting_started/install). +* [Get a Kosli API token](/getting_started/service-accounts). +* Set the `KOSLI_API_TOKEN` environment variable to your token: + + ```shell + export KOSLI_API_TOKEN= + ``` + + +You don't need OPA installed — the Kosli CLI has a built-in Rego evaluator. You just need to write a `.rego` policy file. + + + + + + +Create a file called `pr-approved.rego` with the following content: + +```rego pr-approved.rego +package policy + +import rego.v1 + +default allow = false + +violations contains msg if { + some trail in input.trails + some pr in trail.compliance_status.attestations_statuses["pull-request"].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 +} +``` + +Let's break down what this policy does: + +* **`package policy`** — every evaluate policy must use the `policy` package. +* **`import rego.v1`** — use Rego v1 syntax (the `if`/`contains` keywords). +* **`default allow = false`** — trails are denied unless explicitly allowed. +* **`violations`** — a set of messages describing why the policy failed. The rule iterates over trails, then over pull requests within the `pull-request` attestation, looking for PRs where `approvers` is empty. +* **`allow`** — trails are allowed only when there are no violations. + + +**Policy contract** — these are Kosli-specific conventions, not OPA built-ins: + +* **`package policy`** — required. Kosli queries `data.policy.*` to find your rules. +* **`allow`** — required. Must evaluate to a **boolean**. Kosli exits with code 0 when `true`, code 1 when `false`. +* **`violations`** — optional but recommended. Must be a **set of strings**, where each string is a human-readable reason the policy failed. Kosli displays these when `allow` is `false`. + + + + + + +Let's evaluate several trails from the public `cyber-dojo` org against our policy. The `kosli evaluate trails` command fetches trail data from Kosli and passes it to the policy as `input.trails`: + +```shell +kosli evaluate trails \ + --policy pr-approved.rego \ + --org cyber-dojo \ + --flow dashboard-ci \ + 9978a1ca82c273a68afaa85fc37dd60d1e394f84 \ + b334d371eb85c9a5c811776de1b65fb80b52d952 \ + 5abd63aa1d64af7be5b5900af974dc73ae425bd6 \ + cb3ec71f5ce1103779009abaf4e8f8a3ed97d813 +``` + +The cyber-dojo project doesn't require PR approvals, so you'll see violations: + +```plaintext +RESULT: DENIED +VIOLATIONS: trail '5abd63aa1d64af7be5b5900af974dc73ae425bd6': pull-request https://github.com/cyber-dojo/dashboard/pull/342 has no approvers + trail '9978a1ca82c273a68afaa85fc37dd60d1e394f84': pull-request https://github.com/cyber-dojo/dashboard/pull/344 has no approvers + trail 'b334d371eb85c9a5c811776de1b65fb80b52d952': pull-request https://github.com/cyber-dojo/dashboard/pull/343 has no approvers + trail 'cb3ec71f5ce1103779009abaf4e8f8a3ed97d813': pull-request https://github.com/cyber-dojo/dashboard/pull/341 has no approvers +``` + +Now try the `kosli-public` org, where PRs do have approvers: + +```shell +kosli evaluate trails \ + --policy pr-approved.rego \ + --org kosli-public \ + --flow cli \ + 5a0f3c0 \ + 167ed93 \ + 030cc31 +``` + +```plaintext +RESULT: ALLOWED +``` + + + + + +The `kosli evaluate trail` (singular) command evaluates facts within a single trail — a different use case from comparing across multiple trails. For example, you might check that a snyk container scan found no high-severity vulnerabilities. + +Save this as `snyk-no-high-vulns.rego`: + +```rego +package policy + +import rego.v1 + +default allow = false + +violations contains msg if { + some name, artifact in input.trail.compliance_status.artifacts_statuses + snyk := artifact.attestations_statuses["snyk-container-scan"] + some result in snyk.processed_snyk_results.results + result.high_count > 0 + msg := sprintf("artifact '%v': snyk container scan found %d high severity vulnerabilities", [name, result.high_count]) +} + +allow if { + count(violations) == 0 +} +``` + +This policy iterates over every artifact in the trail, looks up its `snyk-container-scan` attestation, and checks whether any result has a non-zero `high_count`. + +Use `--attestations` to enrich only the snyk data (faster than fetching all attestation details). +The value uses the format `artifact-name.attestation-type` — here, `dashboard` is the artifact name and `snyk-container-scan` is the attestation name: + +```shell +kosli evaluate trail \ + --policy snyk-no-high-vulns.rego \ + --org cyber-dojo \ + --flow dashboard-ci \ + --attestations dashboard.snyk-container-scan \ + 44ca5fa2630947cf375fdbda10972a4bedaaaba3 +``` + +```plaintext +RESULT: ALLOWED +``` + +The trail has zero high-severity vulnerabilities, so the policy allows it. + + +When writing a policy for `kosli evaluate trail`, reference `input.trail` (a single object). For `kosli evaluate trails`, reference `input.trails` (an array). The data shapes differ, so use separate policies for each command. + + + + + + +When writing policies, it helps to see exactly what data is available. Use `--show-input` combined with `--output json` to see the full input that gets passed to the policy: + +```shell +kosli evaluate trail \ + --policy snyk-no-high-vulns.rego \ + --org cyber-dojo \ + --flow dashboard-ci \ + --attestations dashboard.snyk-container-scan \ + --show-input \ + --output json \ + 44ca5fa2630947cf375fdbda10972a4bedaaaba3 +``` + +This outputs the evaluation result along with the complete `input` object. You can pipe it through `jq` to explore the structure: + +```shell +kosli evaluate trail \ + --policy snyk-no-high-vulns.rego \ + --org cyber-dojo \ + --flow dashboard-ci \ + --attestations dashboard.snyk-container-scan \ + --show-input \ + --output json \ + 44ca5fa2630947cf375fdbda10972a4bedaaaba3 2>/dev/null | jq '.input.trail.compliance_status | keys' +``` + +```plaintext +[ + "artifacts_statuses", + "attestations_statuses", + "evaluated_at", + "flow_template_id", + "is_compliant", + "status" +] +``` + + +Use the `--attestations` flag to limit which attestations are enriched with full detail. The flag filters by **attestation name** (not type). For example, `--attestations pull-request` fetches only details for attestations named `pull-request`, which speeds up evaluation and reduces noise when exploring the input. + + + + + + +The `kosli evaluate` commands exit with code 0 when the policy allows and code 1 when it denies. This makes them straightforward to use as gates in CI/CD pipelines: + +```shell +# Example: gate a deployment on policy evaluation +if kosli evaluate trail \ + --policy policies/pr-approved.rego \ + --org "$KOSLI_ORG" \ + --flow "$FLOW_NAME" \ + "$GIT_COMMIT"; then + echo "Policy passed — proceeding with deployment" + # ... deploy commands ... +else + echo "Policy denied — blocking deployment" + exit 1 +fi +``` + +This pattern lets you enforce custom compliance rules as part of your delivery pipeline, using the same trail data that Kosli already collects. + + + + + +After evaluating a trail, you can record the result as an attestation — creating an audit record in Kosli that captures the policy, the full evaluation report, and any violations. + +This step requires write access to your Kosli org. The examples below use variables you'd set in your CI/CD pipeline. In your own pipeline you'd use your own policy file — here we use `my-policy.rego` as a placeholder: + +```shell +# Run the evaluation and save the full JSON report to a file +# (|| true prevents the step from failing when the policy denies) +kosli evaluate trail "$TRAIL_NAME" \ + --policy my-policy.rego \ + --org "$KOSLI_ORG" \ + --flow "$FLOW_NAME" \ + --show-input \ + --output json > eval-report.json 2>/dev/null || true + +# Read the allow/deny result from the report +is_compliant=$(jq -r '.allow' eval-report.json) + +# Extract violations as structured user-data +jq '{violations: .violations}' eval-report.json > eval-violations.json + +# Attest the result +kosli attest generic \ + --name opa-evaluation \ + --flow "$FLOW_NAME" \ + --trail "$TRAIL_NAME" \ + --org "$KOSLI_ORG" \ + --compliant="$is_compliant" \ + --attachments my-policy.rego,eval-report.json \ + --user-data eval-violations.json +``` + +This creates a generic attestation on the trail with: + +* **`--compliant`** set based on whether the policy allowed or denied — read directly from the JSON report rather than relying on the exit code, which avoids issues with `set -e` in CI environments like GitHub Actions +* **`--attachments`** containing the Rego policy (for reproducibility) and the full JSON evaluation report (including the input data the policy evaluated) +* **`--user-data`** containing the violations, which appear in the Kosli UI as structured metadata on the attestation + + +Use `--compliant=value` (with `=`) not `--compliant value` (with a space). Boolean flags in Kosli CLI require the `=` syntax when passing `false` — otherwise `false` is interpreted as a positional argument. + + + + + From f6e659556d6ade607e8a250d71a5d4f2ab57addc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Wed, 18 Mar 2026 13:50:42 +0100 Subject: [PATCH 2/2] docs: add Rego policy reference page and improve OPA tutorial Adds a dedicated Policy Reference page covering the policy contract, input data shape, exit codes, and annotated examples. Refactors the OPA tutorial to link out to the reference rather than embedding reference content mid-step, and removes em dashes from prose. --- docs.json | 12 ++ policy-reference/rego_policy.mdx | 159 +++++++++++++++++++++++++ tutorials/evaluate_trails_with_opa.mdx | 29 +++-- 3 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 policy-reference/rego_policy.mdx diff --git a/docs.json b/docs.json index 7c6c8e5..4de7e53 100644 --- a/docs.json +++ b/docs.json @@ -421,6 +421,18 @@ } ] }, + { + "item": "Policy Reference", + "icon": "scroll", + "groups": [ + { + "group": "Policies", + "pages": [ + "policy-reference/rego_policy" + ] + } + ] + }, { "item": "API Reference", "icon": "code", diff --git a/policy-reference/rego_policy.mdx b/policy-reference/rego_policy.mdx new file mode 100644 index 0000000..e01a5cc --- /dev/null +++ b/policy-reference/rego_policy.mdx @@ -0,0 +1,159 @@ +--- +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. + +## Policy contract + +These rules are Kosli-specific conventions, not OPA built-ins. Kosli queries `data.policy.*` to find them. + + + Every policy must declare `package policy`. Kosli queries `data.policy.allow` and `data.policy.violations` to read the result. + + + + Must evaluate to a boolean. Kosli exits with code `0` when `true`, code `1` when `false`. Typically defined as: + + ```rego + default allow = false + + allow if { + count(violations) == 0 + } + ``` + + + + 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. + + ```rego + violations contains msg if { + # ... rule body ... + msg := sprintf("descriptive message about %v", [resource]) + } + ``` + + +## Input data + +The data structure passed to the policy as `input` depends on which command you use. + +### `kosli evaluate trail` — single trail + +The policy receives `input.trail`, a single trail object. + + + The trail being evaluated. + + + + The trail name (git commit SHA or custom name). + + + + Compliance data for the trail. + + + + Whether the trail is compliant against its flow template. + + + + Compliance status string, e.g. `"COMPLIANT"` or `"INCOMPLIANT"`. + + + + 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 artifact name → artifact status object. Each artifact has its own `attestations_statuses` map with the same structure as above. + + + + + + +### `kosli evaluate trails` — multiple trails + +The policy receives `input.trails`, an array of trail objects with the same structure as `input.trail` above. + + + Array of trail objects. Each element has the same structure as `input.trail` described above. + + + +Use `--show-input` with `--output json` to print the full input structure for a given trail. Pipe through `jq` to explore specific fields: + +```shell +kosli evaluate trail "$TRAIL_NAME" \ + --policy my-policy.rego \ + --org "$ORG" \ + --flow "$FLOW" \ + --show-input \ + --output json 2>/dev/null | jq '.input' +``` + + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Policy allowed (`allow = true`) | +| `1` | Policy denied (`allow = false`) **or** command error (network failure, invalid Rego, policy file not found) | + +Exit code `1` is used for both denial and failure. To distinguish between them in CI, use `--output json` and read the `allow` field directly from the output rather than relying on the exit code. + +## Examples + +### Check pull request approvals across multiple trails + +```rego +package policy + +import rego.v1 + +default allow = false + +violations contains msg if { + some trail in input.trails + some pr in trail.compliance_status.attestations_statuses["pull-request"].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 + +```rego +package policy + +import rego.v1 + +default allow = false + +violations contains msg if { + some name, artifact in input.trail.compliance_status.artifacts_statuses + snyk := artifact.attestations_statuses["snyk-container-scan"] + 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 +} +``` + +## 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` +- [OPA Best Practices](https://www.openpolicyagent.org/docs/latest/best-practices/) +- [Tutorial: Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) diff --git a/tutorials/evaluate_trails_with_opa.mdx b/tutorials/evaluate_trails_with_opa.mdx index 0414144..5007165 100644 --- a/tutorials/evaluate_trails_with_opa.mdx +++ b/tutorials/evaluate_trails_with_opa.mdx @@ -3,7 +3,7 @@ title: "Evaluate trails with OPA policies" description: "Learn how to use kosli evaluate trail and kosli evaluate trails to check your Kosli trails against custom OPA/Rego policies. This tutorial walks through writing a policy that verifies pull requests have been approved." --- -The `kosli evaluate` commands let you evaluate Kosli trails against custom policies written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). This is useful for enforcing rules like "every artifact must have an approved pull request" or "all security scans must pass" — and for gating deployments in CI/CD pipelines based on those rules. +The `kosli evaluate` commands let you evaluate Kosli trails against custom policies written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). This is useful for enforcing rules like "every artifact must have an approved pull request" or "all security scans must pass", and for gating deployments in CI/CD pipelines based on those rules. In this tutorial, we'll write a policy that checks whether pull requests on a trail have been approved, then evaluate it against real trails in public Kosli orgs. @@ -59,11 +59,7 @@ Let's break down what this policy does: * **`allow`** — trails are allowed only when there are no violations. -**Policy contract** — these are Kosli-specific conventions, not OPA built-ins: - -* **`package policy`** — required. Kosli queries `data.policy.*` to find your rules. -* **`allow`** — required. Must evaluate to a **boolean**. Kosli exits with code 0 when `true`, code 1 when `false`. -* **`violations`** — optional but recommended. Must be a **set of strings**, where each string is a human-readable reason the policy failed. Kosli displays these when `allow` is `false`. +See the [Rego Policy reference](/policy-reference/rego_policy) for the full policy contract, input data shape, and exit code behaviour. @@ -113,7 +109,7 @@ RESULT: ALLOWED -The `kosli evaluate trail` (singular) command evaluates facts within a single trail — a different use case from comparing across multiple trails. For example, you might check that a snyk container scan found no high-severity vulnerabilities. +The `kosli evaluate trail` (singular) command evaluates facts within a single trail, which is a different use case from comparing across multiple trails. For example, you might check that a snyk container scan found no high-severity vulnerabilities. Save this as `snyk-no-high-vulns.rego`: @@ -140,7 +136,7 @@ allow if { This policy iterates over every artifact in the trail, looks up its `snyk-container-scan` attestation, and checks whether any result has a non-zero `high_count`. Use `--attestations` to enrich only the snyk data (faster than fetching all attestation details). -The value uses the format `artifact-name.attestation-type` — here, `dashboard` is the artifact name and `snyk-container-scan` is the attestation name: +The value uses the format `artifact-name.attestation-type`. Here, `dashboard` is the artifact name and `snyk-container-scan` is the attestation name: ```shell kosli evaluate trail \ @@ -158,7 +154,7 @@ RESULT: ALLOWED The trail has zero high-severity vulnerabilities, so the policy allows it. -When writing a policy for `kosli evaluate trail`, reference `input.trail` (a single object). For `kosli evaluate trails`, reference `input.trails` (an array). The data shapes differ, so use separate policies for each command. +The `input.trail` / `input.trails` distinction and the full input data shape are documented in the [Rego Policy reference](/policy-reference/rego_policy#input-data). @@ -210,7 +206,9 @@ Use the `--attestations` flag to limit which attestations are enriched with full -The `kosli evaluate` commands exit with code 0 when the policy allows and code 1 when it denies. This makes them straightforward to use as gates in CI/CD pipelines: +The `kosli evaluate` commands exit with `0` on allow and `1` on deny or error — making them straightforward to use as pipeline gates. See the [Rego Policy reference](/policy-reference/rego_policy#exit-codes) for details on distinguishing denial from command failure. + + ```shell # Example: gate a deployment on policy evaluation @@ -233,7 +231,7 @@ This pattern lets you enforce custom compliance rules as part of your delivery p -After evaluating a trail, you can record the result as an attestation — creating an audit record in Kosli that captures the policy, the full evaluation report, and any violations. +After evaluating a trail, you can record the result as an attestation. This creates an audit record in Kosli that captures the policy, the full evaluation report, and any violations. This step requires write access to your Kosli org. The examples below use variables you'd set in your CI/CD pipeline. In your own pipeline you'd use your own policy file — here we use `my-policy.rego` as a placeholder: @@ -277,3 +275,12 @@ Use `--compliant=value` (with `=`) not `--compliant value` (with a space). Boole + +## What you've accomplished + +You have written OPA/Rego policies and evaluated Kosli trails against them, both across multiple trails and within a single trail. You've also recorded evaluation results as attestations, creating a tamper-proof audit record of every policy decision linked to a specific trail. + +From here you can: +* Explore evaluated trails in the [Kosli app](https://app.kosli.com) +* Gate deployments in CI/CD pipelines using `kosli evaluate trail` exit codes +* Extend your policies to check other attestation types. See [`kosli evaluate trail`](/client_reference/kosli_evaluate_trail) and [`kosli evaluate trails`](/client_reference/kosli_evaluate_trails) for the full flag reference