Skip to content

Commit ca69aa7

Browse files
committed
chore: check and report four-eyes to Kosli
1 parent a2ee562 commit ca69aa7

5 files changed

Lines changed: 323 additions & 7 deletions

File tree

.github/workflows/init_kosli.yml

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ on:
2323
required: false
2424
type: string
2525
default: 'none'
26-
secrets:
26+
secrets:
2727
kosli_api_token:
2828
required: true
2929
pr_github_token:
@@ -75,8 +75,8 @@ jobs:
7575
if: ${{ inputs.report_to_kosli != 'none' }}
7676
env:
7777
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
78-
run: kosli begin trail ${{inputs.trail_name}}
79-
--flow ${{inputs.flow_name}}
78+
run: kosli begin trail ${{inputs.trail_name}}
79+
--flow ${{inputs.flow_name}}
8080
--org ${{inputs.kosli_org}}
8181

8282
- name: Report pull-request attestation to Kosli
@@ -85,11 +85,32 @@ jobs:
8585
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
8686
run: kosli attest pullrequest github
8787
--flow ${{inputs.flow_name}}
88-
--trail ${{inputs.trail_name}}
88+
--trail ${{inputs.trail_name}}
8989
--name pr
9090
--github-token ${{ secrets.pr_github_token }}
9191
--org ${{inputs.kosli_org}}
92-
92+
93+
- name: Evaluate trails for four-eyes to Kosli
94+
if: ${{ inputs.report_to_kosli == 'all' }}
95+
env:
96+
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
97+
run: kosli evaluate trails ${{inputs.trail_name}} \
98+
--policy "./bin/never_alone/four-eyes-policy.rego" \
99+
--show-input \
100+
--flow ${{inputs.flow_name}} \
101+
--output json > "4eyes-eval-${{inputs.trail_name}}.json" 2>/dev/null || true
102+
103+
- name: Report four-eyes attestation to Kosli
104+
if: ${{ inputs.report_to_kosli == 'all' }}
105+
env:
106+
KOSLI_API_TOKEN: ${{ secrets.kosli_api_token }}
107+
run: kosli attest custom \
108+
--type "four-eyes-result" \
109+
--name "four-eyes-result" \
110+
--attestation-data "4eyes-eval-${{inputs.trail_name}}.json" \
111+
--attachments "./bin/never_alone/four-eyes-policy.rego" \
112+
--trail ${{inputs.trail_name}} \
113+
--flow ${{inputs.flow_name}}
93114

94115
- name: Report never-alone attestation to Kosli
95116
if: ${{ inputs.report_to_kosli == 'all' }}
@@ -99,8 +120,8 @@ jobs:
99120
GH_TOKEN: ${{ github.token }}
100121
run: |
101122
USER_DATA_FILENAME=never-alone-user-data.json
102-
./bin/never_alone/get_commit_and_pr_info.sh -c ${GITHUB_SHA} -o ${USER_DATA_FILENAME}
103-
123+
./bin/never_alone/get_commit_and_pr_info.sh -c ${GITHUB_SHA} -o ${USER_DATA_FILENAME}
124+
104125
PR_URL=$(cat ${USER_DATA_FILENAME} | jq -r '.pullRequest.url // empty')
105126
if [ -n "$PR_URL" ]; then
106127
PR_ANNOTATE_ARG="--annotate pull_request=$PR_URL"

