Skip to content
Open
41 changes: 34 additions & 7 deletions .github/workflows/init_kosli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ on:
required: false
type: string
default: 'none'
secrets:
secrets:
kosli_api_token:
required: true
pr_github_token:
Expand Down Expand Up @@ -75,8 +75,8 @@ jobs:
if: ${{ inputs.report_to_kosli != 'none' }}
env:
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
run: kosli begin trail ${{inputs.trail_name}}
--flow ${{inputs.flow_name}}
run: kosli begin trail ${{inputs.trail_name}}
--flow ${{inputs.flow_name}}
--org ${{inputs.kosli_org}}

- name: Report pull-request attestation to Kosli
Expand All @@ -85,11 +85,38 @@ jobs:
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
run: kosli attest pullrequest github
--flow ${{inputs.flow_name}}
--trail ${{inputs.trail_name}}
--trail ${{inputs.trail_name}}
--name pr
--github-token ${{ secrets.pr_github_token }}
--org ${{inputs.kosli_org}}


- name: Evaluate trails for four-eyes to Kosli
if: ${{ inputs.report_to_kosli == 'all' }}
env:
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
run: |
kosli evaluate trails ${{inputs.trail_name}} \
--policy "./bin/never_alone/four-eyes-policy.rego" \
--show-input \
--flow ${{inputs.flow_name}} \
--org ${{inputs.kosli_org}} \
--no-assert \
--params '{"attestation_name": "pr"}' \
--output json > "4eyes-eval-${{inputs.trail_name}}.json" || echo '{"allow":false,"violations":["evaluate command failed"]}' > "4eyes-eval-${{inputs.trail_name}}.json"

- name: Report four-eyes attestation to Kosli
if: ${{ inputs.report_to_kosli == 'all' }}
env:
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
run: |
kosli attest custom \
--type "four-eyes-result" \
--name "four-eyes-result" \
--attestation-data "4eyes-eval-${{inputs.trail_name}}.json" \
--attachments "./bin/never_alone/four-eyes-policy.rego" \
--trail ${{inputs.trail_name}} \
--flow ${{inputs.flow_name}} \
--org ${{inputs.kosli_org}}

- name: Report never-alone attestation to Kosli
if: ${{ inputs.report_to_kosli == 'all' }}
Expand All @@ -99,8 +126,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
USER_DATA_FILENAME=never-alone-user-data.json
./bin/never_alone/get_commit_and_pr_info.sh -c ${GITHUB_SHA} -o ${USER_DATA_FILENAME}
./bin/never_alone/get_commit_and_pr_info.sh -c ${GITHUB_SHA} -o ${USER_DATA_FILENAME}

PR_URL=$(cat ${USER_DATA_FILENAME} | jq -r '.pullRequest.url // empty')
if [ -n "$PR_URL" ]; then
PR_ANNOTATE_ARG="--annotate pull_request=$PR_URL"
Expand Down
209 changes: 209 additions & 0 deletions bin/never_alone/four-eyes-policy.rego
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

# Set PR attestation name
attestation_name := name if {
name := data.params.attestation_name
is_string(name)
} else := "pr-review"
Comment thread
mbevc1 marked this conversation as resolved.
Comment thread
mbevc1 marked this conversation as resolved.
Comment thread
mbevc1 marked this conversation as resolved.
Comment thread
mbevc1 marked this conversation as resolved.
Comment thread
mbevc1 marked this conversation as resolved.
Comment thread
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[attestation_name]
#
# Attested via: kosli attest pullrequest github --name <attestation_name> --commit <sha>
# ---------------------------------------------------------------------------

# 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
}
Comment thread
mbevc1 marked this conversation as resolved.
}
Comment thread
mbevc1 marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: every/some over empty pr.approvers is vacuously true.

In Rego, every author in all_authors { some approver in pr.approvers ... } succeeds when pr.approvers is empty — there's nothing to iterate, so no condition fails. The count(all_authors) > 0 guard only protects against empty authors, not empty approvers.

This was flagged before and marked as intentional, so just confirming: if a PR has zero approvers, this rule can still evaluate to true (for the non-merge-commit case). Is that the desired behavior? If not, add count(pr.approvers) > 0 as a guard.


# 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"
Comment thread
mbevc1 marked this conversation as resolved.
}
Comment thread
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.
# ---------------------------------------------------------------------------
Comment thread
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)],
)
}
46 changes: 46 additions & 0 deletions bin/never_alone/four-eyes-result-schema.json
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,
Comment thread
mbevc1 marked this conversation as resolved.
"properties": {
Comment thread
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"
}
}
}
25 changes: 25 additions & 0 deletions bin/never_alone/setup_attestation_type.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail

# Use first arg, existing ENV or set a default
KOSLI_ORG="${1:-${KOSLI_ORG:-kosli-public}}"

# 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 type ready."
2 changes: 2 additions & 0 deletions main-flow-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ trail:
type: snyk
- name: never-alone-data
type: generic
- name: four-eyes-result
type: custom
artifacts:
- name: cli-docker
attestations:
Expand Down
Loading