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:

  • If you replace --output yaml with --output json you can easily parse the output with jq or some other means of JSON processing

  • The --show-successes adds the passed rules. If you omit that, you’ll only see rule violations along with the other metadata

  • The --info adds more context to both passed and failed rule violations.

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"
        ]
      }
    }]
  }
  • sources is an array, meaning you can specify more than one source, 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 a git: 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 git: location, the double slash // denotes the separation of the git repository and the location of the files inside the repository, both for data and policy

https://github.com/enterprise-contract/ec-policies//example/data

You can also add a git reference for a branch

https://github.com/enterprise-contract/ec-policies//example/data?ref=main

or a commit

https://github.com/enterprise-contract/ec-policies//example/data?ref=abc123def

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 rules

    In 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

  • you go back to our component on {rhdh_url}/catalog/default/component/tekton-chains-test[Red Hat Developer Hub^,window="rhdh"]

  • edit the source code directly in GitLab by following the {gitlab_url}/development/tekton-chains-test["View Source"] link and commit your change

  • after the pipeline finishes, run the get-chains-image-sha.sh script again

  • Now try without the "exclude" section

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) serviceNowTickets[], but it could be more than one and basically anything that can we wrapped into JSON.
It’s a custom predicate, therefore we’re free to add anything we want to verify later (or record for posterity and auditability purposes).

Remember, through the in-toto format, the predicate (what we record here) will be tied to the image via the subject definition.

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 validates attestations that we’d like to highlight - therefore we start with the image first.

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…​.

wait what

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"
}
  • input is everything that is passed to ec - in this case our file.

  • the variable att contains all attestations in input

  • then, we check for all att that contain our custom predicateType in statement.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 ec validate image and ec validate input.

With validate image ec interprets the in-toto statement and feeds the attestations into its validation (that’s why the policy works).

With validate input, ec takes the input verbatim and doesn’t transform anything - and by just downloading and decoding the attestation, we’re missing the attestations[].statement. structure.

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?

wait hmm

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_exists rule:

    • Explicitly checks count(…​) == 0 and 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.

3. Separation of Policy and Code

Rego enables you to decouple policy decisions from application logic. This allows security teams and compliance officers to manage policies independently of development teams.

4. Composability

Policies can reference and build upon other policies, enabling modular and reusable policy definitions across your organization.


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: