Enterprise Contract (Conforma)
As outlined in previous chapters (e.g. Module 7: Getting started with security best practices), downloading attestations and validating them, parsing attestation (predicate) content and applying rules to it could be done manually or scripted via bash scripts (in theory).
But, why would we - we have Conforma / Enterprise Contract to do just that in a very convenient and reproducible way.
|
We are continuing this exercise with the results (specifically, with the image and its attestations) that we build in the previous exercise. If you haven’t already done so, please go through the previous exercise to create an image via a Tekton Pipeline Run (with Tekton Chains attestations). |
Working with existing SLSA policies
As a follow-up to the previous chapter, let’s see what we already have available as policies and rules and how to apply them to the image and attestations that Tekton Chains created.
The Conforma SLSA Policies
The Conforma community provides a huge set of rules grouped by
that are ready to use and can cloned and tailored to match specific requirements.
Each policy contains a number of Rego-language policy rules that can be used in your image verification to check for policy compliance.
Let’s quickly use one and then go through the command and its parameters:
In our terminal, check if the $CHAINS_IMAGE variable is still available (we set that in the previous exercise):
echo $CHAINS_IMAGE
If it isn’t, please refer to the previous exercise (where we source ./get-chains-image-sha.sh).
Then, execute the below ec validate image command. If you followed Module 7, Exercise 3 - "Provenance & Attestations" you will notice that we’re extending the basic command with a policy configuration now:
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy '{
"sources": [{
"policy": ["oci::quay.io/enterprise-contract/ec-release-policy:latest"],
"data": ["git::https://github.com/enterprise-contract/ec-policies//example/data"],
"config": {
"include": [
"attestation_type",
"slsa_provenance_available",
"slsa_source_version_controlled"
]
}
}]
}' \
--output yaml \
--show-successes \
--info
If you inspect the results, you will see that it
-
validated the image signature
containerImage: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:7c68fabb7701e5d63f6cd17914fc026501e91ff0696570da780a7d95a2a98837
name: Unnamed
signatures:
- keyid: ""
sig: MEUCIGPqLX+O3Hg9jLJ6i7DoXUnNIL3cDenXW+BhgyPVEi/8AiEAgfJmO9tP8N6KVX2nPE+qgrySlPeKo68+Q8QiNI+Hy/8=
-
found and validated the two attestations (for integrity and syntax)
- attestations:
- predicateBuildType: tekton.dev/v1beta1/TaskRun
predicateType: https://slsa.dev/provenance/v0.2
signatures:
- keyid: SHA256:gElT0CKh7ref5P9CLalcPOSJB2ojX/28MCZFEhUrDSU
sig: MEUCIFAEpX6AElGd+AjsJRwRPKYfRj/LuCqLix0011BWm0rQAiEAp2zYjpkMFTLf54PTMX/RkD5OKpYLGnXGLSd0rtzgnG8=
type: https://in-toto.io/Statement/v0.1
- predicateBuildType: tekton.dev/v1beta1/PipelineRun
predicateType: https://slsa.dev/provenance/v0.2
signatures:
- keyid: SHA256:gElT0CKh7ref5P9CLalcPOSJB2ojX/28MCZFEhUrDSU
sig: MEUCIH/Fr8cDTSrsitgfVS/OHGBv/5XhvB8wjrk1mIxZfpsrAiEA79EFeVM37iyp3iCJk2QSAPMsnCuH5m43qCtEYq5+2eU=
type: https://in-toto.io/Statement/v0.1
Up to this point, this is what we’ve already seen in previous exercises.
Here, we add the policies for validation now - that you can see are executed and pass or fail.
successes:
- metadata:
code: attestation_type.deprecated_policy_attestation_format
collections:
- minimal
- redhat
- redhat_rpms
description: The Conforma CLI now places the attestation data in a different
location. This check fails if the expected new format is not found.
effective_on: "2023-08-31T00:00:00Z"
title: Deprecated policy attestation format
msg: Pass
- metadata:
code: attestation_type.known_attestation_type
collections:
- minimal
- redhat
- redhat_rpms
depends_on:
- attestation_type.pipelinerun_attestation_found
description: Confirm the attestation found for the image has a known attestation
type.
title: Known attestation type found
msg: Pass
|
Some noteworthy things about the last command:
|
The --policy parameter explained
The --policy parameter in our command tells ec where to find the policyConfiguration, which we have inline in JSON format here:
{
"sources": [{
"policy": ["oci::quay.io/enterprise-contract/ec-release-policy:latest"],
"data": ["git::https://github.com/enterprise-contract/ec-policies//example/data"],
"config": {
"include": [
"attestation_type",
"slsa_provenance_available",
"slsa_source_version_controlled"
]
}
}]
}
-
sourcesis an array, meaning you can specify more than onesource, combining multiple policy sources.
For each source, you can configure
-
policy- The Rule Logic (or its location)The policy field contains the Rego policy files (.rego files) that define:
-
What to check (e.g., "verify all tasks are from trusted sources")
-
How to evaluate it (the actual rule logic)
-
The rule metadata (titles, descriptions, failure messages)
These are the actual rule implementations - the code that performs the validation. In this case, we are using an
oci:image that contains the rules, we could also use agit:reference or a file path. -
-
data- The Configuration Values (or their location)The data field contains rule data - the configuration that parameterises the policies:
-
Allowlists/denylists (e.g., list of trusted task bundles)
-
Thresholds (e.g., max CVE severity levels, leeway days)
-
Expected values (e.g., required git branches, required tasks)
-
Environment-specific configuration
This is the input data that the policy rules reference when making decisions.
-
This allows for separation of policies (the validation rules) and the data they act on.
|
When referencing a
You can also add a git reference for a branch
or a commit
Take a look the the data used for our example command here |
-
config- for fine-tuning the scope of the validation by including or excluding rulesIn our example, we specifically included three rule packages to be verified. Had we not done that, all rules would have been applied.
Similarly, we can exclude specific rules (see below).
Rule Collections
Instead of explicitly listing all rules we want to use, we can also use available Rule Collections
Let’s try that by using the slsa3 rule collection from the Release Policy:
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy '{
"sources": [{
"policy": ["oci::quay.io/enterprise-contract/ec-release-policy:latest"],
"data": ["git::https://github.com/enterprise-contract/ec-policies//example/data"],
"config": {
"include": [
"@slsa3"
]
}
}]
}' \
--info
In this clear-text format (we omitted the --output formatting) we can see that we have 20 rules in this rule collection
Violations: 1, Warnings: 0, Successes: 19
and one failed.
Since we added --info it provided us with the context on how to fix it or how to disable the rule in the format <rule-package>.<rule-name>:
Results:
✕ [Violation] slsa_source_correlated.source_code_reference_provided
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:7c68fabb7701e5d63f6cd17914fc026501e91ff0696570da780a7d95a2a98837
Reason: Expected source code reference was not provided for verification
Title: Source code reference provided
Description: Check if the expected source code reference is provided. To exclude this rule add
"slsa_source_correlated.source_code_reference_provided" to the `exclude` section of the policy configuration.
Solution: Provide the expected source code reference in inputs.
Error: success criteria not met
So, we’ll add that exception (just to make it work):
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy '{
"sources": [{
"policy": ["oci::quay.io/enterprise-contract/ec-release-policy:latest"],
"data": ["git::https://github.com/enterprise-contract/ec-policies//example/data"],
"config": {
"include": [
"@slsa3"
],
"exclude": [
"slsa_source_correlated.source_code_reference_provided"
]
}
}]
}' --info
Now the validation succeeded: Violations: 0, Warnings: 0, Successes: 19
|
You might be wondering why the SLSA validation failed if we built (and attested all the inputs) via Tekton Chains. The reason here is that we took the automatically built image (right after we instantiated the template in the previous chapter). An "empty commit" triggered that pipeline run - and github doesn’t add all the necessary metadata to the webhook on empty commits. You can try if
|
Parameter formats
Including the policy inline as JSON is not the only way to use it. You can include inline YAML, but for production use cases, you’d want to include the policyConfig as a YAML file or, in a git repository:
Inline YAML
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy '
sources:
- policy:
- "oci::quay.io/enterprise-contract/ec-release-policy:latest"
data:
- "git::https://github.com/enterprise-contract/ec-policies//example/data"
config:
include:
- "@slsa3"
exclude:
- "slsa_source_correlated.source_code_reference_provided"
' --info
External YAML File
Create the configuration:
cat > policy.yaml << 'EOF'
sources:
- policy:
- "oci::quay.io/enterprise-contract/ec-release-policy:latest"
data:
- "git::https://github.com/enterprise-contract/ec-policies//example/data"
config:
include:
- "@slsa3"
exclude:
- "slsa_source_correlated.source_code_reference_provided"
EOF
and then run the ec statement with the file reference
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy policy.yaml \
--info
git YAML reference
Assuming we have the file in a git repo, we would use
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy git::https://github.com/your-org/your-repo//path/to/policy.yaml \
--info
Again, the double forward slash // is used for separation of repository (https://github.com/your-org/your-repo) from the path in the repo (path/to/policy.yaml)
|
Building a custom attestation
The major use case for attestations is to generate (and attest) artifact provenance for SLSA compliance. The predicate types and formats are well-known and well defined - and the Conforma policies are available to use (or to tailor) as needed.
However, Conforma & attestations (via Trusted Artifact Signer / cosign) can do much more.
Imagine a use case, where a customer wants to have verifiable and auditable proof of a release approval given via their ServiceNow ticketing system.
In addition to the SLSA provenance (in our case generated by Tekton Chains), we would use a task or process that attests the release approval - and then verify it before deployment.
First of all, we’d have to define a custom predicate (and we’re free to add anything that might be worthwhile to record (for auditability) and can be used for verification).
Custom Predicate
We’ll define a custom predicate type (https://redhat.com/ads-scholars/v1) and for our simple example, we’ll store it as a file (in a real process that would be dynamically generated from the data in e.g. ServiceNow).
cat > release-approval-predicate.json <<'EOF'
{
"release": {
"approved": true,
"approvalDate": "2025-11-08T12:00:00Z",
"approver": "Jane Smith",
"approverEmail": "jane.smith@example.com"
},
"serviceNowTickets": [
{
"ticketId": "CHG0012345",
"url": "https://servicenow.example.com/nav_to.do?uri=change_request.do?sys_id=abc123",
"type": "Change Request",
"status": "Approved",
"approvalDate": "2025-11-07T15:30:00Z"
},
{
"ticketId": "RITM0067890",
"url": "https://servicenow.example.com/nav_to.do?uri=sc_req_item.do?sys_id=def456",
"type": "Requested Item",
"status": "Closed Complete",
"completionDate": "2025-11-08T09:00:00Z"
}
],
"metadata": {
"processVersion": "1.0",
"complianceFramework": "ADS-Scholars Release Policy",
"notes": "Standard release approval process completed"
}
}
EOF
|
Here, we have a single data structure (array) Remember, through the |
Attesting the custom predicate
We could use keyless attestation via cosign (see Provenance & Attestations), but we’ll use the current Tekton Chains configuration and the k8s://openshift-pipelines/signing-secrets, which also contains the private key and its password.
cosign attest \
--key k8s://openshift-pipelines/signing-secrets \
--type https://redhat.com/ads-scholars/v1 \
--predicate release-approval-predicate.json \
$CHAINS_IMAGE
For sake of completeness, we quickly verify the attestation (although ec will also do that):
cosign verify-attestation \
--type https://redhat.com/ads-scholars/v1 \
--key k8s://openshift-pipelines/signing-secrets \
$CHAINS_IMAGE | jq -r '.payload' | base64 -d | jq .
cosign verify-attestation \
--type https://redhat.com/ads-scholars/v1 \
--key k8s://openshift-pipelines/signing-secrets \
$CHAINS_IMAGE | jq -r '.payload' | base64 -d | jq .
Verification for quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:7c68fabb7701e5d63f6cd17914fc026501e91ff0696570da780a7d95a2a98837 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://redhat.com/ads-scholars/v1",
"subject": [
{
"name": "quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test",
"digest": {
"sha256": "7c68fabb7701e5d63f6cd17914fc026501e91ff0696570da780a7d95a2a98837"
}
}
],
"predicate": {
"metadata": {
"complianceFramework": "ADS-Scholars Release Policy",
"notes": "Standard release approval process completed",
"processVersion": "1.0"
},
"release": {
"approvalDate": "2025-11-08T12:00:00Z",
"approved": true,
"approver": "Jane Smith",
"approverEmail": "jane.smith@example.com"
},
"serviceNowTickets": [
{
"approvalDate": "2025-11-07T15:30:00Z",
"status": "Approved",
"ticketId": "CHG0012345",
"type": "Change Request",
"url": "https://servicenow.example.com/nav_to.do?uri=change_request.do?sys_id=abc123"
},
{
"completionDate": "2025-11-08T09:00:00Z",
"status": "Closed Complete",
"ticketId": "RITM0067890",
"type": "Requested Item",
"url": "https://servicenow.example.com/nav_to.do?uri=sc_req_item.do?sys_id=def456"
}
]
}
}
Building a custom policy to validate
The Policy
Take your time to inspect it (below) - in the end it comes down to
1) From all the attestations present (all will be part of the input), filter the ones with our predicateType:
release_approval_attestations contains att if {
some att in input.attestations
att.statement.predicateType == "https://redhat.com/ads-scholars/v1"
}
2) Check for values in the JSON structure, for example
approved := object.get(att, ["statement", "predicate", "release", "approved"], false) or tickets := object.get(att, ["statement", "predicate", "serviceNowTickets"], [])
Here’s the policy:
cat > release-approval-policy.rego <<'EOF'
package redhat.release_approval
import rego.v1
# METADATA
# title: Release Approval Attestation Check
# description: >-
# Verify that the image has a valid release approval attestation
# with ServiceNow ticket approvals.
# custom:
# short_name: release_approval_present
# failure_msg: Release approval attestation is missing or invalid
# solution: >-
# Ensure the release approval process is completed and the attestation
# is created with valid ServiceNow ticket references before deployment.
# collections:
# - release_approval
# Helper to get all attestations of our custom type
release_approval_attestations contains att if {
some att in input.attestations
att.statement.predicateType == "https://redhat.com/ads-scholars/v1"
}
# METADATA
# title: Release Approval Attestation Exists
# description: >-
# At least one release approval attestation must be present.
# custom:
# short_name: attestation_exists
deny contains result if {
count(release_approval_attestations) == 0
result := {
"code": "redhat.release_approval.attestation_exists",
"msg": "No release approval attestation found for image",
"effective_on": "2025-01-01T00:00:00Z",
}
}
# METADATA
# title: Release Must Be Approved
# description: >-
# The release approval attestation must indicate that the release
# has been approved.
# custom:
# short_name: release_approved
deny contains result if {
some att in release_approval_attestations
approved := object.get(att, ["statement", "predicate", "release", "approved"], false)
approved != true
result := {
"code": "redhat.release_approval.release_approved",
"msg": "Release is not approved in the attestation",
"effective_on": "2025-01-01T00:00:00Z",
}
}
# METADATA
# title: ServiceNow Tickets Present
# description: >-
# The release approval attestation must contain at least one
# ServiceNow ticket reference.
# custom:
# short_name: servicenow_tickets_present
deny contains result if {
some att in release_approval_attestations
tickets := object.get(att, ["statement", "predicate", "serviceNowTickets"], [])
count(tickets) == 0
result := {
"code": "redhat.release_approval.servicenow_tickets_present",
"msg": sprintf(
"No ServiceNow tickets found in release approval attestation (found %d tickets)",
[count(tickets)]
),
"effective_on": "2025-01-01T00:00:00Z",
}
}
# METADATA
# title: All ServiceNow Tickets Are Approved or Complete
# description: >-
# All ServiceNow tickets referenced in the attestation must be
# in an approved or completed state.
# custom:
# short_name: servicenow_tickets_approved
deny contains result if {
some att in release_approval_attestations
tickets := object.get(att, ["statement", "predicate", "serviceNowTickets"], [])
some ticket in tickets
status := object.get(ticket, "status", "Unknown")
not ticket_is_approved(status)
ticket_id := object.get(ticket, "ticketId", "Unknown")
result := {
"code": "redhat.release_approval.servicenow_tickets_approved",
"msg": sprintf(
"ServiceNow ticket %s has invalid status: %s (expected 'Approved' or 'Closed Complete')",
[ticket_id, status]
),
"effective_on": "2025-01-01T00:00:00Z",
}
}
# Helper function to check if a ticket status indicates approval
ticket_is_approved(status) if {
status == "Approved"
}
ticket_is_approved(status) if {
status == "Closed Complete"
}
# METADATA
# title: Approver Information Present
# description: >-
# The release approval attestation must include approver information.
# custom:
# short_name: approver_info_present
deny contains result if {
some att in release_approval_attestations
release := object.get(att, ["statement", "predicate", "release"], {})
# Check if approver name is missing or empty
approver := object.get(release, "approver", "")
approver == ""
result := {
"code": "redhat.release_approval.approver_info_present",
"msg": "Release approval attestation missing approver information",
"effective_on": "2025-01-01T00:00:00Z",
}
}
# Optional: Warning for old approvals (e.g., older than 30 days)
# METADATA
# title: Release Approval Not Too Old
# description: >-
# The release approval should not be older than 30 days to ensure
# current compliance with release policies.
# custom:
# short_name: approval_freshness
warn contains result if {
some att in release_approval_attestations
approval_date_str := object.get(att, ["statement", "predicate", "release", "approvalDate"], "")
approval_date_str != ""
# Parse the approval date
approval_time := time.parse_rfc3339_ns(approval_date_str)
current_time := time.now_ns()
# Calculate age in days (nanoseconds to days)
age_days := (current_time - approval_time) / (1000000000 * 60 * 60 * 24)
age_days > 30
result := {
"code": "redhat.release_approval.approval_freshness",
"msg": sprintf(
"Release approval is %d days old (older than 30 days)",
[floor(age_days)]
),
"effective_on": "2025-01-01T00:00:00Z",
}
}
EOF
Testing the policy
Before we can test it, we need to tell ec where to find it.
ec will automatically use all *.rego files in a given directory, so in general it is a good idea to have a local policy directory (if the policy is on the local filesystem). For git repos, you would use the git: reference - but development starts locally…
Therefore, we’re creating a local-policy.yaml file (the policyConfig) that points to our current working directory, where the release-approval-policy.rego that we just created resides (copy the policy somewhere else and change the path reference, if you want to):
cat > local-policy.yaml <<EOF
sources:
- policy:
- ./
EOF
Testing with the image attestation
|
Yes, normally one would test (and debug) locally, but there is one difference between local predicate files and the way that |
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy local-policy.yaml \
--info \
--show-successes
ec validate image \
--image $CHAINS_IMAGE \
--public-key k8s://openshift-pipelines/signing-secrets \
--rekor-url $SIGSTORE_REKOR_URL \
--policy local-policy.yaml \
--info \
--show-successes
Success: true
Result: SUCCESS
Violations: 0, Warnings: 0, Successes: 10
Component: Unnamed
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Results:
✓ [Success] builtin.attestation.signature_check
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Attestation signature check passed
Description: The attestation signature matches available signing materials.
✓ [Success] builtin.attestation.syntax_check
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Attestation syntax check passed
Description: The attestation has correct syntax.
✓ [Success] builtin.image.signature_check
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Image signature check passed
Description: The image signature matches available signing materials.
✓ [Success] redhat.release_approval.approval_freshness
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Release Approval Not Too Old
Description: The release approval should not be older than 30 days to ensure current compliance with release policies.
✓ [Success] redhat.release_approval.approver_info_present
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Approver Information Present
Description: The release approval attestation must include approver information.
✓ [Success] redhat.release_approval.attestation_exists
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Release Approval Attestation Exists
Description: At least one release approval attestation must be present.
✓ [Success] redhat.release_approval.release_approval_present
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Release Approval Attestation Check
Description: Verify that the image has a valid release approval attestation with ServiceNow ticket approvals.
✓ [Success] redhat.release_approval.release_approved
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: Release Must Be Approved
Description: The release approval attestation must indicate that the release has been approved.
✓ [Success] redhat.release_approval.servicenow_tickets_approved
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: All ServiceNow Tickets Are Approved or Complete
Description: All ServiceNow tickets referenced in the attestation must be in an approved or completed state.
✓ [Success] redhat.release_approval.servicenow_tickets_present
ImageRef: quay-vtm4r.apps.cluster-vtm4r.dynamic.redhatworkshops.io/tssc/tekton-chains-test@sha256:e12b7912703a8b07949b21a2fe4eb0603777d827f3d43c6082d6f71700a5334e
Title: ServiceNow Tickets Present
Description: The release approval attestation must contain at least one ServiceNow ticket reference.
Testing (and debugging) with local attestations/predicates
For local testing, we can use the attestation in a local file, which is good for debugging, as we can massage both the policy and the attestation.
First, we need to download the attestation data:
cosign download attestation \
--predicate-type https://redhat.com/ads-scholars/v1 \
$CHAINS_IMAGE > attestation-payload.json
This gives us the the base64-encoded attestation - if we need to tweak it for debugging purposes, this doesn’t help us much, so we should also create a decoded version:
ec automatically decodes the base64 payload, so there is no need for that - unless you’re debugging and need to see what ec and your policy is actually evaluating.
|
cat attestation-payload.json | jq '.payload |= (@base64d | fromjson)' > attestation-payload-decoded.json
With that, we can use ec validate input (not image) to validate a local attestation file.
ec validate input \
--file attestation-payload-decoded.json \
--policy local-policy.yaml \
--show-successes \
--info \
--output yaml
filepaths:
- filepath: attestation-payload-decoded.json
success: false
success-count: 6
successes:
- metadata:
code: redhat.release_approval.approval_freshness
description: The release approval should not be older than 30 days to ensure
current compliance with release policies.
title: Release Approval Not Too Old
msg: Pass
[...]
violations:
- metadata:
code: redhat.release_approval.attestation_exists
description: At least one release approval attestation must be present. To exclude
this rule add "redhat.release_approval.attestation_exists" to the `exclude`
section of the policy configuration.
title: Release Approval Attestation Exists
msg: No release approval attestation found for image
warnings: []
policy:
sources:
- policy:
- ./
success: false
Error: success criteria not met
Ummm… wait a moment….
We just downloaded that same attestation from the image we just validated successfully - so it should show the same results, shouldn’t it?
Why is redhat.release_approval.attestation_exists failing here when it succeeds against the image directly?
Let’s take a look at the rule:
# short_name: attestation_exists
deny contains result if {
count(release_approval_attestations) == 0
[...]
Ok, so release_approval_attestations is empty - where does it come from?
A few lines above, we have
# Helper to get all attestations of our custom type
release_approval_attestations contains att if {
some att in input.attestations
att.statement.predicateType == "https://redhat.com/ads-scholars/v1"
}
-
inputis everything that is passed toec- in this case our file. -
the variable
attcontains allattestationsininput -
then, we check for all
attthat contain our custom predicateType instatement.predicateType
Looking at our file structure (attestation-payload-decoded.json), we see that we don’t have the attestations.statement.* structure.
|
This is the fundamental difference between With With |
So, for local testing, we’ll "massage" our input first to include the necessary structure:
We can do that in "one go" directly from the image:
cosign download attestation --predicate-type https://redhat.com/ads-scholars/v1 $CHAINS_IMAGE | jq '{attestations: [{statement: (.payload | @base64d | fromjson)}]}' > attestation-input.json
or, as we have already downloaded and decoded the attestation, we can (without the decoding) use our existing attestation-payload-decoded.json to create the same:
cat attestation-payload-decoded.json | jq '{attestations: [{statement: .payload}]}' > attestation-input.json
With this file, we can now run the validation, as we tried before
ec validate input \
--file attestation-input.json \
--policy local-policy.yaml \
--show-successes \
--info \
--output yaml
filepaths:
- filepath: attestation-input.json
success: true
success-count: 7
successes:
[...]
- metadata:
code: redhat.release_approval.attestation_exists
description: At least one release approval attestation must be present.
title: Release Approval Attestation Exists
msg: Pass
[...]
violations: []
warnings: []
policy:
sources:
- policy:
- ./
success: true
Now we can test and modify our rules (or attestations) locally to start developing our custom rules (and attestations).
Rego’s deny pattern
You wanted to ask something? Yes?
You’re right - WHY DID IT WORK AT ALL?
Or, in other words - only one rule failed (attestation_exists), but all the other rules are dependent on release_approval_attestations - and if that is empty, why did all the other rules succeed?
Excellent question, glad you asked! 😉
This is a subtle but important Rego behaviour.
When release_approval_attestations is empty (count == 0):
-
attestation_existsrule:-
Explicitly checks
count(…) == 0and generates a denial → FAILS ❌
-
-
Other rules:
-
Use
some att in release_approval_attestations, which never matches when the set is empty, so the rule body never executes and no denials are generated → PASSES ✅
-
In Rego’s deny pattern:
-
A rule that produces no results = no violations found = success
-
A rule that produces results = violations found = failure
So the other rules "pass" not because they validated correct data, but because they couldn’t iterate over an empty set and therefore generated no violations.
This is actually a logic issue with the policy design! If there are no attestations of the custom type:
- Only attestation_exists catches it and fails
- All other rules silently pass
If you want the other rules to also fail when attestations are missing, you’d need to add explicit checks like:
deny contains result if {
count(release_approval_attestations) > 0 # Ensure attestations exist
some att in release_approval_attestations
# ... rest of logic
}
This is IMPORTANT TO KEEP IN MIND when building rego rules and logic!
In our case, having just the attestation_exists check is sufficient - if that passes, the others will have data to validate. And if it fails, the policy validation will have failed (all rules in a policy have to pass for it to succeed).
Reference: Introduction to Rego
Policy Language for Validation
What is Rego?
Rego is a declarative policy language developed by the Open Policy Agent (OPA) project. It’s designed specifically for expressing policies as code and making authorization and validation decisions across various environment and in multiple scenarios.
As shown in previous chapters, Rego is the policy engine behind tools like Enterprise Contract (now Conforma), which validates software supply chain security.
Unlike imperative programming languages that describe how to compute something, Rego is declarative—you describe what the desired outcome should be, and the engine figures out how to evaluate it.
Core Principles of Rego
1. Declarative Nature
Rego policies describe the conditions that must be true or false, not the steps to check them. The policy engine automatically determines the evaluation order and handles complex logical relationships.
2. Query-Based Evaluation
Policies are evaluated by querying them with input data. The engine returns whether the policy allows or denies an action, along with any computed values or reasons.
Language Constructs
Packages and Imports
Every Rego file begins with a package declaration that defines the namespace for the rules within:
package myapp.authz
import rego.v1
import future.keywords.if
import future.keywords.in
The imports enable modern Rego syntax and keywords for cleaner, more readable policies.
Rules
Rules are the fundamental building blocks of Rego. They define conditions and assign values. Rules have the form:
rule_name := value if { conditions }
Or for boolean rules:
rule_name if { conditions }
Variables
Variables in Rego are assigned using the := operator for simple assignment or = for unification:
user := input.user
user_role = input.user.role
Input and Data
Rego policies operate on two types of data:
-
input: The JSON document provided when querying the policy (request context, attestations, image metadata)
-
data: Pre-loaded documents available to all policies (configuration, reference data, approved lists)
| Remember the Conforma SLSA policies that we checked earlier? We explicitly passed in the reference data, such as lists to use in evaluations. |
Logical Operators
Rego supports standard logical operations:
-
==, !=: Equality and inequality
-
<, >, ⇐, >=: Comparison operators
-
not: Negation
-
Conditions in a rule body are implicitly ANDed together
Built-in Functions
Rego provides numerous built-in functions for string manipulation, array operations, mathematical computations, and more. Examples include:
-
count(collection)- Returns the number of elements -
startswith(string, prefix)- Checks if string starts with prefix -
time.parse_rfc3339_ns(string)- Parses RFC3339 timestamp -
regex.match(pattern, string)- Pattern matching
A comprehensive list can be found at the Open Policy Agent Docs page.
Simple Examples
Example 1: Basic Authorization
A simple policy to check if a user is an admin:
package example.authz
import rego.v1
default allow := false
allow if {
input.user.role == "admin"
}
Input Example:
{
"user": {
"name": "alice",
"role": "admin"
}
}
Result: allow = true
Example 2: Multi-Condition Policy
A more complex policy with multiple conditions (OR logic):
package example.access
import rego.v1
allow if {
input.method == "GET"
input.path[0] == "public"
}
allow if {
input.user.authenticated
input.user.role == "member"
}
Multiple rule definitions with the same name create OR logic. This policy allows access if either: (1) the request is a GET to a public path, OR (2) the user is authenticated and has the member role.
Example 3: Iteration and Collection Operations
Checking if a user has required permissions:
package example.permissions
import rego.v1
required_permissions := ["read", "write"]
has_all_permissions if {
every perm in required_permissions {
perm in input.user.permissions
}
}
The every keyword iterates over the collection and ensures all conditions are met. The in operator checks for set membership.
Further reading
To continue learning Rego and Enterprise Contract:
-
Explore the OPA Playground at https://play.openpolicyagent.org for interactive learning
-
Review the official Rego documentation at https://openpolicyagent.org/docs
-
Study the Enterprise Contract policy examples at https://conforma.dev
-
Browse existing policies at https://github.com/conforma/policy