Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
714fd59
feat: add direct OPA evaluator with shared base infrastructure
joejstuart May 6, 2026
5fcc10c
fix: include component_name and policy_spec in BuildInput, fix lint
joejstuart May 7, 2026
c5efde8
test: add comprehensive OPA evaluator and base evaluator tests, fix gci
joejstuart May 7, 2026
8ba8269
test: add OPA evaluator integration tests with real policy files
joejstuart May 7, 2026
a252183
test: add evaluator comparison tests asserting OPA/conftest parity
joejstuart May 7, 2026
a609a80
test: pass EC_USE_OPA through to acceptance test scenarios
joejstuart May 7, 2026
0aaa754
test: add OPA evaluator acceptance test scenarios
joejstuart May 7, 2026
2079918
test: add snapshot files for OPA evaluator acceptance tests
joejstuart May 7, 2026
19cc2a1
fix: use effectiveTime instead of time.Now() in computeSuccesses
joejstuart May 7, 2026
03b4ec5
fix: address review feedback on OPA evaluator
joejstuart May 21, 2026
29013cc
log a skipped exception rule
joejstuart May 27, 2026
bc477e6
fix: remove implicit EC_USE_OPA propagation from acceptance tests
joejstuart May 27, 2026
9e5f238
test: update OPA evaluator acceptance test snapshots
joejstuart May 27, 2026
8554c9d
test: move OPA evaluator tests to task feature files
joejstuart May 27, 2026
91b10d6
test: add snapshot entries for OPA evaluator task scenarios
joejstuart May 27, 2026
e28ec51
feat: add EC_USE_OPA parameter to Tekton task definitions
joejstuart May 28, 2026
a00b0b3
should return err and not skip
joejstuart May 28, 2026
7d2ffdf
fix: add missing return in prepareDataDirs error handling
joejstuart May 28, 2026
e683520
docs: regenerate task docs for EC_USE_OPA parameter
joejstuart May 28, 2026
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
3 changes: 2 additions & 1 deletion cmd/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
var c evaluator.Evaluator
var err error
if utils.IsOpaEnabled() {
c, err = newOPAEvaluator()
c, err = newOPAEvaluator(
cmd.Context(), policySources, data.policy, sourceGroup, nil)
} else {
// Use the unified filtering approach with the specified filter type
c, err = evaluator.NewConftestEvaluatorWithFilterType(
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/verify-conforma-konflux-ta.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ paths can be provided by using the `:` separator.

+
*Default*: `4`
*EC_USE_OPA* (`string`):: Use the OPA evaluator instead of the default conftest evaluator. Set to "1" to enable.
*SINGLE_COMPONENT* (`string`):: Reduce the Snapshot to only the component whose build caused the Snapshot to be created
+
*Default*: `false`
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/verify-enterprise-contract.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ paths can be provided by using the `:` separator.
*WORKERS* (`string`):: Number of parallel workers to use for policy evaluation.
+
*Default*: `1`
*EC_USE_OPA* (`string`):: Use the OPA evaluator instead of the default conftest evaluator. Set to "1" to enable.
*SINGLE_COMPONENT* (`string`):: Reduce the Snapshot to only the component whose build caused the Snapshot to be created
+
*Default*: `false`
Expand Down
7 changes: 7 additions & 0 deletions features/__snapshots__/ta_task_validate_image.snap
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@
}
---

[TestFeatures/Golden container image with OPA evaluator:results - 1]
{
"TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n",
"VSA_GENERATED": "false"
}
---

[TestFeatures/Golden container image with trusted artifacts:show-config - 1]
{
"policy": {
Expand Down
6 changes: 6 additions & 0 deletions features/__snapshots__/task_validate_image.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ ${TIMESTAMP} INFO Step was skipped due to when expressions were evaluated to fal
}
---

[TestFeatures/Golden container image with OPA evaluator:results - 1]
{
"TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n"
}
---

[TestFeatures/Initialize TUF fails:initialize-tuf - 1]
Error: Get "http://tuf.invalid/root.json": dial tcp: lookup tuf.invalid on 10.96.0.10:53: no such host

Expand Down
43 changes: 43 additions & 0 deletions features/ta_task_validate_image.feature
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,49 @@ Feature: Verify Conforma Trusted Artifact Tekton Task
And the task results should match the snapshot
And the task logs for step "show-config" should match the snapshot

Scenario: Golden container image with OPA evaluator
Given a working namespace
Given a snapshot artifact with content:
```
{
"components": [
{
"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"
}
]
}
```
Given a cluster policy with content:
```
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----",
"sources": [
{
"policy": [
"git::github.com/conforma/policy//policy/release?ref=d34eab36b23d43748e451004177ca144296bf323",
"git::github.com/conforma/policy//policy/lib?ref=d34eab36b23d43748e451004177ca144296bf323"
],
"config": {
"include": [
"slsa_provenance_available"
]
}
}
]
}
```
When version 0.1 of the task named "verify-conforma-konflux-ta" is run with parameters:
| SNAPSHOT_FILENAME | snapshotartifact |
| SOURCE_DATA_ARTIFACT | oci:${REGISTRY}/acceptance/snapshotartifact@${BUILD_SNAPSHOT_DIGEST} |
| POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} |
| STRICT | true |
| IGNORE_REKOR | true |
| EC_USE_OPA | 1 |
| TRUSTED_ARTIFACTS_DEBUG | "true" |
| ORAS_OPTIONS | --plain-http |
Then the task should succeed
And the task results should match the snapshot

Scenario: VSA generation with predicate format
Given a working namespace
Given a snapshot artifact with content:
Expand Down
30 changes: 30 additions & 0 deletions features/task_validate_image.feature
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,36 @@ Feature: Verify Enterprise Contract Tekton Tasks
And the task results should match the snapshot
And the task logs for step "show-config" should match the snapshot

Scenario: Golden container image with OPA evaluator
Given a working namespace
Given a cluster policy with content:
```
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERhr8Zj4dZW67zucg8fDr11M4lmRp\nzN6SIcIjkvH39siYg1DkCoa2h2xMUZ10ecbM3/ECqvBV55YwQ2rcIEa7XQ==\n-----END PUBLIC KEY-----",
"sources": [
{
"policy": [
"github.com/conforma/policy//policy/release?ref=0de5461c14413484575e63e96ddb514d8ab954b5",
"github.com/conforma/policy//policy/lib?ref=0de5461c14413484575e63e96ddb514d8ab954b5"
],
"config": {
"include": [
"slsa_provenance_available"
]
}
}
]
}
```
When version 0.1 of the task named "verify-enterprise-contract" is run with parameters:
| IMAGES | {"components": [{"containerImage": "quay.io/hacbs-contract-demo/golden-container@sha256:e76a4ae9dd8a52a0d191fd34ca133af5b4f2609536d32200a4a40a09fdc93a0d"}]} |
| POLICY_CONFIGURATION | ${NAMESPACE}/${POLICY_NAME} |
| STRICT | true |
| IGNORE_REKOR | true |
| EC_USE_OPA | 1 |
Then the task should succeed
And the task results should match the snapshot

Scenario: Pin policy bundle digest
Given a working namespace
Given a cluster policy with content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,54 @@ type Input struct {
PolicySpec ecc.EnterpriseContractPolicySpec `json:"policy_spec,omitempty"`
}

// BuildInput constructs the OPA input as a Go map and JSON bytes without disk I/O.
// The JSON marshal/unmarshal round-trip ensures correct types for OPA (e.g. numbers
// as float64, consistent key ordering).
func (a *ApplicationSnapshotImage) BuildInput(_ context.Context) (map[string]any, []byte, error) {
Comment thread
st3penta marked this conversation as resolved.
log.Debugf("Building input for %d attestations", len(a.attestations))

var attestations []attestationData
for _, att := range a.attestations {
attestations = append(attestations, attestationData{
Statement: att.Statement(),
Signatures: att.Signatures(),
})
}

input := Input{
Attestations: attestations,
Image: image{
Ref: a.reference.String(),
Signatures: a.signatures,
Config: a.configJSON,
Files: a.files,
Source: a.component.Source,
},
AppSnapshot: a.snapshot,
ComponentName: a.component.Name,
PolicySpec: a.policySpec,
}

if a.parentRef != nil {
input.Image.Parent = image{
Ref: a.parentRef.String(),
Config: a.parentConfigJSON,
}
}

inputJSON, err := json.Marshal(input)
if err != nil {
return nil, nil, fmt.Errorf("input to JSON: %w", err)
}

var parsed map[string]any
if err := json.Unmarshal(inputJSON, &parsed); err != nil {
return nil, nil, fmt.Errorf("parse input JSON: %w", err)
}

return parsed, inputJSON, nil
}

// WriteInputFile writes the JSON from the attestations to input.json in a random temp dir
func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, []byte, error) {
log.Debugf("Attempting to write %d attestations to input file", len(a.attestations))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,100 @@ func TestWriteInputFileMultipleAttestations(t *testing.T) {
assert.JSONEq(t, string(inputJSON), string(bytes))
}

func TestBuildInput(t *testing.T) {
t.Run("basic round-trip", func(t *testing.T) {
a := ApplicationSnapshotImage{
reference: name.MustParseReference("registry.io/repository/image:tag"),
attestations: []attestation.Attestation{createSimpleAttestation(nil)},
snapshot: app.SnapshotSpec{
Components: []app.SnapshotComponent{
{ContainerImage: "registry.io/repository/image:tag"},
},
},
component: app.SnapshotComponent{
Name: "my-component",
ContainerImage: "registry.io/repository/image:tag",
},
}

parsed, inputJSON, err := a.BuildInput(context.Background())
require.NoError(t, err)
assert.NotNil(t, parsed)
assert.NotEmpty(t, inputJSON)

img, ok := parsed["image"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "registry.io/repository/image:tag", img["ref"])

assert.Equal(t, "my-component", parsed["component_name"])

var fromJSON map[string]any
require.NoError(t, json.Unmarshal(inputJSON, &fromJSON))
assert.Equal(t, parsed["component_name"], fromJSON["component_name"])
})

t.Run("with parent image", func(t *testing.T) {
a := ApplicationSnapshotImage{
reference: name.MustParseReference("registry.io/repository/image:tag"),
parentRef: name.MustParseReference("registry.io/repository/parent:latest"),
parentConfigJSON: json.RawMessage(`{"Labels":{"io.k8s.display-name":"Base"}}`),
}

parsed, _, err := a.BuildInput(context.Background())
require.NoError(t, err)

img, ok := parsed["image"].(map[string]any)
require.True(t, ok)
parent, ok := img["parent"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "registry.io/repository/parent:latest", parent["ref"])
})

t.Run("without parent image", func(t *testing.T) {
a := ApplicationSnapshotImage{
reference: name.MustParseReference("registry.io/repository/image:tag"),
}

parsed, _, err := a.BuildInput(context.Background())
require.NoError(t, err)

img, ok := parsed["image"].(map[string]any)
require.True(t, ok)
parent, exists := img["parent"]
if exists {
parentMap, ok := parent.(map[string]any)
assert.True(t, !ok || parentMap["ref"] == nil || parentMap["ref"] == "")
}
})

t.Run("matches WriteInputFile output", func(t *testing.T) {
a := ApplicationSnapshotImage{
reference: name.MustParseReference("registry.io/repository/image:tag"),
attestations: []attestation.Attestation{createSimpleAttestation(nil)},
snapshot: app.SnapshotSpec{
Components: []app.SnapshotComponent{
{ContainerImage: "registry.io/repository/image:tag"},
},
},
component: app.SnapshotComponent{
Name: "test-component",
ContainerImage: "registry.io/repository/image:tag",
},
}

parsed, buildJSON, err := a.BuildInput(context.Background())
require.NoError(t, err)

fs := afero.NewMemMapFs()
ctx := utils.WithFS(context.Background(), fs)
_, writeJSON, err := a.WriteInputFile(ctx)
require.NoError(t, err)

assert.JSONEq(t, string(writeJSON), string(buildJSON))
assert.NotNil(t, parsed)
})
}

func TestNewApplicationSnapshotImage(t *testing.T) {
ctx := context.Background()

Expand Down
14 changes: 11 additions & 3 deletions internal/evaluation_target/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import (
"github.com/conforma/cli/internal/evaluator"
"github.com/conforma/cli/internal/policy"
"github.com/conforma/cli/internal/policy/source"
"github.com/conforma/cli/internal/utils"
)

var newConftestEvaluator = evaluator.NewConftestEvaluator
var newOPAEvaluator = evaluator.NewOPAEvaluator

// Input represents the structure needed to evaluate a generic file input
type Input struct {
Expand All @@ -48,13 +50,19 @@ func NewInput(ctx context.Context, paths []string, p policy.Policy) (*Input, err
log.Debugf("policySource: %#v", policySource)
}

c, err := newConftestEvaluator(ctx, policySources, p, sourceGroup)
var c evaluator.Evaluator
var err error
if utils.IsOpaEnabled() {
c, err = newOPAEvaluator(ctx, policySources, p, sourceGroup, nil)
} else {
c, err = newConftestEvaluator(ctx, policySources, p, sourceGroup)
}
if err != nil {
log.Debug("Failed to initialize the conftest evaluator!")
log.Debug("Failed to initialize the evaluator!")
return nil, err
}

log.Debug("Conftest evaluator initialized")
log.Debug("Evaluator initialized")
i.Evaluators = append(i.Evaluators, c)

}
Expand Down
Loading
Loading