bin/never_alone/4eyes.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
KOSLI_ORG=kosli-public
4+
KOSLI_FLOW=cli-release
5+
6+
#kosli list trails --flow $KOSLI_FLOW
7+
kosli evaluate trails "a2ee562" --flow cli --policy four-eyes-policy.rego --params '{"attestation_name": "pr"}' --show-input --output json
8+
9+
#echo "Attesting release evaluation result to trail ${CURRENT_TAG}..."
10+
#kosli attest custom \
11+
# --type "four-eyes-result" \
12+
# --name "four-eyes-result" \
13+
# --attestation-data "${EVAL_FILE}" \
14+
# --attachments "${SCRIPT_DIR}/four-eyes-policy.rego" \
15+
# --trail "${CURRENT_TAG}" \
16+
# --flow "${KOSLI_FLOW}"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package policy
2+
3+
import rego.v1
4+
5+
# Four-eyes principle enforcement: every commit must have independent review.
6+
# This policy evaluates per-commit attestation data from Kosli and passes only
7+
# when all violations are resolved.
8+
default allow = false
9+
10+
allow if count(violation_reasons) == 0
11+
12+
# Set PR attestation name
13+
attestation_name := name if {
14+
name := data.params.attestation_name
15+
is_string(name)
16+
} else := "pr-review"
17+
18+
# ---------------------------------------------------------------------------
19+
# Attestation data
20+
#
21+
# Used with `kosli evaluate trails` (plural). Each trail in input.trails
22+
# represents one commit. The PR attestation payload is at:
23+
# trail.compliance_status.attestations_statuses["pr-review"]
24+
#
25+
# Attested via: kosli attest pullrequest github --name pr-review --commit <sha>
26+
# ---------------------------------------------------------------------------
27+
28+
# Extract PR attestation payload from a trail.
29+
pr_attest(trail) := trail.compliance_status.attestations_statuses[attestation_name]
30+
31+
# ---------------------------------------------------------------------------
32+
# Helpers
33+
# ---------------------------------------------------------------------------
34+
35+
# GitHub usernames of all PR branch commit authors whose identity was resolved.
36+
pr_commit_authors(pr) := {u |
37+
some c in pr.commits
38+
u := c.author_username
39+
u != null
40+
}
41+
42+
# Latest Unix timestamp among PR branch commits.
43+
latest_commit_ts(pr) := max({c.timestamp | some c in pr.commits})
44+
45+
# A commit is the merge commit when the PR's merge_commit field matches the
46+
# trail name (which is the commit SHA). Covers squash, regular, and rebase merges.
47+
is_merge_commit(trail, pr) if {
48+
trail.name == pr.merge_commit
49+
}
50+
51+
# Regular commit: PR branch authors + PR author all need independent approval after last code commit.
52+
has_independent_approval(trail, pr) if {
53+
not is_merge_commit(trail, pr)
54+
cutoff := latest_commit_ts(pr)
55+
all_authors := pr_commit_authors(pr) | {pr.author}
56+
count(all_authors) > 0
57+
every author in all_authors {
58+
some approver in pr.approvers
59+
approver.state == "APPROVED"
60+
is_string(approver.username)
61+
approver.username != author
62+
approver.timestamp > cutoff
63+
}
64+
}
65+
66+
# Merge commit: only PR branch commit authors need independent approval.
67+
# The merge button clicker did not write code and requires no separate review.
68+
has_independent_approval(trail, pr) if {
69+
is_merge_commit(trail, pr)
70+
cutoff := latest_commit_ts(pr)
71+
all_authors := pr_commit_authors(pr)
72+
count(all_authors) > 0
73+
every author in all_authors {
74+
some approver in pr.approvers
75+
approver.state == "APPROVED"
76+
is_string(approver.username)
77+
approver.username != author
78+
approver.timestamp > cutoff
79+
}
80+
}
81+
82+
# ---------------------------------------------------------------------------
83+
# Service account exemption
84+
#
85+
# Matched against trail.git_commit_info.author, which is "Name <email>" format.
86+
# Patterns work against the full string, e.g.:
87+
# "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
88+
# ---------------------------------------------------------------------------
89+
90+
service_account_patterns := {
91+
"svc_.*",
92+
".*\\[bot\\]",
93+
"noreply@github.com"
94+
}
95+
96+
# Commit author is a service account (CI, GitHub Actions, dependabot, etc).
97+
is_service_account(trail) if {
98+
some pattern in service_account_patterns
99+
regex.match(pattern, trail.git_commit_info.author)
100+
}
101+
102+
# PR commit author is unresolvable (web-flow edits, Copilot co-auth).
103+
is_web_flow_commit(c) if {
104+
some pattern in service_account_patterns
105+
regex.match(pattern, object.get(c, "author", ""))
106+
}
107+
108+
# ---------------------------------------------------------------------------
109+
# Helpers — multi-PR support
110+
# ---------------------------------------------------------------------------
111+
112+
# Check if any associated PR has independent approval for the commit.
113+
has_any_pr_approval(trail, attest) if {
114+
some pr in attest.pull_requests
115+
has_independent_approval(trail, pr)
116+
}
117+
118+
# ---------------------------------------------------------------------------
119+
# Violation reasons — detection only, no sprintf
120+
#
121+
# allow is derived from this set. Keeping detection logic here (no sprintf)
122+
# means a formatting failure in the violations rules below cannot silently
123+
# empty this set and flip allow to true.
124+
# ---------------------------------------------------------------------------
125+
126+
# Guard: if input.trails is absent or not an array, every other rule silently
127+
# skips iteration and violation_reasons stays empty, making allow=true.
128+
# object.get ensures the argument to is_array is always defined
129+
# (avoids undefined-arg propagation that would make `not is_array(undefined)`
130+
# silently skip the rule).
131+
violation_reasons contains "missing_trails_input" if {
132+
trails := object.get(input, "trails", null)
133+
not is_array(trails)
134+
}
135+
136+
# Missing attestation: no PR review data collected for this commit.
137+
violation_reasons contains {"type": "missing_attestation", "trail": trail.name} if {
138+
some trail in input.trails
139+
not trail.compliance_status.attestations_statuses[attestation_name]
140+
}
141+
142+
# Unverifiable identity: commit author has no resolvable GitHub account and is not a known service account.
143+
violation_reasons contains {"type": "unverifiable_identity", "pr_url": pr.url, "sha": c.sha1} if {
144+
some trail in input.trails
145+
attest := pr_attest(trail)
146+
some pr in attest.pull_requests
147+
some c in pr.commits
148+
object.get(c, "author_username", null) == null
149+
not is_service_account(trail)
150+
not is_web_flow_commit(c)
151+
}
152+
153+
# Missing PR: commit has no associated merged pull request (non-service-account commits must come through a PR).
154+
violation_reasons contains {"type": "missing_pr", "trail": trail.name} if {
155+
some trail in input.trails
156+
not is_service_account(trail)
157+
attest := pr_attest(trail)
158+
count(attest.pull_requests) == 0
159+
}
160+
161+
# Missing approval: commit has an associated PR but no independent approval from someone other than the authors.
162+
violation_reasons contains {"type": "missing_approval", "trail": trail.name} if {
163+
some trail in input.trails
164+
not is_service_account(trail)
165+
attest := pr_attest(trail)
166+
count(attest.pull_requests) > 0
167+
not has_any_pr_approval(trail, attest)
168+
}
169+
170+
# ---------------------------------------------------------------------------
171+
# Violations — message formatting only
172+
#
173+
# allow does NOT depend on this set. A sprintf failure here cannot affect
174+
# the compliance decision; it only affects the human-readable output.
175+
# ---------------------------------------------------------------------------
176+
177+
violations contains "Policy error: input.trails is missing or not an array — cannot evaluate" if {
178+
"missing_trails_input" in violation_reasons
179+
}
180+
181+
violations contains msg if {
182+
some r in violation_reasons
183+
r.type == "missing_attestation"
184+
msg := sprintf("Trail %v: %v attestation is missing", [r.trail, attestation_name])
185+
}
186+
187+
violations contains msg if {
188+
some r in violation_reasons
189+
r.type == "unverifiable_identity"
190+
msg := sprintf(
191+
"PR %v: commit %v has no linked GitHub account — identity unverifiable",
192+
[r.pr_url, substring(r.sha, 0, 7)],
193+
)
194+
}
195+
196+
violations contains msg if {
197+
some r in violation_reasons
198+
r.type == "missing_pr"
199+
msg := sprintf("Commit %v: no associated PR found", [substring(r.trail, 0, 7)])
200+
}
201+
202+
violations contains msg if {
203+
some r in violation_reasons
204+
r.type == "missing_approval"
205+
msg := sprintf(
206+
"Commit %v: no independent approval after latest code commit",
207+
[substring(r.trail, 0, 7)],
208+
)
209+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "four-eyes-result",
4+
"title": "FourEyesResult",
5+
"description": "Policy evaluation result for a release commit range, produced by kosli evaluate trails against four-eyes.rego",
6+
"type": "object",
7+
"required": ["allow", "violations"],
8+
"additionalProperties": false,
9+
"properties": {
10+
"allow": {
11+
"type": "boolean",
12+
"description": "true if every commit in the evaluated range passes the four-eyes check"
13+
},
14+
"violations": {
15+
"oneOf": [
16+
{ "type": "null" },
17+
{
18+
"type": "array",
19+
"items": { "type": "string" }
20+
}
21+
],
22+
"description": "Violation messages, one per failing commit. null or empty array when allow is true."
23+
},
24+
"evaluated_at": {
25+
"type": "string",
26+
"format": "date-time",
27+
"description": "ISO 8601 timestamp of the policy evaluation"
28+
},
29+
"repository": {
30+
"type": "string",
31+
"description": "Repository in owner/repo format"
32+
},
33+
"base_commit": {
34+
"type": "string",
35+
"description": "SHA of the exclusive range start (the last commit of the previous release)"
36+
},
37+
"current_commit": {
38+
"type": "string",
39+
"description": "SHA of the inclusive range end (the HEAD commit of this release, named as the attestation trail)"
40+
},
41+
"input": {
42+
"type": "object",
43+
"description": "Raw trails input data included when kosli evaluate trails is run with --show-input"
44+
}
45+
}
46+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
KOSLI_ORG=kosli-public
5+
6+
# One-time setup: create custom attestation types for never-alone.
7+
# Run this after any schema change. Types cannot be updated in place;
8+
# delete via the Kosli UI/API first if re-creating an existing type.
9+
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
12+
if [[ -z "${KOSLI_API_TOKEN:-}" ]]; then
13+
echo "ERROR: KOSLI_API_TOKEN is not set" >&2
14+
exit 1
15+
fi
16+
17+
echo "Creating four-eyes-result attestation type (release-level policy evaluation result)..."
18+
kosli create attestation-type four-eyes-result \
19+
--description "Four-eyes policy evaluation result for a release commit range (never-alone)" \
20+
--schema "${SCRIPT_DIR}/four-eyes-result-schema.json" \
21+
--jq ".allow == true" \
22+
--org "${KOSLI_ORG}"
23+
24+
echo "Done — four-eyes-result attestation types ready."

0 commit comments

Comments
 (0)