> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kosli.com/llms.txt
> Use this file to discover all available pages before exploring further.

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.kosli.com/feedback

```json
{
  "path": "/tutorials/evaluate_trails_with_opa",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# Evaluate trails with OPA policies

> 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 <Tooltip tip="Rego is the purpose-built declarative policy language used by the Open Policy Agent (OPA) project. It is designed for expressing rules over structured data and is widely used for policy enforcement in cloud-native environments." cta="Learn more" href="https://www.openpolicyagent.org/">[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)</Tooltip>. 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.

<Steps>
  <Step title="Prerequisites">
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
      export KOSLI_API_TOKEN=<your-api-token>
      ```

    <Info>
      You don't need OPA installed — the Kosli CLI has a built-in Rego evaluator. You just need to write a `.rego` policy file.
    </Info>
  </Step>

  <Step title="Write a policy">
    Create a file called `pr-approved.rego` with the following content:

    ```rego pr-approved.rego theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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.

    <Info>
      See the [Rego Policy reference](/policy-reference/rego_policy) for the full policy contract, input data shape, and exit code behaviour.
    </Info>
  </Step>

  <Step title="Evaluate multiple trails">
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    kosli evaluate trails \
      --policy pr-approved.rego \
      --org kosli-public \
      --flow cli \
      5a0f3c0 \
      167ed93 \
      030cc31
    ```

    ```plaintext theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    RESULT:  ALLOWED
    ```
  </Step>

  <Step title="Evaluate a single trail">
    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`:

    ```rego theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    kosli evaluate trail \
      --policy snyk-no-high-vulns.rego \
      --org cyber-dojo \
      --flow dashboard-ci \
      --attestations dashboard.snyk-container-scan \
      44ca5fa2630947cf375fdbda10972a4bedaaaba3
    ```

    ```plaintext theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    RESULT:  ALLOWED
    ```

    The trail has zero high-severity vulnerabilities, so the policy allows it.

    <Info>
      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).
    </Info>
  </Step>

  <Step title="Pass parameters to a policy">
    Policies sometimes need configurable values — for example, a threshold that varies between environments. Instead of hardcoding these, use the `--params` flag to pass data into the policy as `data.params`.

    Save this as `check-threshold.rego`:

    ```rego check-threshold.rego theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    package policy

    import rego.v1

    default allow := false

    default threshold := 10

    threshold := data.params.threshold if { data.params.threshold }

    allow if { input.score >= threshold }

    violations contains msg if {
    	input.score < threshold
    	msg := sprintf("score %d is below threshold %d", [input.score, threshold])
    }
    ```

    This policy:

    * Defines a **default threshold** of `10` — used when no `--params` are provided.
    * Overrides the threshold with `data.params.threshold` when present.
    * Allows the input only if `input.score` meets the threshold.

    You can test this locally with `kosli evaluate input`. First, create an input file:

    ```shell theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    echo '{"score": 5}' > score-input.json
    ```

    Evaluate without params (uses the default threshold of `10`):

    ```shell theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    kosli evaluate input \
      --input-file score-input.json \
      --policy check-threshold.rego
    ```

    ```plaintext theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    RESULT:      DENIED
    VIOLATIONS:  score 5 is below threshold 10
    ```

    Now pass a lower threshold via `--params`:

    ```shell theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    kosli evaluate input \
      --input-file score-input.json \
      --policy check-threshold.rego \
      --params '{"threshold": 3}'
    ```

    ```plaintext theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    RESULT:  ALLOWED
    ```

    The score of `5` is now above the threshold of `3`, so the policy allows it.

    You can also load parameters from a file using the `@` prefix:

    ```shell theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    echo '{"threshold": 3}' > params.json

    kosli evaluate input \
      --input-file score-input.json \
      --policy check-threshold.rego \
      --params @params.json
    ```

    The `--params` flag works the same way on `kosli evaluate trail` and `kosli evaluate trails` — parameters are always available as `data.params` in the policy.
  </Step>

  <Step title="Explore the policy input with --show-input">
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    [
      "artifacts_statuses",
      "attestations_statuses",
      "evaluated_at",
      "flow_template_id",
      "is_compliant",
      "status"
    ]
    ```

    <Info>
      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.
    </Info>
  </Step>

  <Step title="Use in CI/CD">
    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 theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    # 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.
  </Step>

  <Step title="Record the evaluation">
    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:

    ```shell theme={"theme":"dracula","languages":{"custom":["/languages/rego.json"]}}
    # 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

    <Warning>
      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.
    </Warning>
  </Step>
</Steps>

## 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
