Skip to content

Commit 0c27497

Browse files
Rego tests (#14)
* Added unit tests for the rego policies * add workflow to test rego policies * merge fixes * merge changes * added newline * install opa agent * spelling errors * comments and consistent rego * make sure the predicate is a SLSA build provenance * added test to verify custom predicates too * Updated documentation * Policy updates. Verify that the policy ensures that the expected OIDC issuer is found. Use consistent function naming in policies and constraint files Add a step to compare that the constraint and policies are the same. * use bash * chmod +x * added comment * fixed a spelling error * spelling fix
1 parent 0d88595 commit 0c27497

11 files changed

+834
-5
lines changed

.github/workflows/rego.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Test rego examples
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request: {}
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
19+
20+
- name: Setup OPA
21+
uses: open-policy-agent/setup-opa@34a30e8a924d1b03ce2cf7abe97250bbb1f332b5 # 2.2.0
22+
with:
23+
version: latest
24+
25+
- name: Test
26+
run: |
27+
make test-rego
28+
29+
- name: Verify constraint files
30+
shell: bash
31+
run: |
32+
./scripts/diff_policy.sh

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,16 @@ fmt:
2929

3030
.PHONY: docker
3131
docker:
32-
docker build -t ${IMG} .
32+
docker build --platform linux/arm64 -t ${IMG} .
33+
34+
.PHONY: docker-arm
35+
docker-arm:
36+
docker build --platform linux/arm64 -t ${IMG_ARM} -f Dockerfile.arm .
37+
38+
.PHONY: kind-load-image-arm
39+
kind-load-image:
40+
kind load docker-image ${IMG} --name ${CLUSTER}
41+
42+
.PHONY: test-rego
43+
test-rego:
44+
cd rego && opa test . -v

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,19 @@ The following three examples are provided:
180180
originating from a list of organizations, and built with a reusable
181181
workflow from a list of provided repositories.
182182

183+
The examples are also defined in [policies.rego](rego/policies.rego)
184+
with tests, and example data. An example policy for working with a
185+
custom attestation type is also provided.
186+
187+
Assuming the policy for verifying images originating from a specific
188+
repository is updated to contain the expected repositories, apply
189+
them to OPA Gatekeeper with the following command:
190+
191+
```
192+
$ kubectl apply -f validation/from-repo-constraint-template.yaml
193+
$ kubectl apply -f validation/from-repo-constraint.yaml
194+
```
195+
183196
## Uninstall
184197

185198
```

rego/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Policy tests
2+
3+
The example policies in the OPA Gatekeeper constraint files are also
4+
defined in [policies.rego], with added unit tests. These policies are
5+
the ones that are copied into the [constraint
6+
templates](../validation).
7+
8+
These tests and example data provide a great starting place for users
9+
that want to customize the policies, and would like to have the
10+
opportunity to test the policy on real data.

rego/fixtures.rego

Lines changed: 538 additions & 0 deletions
Large diffs are not rendered by default.

rego/policies.rego

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package policies
2+
3+
fromOrg (resp, orgs) if {
4+
some i, j, k, l
5+
provenance := "https://slsa.dev/provenance/v1"
6+
issuer := "https://token.actions.githubusercontent.com"
7+
8+
provenance == resp.responses[i][j][k].statement.predicateType
9+
issuer == resp.responses[i][j][k].signature.certificate.issuer
10+
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
11+
# Prefix the org name with / before doing comparison
12+
endswith(orgUri, concat("", ["/", orgs[l]]))
13+
}
14+
15+
fromRepo (resp, repos) if {
16+
some i, j, k, l
17+
provenance := "https://slsa.dev/provenance/v1"
18+
issuer := "https://token.actions.githubusercontent.com"
19+
20+
provenance == resp.responses[i][j][k].statement.predicateType
21+
issuer == resp.responses[i][j][k].signature.certificate.issuer
22+
uri := resp.responses[i][j][k].signature.certificate.sourceRepositoryURI
23+
# Prefix the repo name with / before doing comparison
24+
endswith(uri, concat("", ["/", repos[l]]))
25+
}
26+
27+
fromOrgWithSignerRepo(resp, orgs, signerRepos) if {
28+
some i, j, k, l, m
29+
provenance := "https://slsa.dev/provenance/v1"
30+
issuer := "https://token.actions.githubusercontent.com"
31+
32+
provenance == resp.responses[i][j][k].statement.predicateType
33+
issuer == resp.responses[i][j][k].signature.certificate.issuer
34+
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
35+
signerUri := resp.responses[i][j][k].signature.certificate.buildSignerURI
36+
# Verify source owner org is allowed
37+
endswith(orgUri, concat("", ["/", orgs[l]]))
38+
# Verify signer org is allowed
39+
# Remove the path to the repo, workflow and ref
40+
# find the occurence of `/.github/` and trim everything after it
41+
p := indexof(signerUri, "/.github/")
42+
signerRepoTrim := substring(signerUri, 0, p)
43+
# add back the / prefix to get proper delimiter when doing comparison
44+
signerRepo := concat("", ["/", signerRepoTrim])
45+
endswith(signerRepo, concat("", ["/", signerRepos[m]]))
46+
}
47+
48+
# This is an example showing how custom attestations can be verified
49+
customAttestation(resp, val) if {
50+
some i, j, k
51+
custom := "https://example.com/custom/v1"
52+
53+
custom == resp.responses[i][j][k].statement.predicateType
54+
val == resp.responses[i][j][k].statement.predicate.key1
55+
}

rego/policies_test.rego

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package policies_test
2+
3+
import data.policies
4+
import data.fixtures
5+
6+
# From org should pass if at least one correct org is provided
7+
test_from_org_pass if {
8+
policies.fromOrg(fixtures.octo_org, ["unkown", "octoorg"])
9+
}
10+
11+
# Should fail when issuer is not matching
12+
test_from_org_issuer if {
13+
not policies.fromOrg(fixtures.custom_issuer, ["unkown", "octoorg"])
14+
}
15+
16+
# Empty org should fail
17+
test_from_org_empty if {
18+
not policies.fromOrg(fixtures.octo_org, [""])
19+
}
20+
21+
test_from_org_empty if {
22+
not policies.fromOrg(fixtures.octo_org, [])
23+
}
24+
25+
# Verify that no prefix weakness exists
26+
test_from_org_invalid if {
27+
not policies.fromOrg(fixtures.octo_org, ["unkown", "octoorga", "ctoorg", "aoctoorg"])
28+
}
29+
30+
test_from_org_non_provenance if {
31+
not policies.fromOrg(fixtures.non_provenance, ["octoorg"])
32+
}
33+
34+
# From repo should pass if at least one repo is valid
35+
test_from_repo_pass if {
36+
policies.fromRepo(fixtures.octo_org, ["unkown/unkown", "octoorg/octorepo"])
37+
}
38+
39+
# Should fail when issuer is not matching
40+
test_from_repo_issuer if {
41+
not policies.fromRepo(fixtures.custom_issuer, ["unkown/unkown", "octoorg/octorepo"])
42+
}
43+
44+
# Empty repo shoud fail
45+
test_from_repo_empty if {
46+
not policies.fromRepo(fixtures.octo_org, [""])
47+
}
48+
49+
test_from_repo_empty if {
50+
not policies.fromRepo(fixtures.octo_org, [])
51+
}
52+
53+
# Verify that no prefix weakness exists
54+
test_from_repo_invalid if {
55+
not policies.fromRepo(fixtures.octo_org, ["unkown/unkown", "ctoorg/octorepo", "aoctoorg/octorepo", "octoorga/octorepo", "octoorg/aoctorepo", "octoorg/octorep", "octoorg/octorepoa"])
56+
}
57+
58+
test_from_repo_non_provenance if {
59+
not policies.fromRepo(fixtures.non_provenance, ["octoorg/octorepo"])
60+
}
61+
62+
# Same repo and signer
63+
test_with_signer_pass if {
64+
policies.fromOrgWithSignerRepo(fixtures.octo_org, ["unknown", "octoorg"], ["unkown/octorepo", "octoorg/octorepo"])
65+
}
66+
67+
# With a signer from a different org
68+
test_with_signer_pass if {
69+
policies.fromOrgWithSignerRepo(fixtures.reusable, ["unknown", "octoorg"], ["octoorg/octorepo", "buildorg/build-scripts"])
70+
}
71+
72+
# Should fail when issuer is not matching
73+
test_with_signer_issuer if {
74+
not policies.fromOrgWithSignerRepo(fixtures.custom_issuer, ["unknown", "octoorg"], ["octoorg/octorepo", "buildorg/build-scripts"])
75+
}
76+
77+
# Empty input
78+
test_with_signer_empty if {
79+
not policies.fromOrgWithSignerRepo(fixtures.reusable, [], [])
80+
}
81+
82+
test_with_signer_empty if {
83+
not policies.fromOrgWithSignerRepo(fixtures.reusable, [""], [])
84+
}
85+
86+
test_with_signer_empty if {
87+
not policies.fromOrgWithSignerRepo(fixtures.reusable, [], [""])
88+
}
89+
90+
test_with_signer_empty if {
91+
not policies.fromOrgWithSignerRepo(fixtures.reusable, [""], [""])
92+
}
93+
94+
# Verify that no prefix weakness exists for the orgs
95+
test_with_signer_invalid if {
96+
not policies.fromOrgWithSignerRepo(fixtures.reusable, ["unkown", "ctoorg", "octoor", "aoctoorg", "octoorga"], ["octoorg/octorepo"])
97+
}
98+
99+
# Verify that no prefix weakness exists for the signer repos
100+
test_with_signer_invalid if {
101+
not policies.fromOrgWithSignerRepo(fixtures.reusable, ["octoorg"], ["ctoorg/octorepo", "octoorg/octorep", "octoor/octorepo", "octoorg/ctorepo"])
102+
}
103+
104+
# Make sure that a JSON doc matching the provenance does not pass
105+
# if the predicate type is differing
106+
test_with_signer_non_provenance if {
107+
not policies.fromOrgWithSignerRepo(fixtures.non_provenance, ["octoorg"], ["buildorg/build-scripts"])
108+
}
109+
110+
# If multiple attestations are returned, the verification should still pass
111+
test_multiple_attestations if {
112+
policies.fromOrgWithSignerRepo(fixtures.multiple, ["octoorg"], ["octoorg/octorepo"])
113+
}
114+
115+
# Custom attestations should also work
116+
test_custom_attestation if {
117+
policies.customAttestation(fixtures.multiple, "value1")
118+
}

scripts/diff_policy.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
3+
set -ue -o pipefail
4+
5+
# The policy definitions differs slightly from within the constraint
6+
# tempalte, and the rego definitions used during testing.
7+
# Extract the policies from both definitions, strip newlines and white
8+
# spaces, then compare them to detect drift.
9+
10+
diffp() {
11+
file=$1
12+
func=$2
13+
14+
# The regex to match the function definition contains three preceding
15+
# whitespaces to not match the location where it's called.
16+
got=`perl -0777 -ne "print \\$1 if /( ${func}.*?})/s" validation/${file} \
17+
| perl -ne '$. > 1 && print'| tr -d ' \n'`
18+
want=`perl -0777 -ne "print \\$1 if /(${func}.*?})/s" rego/policies.rego \
19+
| perl -ne '$. > 1 && print' | tr -d ' \n'`
20+
21+
if [ "${got}" != "${want}" ]; then
22+
echo "policy ${func} differs in constraint template ${file}"
23+
echo ${got}
24+
echo vs
25+
echo ${want}
26+
exit 1
27+
fi
28+
}
29+
30+
diffp from-org-constraint-template.yaml fromOrg
31+
diffp from-org-with-signer-constraint-template.yaml fromOrgWithSigner
32+
diffp from-repo-constraint-template.yaml fromRepo
33+
34+
echo "No differences detected"

validation/from-org-constraint-template.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ spec:
3838
3939
fromOrg(resp, orgs) {
4040
some i, j, k, l
41+
provenance := "https://slsa.dev/provenance/v1"
42+
issuer := "https://token.actions.githubusercontent.com"
43+
44+
provenance == resp.responses[i][j][k].statement.predicateType
45+
issuer == resp.responses[i][j][k].signature.certificate.issuer
4146
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
47+
# Prefix the org name with / before doing comparison
4248
endswith(orgUri, concat("", ["/", orgs[l]]))
4349
}

validation/from-org-with-signer-constraint-template.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ spec:
3838
3939
fromOrgWithSigner(resp, orgs, signerRepos) {
4040
some i, j, k, l, m
41-
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
41+
provenance := "https://slsa.dev/provenance/v1"
42+
issuer := "https://token.actions.githubusercontent.com"
43+
44+
provenance == resp.responses[i][j][k].statement.predicateType
45+
issuer == resp.responses[i][j][k].signature.certificate.issuer
46+
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
4247
signerUri := resp.responses[i][j][k].signature.certificate.buildSignerURI
4348
# Verify source owner org is allowed
4449
endswith(orgUri, concat("", ["/", orgs[l]]))

0 commit comments

Comments
 (0)