Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/rego.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Test rego examples

on:
push:
branches:
- main
pull_request: {}

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup OPA
uses: open-policy-agent/setup-opa@34a30e8a924d1b03ce2cf7abe97250bbb1f332b5 # 2.2.0
with:
version: latest

- name: Test
run: |
make test-rego

- name: Verify constraint files
shell: bash
run: |
./scripts/diff_policy.sh
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ fmt:

.PHONY: docker
docker:
docker build -t ${IMG} .
docker build --platform linux/arm64 -t ${IMG} .

.PHONY: docker-arm
docker-arm:
docker build --platform linux/arm64 -t ${IMG_ARM} -f Dockerfile.arm .

.PHONY: kind-load-image-arm
kind-load-image:
kind load docker-image ${IMG} --name ${CLUSTER}

.PHONY: test-rego
test-rego:
cd rego && opa test . -v
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ The following three examples are provided:
originating from a list of organizations, and built with a reusable
workflow from a list of provided repositories.

The examples are also defined in [policies.rego](rego/policies.rego)
with tests, and example data. An example policy for working with a
custom attestation type is also provided.

Assuming the policy for verifying images originating from a specific
repository is updated to contain the expected repositories, apply
them to OPA Gatekeeper with the following command:

```
$ kubectl apply -f validation/from-repo-constraint-template.yaml
$ kubectl apply -f validation/from-repo-constraint.yaml
```

## Uninstall

