-
Notifications
You must be signed in to change notification settings - Fork 7
chore: check and report four-eyes to Kosli #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
ca69aa7
6c23acd
61e3e59
0d3224f
03355fb
248830f
9528924
31825ed
a2bbee3
24cb5e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| package policy | ||
|
|
||
| import rego.v1 | ||
|
|
||
| # Four-eyes principle enforcement: every commit must have independent review. | ||
| # This policy evaluates per-commit attestation data from Kosli and passes only | ||
| # when all violations are resolved. | ||
| default allow = false | ||
|
|
||
| allow if count(violation_reasons) == 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mbevc1 I wonder if we need to think about this pattern - as @JonJagger pointed out, this can lead to accidental approval, where the violation_reasons ends up as an empty array not because there were no violoations but because of a mistake in the rego.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a fair point, but I wonder if there is something we can do here or we should record this and circle back
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW - I found that Claude had no problem re-writing the old tutorial to switch to positive assertions (as Tooky suggests).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core principles of using positive assertions is also in the readme in this repo https://github.com/kosli-dev/snyk-blog#3-continuous-environment-scanning-workflows
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah maybe. I was wondering if we should do soemthing where expressly capture that we have checked each "rule" and then check that collection. Something like: {
"results": [
{
"allow": true,
},
{
"allow": false,
},
{
"allow": true,
}
]
}default allow := false
allow if {
count(input.results) > 0
every result in input.results {
result.allow == true
}
} |
||
|
|
||
| # Set PR attestation name | ||
| attestation_name := name if { | ||
| name := data.params.attestation_name | ||
| is_string(name) | ||
| } else := "pr-review" | ||
|
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Attestation data | ||
| # | ||
| # Used with `kosli evaluate trails` (plural). Each trail in input.trails | ||
| # represents one commit. The PR attestation payload is at: | ||
| # trail.compliance_status.attestations_statuses["pr-review"] | ||
| # | ||
| # Attested via: kosli attest pullrequest github --name pr-review --commit <sha> | ||
|
mbevc1 marked this conversation as resolved.
Outdated
|
||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # Extract PR attestation payload from a trail. | ||
| pr_attest(trail) := trail.compliance_status.attestations_statuses[attestation_name] | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # GitHub usernames of all PR branch commit authors whose identity was resolved. | ||
| pr_commit_authors(pr) := {u | | ||
| some c in pr.commits | ||
| u := c.author_username | ||
| u != null | ||
| } | ||
|
|
||
| # Latest Unix timestamp among PR branch commits. | ||
| latest_commit_ts(pr) := max({c.timestamp | some c in pr.commits}) | ||
|
|
||
| # A commit is the merge commit when the PR's merge_commit field matches the | ||
| # trail name (which is the commit SHA). Covers squash, regular, and rebase merges. | ||
| is_merge_commit(trail, pr) if { | ||
| trail.name == pr.merge_commit | ||
| } | ||
|
|
||
| # Regular commit: PR branch authors + PR author all need independent approval after last code commit. | ||
| has_independent_approval(trail, pr) if { | ||
| not is_merge_commit(trail, pr) | ||
| cutoff := latest_commit_ts(pr) | ||
| all_authors := pr_commit_authors(pr) | {pr.author} | ||
| count(all_authors) > 0 | ||
| every author in all_authors { | ||
| some approver in pr.approvers | ||
| approver.state == "APPROVED" | ||
| is_string(approver.username) | ||
| approver.username != author | ||
| approver.timestamp > cutoff | ||
| } | ||
|
mbevc1 marked this conversation as resolved.
|
||
| } | ||
|
mbevc1 marked this conversation as resolved.
mbevc1 marked this conversation as resolved.
|
||
|
|
||
| # Merge commit: only PR branch commit authors need independent approval. | ||
| # The merge button clicker did not write code and requires no separate review. | ||
| has_independent_approval(trail, pr) if { | ||
| is_merge_commit(trail, pr) | ||
| cutoff := latest_commit_ts(pr) | ||
| all_authors := pr_commit_authors(pr) | ||
| count(all_authors) > 0 | ||
| every author in all_authors { | ||
| some approver in pr.approvers | ||
| approver.state == "APPROVED" | ||
| is_string(approver.username) | ||
| approver.username != author | ||
| approver.timestamp > cutoff | ||
| } | ||
| } | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Service account exemption | ||
| # | ||
| # Matched against trail.git_commit_info.author, which is "Name <email>" format. | ||
| # Patterns work against the full string, e.g.: | ||
| # "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| service_account_patterns := { | ||
| "svc_.*", | ||
| ".*\\[bot\\]", | ||
| "noreply@github.com" | ||
|
mbevc1 marked this conversation as resolved.
|
||
| } | ||
|
mbevc1 marked this conversation as resolved.
|
||
|
|
||
| # Commit author is a service account (CI, GitHub Actions, dependabot, etc). | ||
| is_service_account(trail) if { | ||
| some pattern in service_account_patterns | ||
| regex.match(pattern, trail.git_commit_info.author) | ||
| } | ||
|
|
||
| # PR commit author is unresolvable (web-flow edits, Copilot co-auth). | ||
| is_web_flow_commit(c) if { | ||
| some pattern in service_account_patterns | ||
| regex.match(pattern, object.get(c, "author", "")) | ||
| } | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers — multi-PR support | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # Check if any associated PR has independent approval for the commit. | ||
| has_any_pr_approval(trail, attest) if { | ||
| some pr in attest.pull_requests | ||
| has_independent_approval(trail, pr) | ||
| } | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Violation reasons — detection only, no sprintf | ||
| # | ||
| # allow is derived from this set. Keeping detection logic here (no sprintf) | ||
| # means a formatting failure in the violations rules below cannot silently | ||
| # empty this set and flip allow to true. | ||
| # --------------------------------------------------------------------------- | ||
|
mbevc1 marked this conversation as resolved.
|
||
|
|
||
| # Guard: if input.trails is absent or not an array, every other rule silently | ||
| # skips iteration and violation_reasons stays empty, making allow=true. | ||
| # object.get ensures the argument to is_array is always defined | ||
| # (avoids undefined-arg propagation that would make `not is_array(undefined)` | ||
| # silently skip the rule). | ||
| violation_reasons contains "missing_trails_input" if { | ||
| trails := object.get(input, "trails", null) | ||
| not is_array(trails) | ||
| } | ||
|
|
||
| # Missing attestation: no PR review data collected for this commit. | ||
| violation_reasons contains {"type": "missing_attestation", "trail": trail.name} if { | ||
| some trail in input.trails | ||
| not trail.compliance_status.attestations_statuses[attestation_name] | ||
| } | ||
|
|
||
| # Unverifiable identity: commit author has no resolvable GitHub account and is not a known service account. | ||
| violation_reasons contains {"type": "unverifiable_identity", "pr_url": pr.url, "sha": c.sha1} if { | ||
| some trail in input.trails | ||
| attest := pr_attest(trail) | ||
| some pr in attest.pull_requests | ||
| some c in pr.commits | ||
| object.get(c, "author_username", null) == null | ||
| not is_service_account(trail) | ||
| not is_web_flow_commit(c) | ||
| } | ||
|
|
||
| # Missing PR: commit has no associated merged pull request (non-service-account commits must come through a PR). | ||
| violation_reasons contains {"type": "missing_pr", "trail": trail.name} if { | ||
| some trail in input.trails | ||
| not is_service_account(trail) | ||
| attest := pr_attest(trail) | ||
| count(attest.pull_requests) == 0 | ||
| } | ||
|
|
||
| # Missing approval: commit has an associated PR but no independent approval from someone other than the authors. | ||
| violation_reasons contains {"type": "missing_approval", "trail": trail.name} if { | ||
| some trail in input.trails | ||
| not is_service_account(trail) | ||
| attest := pr_attest(trail) | ||
| count(attest.pull_requests) > 0 | ||
| not has_any_pr_approval(trail, attest) | ||
| } | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Violations — message formatting only | ||
| # | ||
| # allow does NOT depend on this set. A sprintf failure here cannot affect | ||
| # the compliance decision; it only affects the human-readable output. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| violations contains "Policy error: input.trails is missing or not an array — cannot evaluate" if { | ||
| "missing_trails_input" in violation_reasons | ||
| } | ||
|
|
||
| violations contains msg if { | ||
| some r in violation_reasons | ||
| r.type == "missing_attestation" | ||
| msg := sprintf("Trail %v: %v attestation is missing", [r.trail, attestation_name]) | ||
| } | ||
|
|
||
| violations contains msg if { | ||
| some r in violation_reasons | ||
| r.type == "unverifiable_identity" | ||
| msg := sprintf( | ||
| "PR %v: commit %v has no linked GitHub account — identity unverifiable", | ||
| [r.pr_url, substring(r.sha, 0, 7)], | ||
| ) | ||
| } | ||
|
|
||
| violations contains msg if { | ||
| some r in violation_reasons | ||
| r.type == "missing_pr" | ||
| msg := sprintf("Commit %v: no associated PR found", [substring(r.trail, 0, 7)]) | ||
| } | ||
|
|
||
| violations contains msg if { | ||
| some r in violation_reasons | ||
| r.type == "missing_approval" | ||
| msg := sprintf( | ||
| "Commit %v: no independent approval after latest code commit", | ||
| [substring(r.trail, 0, 7)], | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "$id": "four-eyes-result", | ||
| "title": "FourEyesResult", | ||
| "description": "Policy evaluation result for a release commit range, produced by kosli evaluate trails against four-eyes.rego", | ||
| "type": "object", | ||
| "required": ["allow", "violations"], | ||
| "additionalProperties": false, | ||
|
mbevc1 marked this conversation as resolved.
|
||
| "properties": { | ||
|
mbevc1 marked this conversation as resolved.
|
||
| "allow": { | ||
| "type": "boolean", | ||
| "description": "true if every commit in the evaluated range passes the four-eyes check" | ||
| }, | ||
| "violations": { | ||
| "oneOf": [ | ||
| { "type": "null" }, | ||
| { | ||
| "type": "array", | ||
| "items": { "type": "string" } | ||
| } | ||
| ], | ||
| "description": "Violation messages, one per failing commit. null or empty array when allow is true." | ||
| }, | ||
| "evaluated_at": { | ||
| "type": "string", | ||
| "format": "date-time", | ||
| "description": "ISO 8601 timestamp of the policy evaluation" | ||
| }, | ||
| "repository": { | ||
| "type": "string", | ||
| "description": "Repository in owner/repo format" | ||
| }, | ||
| "base_commit": { | ||
| "type": "string", | ||
| "description": "SHA of the exclusive range start (the last commit of the previous release)" | ||
| }, | ||
| "current_commit": { | ||
| "type": "string", | ||
| "description": "SHA of the inclusive range end (the HEAD commit of this release, named as the attestation trail)" | ||
| }, | ||
| "input": { | ||
| "type": "object", | ||
| "description": "Raw trails input data included when kosli evaluate trails is run with --show-input" | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| KOSLI_ORG=kosli-public | ||
|
mbevc1 marked this conversation as resolved.
Outdated
|
||
|
|
||
| # One-time setup: create custom attestation types for never-alone. | ||
| # Run this after any schema change. Types cannot be updated in place; | ||
| # delete via the Kosli UI/API first if re-creating an existing type. | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
|
|
||
| if [[ -z "${KOSLI_API_TOKEN:-}" ]]; then | ||
| echo "ERROR: KOSLI_API_TOKEN is not set" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Creating four-eyes-result attestation type (release-level policy evaluation result)..." | ||
| kosli create attestation-type four-eyes-result \ | ||
| --description "Four-eyes policy evaluation result for a release commit range (never-alone)" \ | ||
| --schema "${SCRIPT_DIR}/four-eyes-result-schema.json" \ | ||
| --jq ".allow == true" \ | ||
| --org "${KOSLI_ORG}" | ||
|
|
||
| echo "Done — four-eyes-result attestation types ready." | ||
|
mbevc1 marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.