diff --git a/config/navigation.json b/config/navigation.json index e2f857e4..cdb5f2bd 100644 --- a/config/navigation.json +++ b/config/navigation.json @@ -126,7 +126,8 @@ { "group": "Evaluation", "pages": [ - "tutorials/evaluate_trails_with_opa" + "tutorials/evaluate_trails_with_opa", + "tutorials/evaluate_policies_server_side" ] }, { diff --git a/images/tutorials/policy-decision-detail.png b/images/tutorials/policy-decision-detail.png new file mode 100644 index 00000000..dfded4cb Binary files /dev/null and b/images/tutorials/policy-decision-detail.png differ diff --git a/images/tutorials/policy-decisions.png b/images/tutorials/policy-decisions.png new file mode 100644 index 00000000..c63cf716 Binary files /dev/null and b/images/tutorials/policy-decisions.png differ diff --git a/images/tutorials/policy-list.png b/images/tutorials/policy-list.png new file mode 100644 index 00000000..ea5d1b8e Binary files /dev/null and b/images/tutorials/policy-list.png differ diff --git a/images/tutorials/policy-source.png b/images/tutorials/policy-source.png new file mode 100644 index 00000000..d1940976 Binary files /dev/null and b/images/tutorials/policy-source.png differ diff --git a/mockups/policies-ui/.gitignore b/mockups/policies-ui/.gitignore new file mode 100644 index 00000000..f3f494d5 --- /dev/null +++ b/mockups/policies-ui/.gitignore @@ -0,0 +1,5 @@ +# generated by build.py + Chrome headless +# NB: the stylesheet must NOT be committed -- Mintlify auto-applies any .css in the project +kosli-mock.css +policy-*.html +policy-*.png diff --git a/mockups/policies-ui/README.md b/mockups/policies-ui/README.md new file mode 100644 index 00000000..0dea1c85 --- /dev/null +++ b/mockups/policies-ui/README.md @@ -0,0 +1,25 @@ +# Proposed Policies UI — wireframe mockups + +Sources for the mockup images used by +[`tutorials/evaluate_policies_server_side.mdx`](../../tutorials/evaluate_policies_server_side.mdx). +They depict a **proposed** UI that does not exist yet, so every rendered page carries a +"Mockup · proposed design" marker. + +Chrome and palette were matched against the real Controls screenshots in +`images/tutorials/controls-*.png` (sidebar `#101c2b`, primary `#1e55cd`, compliant `#3ca36f`, +non-compliant `#ce4a3e`). `logo.png` / `logo-footer.png` are crops of the real logo with the ICC +profile stripped, so they blend with the CSS colors. + +## Regenerate + +```bash +python3 build.py # also writes kosli-mock.css (gitignored) +for p in policy-list policy-source policy-decisions policy-decision-detail; do + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \ + --window-size=1535,1011 --screenshot="$p.png" "file://$PWD/$p.html" +done +cp policy-*.png ../../images/tutorials/ +``` + +1535x1011 at 1x matches the existing Controls screenshots. diff --git a/mockups/policies-ui/_chrome.html b/mockups/policies-ui/_chrome.html new file mode 100644 index 00000000..a9f0568d --- /dev/null +++ b/mockups/policies-ui/_chrome.html @@ -0,0 +1,24 @@ + diff --git a/mockups/policies-ui/_footer.html b/mockups/policies-ui/_footer.html new file mode 100644 index 00000000..d1c7831e --- /dev/null +++ b/mockups/policies-ui/_footer.html @@ -0,0 +1,6 @@ + diff --git a/mockups/policies-ui/_topbar.html b/mockups/policies-ui/_topbar.html new file mode 100644 index 00000000..221ce3c1 --- /dev/null +++ b/mockups/policies-ui/_topbar.html @@ -0,0 +1,5 @@ +
+
CRUMB
+ +
+
diff --git a/mockups/policies-ui/build.py b/mockups/policies-ui/build.py new file mode 100644 index 00000000..09aa3018 --- /dev/null +++ b/mockups/policies-ui/build.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +"""Assemble the proposed-Policies-UI wireframe mockups from shared partials.""" +import pathlib, re + +HERE = pathlib.Path(__file__).parent + +CSS = r"""/* Wireframe mockup chrome for proposed Kosli Policies UI. + Palette sampled from real screenshots in images/tutorials/controls-*.png */ +* { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --navy: #101c2b; + --ink: #14263a; + --grey: #6b7280; + --line: #e6e7e9; + --line-soft: #eef0f2; + --band: #f7f9fc; + --topbar: #f2f2f6; + --blue: #1e55cd; + --green: #3ca36f; + --red: #ce4a3e; + --chip: #eceef2; +} +body { + width: 1535px; min-height: 1011px; display: flex; + font: 400 15px/1.45 -apple-system, "Segoe UI", Inter, Helvetica, Arial, sans-serif; + color: var(--ink); background: #fcfcff; -webkit-font-smoothing: antialiased; +} + +/* ---------- sidebar ---------- */ +.sidebar { width: 250px; flex: 0 0 250px; background: var(--navy); padding: 16px 0 0; } +.logo { display: block; width: 130px; height: 48px; margin: 0 0 18px 10px; } +.org { display: flex; align-items: center; gap: 10px; color: #fff; font-weight: 600; + padding: 0 24px 0 20px; height: 34px; } +.org .home { opacity: .85; } +.org .name { flex: 1; } +.org .chev { opacity: .7; } +hr.sep { border: 0; border-top: 1px solid rgba(255,255,255,.16); margin: 14px 24px 12px; } +nav a { display: flex; align-items: center; gap: 14px; padding: 9px 20px; + color: #b9c2cd; text-decoration: none; font-size: 15px; } +nav a.on { color: #fff; font-weight: 600; } +nav svg { width: 19px; height: 19px; flex: 0 0 19px; } + +/* ---------- top bar ---------- */ +.main { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.topbar { height: 51px; background: var(--topbar); display: flex; align-items: center; + padding: 0 22px 0 36px; gap: 14px; } +.crumb { font-size: 13.5px; color: var(--grey); flex: 1; } +.crumb b { color: var(--ink); } +.search { width: 278px; height: 32px; border: 1px solid #d8dbe0; border-radius: 999px; + background: #fff; display: flex; align-items: center; gap: 8px; padding: 0 14px; + font-size: 13px; color: #9aa1ab; } +.avatar { display: flex; align-items: center; gap: 5px; color: #5b6472; } +.avatar .ring { width: 26px; height: 26px; border-radius: 50%; background: #dfe3e9; + display: grid; place-items: center; } + +/* ---------- page ---------- */ +.page { flex: 1; padding: 26px 36px 0; } +h1 { font-size: 34px; font-weight: 700; letter-spacing: -.4px; line-height: 1.1; } +.h1row { display: flex; align-items: flex-start; gap: 12px; } +.sub { font-size: 16px; font-weight: 600; margin-top: 9px; } +.desc { font-size: 13.5px; color: var(--grey); margin-top: 6px; max-width: 1030px; } +.spacer { flex: 1; } +.toolrow { display: flex; align-items: center; gap: 10px; margin: 18px 0 22px; } + +/* ---------- controls ---------- */ +.field { width: 240px; height: 34px; border: 1px solid #d8dbe0; border-radius: 7px; + background: #fff; display: flex; align-items: center; gap: 8px; padding: 0 11px; + font-size: 13px; color: #9aa1ab; } +.btn { height: 34px; padding: 0 15px; border-radius: 7px; border: 1px solid #d8dbe0; + background: #fff; color: #374151; font-size: 13.5px; font-weight: 500; + display: inline-flex; align-items: center; gap: 7px; } +.btn.pri { background: var(--blue); border-color: var(--blue); color: #fff; font-weight: 600; } +.btn.danger { border-color: #ffdad5; color: #b4433a; } +.btn.sm { height: 27px; padding: 0 10px; font-size: 12.5px; border-radius: 6px; } + +.mock { margin-left: auto; align-self: flex-start; background: #fff7e6; border: 1px solid #f0d18a; + color: #8a6212; border-radius: 6px; font-size: 11.5px; font-weight: 600; + padding: 5px 10px; letter-spacing: .2px; white-space: nowrap; } + +/* ---------- tabs ---------- */ +.tabs { display: flex; gap: 4px; margin-top: 20px; } +.tab { display: flex; align-items: center; gap: 8px; padding: 9px 16px; font-size: 14px; + color: var(--grey); border: 1px solid transparent; border-radius: 8px 8px 0 0; } +.tab.on { background: #fff; border-color: var(--line); border-bottom-color: #fff; + color: var(--ink); font-weight: 600; } + +/* ---------- card ---------- */ +.card { border: 1px solid var(--line); border-radius: 10px; background: #fff; overflow: hidden; } +.card + .card { margin-top: 18px; } +.chead { background: var(--band); border-bottom: 1px solid var(--line); + display: flex; align-items: center; gap: 11px; padding: 13px 18px; font-size: 14.5px; + font-weight: 600; } +.count { background: var(--chip); color: #4b5563; border-radius: 6px; font-size: 12px; + font-weight: 600; padding: 2px 9px; } +.ctab { font-weight: 400; color: var(--grey); font-size: 14px; margin-left: 14px; } +.filters { margin-left: auto; display: flex; gap: 18px; font-size: 13px; color: #4b5563; + font-weight: 400; } +.row { display: flex; align-items: center; gap: 14px; padding: 14px 18px; + border-bottom: 1px solid var(--line-soft); } +.row:last-child { border-bottom: 0; } +.row .grow { flex: 1; min-width: 0; } +.id { font-size: 15px; font-weight: 700; } +.badge { display: inline-block; border: 1px solid #e2e4e8; background: #f7f8fa; color: #4b5563; + border-radius: 4px; font-size: 11px; font-weight: 600; padding: 1px 6px; + vertical-align: 2px; } +.name { font-weight: 400; } +.line2 { font-size: 13px; color: var(--grey); margin-top: 5px; } +.tags { display: flex; gap: 6px; margin-top: 8px; } +.tag { background: #eef2f7; color: #44526a; border-radius: 4px; font-size: 11px; + padding: 2px 8px; } +.kebab { color: #9aa1ab; font-size: 17px; line-height: 1; } +.pill { border-radius: 999px; color: #fff; font-size: 12.5px; font-weight: 600; + padding: 6px 15px 6px 11px; display: inline-flex; align-items: center; gap: 7px; + white-space: nowrap; } +.pill.ok { background: var(--green); } +.pill.no { background: var(--red); } +.pill .dot { width: 15px; height: 15px; border-radius: 50%; background: rgba(255,255,255,.28); + display: grid; place-items: center; font-size: 10px; } +.mono { font: 12.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; } +.hint { font-size: 13px; color: var(--grey); margin-top: 14px; } +.hint code { font: 12.5px ui-monospace, Menlo, monospace; background: #f3f4f6; + border-radius: 4px; padding: 1px 5px; } + +/* ---------- footer ---------- */ +footer { display: flex; align-items: center; gap: 18px; padding: 20px 36px 22px; + font-size: 12.5px; color: #4b5563; } +footer .gen { background: var(--chip); color: #6b7280; border-radius: 5px; font-size: 11px; + padding: 3px 9px; } +footer img { width: 82px; height: 34px; } +footer .links { display: flex; gap: 15px; color: var(--blue); } + +/* ---------- detail tray ---------- */ +.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.5); } +.tray { position: fixed; top: 0; right: 0; bottom: 0; width: 843px; background: #fff; + display: flex; flex-direction: column; } +.thead { padding: 30px 32px 20px; } +.trow1 { display: flex; align-items: center; gap: 16px; } +.trow1 h2 { font-size: 30px; font-weight: 700; letter-spacing: -.3px; } +.close { margin-left: auto; color: #6b7280; font-size: 22px; line-height: 1; } +.tmeta { display: flex; align-items: center; gap: 12px; margin-top: 13px; font-size: 13.5px; + color: #4b5563; } +.kv { margin: 0 32px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; } +.kv > div { display: flex; border-bottom: 1px solid var(--line); } +.kv > div:last-child { border-bottom: 0; } +.kv dt { flex: 0 0 168px; background: var(--band); border-right: 1px solid var(--line); + padding: 13px 15px; font-size: 13.5px; color: var(--grey); } +.kv dd { flex: 1; min-width: 0; padding: 13px 15px; font-size: 13.5px; font-weight: 600; + word-break: break-all; } +.kv dd.plain { font-weight: 400; } +.kv dd ul { margin: 0; padding-left: 18px; font-weight: 400; } +.kv dd li + li { margin-top: 5px; } + +/* ---------- source view ---------- */ +.srcmeta { display: flex; gap: 26px; padding: 13px 18px; border-bottom: 1px solid var(--line-soft); + font-size: 13px; color: var(--grey); } +.srcmeta b { color: var(--ink); font-weight: 600; } +.src { display: flex; font: 12.5px/1.75 ui-monospace, SFMono-Regular, Menlo, monospace; } +.gutter { flex: 0 0 46px; text-align: right; padding: 14px 12px 16px 0; color: #b6bcc5; + background: #fcfcfd; border-right: 1px solid var(--line-soft); user-select: none; } +.code { flex: 1; padding: 14px 0 16px 16px; white-space: pre; overflow: hidden; color: var(--ink); } +.k { color: #8250df; } +.s { color: #0a6e5c; } +.vsel { display: inline-flex; align-items: center; gap: 7px; border: 1px solid #d8dbe0; + background: #fff; border-radius: 6px; padding: 3px 9px; font-size: 12.5px; + font-weight: 500; color: #374151; } + +/* ---------- bundle: file tree + viewer ---------- */ +.bundle { display: flex; align-items: stretch; } +.files { flex: 0 0 252px; border-right: 1px solid var(--line); background: #fcfcfd; + padding: 14px 0 16px; } +.files .cap { font-size: 11px; font-weight: 700; letter-spacing: .6px; color: #9aa1ab; + padding: 0 16px 10px; } +.fi { display: flex; align-items: center; gap: 7px; padding: 6px 16px; font-size: 13px; + color: #374151; border-left: 2px solid transparent; } +.fi.sel { background: #eef3fd; border-left-color: var(--blue); color: var(--blue); + font-weight: 600; } +.fi.nest { padding-left: 34px; } +.fi .flag { margin-left: auto; font-size: 10.5px; color: #9aa1ab; font-weight: 400; } +.dir { padding: 7px 16px 3px; font-size: 13px; color: #6b7280; } +.files .tot { border-top: 1px solid var(--line-soft); margin-top: 12px; padding: 11px 16px 0; + font-size: 12px; color: #9aa1ab; } +.viewer { flex: 1; min-width: 0; } +.vhead { display: flex; align-items: baseline; gap: 12px; padding: 13px 18px; + border-bottom: 1px solid var(--line-soft); font-size: 13.5px; } +.vhead .fn { font-weight: 700; font-size: 14px; } +.vhead .ep { background: #eef3fd; color: var(--blue); border-radius: 4px; font-size: 11px; + font-weight: 600; padding: 2px 7px; } +.vhead .meta { margin-left: auto; color: var(--grey); font-size: 12.5px; } +""" + +(HERE / "kosli-mock.css").write_text(CSS) +chrome = (HERE / "_chrome.html").read_text() +topbar = (HERE / "_topbar.html").read_text() +footer = (HERE / "_footer.html").read_text() + +ICON = ('' + '') +CHEV = ' ▾' +TICK = '✓' +CROSS = '✕' + + +def shell(crumb, body, title): + return f"""{title} + +{chrome} +
+{topbar.replace('CRUMB', crumb)} +{body} +{footer} +
""" + + +POLICIES = [ + ("pr-approved", "V1", "Pull request approved", + "Every pull request on the trail has at least one approver.", + ["team=platform", "framework=soc2"]), + ("no-hardcoded-secrets", "V3", "No hard-coded credentials", + "No attested source file matches a known credential pattern.", + ["team=platform", "framework=soc2"]), + ("vuln-scan-clean", "V2", "Vulnerability scan clean", + "The Snyk attestation reports no critical or high findings.", + ["team=security"]), + ("test-evidence-present", "V1", "Test evidence present", + "A JUnit attestation exists on the trail and reports zero failures.", + ["team=quality"]), +] + +DECISIONS = [ + ("14:02:11", "14:02:19", "release-99", "nginx:665d6dd", True), + ("14:00:03", "14:00:11", "release-98", "nginx:665d6dd", True), + ("13:41:55", "13:42:02", "release-97", "nginx:a6e433a", False), + ("13:20:47", "13:20:55", "release-96", "nginx:a6e433a", True), + ("12:58:12", "12:58:19", "release-95", "dashboard:26c6997", True), + ("12:31:09", "12:31:16", "release-94", "dashboard:26c6997", True), + ("11:57:44", "11:57:51", "release-93", "languages-start-points:ca386e0", False), + ("11:12:38", "11:12:46", "release-92", "languages-start-points:ca386e0", True), +] +ECR = "244531986313.dkr.ecr.eu-central-1.amazonaws.com" + + +def policy_rows(): + out = [] + for ident, ver, name, desc, tags in POLICIES: + pills = "".join(f'{t}' for t in tags) + out.append(f"""
+
+ {ident} {ver} +  {name} +
{desc}
+
{pills}
+
+ ⋮ +
""") + return "\n".join(out) + + +def decision_rows(): + out = [] + for req, rec, trail, art, ok in DECISIONS: + pill = (f'{TICK}Compliant' if ok + else f'{CROSS}Non-compliant') + out.append(f"""
+
+ 2026-07-27 • {req} +  requested · recorded {rec} +   my-release-flow / {trail} +
{ECR}/{art}
+
+ V1 + {pill} +
""") + return "\n".join(out) + + +# ---------------------------------------------------------------- page 1: list +list_body = f"""
+

Policies

+ Mockup • proposed design
+
+
+ Search by name or identifier
+
+
+
{ICON}Evaluation
+
Environment
+
+
+
{ICON}Policies 4 + Archived + Tags{CHEV}Sort{CHEV} +
+{policy_rows()} +
+
Policies are created and updated with the CLI or API + (kosli create policy --type rego). In the beta the UI is read-only, + apart from tagging.
+
""" + +# ----------------------------------------------------------- page 2: decisions +detail_head = f"""

pr-approved

V1 + + + Mockup • proposed design +
+
Pull request approved
+
Every pull request on the trail has at least one approver.
+
+ team=platformframework=soc2 + +
+
+
{ICON}Policy
+
{ICON}Decisions
+
{ICON}Versions
+
""" + +head_decisions = detail_head.replace("TAB_POLICY", "").replace("TAB_DECISIONS", "on") +head_source = detail_head.replace("TAB_POLICY", "on").replace("TAB_DECISIONS", "") + +decisions_card = f"""
+
{ICON}Decisions 128 + Timestamp{CHEV}Compliance{CHEV} + Version{CHEV}Sort{CHEV} + +
+{decision_rows()} +
""" + +decisions_body = f'
\n{head_decisions}\n{decisions_card}\n
' + +# ------------------------------------------------------ page 3: decision detail +tray = f"""
+
+
+

pr-approval-decision

+ {CROSS}Non-compliant + ✕
+
V1 + requested 2026-07-27 • 13:41:55 + ·recorded 13:42:02 + Mockup • proposed design
+
+
+
Evaluation context
evc_7f31a92c +  view
+
Control
RCTL-043 + — Source code review
+
Trail
my-release-flow / release-97
+
Artifact
{ECR}/nginx:a6e433a
+ sha256:a6e433a6fd3eb29c499b75310756420864b6c346
+
Policy
pr-approved V1
+ digest + sha256:9f2c1d84e0b7a35c6f1e8d2b47a90c3518fe64d7
+
Parameters
{{ "pr_attestation_name": "pull-request" }}
+
Evaluation engine
opa 1.18.2
+
Violations
    +
  • pull-request https://github.com/cyber-dojo/nginx/pull/42 has no approvers
  • +
  • pull-request https://github.com/cyber-dojo/nginx/pull/45 has no approvers
  • +
+
Evaluation report
+
+
""" + + +# ------------------------------------------------------- page 4: policy source +REGO = """package policy + +import rego.v1 +import data.lib.approvals + +# METADATA +# scope: document +# schemas: +# - input: schema["trail-v1"] +default allow = false + +allow if { + approvals.all_approved(input.trail, data.params.pr_attestation_name) +} + +violations contains msg if { + some pr in approvals.unapproved(input.trail, data.params.pr_attestation_name) + msg := sprintf("pull-request %v has no approvers", [pr.url]) +}""" + +KEYWORDS = ("package", "import", "default", "allow", "violations", "contains", + "every", "some", "if", "in") + + +def highlight(src): + out = [] + for line in src.split("\n"): + line = (line.replace("&", "&").replace("<", "<").replace(">", ">")) + line = re.sub(r'"([^"]*)"', lambda m: f'"{m.group(1)}"', line) + line = re.sub(r'\b(' + "|".join(KEYWORDS) + r')\b', + lambda m: f'{m.group(1)}', line) + out.append(line) + return "\n".join(out) + + +nlines = len(REGO.split("\n")) +gutter = "
".join(str(i) for i in range(1, nlines + 1)) + +FILES = """
+
FILES
+
policy.rego
+
lib/
+
approvals.rego
+
severity.rego
+
policy_test.regonot executed
+
4 files · 110 lines
+
""" + +source_card = f"""
+
{ICON}Policy bundle + V1 · current{CHEV} + + +
+
+ Bundle sha256:9f2c1d84… + roots policy + entrypoint policy.allow + schema trail + params read pr_attestation_name + published 2026-07-20 by alex@kosli.com +
+
+{FILES} +
+
policy.rego + entrypoint + sha256:9f2c1d84… · {nlines} lines
+
{gutter}
{highlight(REGO)}
+
+
+
+
The bundle is read-only here. Edit it in your repository and publish a new + version with kosli create policy — the version shown is the one that runs + when an evaluation names this policy. Tests are stored with the bundle for audit but are not + executed server-side.
""" + +source_body = f'
\n{head_source}\n{source_card}\n
' + +PAGES = { + "policy-list.html": ("cyber-dojo / policies", list_body, "Policies", ""), + "policy-decisions.html": ("cyber-dojo / policies / pr-approved", + decisions_body, "pr-approved", ""), + "policy-decision-detail.html": ("cyber-dojo / policies / pr-approved", + decisions_body, "pr-approved decision", tray), + "policy-source.html": ("cyber-dojo / policies / pr-approved", + source_body, "pr-approved policy", ""), +} + +for fn, (crumb, body, title, extra) in PAGES.items(): + html = shell(crumb, body, title) + if extra: + html = html.replace("", extra + "\n") + (HERE / fn).write_text(html) + print("wrote", fn) diff --git a/mockups/policies-ui/logo-footer.png b/mockups/policies-ui/logo-footer.png new file mode 100644 index 00000000..1709f8b7 Binary files /dev/null and b/mockups/policies-ui/logo-footer.png differ diff --git a/mockups/policies-ui/logo.png b/mockups/policies-ui/logo.png new file mode 100644 index 00000000..a290c35f Binary files /dev/null and b/mockups/policies-ui/logo.png differ diff --git a/tutorials/evaluate_policies_server_side.mdx b/tutorials/evaluate_policies_server_side.mdx new file mode 100644 index 00000000..a3bb753e --- /dev/null +++ b/tutorials/evaluate_policies_server_side.mdx @@ -0,0 +1,1198 @@ +--- +title: "Evaluate policies server-side" +description: "Proposed beta design: ship a versioned policy to Kosli, then evaluate it server-side to record a trustworthy, replicable decision." +--- + + +**Beta — enabled per organization.** Server-side policy evaluation is in beta. The commands on +this page are hidden in the Kosli CLI (v2.47.2 or later) and work only for organizations with +server-side evaluation enabled — ask the Kosli team to turn it on. Commands, flags, and output +may still change before general availability. Share comments with the Kosli team. + + +Today, `kosli evaluate` runs **client-side**: the policy lives in your repository, the check +runs on your CI runner, and Kosli only learns the outcome if you **explicitly attest it back** — +a self-reported result Kosli takes on trust. Server-side policy evaluation moves the Policy +Decision Point (PDP) **into Kosli**. You ship a versioned policy to Kosli, Kosli executes it +against facts it already holds, and Kosli **records the decision itself**, along with the exact +policy version and inputs it used. + +## Start here + +### Publish a policy, then evaluate it + +Two commands, which the rest of this page fills in: + +```shell +# Publish a versioned policy into Kosli, once. +kosli publish rego-policy pr-approved \ + --path pr-approved.rego \ + --input-schema trail \ + --name "Pull request approved" + +# Evaluate it from your pipeline, as often as you like. +kosli evaluate policy \ + --identifier pr-approved \ + --context trail=my-release-flow/my-release-trail \ + --control RCTL-043 \ + --flow my-release-flow \ + --trail my-release-trail \ + --fingerprint "$ARTIFACT_FINGERPRINT" \ + --name pr-approval-decision \ + --params '{"pr_attestation_name": "pull-request"}' \ + --assert +``` + +Kosli evaluates the attestations on `my-release-trail` and records a **decision** against control +`RCTL-043`, citing the policy version, the parameters, and the trails it evaluated. `--flow` and +`--trail` say where the decision is recorded, as on any other Kosli command, and `--context` says +which trail's facts to evaluate, as `trail=/` — the same trail here, a different one +when you need it. `--assert` fails the pipeline step if the policy denies. Everything else on this +page is detail around those two commands. + +**Adding `--control` is what records a decision.** Omit it and Kosli still runs the policy and keeps +the result, but writes no decision — an +[ad-hoc evaluation](#evaluate-without-recording-a-decision) you can inspect without adding anything +to the audit trail. That makes this one command two things: a way to check a policy against real +facts, and a way to claim a control is satisfied. + +### What is in the beta, and what is not + +**In:** publishing versioned policies, evaluating them against a trail or trails, running evaluations +that record no decision, recording decisions against controls, checking policies locally against +Kosli's published input schemas and capabilities, and reviewing the evaluations a policy has +produced — exporting any of them to re-run offline. + +**Not in:** evaluating anything other than trails, evaluating the contents of attachments, composing +several **published** policies into one evaluation, scheduled evaluations, pinning an evaluation to +a policy version, and advisory-only outcomes. See the +[full list, with reasons](#out-of-scope-for-the-beta). + +### Where to go next + + + + +For **governance platform engineers**. Publishing and evaluating, the Rego contract, testing, and +managing policies over time. + + + +For **auditors and control owners**. Review and filter the evaluations and decisions a policy has +produced — and re-run one yourself to check it. + + + +For **application teams**. Understand why a policy denied and what to do about it. + + + + + +This is distinct from the existing client-side +[Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) tutorial, where the +CLI runs the policy locally. Here, Kosli stores the policy and performs the evaluation. + + +## Why server-side + +Running evaluation inside Kosli changes **who computed the decision** — and where the data is +processed: + +| | Client-side `kosli evaluate trail` | Server-side `kosli evaluate policy` (beta) | +|---|---|---| +| **Where the policy lives** | Your repository | Stored and versioned in Kosli | +| **Where it runs** | Your CI runner | Kosli (isolated from the API) | +| **What Kosli stores** | Whatever you attest back (self-reported) | A decision Kosli computed, with the policy version and inputs it used | +| **Data movement** | Full trail evidence is fetched to the runner | Evaluated in place — nothing shipped to the client | +| **Replicability** | Depends on your pipeline | Decision cites its exact policy version and inputs | + +Because Kosli both stores the policy and runs it, a decision points at *exactly* what ran — the +policy version and its content digest, its parameters, and the trail facts that were in scope. The +record is complete enough to **reconstruct the evaluation**, which is what makes the decision +trustworthy for audit: you can take a decision, export what it was given, and run the policy again +yourself — see [Re-run a decision offline](#re-run-a-decision-offline). + +Running evaluation next to the data is also faster and scales further: Kosli doesn't have to +ship a trail's full evidence out to your runner before evaluating, so evaluations can cover much +larger volumes of facts in the cloud. + +## Prerequisites + +- [Install Kosli CLI](/getting_started/install) v2.47.2 or later. +- [Get a Kosli API token](/getting_started/authenticating_to_kosli). +- Server-side evaluation enabled for your organization (see the beta note above). +- Org admin rights to publish policies. Evaluating and recording decisions needs only an ordinary + API token. +- Have at least one [Control](/tutorials/working_with_controls) defined, and a + [Flow](/getting_started/flows) with a [Trail](/getting_started/trails) carrying the + attestations you want to evaluate. + +```shell +export KOSLI_ORG= +export KOSLI_API_TOKEN= +``` + +## Author, ship, and evaluate a policy + +This section takes a policy from local Rego to a recorded decision. The later sections build on +it — [checking](#check-and-test-a-policy) and [managing](#manage-policies) policies, and +[viewing](#view-the-evaluations-of-a-policy) and [inspecting](#inspect-a-failing-evaluation) the +evaluations they produce. + + + + + +Author your policy as code in your own repository. A policy is its **policy-as-code** (Rego), a +human-readable **name**, a short **description** of its purpose, and an **input schema** +declaring whether it evaluates a single trail or multiple trails. You give it a stable +**identifier** when you publish it to Kosli (next step). + +Your repository stays authoritative for **authoring** a policy — it is where the Rego is written, +reviewed, and tested. Kosli is authoritative for **what executed**: once a version is published, +that stored version is what runs and what a decision cites, so the audit record never depends on +your repository still being reachable or unchanged. + +A policy must define a boolean `allow`. Any other rule you annotate as an **entrypoint** is +returned alongside it as an output — `violations` in the example below. Follow the same +fail-safe design rules as client-side policies — see +[Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) for the reasoning behind +them. The full contract a policy is written against — entry point, `input`, parameters, available +built-ins, and result — is in [The Rego authoring contract](#the-rego-authoring-contract). + +```rego pr-approved.rego +package policy + +import rego.v1 + +pr_attestation_name := data.params.pr_attestation_name + +default allow := false + +allow if { + every pr in input.trail.compliance_status.attestations_statuses[pr_attestation_name].pull_requests { + count(pr.approvers) > 0 + } +} + +# METADATA +# entrypoint: true +violations contains msg if { + some pr in input.trail.compliance_status.attestations_statuses[pr_attestation_name].pull_requests + count(pr.approvers) == 0 + msg := sprintf("pull-request %v has no approvers", [pr.url]) +} + +violations contains msg if { + not input.trail.compliance_status.attestations_statuses[pr_attestation_name] + msg := sprintf("expected attestation %v is missing from the trail", [pr_attestation_name]) +} +``` + +Note the second `violations` rule. Without it, a trail that is missing the attestation entirely +leaves both `allow` and the first `violations` rule undefined — a denial with an empty violations +list and nothing for the team to act on. Cover the missing-fact case explicitly, so every way the +policy can deny produces something to fix. + +Before you ship it, [check and test the policy](#check-and-test-a-policy) locally, and try it +against real trail data. + + +In the beta, a policy evaluates the facts on the trail(s) you point it at — attestations on +`trail` / `trails`. Evaluating other targets (snapshots, artifacts, generic input) and +composing several **published** policies into one are **not included** (see [What's out of scope](#out-of-scope-for-the-beta)). + + + + + + +Publish the policy into Kosli with `kosli publish rego-policy` (or the API). The positional +argument is the policy's **stable identifier** (`pr-approved` in the example below) — this is what +decisions and evaluations use to refer to the policy, so it is **immutable once created**; choose +one that matches how your organization names its policies. Identifiers are matched +case-insensitively. `--name` and `--description` are the human-readable label and purpose; +`--name` is required the first time you publish, and both can be changed later without changing +the identifier. + +Pass a single `.rego` file to `--path`, as in the example below, or a directory when the policy is +split across several files. Repeat `--path` to bundle a shared library kept elsewhere alongside +your policy. Together they make **one policy's bundle**, not a collection of separate policies — +Kosli stores the whole set as one versioned bundle with one digest, so a decision still points at one +unambiguous thing. Tests — files named `*_test.rego`, following OPA's convention — are stored with +the bundle but never executed server-side. Data files (`data.json`, `data.yaml`, `*.ergo.yaml`) are +loaded into `data` at their directory's path; other files, such as a README, are stored but ignored. +A bundle is limited to 100 files and 1 MiB. + +Kosli records the first **version** on publish. Every later change to the policy's files or input +schema creates a new version and records who made it, so a decision can always point back to the +exact policy that produced it — see [Manage policies](#manage-policies) for updating, tagging, and +archiving over time. + +**Only org admins can publish a policy.** Creating, updating, and archiving a policy all require +org admin, the same as [controls](/tutorials/working_with_controls#creating-a-control). Running an +evaluation and recording a decision does not — an ordinary API token or service account is enough, +so pipelines can invoke a policy without being able to change one. That split means the team being +audited can *run* the control, but only an admin can decide what the control checks. + +```shell +kosli publish rego-policy pr-approved \ + --path pr-approved.rego \ + --input-schema trail \ + --name "Pull request approved" \ + --description "Every pull request on the trail has at least one approver." +``` + +```plaintext +rego policy 'pr-approved' version 1 is valid (sha256:3f9c…) +``` + +Kosli **validates every version** before it can be evaluated: the Rego must compile in OPA's strict +mode, type-check against the declared input schema, and define a boolean `allow`. The command waits +for that result. A version is `valid`, `rejected` (the policy is at fault — the reason is printed), +or `failed` (Kosli could not validate it — publish again to retry); the command exits non-zero unless +the version is valid. Only a valid version ever becomes the policy's **current** version, so a +rejected publish leaves the policy in service unchanged. + + +Rego policies are separate from [environment policies](/getting_started/policies), which keep +their own `kosli create policy` command. In the beta, authoring is **CLI/API only** — the Kosli UI +is read-only. + + + + + + +With `kosli evaluate policy`, Kosli fetches the in-scope facts and runs the policy **server-side**. +Passing `--control` additionally records a **decision** against that control, which is what this +step does; [omitting it](#evaluate-without-recording-a-decision) runs the evaluation on its own. + +```shell +kosli evaluate policy \ + --identifier pr-approved \ + --context trail="$FLOW_NAME/$TRAIL_NAME" \ + --control RCTL-043 \ + --flow "$FLOW_NAME" \ + --trail "$TRAIL_NAME" \ + --fingerprint "$ARTIFACT_FINGERPRINT" \ + --name pr-approval-decision \ + --params '{"pr_attestation_name": "pull-request"}' +``` + +```plaintext +RESULT: ALLOWED +POLICY: pr-approved, version 1 (sha256:3f9c…) +DECISION: 9d2e41b0… +``` + +The command queues the evaluation and waits for its outcome, for up to 30 seconds. `DECISION` is +the ID of the decision attestation it recorded. If the evaluation is still queued when the wait +runs out, the command exits non-zero and prints the evaluation's ID — read the outcome later with +[`kosli get evaluation`](#check-an-evaluation). + +The flags that carry the meaning, in two groups: + +| Flag | Group | Description | +|------|-------|-------------| +| `--identifier` / `--path` | Source | The policy to run — a published policy's **identifier** (uses its current version), or a local `.rego` file or bundle directory. | +| `--context` | Source | What it runs against. | +| `--control` | Destination | The control the resulting decision is recorded against. **Optional** — supplying it is what records a decision. | +| `--flow` / `--trail` | Destination | Where the decision is recorded. | + +A policy (`--identifier` or `--path`, not both) and `--context` are always **required** — they are +the evaluation. With `--control`, `--flow` and `--trail` are required too, and Kosli checks before +queuing that the trail exists, its flow is not archived, the control exists, and — if you pass +`--fingerprint` — that the artifact is on the trail. + +`--context` names the trail whose facts are **read**; `--flow` and `--trail` name the trail the +decision is **written** to. They are often the same trail, as above — see the *Evaluating facts +from another flow* example below for when they are not. + +Add `--assert` when the step should also fail on a denial — that is how an evaluation becomes a +pipeline gate. Once recorded, a decision appears against the policy — see +[View the evaluations of a policy](#view-the-evaluations-of-a-policy). + + + +| Flag | Description | +|------|-------------| +| `--identifier` | A published policy's identifier. Evaluates its **current version**. Exactly one of `--identifier` and `--path` is required. | +| `--path` | A **local** `.rego` file or bundle directory, sent with the evaluation (see [Evaluate a local policy](#evaluate-a-local-policy)). Repeat it to bundle a shared library alongside the policy. Exactly one of `--identifier` and `--path` is required. A local policy cannot record a decision. | +| `--context` | A trail to evaluate, as `trail=/`. Repeat it to evaluate several trails, up to 100. **Required.** | +| `--control` | The control the resulting decision is recorded against. **Optional** — supplying it records a decision; omitting it runs the evaluation and records none (see [Evaluate without recording a decision](#evaluate-without-recording-a-decision)). An empty value is an error, not an omission. Requires `--identifier` — not valid with a local `--path` policy, see [Evaluate a local policy](#evaluate-a-local-policy). | +| `--flow` | The flow the decision is recorded on. **Required with `--control`**; `KOSLI_FLOW` also satisfies it. | +| `--trail` | The trail the decision is recorded on. **Required with `--control`**; `KOSLI_TRAIL` also satisfies it. | +| `--name` | The attestation slot name the decision is recorded under on the trail. Defaults to `-decision`. Only valid with `--control`. A decision recorded here satisfies a `type: decision` attestation in the trail's [flow template](/template-reference/flow_template) when the names match — see [Requiring decisions in flow templates](/tutorials/working_with_controls#requiring-decisions-in-flow-templates). | +| `--fingerprint` | The SHA256 fingerprint of the artifact the decision applies to. **Optional** — without it, the decision is about the trail. Only valid with `--control`. | +| `--params` | Parameters passed to the policy as `data.params`, as JSON or `@file`. For a published policy that declares a params schema, Kosli rejects params that don't match it before queuing — see [Parameters](#parameters). Every evaluation records the params it used. | +| `--assert` | Exit non-zero when the policy denies, for use as a pipeline gate. Off by default. Valid without `--control`, in which case it gates on the outcome without recording a decision. | +| `--output` / `-o` | `table` (default) or `json`, which prints the whole evaluation. | + +**More on `--context`.** In the beta the only reference *type* is a trail, and each reference names +its flow: `trail=/`. Repeat the flag to evaluate several trails — see +[Evaluating multiple trails](#evaluating-multiple-trails). Kosli resolves these references into the +[evaluation](#check-an-evaluation) a decision cites; they are not a cluster or account selector. The +typed form leaves room for other context types — snapshots and reported workloads, for example — +later, without new flags. + + + +**Evaluating facts from another flow.** Usually `--context` and `--flow` / `--trail` name the same +trail. Because each `--context` reference carries its own flow, you can evaluate a trail in a +*different* flow — for example, evaluating a build flow's trail but recording the decision on a +release flow's trail: + +```shell +kosli evaluate policy \ + --identifier pr-approved \ + --context trail=build-ci/build-42 \ + --control RCTL-043 \ + --flow release \ + --trail release-99 \ + --fingerprint "$ARTIFACT_FINGERPRINT" \ + --name pr-approval-decision +``` + +The decision links to the flow and trail the facts came from, so a reviewer can always see that a +release decision was reached from a build flow's evidence rather than from the release trail itself. + + +Any flow you can write to can supply the facts for a decision on any other flow, and the beta does +not check that a given policy or source flow is acceptable for a given control. That makes it +possible for evidence from a low-assurance flow — a sandbox or an experiment — to close a control +that gates production. Until Kosli can constrain this, treat the set of flows that feed production +controls as something to decide and review, not something to leave open. + + + +**The server captures the evidence for you.** Because Kosli runs the evaluation, the decision +automatically records the policy's identifier, version, and digest, the parameters, the OPA +version, the policy's full result — `allow` and every output — and the policy bundle itself, as a +`policy.tar.gz` evidence file. You don't attach them by hand with `--attachments` / `--user-data` +as you do today when you +attest a client-side result yourself. Likewise, `--compliant` is **derived** from the policy's +`allow` result rather than passed in. This is the core difference from +[recording a decision yourself](/tutorials/working_with_controls#recording-a-decision-against-a-control). + + + +The decision this records is the same kind of decision described in +[Working with controls](/tutorials/working_with_controls) — the difference is that Kosli +computed it server-side rather than you attesting the outcome yourself. **Enforcement is +unchanged:** require the control via `for_control` in an [environment policy](/getting_started/policies) +and gate promotion with [`kosli assert artifact --environment`](/getting_started/enforce_policies), +exactly as before. Recording the decision is the PDP; asserting it is the PEP. + + + + + + +As soon as the evaluation completes, the decision appears in Kosli against the control and the +policy that produced it. Open it in the [Kosli app](https://app.kosli.com) — from the control's +decisions, or from the decision attestation on the trail — to see the outcome, the policy's full +result, and the **evaluation** it was reached from: + +| Recorded | Why it is there | +|---|---| +| The **evaluation** the decision came from | The evaluation holds what it was given. Naming it is what makes the decision traceable — and [re-runnable](#re-run-a-decision-offline). | +| The policy **identifier** and **version**, and a **digest** of the exact policy source that ran | A version number says which release; a digest proves the bytes. | +| The **parameters** used | Parameters are the only outside input, so a decision is not interpretable without them. | +| The **trails** evaluated, each at the moment it was read | Each links to the trail page as it stood when the evaluation was requested, so a reviewer sees the facts the policy was given, not the trail as it is now. | +| The **evaluation engine version** | The same policy on a different engine is not guaranteed to be the same evaluation. | +| The **requested** and **recorded** times | See below. | + +The decision names the trails a policy was **given**, not which attestations it **used** — a policy +may ignore half a trail. Tracing a check back to the attestation it read is part of the policy's +output: a policy written with Kosli's ergo library returns a report in which each check links to +the attestation it read. A plain Rego policy can do the same by returning attestation IDs in an +output of its own. + +**A decision carries two timestamps.** The **requested** time is when the evaluation was enqueued; +the **recorded** time is when the decision was written, once the evaluation ran. Kosli fixes the +trail moment — which attestations are in scope — at the requested time. The trail data itself is +only queried and serialized once the evaluation executes, but always **as of** that fixed timestamp, +not as of whatever the trail contains by then — so a queued evaluation is deterministic no matter +how long it waits. Kosli never backdates the record. + +For example, evaluation `ev_7f3a91c2` is requested at 09:14:22 and executes at 09:14:25. If a new +pull-request approval landed on the trail at 09:14:24 — after the request, before execution — it is +still **not** part of this decision: the query at 09:14:25 reads the trail as it stood at 09:14:22, +not as it stands when the evaluation happens to run. If that new fact changes the answer, it takes a +new evaluation to say so. + +Because the policy version, parameters, and evaluated trails are all pinned to that requested time, +the decision states exactly what was evaluated and when it applied — enough to +[re-run it offline](#re-run-a-decision-offline). To review and filter every evaluation a policy has +produced, see [View the evaluations of a policy](#view-the-evaluations-of-a-policy). + + + + + +### Check an evaluation + +Every run of `kosli evaluate policy` creates an **evaluation**, with or without a decision. An +evaluation exists from the moment it is queued, so you can ask about it before there is any +outcome — or any decision — to look at: + +```shell +kosli get evaluation ev_7f3a91c2 +``` + +```plaintext +EVALUATION: ev_7f3a91c2 +STATUS: completed +RESULT: ALLOWED +POLICY: pr-approved, version 1 (sha256:3f9c…) +REQUESTED: 2026-07-30T09:14:22Z +RECORDED: 2026-07-30T09:14:25Z +DECISION: 9d2e41b0… (pr-approval-decision on my-release-flow/my-release-trail) +``` + +`STATUS` is `pending` until the evaluation runs, then `completed` or `failed` — see +[When evaluation itself fails](#when-evaluation-itself-fails). Branch on `STATUS`, not on whether a +field is present. Once it is `completed`, the evaluation names the decision it produced, if any, so +an evaluation and the decision it becomes are one hop apart in both directions. + +An evaluation reaches a terminal status **once**. Its result is written a single time and never +revised, so `completed` and `failed` are final — re-evaluating creates a new evaluation rather than +changing this one. + +The same command returns what the evaluation was **given**, not just what it concluded: + +| Form | Returns | +|---|---| +| `kosli get evaluation ID` | The summary above. | +| `--output json` | The same, as JSON. | +| `--show-input --output json` | The above plus the policy input that was evaluated, under `.input`. | +| `--fixture --dir ./replay` | A replay bundle on disk — see [Re-run a decision offline](#re-run-a-decision-offline). | + +`--show-input` behaves as it does on the client-side +[`kosli evaluate trail`](/client_reference/kosli_evaluate_trail), so `jq '.input'` extracts the +policy input and `kosli evaluate input` consumes it unchanged. + +### Evaluate without recording a decision + +Leave `--control` off and Kosli runs the policy exactly as it would otherwise — same facts, same +pinning, same recorded result — but writes no decision: + +```shell +kosli evaluate policy \ + --identifier pr-approved \ + --context trail=my-release-flow/my-release-trail \ + --params '{"pr_attestation_name": "pull-request"}' +``` + +```plaintext +RESULT: ALLOWED +POLICY: pr-approved, version 1 (sha256:3f9c…) +``` + +With no decision recorded, the output has no `DECISION` line. + +The evaluation is still a full record: it has an ID, it pins the policy version and parameters it +used, and it can be [replayed](#re-run-a-decision-offline) like any other. What it doesn't do is +claim that a control is satisfied. Use it to check a policy against real facts — while you are +[iterating on one](#evaluate-a-local-policy), or to answer a one-off question — without adding to +the decisions an auditor samples. It still appears among the policy's +[evaluations](#view-the-evaluations-of-a-policy), marked as having recorded no decision. + +Because `--name` and `--fingerprint` describe the decision, they are rejected here. `--flow` and +`--trail` are accepted and ignored, so a pipeline that sets `KOSLI_FLOW` and `KOSLI_TRAIL` for every +step can run an evaluation without unsetting them. + + +An empty `--control` is an **error**, not an omission. Otherwise an unset `$CONTROL_ID` would quietly +turn a step that records a decision into one that records nothing, and the step would still pass. If +a control must be closed on every run, require it in the trail's +[flow template](/template-reference/flow_template) as a `type: decision` slot as well — a missing +decision then surfaces as a non-compliant trail rather than as silence. + + +`--assert` works here too, and gates the pipeline on the outcome without recording a decision. That +is a reasonable way to try a policy against real pipelines before you let it close a control — but it +is not a control: the evaluation records that the check ran, and nothing records that the control was +satisfied. + +### Evaluating multiple trails + +A policy published with `--input-schema trails` evaluates several trails at once and receives +them as `input.trails`. Repeat `--context` to name each one, up to 100: + +```shell +kosli evaluate policy \ + --identifier pr-approved-across-builds \ + --context trail=build-ci/build-41 \ + --context trail=build-ci/build-42 \ + --control RCTL-043 \ + --flow release \ + --trail release-99 \ + --fingerprint "$ARTIFACT_FINGERPRINT" \ + --name pr-approval-decision +``` + +Passing several references to a `trail`-schema policy is rejected before the evaluation is queued; a +trail named twice counts once. A `trails`-schema policy accepts any number, including one — it +always receives `input.trails` as an array. Several trails are read, but the decision is still +recorded on exactly one trail — the one named by `--flow` and `--trail`. + +## The Rego authoring contract + +Kosli evaluates your policy with OPA, so the tooling you already use locally — `opa fmt`, +`opa check`, `opa test` — works on these policies unchanged. What Kosli fixes is the contract +around the policy: the entry point it evaluates, the shape of `input`, the parameters and +built-ins a policy may use, and the result Kosli reads back. + + +This contract is part of the **beta**. If a constraint here would stop you shipping a control you +need, that is the most useful feedback you can give us. + + +### Entry point + +Kosli evaluates `allow` in the package that declares it, so the package can have any name and a +library alongside it needs no marking. When more than one package in a bundle declares `allow`, put +an `entrypoint: true` [metadata annotation](https://www.openpolicyagent.org/docs/policy-language#metadata) +in the policy's package — on the package or on any of its rules — and in no other package that +declares `allow`; Kosli rejects a bundle it cannot resolve to one entry point. The examples here use +`package policy`, which keeps your `opa test` cases simple: they refer to the policy under test as +`policy.allow`. + +### What the policy sees in `input` + +A policy evaluates the **attestations in the slots on the trail** — nothing else. Trail events +(`trail started`, `trail updated`) are not attestations and are not in `input`, and neither are +attestations from earlier moments of the same trail. Slots are matched **by name**, not by +attestation type: trail-level attestations are at +`compliance_status.attestations_statuses[]`, and an artifact's at +`compliance_status.artifacts_statuses[].attestations_statuses[]`, each carrying +the fields of the stored attestation. Alongside `compliance_status`, each trail carries its +`flow_name`, `trail_name`, `moment_number`, `created_at`, and `template_id`. + +**Attachments are not opened.** A policy sees an attestation's own fields, not the bytes of the +files attached to it, so it cannot parse a SARIF file, an SBOM, or a PDF report to reason about +individual findings. To evaluate that content, summarize it into JSON at attestation time — the +counts, severities, or package identifiers the policy needs — using whichever field the attestation +type gives you for structured data: + +- For a [custom attestation type](/administration/managing_custom_attestation_types/overview) you + define, pass the summary as `--attestation-data`, validated against your type's schema. This is + the better fit when the summary is a recurring shape worth its own schema. +- For a built-in type (`generic`, `snyk`, `sonar`, and others) with no schema field for your + summary, pass it as `--user-data` instead. + +Keep the original file on the same attestation with `--attachments`. The summary is what the policy +reads; the attachment is what lets an auditor go back to the file the summary was derived from and +check that it is faithful. + +The `--input-schema` you declare when you publish the policy selects the shape of `input`: + +| `--input-schema` | `input` contains | Reference schema | +|---|---|---| +| `trail` | `input.trail` — a single trail object, plus `input.trails` holding the same object, and `input.evaluation` | `https://docs.kosli.com/schemas/evaluation-input/trail/v1.json` | +| `trails` | `input.trails` — an array of trail objects, and `input.evaluation` | `https://docs.kosli.com/schemas/evaluation-input/trails/v1.json` | + +`input.evaluation.time` is described under [Evaluation time](#evaluation-time). + +Those schemas are published as JSON Schema so you can type-check a policy against `input` before +you ship it. Associate one with your rules using an OPA +[metadata annotation](https://www.openpolicyagent.org/docs/policy-language#metadata): + +```rego pr-approved.rego +package policy + +import rego.v1 + +# METADATA +# scope: document +# schemas: +# - input: schema["trail-v1"] +default allow := false +``` + +Then check the policy against it. `opa check` resolves `schema["trail-v1"]` to `trail-v1.json` in +the directory you pass to `--schema`: + +```shell +mkdir -p schemas +curl -so schemas/trail-v1.json https://docs.kosli.com/schemas/evaluation-input/trail/v1.json + +opa check --strict --schema schemas pr-approved.rego +``` + +This turns a misspelled field into a compile error naming the fields that do exist, rather than an +undefined reference that silently leaves `allow` at its `false` default. `--strict` additionally +rejects unused imports and unused local assignments. + + +These are the same schemas Kosli type-checks a policy against when you +[publish it](#author-ship-and-evaluate-a-policy), so a policy that passes `opa check` with them +passes that part of publishing too. See [Check and test a policy](#check-and-test-a-policy). + + +### Parameters + +Parameters are the only input to a policy from outside the trail. They arrive as `data.params`, +and every evaluation records the parameters it used, so a decision always shows what the policy +was run with. + +```rego +pr_attestation_name := data.params.pr_attestation_name +``` + +Alias parameters at the top of the policy, as above, so a missing parameter leaves the alias +undefined and any rule referencing it fails to evaluate — rather than the policy proceeding with a +silently absent value. + +To have Kosli check the parameters a published policy is given, **declare them** as a JSON Schema +in a metadata annotation on `data.params`, in the policy's package: + +```rego +# METADATA +# scope: package +# schemas: +# - data.params: +# type: object +# properties: +# pr_attestation_name: +# type: string +# required: [pr_attestation_name] +# additionalProperties: false +package policy +``` + +Kosli reads the schema when you publish, and **validates the parameters you supply at evaluation +time** against it — params that don't match are rejected before the evaluation is queued, naming +the field that failed. A policy that declares no schema accepts whatever params it is given, and a +[local policy](#evaluate-a-local-policy) is never checked. + +This closes off a failure that is otherwise very hard to spot. Supplying `maxHigh` to a policy that +reads `max_high` leaves the alias undefined, so `allow` falls back to `false` *and* the `violations` +rules that reference the same alias don't fire either — the result is a recorded non-compliant +decision with no violations explaining it, and a failed pipeline with nothing to fix. A schema with +`required` and `additionalProperties: false` turns that into an immediate, obvious error. + +A data file in the bundle cannot set `data.params`; parameters only ever come from the evaluation. + + +Parameters are recorded on every decision and are visible to anyone who can read that decision. +Pass thresholds, names, and flags — never secrets or personal data. + + +### Result + +| Field | Required | Purpose | +|---|---|---| +| `allow` | **Yes** | Boolean. The decision outcome — this is what `--compliant` is derived from. An `allow` that is undefined or not a boolean fails the evaluation rather than denying. | +| Outputs | No | Every rule in the policy's package annotated `entrypoint: true`, returned under `outputs` keyed by rule name, in whatever shape the rule produces. A rule undefined for this input is left out. | + +By convention, a `violations` output is a list of strings, each naming one thing that failed; +`kosli evaluate policy` prints them as `VIOLATIONS` rows. Every output is captured on the decision +and shown in its JSON view in the Kosli UI, and a report produced with Kosli's ergo library is +rendered as a report. + +Give `allow` a `default allow := false` and drive it through positive assertions, so an +evaluation that goes wrong denies rather than allows. + +### Available built-ins + +Because Kosli executes the policy, evaluation runs with **no network access** and must produce the +same decision every time it is re-evaluated. Both constraints are expressed as an OPA +[capabilities](https://www.openpolicyagent.org/docs/cli#capabilities) file, which you can check a +policy against locally before publishing: + +```shell +curl -so kosli-capabilities.json https://docs.kosli.com/rego/capabilities/v1.json +opa check --capabilities kosli-capabilities.json pr-approved.rego +``` + +Kosli removes every built-in that OPA marks **non-deterministic**, so one added by a later OPA +version is excluded too: + +| Built-ins | Status | Reason | +|---|---|---| +| `http.send`, `net.lookup_ip_addr` | Not available | Evaluation has no network access. | +| `time.now_ns`, `rand.intn`, `uuid.rfc4122`, `opa.runtime` | Not available | Non-deterministic — see [Evaluation time](#evaluation-time) for time windows. | +| `io.jwt.encode_sign`, `io.jwt.encode_sign_raw`, `io.jwt.decode_verify` | Not available | Non-deterministic. `io.jwt.decode` and the `io.jwt.verify_*` functions are available. | +| Everything else | Available | The rest of the OPA built-in set. | + +### Evaluation time + +A policy cannot read the clock, because a decision that depends on when it happened to run is not +replicable. Instead, Kosli passes the evaluation's request time — the moment the decision is +**valid as of** — as `input.evaluation.time`, an RFC 3339 UTC timestamp such as +`2026-07-30T09:14:22.000000Z`. Parse it to compare it with an attestation's timestamp: + +```rego +# 30 days before the moment this evaluation was requested. +cutoff := time.parse_rfc3339_ns(input.evaluation.time) - time.parse_duration_ns("720h") + +approval_is_current if { + time.parse_rfc3339_ns(approval_timestamp) > cutoff +} +``` + +Here `approval_timestamp` is whichever timestamp the attestation you are evaluating carries — the +field depends on the attestation type. Time windows work as normal, they are anchored to the moment +the decision applies to, and re-evaluating the stored context produces the same answer. + +### Engine and Rego version + +Kosli pins the OPA version it evaluates with — OPA 1.x, so Rego v1 syntax is the default — and +records it on every evaluation, decision, and policy version, so a decision states exactly what +ran. `kosli get rego-policy --policy-version ` shows the OPA version a version was +validated with. Author against that version locally to avoid a policy that compiles on your machine +but not in Kosli. + +### When evaluation itself fails + +A policy that times out or hits a runtime error is a *broken policy*, not a non-compliant +artifact, and Kosli reports the two differently: a failed evaluation records a typed error and +**no decision**, rather than a `--compliant=false` decision. `kosli evaluate policy` prints +`server-side evaluation failed (): ` instead of a `RESULT`, so a pipeline log tells +"this change is not compliant" from "this policy is broken". Both exit non-zero under `--assert`; +use `--output json` to branch on which one happened. + +The typed error lands on the evaluation, so a failure is visible whenever you +[check the evaluation](#check-an-evaluation): + +```plaintext +EVALUATION: ev_7f3a91c2 +STATUS: failed +POLICY: pr-approved, version 1 (sha256:3f9c…) +REQUESTED: 2026-07-30T09:14:22Z +ERROR: evaluate: evaluation exceeded its time budget +``` + +| Error kind | Meaning | +|---|---| +| `compile`, `entrypoint`, `no_policy` | The bundle could not be compiled, or no single entry point could be found. | +| `result_shape` | `allow` was undefined or not a boolean. | +| `input_shape` | The input or params could not be read. | +| `evaluate` | The policy failed while running — including running out of time, or a built-in that errored. | +| `policy_mismatch` | The stored policy no longer matched the digest the evaluation pinned. | +| `invoke_failed`, `enqueue_failed` | Kosli could not run the evaluation. Evaluate again. | +| `stranded` | The evaluation was never completed, and Kosli closed it after two hours. | + +An evaluation that is never completed — a worker lost, a queue drained — does not stay `pending` +for ever: Kosli marks it `stranded`, so every evaluation reaches a terminal status. + +A built-in that errors **aborts** the evaluation rather than evaluating to undefined — an undefined +built-in would deny with no violations, which is the outcome this design avoids everywhere else. +`print` and `trace` output is discarded rather than captured on the report; express diagnostics +through `violations` or another output instead. + +## Check and test a policy + +Before you ship a policy — or wire it into a pipeline gate — check that it holds up. + + +A policy is code, so treat it like code: keep **Rego tests alongside each policy** that assert it +allows and denies the cases you expect. Rego's built-in +[test framework](https://www.openpolicyagent.org/docs/policy-testing) runs these locally and in CI, +catching regressions before you publish a new version. + + +### Check locally + +There is no separate validate command: Kosli validates every version when you +[publish it](#author-ship-and-evaluate-a-policy). To catch problems before that, check the policy +with stock OPA against the same input schema and capabilities Kosli uses: + +```shell +mkdir -p schemas +curl -so schemas/trail-v1.json https://docs.kosli.com/schemas/evaluation-input/trail/v1.json +curl -so kosli-capabilities.json https://docs.kosli.com/rego/capabilities/v1.json + +opa check --strict \ + --schema schemas \ + --capabilities kosli-capabilities.json \ + pr-approved.rego +opa test . +``` + +This covers most of what publishing checks: strict-mode compilation, type-checking against the +input schema, and the built-ins the evaluation allows. Publishing additionally checks that the +bundle resolves to one entry point and that `allow` is a boolean. Pass a directory instead of a +file to check a bundle as a whole, as Kosli does. + +### Evaluate a local policy + +You don't have to publish a policy before it can evaluate anything. Point `--path` at a local, +unpublished `.rego` file — or at a directory holding the policy's bundle — and Kosli evaluates it +server-side just as it would a stored policy. This is how you iterate on a policy you haven't +shipped, against the facts it will really run on: + +```shell +kosli evaluate policy \ + --path ./pr-approved.rego \ + --context trail="$FLOW_NAME/$TRAIL_NAME" \ + --params '{"pr_attestation_name": "pull-request"}' +``` + +Because there is no stored version to cite, the evaluation records the exact policy source that ran, +so the result is still [replayable](#re-run-a-decision-offline). A local policy's params are not +checked against a schema. + +A local policy **cannot record a decision** — `--path` and `--control` are mutually exclusive. +[Publishing](#author-ship-and-evaluate-a-policy) is what puts a policy through the org-admin gate and +validation, so a decision from unpublished Rego would let anyone who can record a decision supply the +code behind it. Publish a version — [a no-op if nothing changed](#update-a-policy) — and evaluate it +with `--identifier` to record a decision. + +## Manage policies + +### Update a policy + +Publishing a change is the same command as the initial publish: re-run `kosli publish rego-policy` +with the updated files. A change to the files or the input schema creates a **new version** and +records who made it — add `--comment` to note what changed — so a decision always points back to the +exact version that produced it. The identifier never changes. + +```shell +kosli publish rego-policy pr-approved \ + --path pr-approved.rego \ + --input-schema trail \ + --name "Pull request approved by a code owner" \ + --description "Every pull request on the trail has an approver from a code owner." \ + --comment "Require approval from a code owner" +``` + +An evaluation that references a policy by identifier uses its **current version** — the newest one +that validated. Decisions recorded earlier keep pointing at the version that was current when they +were made. + +**Publishing an unchanged policy is a no-op.** If the files and input schema are identical to the +newest version, Kosli creates no new version, says so, and exits `0`: + +```plaintext +rego policy 'pr-approved' version 4 is valid (sha256:3f9c…); nothing new was stored, as the files and input schema are the same as this version +``` + +Publishing is idempotent, so it is safe to run on every merge: a pipeline that republishes an +untouched policy leaves the history alone instead of filling it with identical versions. + +What counts as a change: + +| Publish | Result | +|---|---| +| Same files and input schema as the newest version | **No-op.** No version created. | +| Any change to the files | New version. The digest is over the bytes, so reformatting counts. | +| A changed input schema | New version. | +| Same files and input schema, changed name or description | **No new version.** The policy's name and description are updated in place. | +| Only `--comment` differs | **No-op**, and the comment is not kept. A comment describes a change; with no change there is nothing to describe. | +| Same files as a newest version that was `rejected` or `failed` | New version, validated again — how you retry after a failure on Kosli's side. | + +Comparison is against the **newest** version, not the whole history. So republishing the bundle +from an earlier version is a real change and does create a new version — that is how you roll back, +and the history records that the rollback happened rather than hiding it. + +### Review a policy change + +A policy is the code that decides whether a control passes, so a change to it is a change to your +control environment. Reviewing that change is part of the lifecycle — author, test, version, +**review**, ship — and in the beta the review happens in your policy repository, not in Kosli. + +The practice we recommend, and the one Kosli's records are designed to corroborate: + + + + +Require pull requests, and require review from the people accountable for the control — a +`CODEOWNERS` entry per policy is the simplest way to make sure the right control owner sees a change +to *their* control. Restrict who can push directly. + + + +The question a control owner has to answer is not "is this valid Rego?" but "does this policy +actually meet the control objective?" Reviewing the change alongside its +[tests](#check-and-test-a-policy) is what makes that answerable — the tests state, in cases, what +the policy is claiming to enforce. + + + +Because [only org admins can publish](#author-ship-and-evaluate-a-policy), publish from a CI job +running with an admin credential on merge, rather than by hand from a laptop. The reviewed commit +then becomes the published version, and the two cannot drift. + +You can publish every policy on every merge without thinking about which ones changed — +[publishing an unchanged policy is a no-op](#update-a-policy), so only the policies you actually +edited get a new version. + + + +Use `--comment` to reference the change record, so Kosli's version history and your review history +join up: + +```shell +kosli publish rego-policy pr-approved \ + --path pr-approved.rego \ + --input-schema trail \ + --comment "Require approval from a code owner (acme/policies#214)" +``` + + + + +Kosli records who published each version, when, what they said changed, and the digest of the source +that was published. Combined with the review in your repository, that is a complete change-control +trail for the policy itself — which is what an auditor asking "who decided this is what the control +means?" is looking for. + + +There is **no review or approval step inside Kosli** in the beta: a policy version is live as soon as +it is published, and there is no draft state or second-approver requirement. If your control +environment needs approval to be enforced by Kosli rather than by your repository, tell us — it +affects how much of this belongs in the product. + + +### Tag a policy + +Tags are versionless labels for organizing and filtering policies — grouping by team, framework, +or control area. Tagging does **not** create a new version. + +```shell +kosli tag rego-policy pr-approved --set team=platform --set framework=soc2 +kosli tag rego-policy pr-approved --unset framework +``` + +### Archive and unarchive + +When a policy is retired, archive it rather than deleting it. Archiving is light-touch: the policy +is removed from the main list in the UI and CLI but **remains evaluable**, and its versions and past +decisions are preserved. Archiving records who archived it and when, and does not create a new +version. Like publishing, it requires org admin. Unarchive it if it comes back into use. + +```shell +kosli archive rego-policy pr-approved +kosli unarchive rego-policy pr-approved +``` + +### List and filter policies + +Browse your policies in the [Kosli app](https://app.kosli.com) — select **Policies** in the +sidebar and open the Rego policies tab to see the full catalog. Search by name or identifier, and +filter by tag to narrow the list. Each policy shows its identifier, current version, description, +and tags. + + + Rego policies catalog listing policies with their identifier, current version, description, and tags, with a search box and a tag filter + + +You can also list policies from the CLI, searching by name or identifier and filtering by tag: + +```shell +kosli list rego-policies --search pr- --tag team=platform +``` + +### View a policy + +Select a policy to open it. The page shows the policy-as-code that Kosli actually runs, read-only, +for the version you choose — not a name and description of it. + + + Rego policy page showing its versions, the selected version's status, digest, input schema and params schema, a file list, and the read-only Rego source of the selected file + + +The page lists every version with its status — `valid`, `rejected` with the reason, `pending`, or +`failed` — and the comment it was published with. For the version you select it shows what an +auditor needs to check the record: the **digest** a decision cites, the declared **input schema**, +the **params schema**, and the files in the bundle, opening on the file that holds the entry point. +Download the version's files to get exactly what Kosli stored. + +The same detail is available from the CLI. Without `--policy-version`, it shows the policy and its +current version: + +```shell +kosli get rego-policy pr-approved --policy-version 4 +``` + + +When you open a policy from a decision, the page shows the version **that decision used**, not the +current one — so the source in front of you is the source that produced the outcome you are looking +at. + + +Policies are read-only in the Kosli UI. Edit them in your repository and publish a new version with +`kosli publish rego-policy`. + +## View the evaluations of a policy + +This is where **auditors and control owners** review what a policy has decided. In the +[Kosli app](https://app.kosli.com), select **Policies**, choose a policy, and open its +**Evaluations** tab. It lists the policy's evaluations, newest first. Each row shows when the +evaluation was requested, the policy version it ran, the trails it evaluated, the outcome — +allow, deny, error, or pending — and a link to the decision it recorded, if any. + +Filter by version — the tab opens on the current version, and you can choose another or all of +them — by outcome, and by whether a decision was recorded, then page through the results to +sample them. Evaluations of [local policies](#evaluate-a-local-policy) are not listed, since they +cite no published policy. + + + Evaluations tab for a policy listing evaluations with requested time, policy version, evaluated trails, outcome, and a link to the recorded decision, with version, outcome, and decision filters + + +Select a decision to open it. The decision shows the trail and artifact it applies to, the trails +it was evaluated against at the moment they were read, the policy version and parameters that were +used, the policy's report, and the policy's full output. + + + Decision detail showing the trail, artifact, evaluated trails, policy version and parameters, the policy's report, and its full output + + + +To sample by **control** instead, open the control's Decisions tab. It lists every decision recorded +against the control, whichever policy produced it, and filters by outcome, time range, and flow. See +[Working with controls](/tutorials/working_with_controls). + + +### Re-run a decision offline + +Sampling a decision usually raises the question of whether it was *right*. Everything the +evaluation was given is held on the **evaluation** itself, which the decision names — so you can +export it and evaluate the policy again yourself, with stock OPA, on your own machine: + +```shell +kosli get evaluation EVALUATION-ID --fixture --dir ./replay +``` + +Every decision shows the evaluation it came from in the Kosli UI, so the evaluation ID is one hop +from the decision you are looking at. + +The export contains what the evaluation was given, as ordinary OPA inputs: + +| File | Contents | +|---|---| +| `input.json` | The input that was evaluated — `input.trail` and `input.trails` at the moment they were read, and `input.evaluation.time`. | +| `data.json` | `params` as supplied. | +| `policy/` | The exact policy source that ran, laid out as it was published — one `.rego` file, or every file in the bundle. Re-digesting this directory reproduces the digest on the decision. | + + +Because an evaluation exists from the moment it is queued, this export also works when there +is **no decision to look at** — an evaluation still in the queue, one that failed, or one +[run without `--control`](#evaluate-without-recording-a-decision). That is usually the case you most +want to inspect. + + +Evaluate it with OPA directly. `--bundle` loads the whole policy directory, so the same command +works whether the policy was one file or many. Query `allow` in the policy's package, and pass +`--strict-builtin-errors` so a built-in that errors aborts as it does in Kosli: + +```shell +opa eval --input replay/input.json \ + --data replay/data.json \ + --bundle replay/policy/ \ + --strict-builtin-errors \ + 'data.policy.allow' +``` + +Because the evaluation sandbox excludes anything non-deterministic — no clock, no network, no +randomness, with time supplied as input — this returns the outcome the decision recorded. If it +doesn't, that is a finding in itself. + +The export preserves the bundle's file layout because the digest is over the whole set — so an +auditor can verify the digest on the decision as well as re-run the policy, rather than taking on +trust that the exported source is what ran. + + +An auditor can reach the same conclusion independently, with a tool Kosli doesn't control. +The same export doubles as a **regression fixture**: commit `input.json` and `data.json` beside your +policy and assert against them with `opa test`, and a real past decision becomes a permanent test +case. + + +Re-running a stored decision through a *different* policy or version — to ask "what would the new +policy have said?" — is [out of scope for the beta](#out-of-scope-for-the-beta). Exporting the +fixture and running it locally is the way to answer that question today. + +## Inspect a failing evaluation + +When a policy denies, an **application team** needs to know why — and the decision records it +through the policy's outputs: a list of `violations`, a report, or both. Whether the policy's rules +weren't met (for example, a pull request with no approver) or an expected attestation was missing +from the trail, a well-written policy says so in its outputs. + +A policy can return **more than `allow` and `violations`** — every output is captured on the +decision, so you can surface richer, human-readable context beyond a bare pass/fail. + +When you gate a pipeline with `--assert`, Kosli prints each violation as its own row so failures are +readable directly in CI logs: + +```shell +kosli evaluate policy \ + --identifier pr-approved \ + --context trail="$FLOW_NAME/$TRAIL_NAME" \ + --control RCTL-043 \ + --flow "$FLOW_NAME" \ + --trail "$TRAIL_NAME" \ + --fingerprint "$ARTIFACT_FINGERPRINT" \ + --name pr-approval-decision \ + --params '{"pr_attestation_name": "pull-request"}' \ + --assert +``` + +```plaintext +RESULT: DENIED +VIOLATIONS: pull-request https://github.com/acme/app/pull/42 has no approvers +POLICY: pr-approved, version 1 (sha256:3f9c…) +DECISION: 9d2e41b0… +``` + +In the Kosli UI, the decision renders the policy's report and shows its full output — violations +included — in its JSON view (see [View the evaluations of a policy](#view-the-evaluations-of-a-policy)), +so an application team can see exactly what to fix. + +## Out of scope for the beta + +The following are explicitly **not included** in the beta and may be addressed later. They are +listed so you can tell what has been deferred from what has not been considered — if something here +blocks you, that is worth telling us. + +- **Evidence-attachment content evaluation** — evaluating the contents of attached files. Summarize + the attachment into JSON at attestation time — `--attestation-data` for a custom attestation type, + `--user-data` otherwise — and a policy can evaluate that instead — see + [What the policy sees in `input`](#what-the-policy-sees-in-input). +- **Composable policies** — combining several **separately published** policies, each with its own + identifier and version, into one evaluation. This is distinct from a bundle, where one policy spans + several Rego files under a single identifier, version, and digest. Bundles are supported. +- **Non-trail evaluate targets** — snapshots, artifacts, and generic input. +- **Scheduled evaluations** and **risk measurement**. +- **Associating policies to controls** — a stored policy-to-control link that lets Kosli infer + which policy closes which control. In the beta you name the control per evaluation with + `--control`; there is no stored mapping. +- **Pinning an evaluation to a policy version** — an evaluation by identifier always uses the + policy's current version, fixed at the moment you ask. There is no way to hold a pipeline on + version 3 while version 4 is rolled out. +- **Re-running an evaluation in Kosli** — you cannot replay a stored evaluation, either through a + different policy or version to compare outcomes, or as-is to check that it still reaches the same + answer. Export the fixture and run it locally. +- **Advisory outcomes** — a decision is compliant or non-compliant. There is no advisory or + warn-only mode that records a concern without failing the control. To try a policy without + affecting a control, [evaluate it without `--control`](#evaluate-without-recording-a-decision). +- **Verbose evaluation tracing** — beyond the outputs the policy returns, there is no rule-by-rule + trace of how a policy reached its answer. +- **Filtering by branch or environment** — a policy's evaluations filter by version, outcome, and + whether a decision was recorded, and a control's decisions by outcome, time, and flow, but neither + by where the change came from or where it was heading. +- **Listing evaluations or decisions from the CLI, and CSV export** — review them in the + [Kosli app](#view-the-evaluations-of-a-policy). + +## Next steps + +- [Working with controls](/tutorials/working_with_controls) — define controls and enforce them with `for_control` and `kosli assert`. +- [Environment policies](/getting_started/policies) — require controls before an artifact runs in an environment. +- [Enforce policies](/getting_started/enforce_policies) — gate promotion on compliance in your pipeline. +- [Evaluate trails with OPA policies](/tutorials/evaluate_trails_with_opa) — the client-side `kosli evaluate` this builds on. + +## Feedback + +Server-side policy evaluation is in beta. The most useful feedback is on the command surface +(`kosli evaluate policy` and how policies are published), +[the Rego authoring contract](#the-rego-authoring-contract) — particularly any constraint there +that would stop you shipping a control you need — the decision and audit model, and anything in +[What's out of scope](#out-of-scope-for-the-beta) that you would expect in the beta. Share +comments with the Kosli team. diff --git a/tutorials/working_with_controls.mdx b/tutorials/working_with_controls.mdx index 78515715..d566afe1 100644 --- a/tutorials/working_with_controls.mdx +++ b/tutorials/working_with_controls.mdx @@ -21,7 +21,7 @@ This tutorial covers how to: ## Prerequisites - [Install Kosli CLI](/getting_started/install). -- [Get a Kosli API token](/getting_started/service-accounts). +- [Get a Kosli API token](/getting_started/authenticating_to_kosli). - Have at least one [Flow](/getting_started/flows) and [Trail](/getting_started/trails) already created. ## Setup @@ -97,7 +97,7 @@ You can also list controls from the CLI with `kosli list controls`, fetch a sing When a control is retired, archive it rather than deleting it. Archiving moves the control out of the main catalog into its **Archived** view but preserves its full history — past decisions and every version of the definition remain intact for audit — and an archived control can be unarchived later if it comes back into use. -Archive or unarchive a control from its detail page (org admins only), from the CLI with `kosli archive control CONTROL-IDENTIFIER` and `kosli unarchive control CONTROL-IDENTIFIER`, or through the controls API (`POST /api/v2/controls/{org}/{identifier}/archive` and `.../unarchive`). Both actions appear as events in the control's **Versions** tab, alongside Created and Edited events, so the full lifecycle is auditable. +Archive or unarchive a control from its detail page (org admins only), from the CLI with `kosli archive control CONTROL-IDENTIFIER` and `kosli unarchive control CONTROL-IDENTIFIER`, or through the controls API (`POST /api/v2/controls/{org}/{identifier}/archive` and `.../unarchive`). Archiving and unarchiving are recorded as events with the actor and time, and do not create a new version of the definition. Both appear in the control's **Versions** tab alongside Created and Edited events, so the full lifecycle is auditable in one place. ## Recording a decision against a control