```
Expand Down
10 changes: 10 additions & 0 deletions rego/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Policy tests

The example policies in the OPA Gatekeeper constraint files are also
defined in [policies.rego], with added unit tests. These policies are
the ones that are copied into the [constraint
templates](../validation).

These tests and example data provide a great starting place for users
that want to customize the policies, and would like to have the
opportunity to test the policy on real data.
538 changes: 538 additions & 0 deletions rego/fixtures.rego

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions rego/policies.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package policies

fromOrg (resp, orgs) if {
some i, j, k, l
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
# Prefix the org name with / before doing comparison
endswith(orgUri, concat("", ["/", orgs[l]]))
}

fromRepo (resp, repos) if {
some i, j, k, l
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
uri := resp.responses[i][j][k].signature.certificate.sourceRepositoryURI
# Prefix the repo name with / before doing comparison
endswith(uri, concat("", ["/", repos[l]]))
}

fromOrgWithSignerRepo(resp, orgs, signerRepos) if {
some i, j, k, l, m
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
signerUri := resp.responses[i][j][k].signature.certificate.buildSignerURI
# Verify source owner org is allowed
endswith(orgUri, concat("", ["/", orgs[l]]))
# Verify signer org is allowed
# Remove the path to the repo, workflow and ref
# find the occurence of `/.github/` and trim everything after it
p := indexof(signerUri, "/.github/")
signerRepoTrim := substring(signerUri, 0, p)
# add back the / prefix to get proper delimiter when doing comparison
signerRepo := concat("", ["/", signerRepoTrim])
endswith(signerRepo, concat("", ["/", signerRepos[m]]))
}

# This is an example showing how custom attestations can be verified
customAttestation(resp, val) if {
some i, j, k
custom := "https://example.com/custom/v1"

custom == resp.responses[i][j][k].statement.predicateType
val == resp.responses[i][j][k].statement.predicate.key1
}
118 changes: 118 additions & 0 deletions rego/policies_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package policies_test

import data.policies
import data.fixtures

# From org should pass if at least one correct org is provided
test_from_org_pass if {
policies.fromOrg(fixtures.octo_org, ["unkown", "octoorg"])
}

# Should fail when issuer is not matching
test_from_org_issuer if {
not policies.fromOrg(fixtures.custom_issuer, ["unkown", "octoorg"])
}

# Empty org should fail
test_from_org_empty if {
not policies.fromOrg(fixtures.octo_org, [""])
}

test_from_org_empty if {
not policies.fromOrg(fixtures.octo_org, [])
}

# Verify that no prefix weakness exists
test_from_org_invalid if {
not policies.fromOrg(fixtures.octo_org, ["unkown", "octoorga", "ctoorg", "aoctoorg"])
}

test_from_org_non_provenance if {
not policies.fromOrg(fixtures.non_provenance, ["octoorg"])
}

# From repo should pass if at least one repo is valid
test_from_repo_pass if {
policies.fromRepo(fixtures.octo_org, ["unkown/unkown", "octoorg/octorepo"])
}

# Should fail when issuer is not matching
test_from_repo_issuer if {
not policies.fromRepo(fixtures.custom_issuer, ["unkown/unkown", "octoorg/octorepo"])
}

# Empty repo shoud fail
test_from_repo_empty if {
not policies.fromRepo(fixtures.octo_org, [""])
}

test_from_repo_empty if {
not policies.fromRepo(fixtures.octo_org, [])
}

# Verify that no prefix weakness exists
test_from_repo_invalid if {
not policies.fromRepo(fixtures.octo_org, ["unkown/unkown", "ctoorg/octorepo", "aoctoorg/octorepo", "octoorga/octorepo", "octoorg/aoctorepo", "octoorg/octorep", "octoorg/octorepoa"])
}

test_from_repo_non_provenance if {
not policies.fromRepo(fixtures.non_provenance, ["octoorg/octorepo"])
}

# Same repo and signer
test_with_signer_pass if {
policies.fromOrgWithSignerRepo(fixtures.octo_org, ["unknown", "octoorg"], ["unkown/octorepo", "octoorg/octorepo"])
}

# With a signer from a different org
test_with_signer_pass if {
policies.fromOrgWithSignerRepo(fixtures.reusable, ["unknown", "octoorg"], ["octoorg/octorepo", "buildorg/build-scripts"])
}

# Should fail when issuer is not matching
test_with_signer_issuer if {
not policies.fromOrgWithSignerRepo(fixtures.custom_issuer, ["unknown", "octoorg"], ["octoorg/octorepo", "buildorg/build-scripts"])
}

# Empty input
test_with_signer_empty if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, [], [])
}

test_with_signer_empty if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, [""], [])
}

test_with_signer_empty if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, [], [""])
}

test_with_signer_empty if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, [""], [""])
}

# Verify that no prefix weakness exists for the orgs
test_with_signer_invalid if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, ["unkown", "ctoorg", "octoor", "aoctoorg", "octoorga"], ["octoorg/octorepo"])
}

# Verify that no prefix weakness exists for the signer repos
test_with_signer_invalid if {
not policies.fromOrgWithSignerRepo(fixtures.reusable, ["octoorg"], ["ctoorg/octorepo", "octoorg/octorep", "octoor/octorepo", "octoorg/ctorepo"])
}

# Make sure that a JSON doc matching the provenance does not pass
# if the predicate type is differing
test_with_signer_non_provenance if {
not policies.fromOrgWithSignerRepo(fixtures.non_provenance, ["octoorg"], ["buildorg/build-scripts"])
}

# If multiple attestations are returned, the verification should still pass
test_multiple_attestations if {
policies.fromOrgWithSignerRepo(fixtures.multiple, ["octoorg"], ["octoorg/octorepo"])
}

# Custom attestations should also work
test_custom_attestation if {
policies.customAttestation(fixtures.multiple, "value1")
}
34 changes: 34 additions & 0 deletions scripts/diff_policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

set -ue -o pipefail

# The policy definitions differs slightly from within the constraint
# tempalte, and the rego definitions used during testing.
# Extract the policies from both definitions, strip newlines and white
# spaces, then compare them to detect drift.

diffp() {
file=$1
func=$2

# The regex to match the function definition contains three preceding
# whitespaces to not match the location where it's called.
got=`perl -0777 -ne "print \\$1 if /( ${func}.*?})/s" validation/${file} \
| perl -ne '$. > 1 && print'| tr -d ' \n'`
want=`perl -0777 -ne "print \\$1 if /(${func}.*?})/s" rego/policies.rego \
| perl -ne '$. > 1 && print' | tr -d ' \n'`

if [ "${got}" != "${want}" ]; then
echo "policy ${func} differs in constraint template ${file}"
echo ${got}
echo vs
echo ${want}
exit 1
fi
}

diffp from-org-constraint-template.yaml fromOrg
diffp from-org-with-signer-constraint-template.yaml fromOrgWithSigner
diffp from-repo-constraint-template.yaml fromRepo

echo "No differences detected"
6 changes: 6 additions & 0 deletions validation/from-org-constraint-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ spec:

fromOrg(resp, orgs) {
some i, j, k, l
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
# Prefix the org name with / before doing comparison
endswith(orgUri, concat("", ["/", orgs[l]]))
}
7 changes: 6 additions & 1 deletion validation/from-org-with-signer-constraint-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ spec:

fromOrgWithSigner(resp, orgs, signerRepos) {
some i, j, k, l, m
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
orgUri := resp.responses[i][j][k].signature.certificate.sourceRepositoryOwnerURI
signerUri := resp.responses[i][j][k].signature.certificate.buildSignerURI
# Verify source owner org is allowed
endswith(orgUri, concat("", ["/", orgs[l]]))
Expand Down
12 changes: 9 additions & 3 deletions validation/from-repo-constraint-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ spec:
}

fromRepo(resp, repos) {
some i
uri := resp.responses[_][_][_].signature.certificate.sourceRepositoryURI
endswith(uri, concat("", ["/", repos[i]]))
some i, j, k, l
provenance := "https://slsa.dev/provenance/v1"
issuer := "https://token.actions.githubusercontent.com"

provenance == resp.responses[i][j][k].statement.predicateType
issuer == resp.responses[i][j][k].signature.certificate.issuer
uri := resp.responses[i][j][k].signature.certificate.sourceRepositoryURI
# Prefix the repo name with / before doing comparison
endswith(uri, concat("", ["/", repos[l]]))
}
Loading