diff --git a/cmd/validate/image.go b/cmd/validate/image.go index 5cf158c4c..8082d168f 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -42,7 +42,7 @@ import ( validate_utils "github.com/enterprise-contract/ec-cli/internal/validate" ) -type imageValidationFunc func(context.Context, app.SnapshotComponent, policy.Policy, []evaluator.Evaluator, bool) (*output.Output, error) +type imageValidationFunc func(context.Context, app.SnapshotComponent, *app.SnapshotSpec, policy.Policy, []evaluator.Evaluator, bool) (*output.Output, error) var newConftestEvaluator = evaluator.NewConftestEvaluator @@ -320,7 +320,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { for comp := range jobs { log.Debugf("Worker %d got a component %q", id, comp.ContainerImage) ctx := cmd.Context() - out, err := validate(ctx, comp, data.policy, evaluators, data.info) + out, err := validate(ctx, comp, data.spec, data.policy, evaluators, data.info) res := result{ err: err, component: applicationsnapshot.Component{ diff --git a/cmd/validate/image_integration_test.go b/cmd/validate/image_integration_test.go index 71a318847..208d89194 100644 --- a/cmd/validate/image_integration_test.go +++ b/cmd/validate/image_integration_test.go @@ -76,7 +76,7 @@ func TestEvaluatorLifecycle(t *testing.T) { newConftestEvaluator = evaluator.NewConftestEvaluator }) - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, evaluators []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, evaluators []evaluator.Evaluator, _ bool) (*output.Output, error) { for _, e := range evaluators { _, _, err := e.Evaluate(ctx, []string{}) require.NoError(t, err) diff --git a/cmd/validate/image_test.go b/cmd/validate/image_test.go index 0432877f1..0f0b898df 100644 --- a/cmd/validate/image_test.go +++ b/cmd/validate/image_test.go @@ -256,7 +256,7 @@ func Test_determineInputSpec(t *testing.T) { } func Test_ValidateImageCommand(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -336,7 +336,7 @@ func Test_ValidateImageCommand(t *testing.T) { } func Test_ValidateImageCommandImages(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -458,7 +458,7 @@ func Test_ValidateImageCommandImages(t *testing.T) { func Test_ValidateImageCommandKeyless(t *testing.T) { called := false - validateImageCmd := validateImageCmd(func(_ context.Context, _ app.SnapshotComponent, p policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validateImageCmd := validateImageCmd(func(_ context.Context, _ app.SnapshotComponent, _ *app.SnapshotSpec, p policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { assert.Equal(t, cosign.Identity{ Issuer: "my-certificate-oidc-issuer", Subject: "my-certificate-identity", @@ -503,7 +503,7 @@ func Test_ValidateImageCommandKeyless(t *testing.T) { } func Test_ValidateImageCommandYAMLPolicyFile(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -621,7 +621,7 @@ spec: } func Test_ValidateImageCommandJSONPolicyFile(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -700,7 +700,7 @@ configuration: } func Test_ValidateImageCommandExtraData(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -825,7 +825,7 @@ spec: } func Test_ValidateImageCommandEmptyPolicyFile(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -893,7 +893,7 @@ func Test_ValidateImageCommandEmptyPolicyFile(t *testing.T) { func Test_ValidateImageErrorLog(t *testing.T) { // TODO: Enhance this test to cover other Error Log messages - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -1057,7 +1057,7 @@ func Test_ValidateErrorCommand(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - validate := func(context.Context, app.SnapshotComponent, policy.Policy, []evaluator.Evaluator, bool) (*output.Output, error) { + validate := func(context.Context, app.SnapshotComponent, *app.SnapshotSpec, policy.Policy, []evaluator.Evaluator, bool) (*output.Output, error) { return nil, errors.New("expected") } @@ -1087,7 +1087,7 @@ func Test_ValidateErrorCommand(t *testing.T) { } func Test_FailureImageAccessibility(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: false, @@ -1158,7 +1158,7 @@ func Test_FailureImageAccessibility(t *testing.T) { } func Test_FailureOutput(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: false, @@ -1227,7 +1227,7 @@ func Test_FailureOutput(t *testing.T) { } func Test_WarningOutput(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -1301,7 +1301,7 @@ func Test_WarningOutput(t *testing.T) { } func Test_FailureImageAccessibilityNonStrict(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, @@ -1369,7 +1369,7 @@ func Test_FailureImageAccessibilityNonStrict(t *testing.T) { } func TestValidateImageCommand_RunE(t *testing.T) { - validate := func(_ context.Context, component app.SnapshotComponent, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { + validate := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { return &output.Output{ ImageSignatureCheck: output.VerificationStatus{ Passed: true, diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index b97b5ded3..958865705 100755 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -2550,6 +2550,17 @@ ${__________known_PUBLIC_KEY} } }, "source": {} + }, + "snapshot": { + "application": "", + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/policy-input-output", + "source": {} + } + ], + "artifacts": {} } } --- @@ -2902,6 +2913,17 @@ Error: success criteria not met } }, "source": {} + }, + "snapshot": { + "application": "", + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/image", + "source": {} + } + ], + "artifacts": {} } } --- @@ -3244,6 +3266,17 @@ Error: success criteria not met } }, "source": {} + }, + "snapshot": { + "application": "", + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/image", + "source": {} + } + ], + "artifacts": {} } } --- diff --git a/internal/evaluation_target/application_snapshot_image/__snapshots__/application_snapshot_image_test.snap b/internal/evaluation_target/application_snapshot_image/__snapshots__/application_snapshot_image_test.snap index 76e2b34b3..0e23966bb 100755 --- a/internal/evaluation_target/application_snapshot_image/__snapshots__/application_snapshot_image_test.snap +++ b/internal/evaluation_target/application_snapshot_image/__snapshots__/application_snapshot_image_test.snap @@ -34,6 +34,22 @@ "image": { "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -77,6 +93,22 @@ "image": { "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -118,6 +150,22 @@ } ], "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -133,6 +181,22 @@ }, "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -151,6 +215,22 @@ }, "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -194,6 +274,22 @@ "image": { "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -221,6 +317,22 @@ "image": { "ref": "registry.io/repository/image:tag", "source": {} + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- @@ -236,6 +348,22 @@ "url": "git.local/repository" } } + }, + "snapshot": { + "application": "", + "artifacts": {}, + "components": [ + { + "containerImage": "registry.io/repository/image:tag", + "name": "", + "source": {} + }, + { + "containerImage": "registry.io/other-repository/image2:tag", + "name": "", + "source": {} + } + ] } } --- diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index b01eecccb..d96f7c812 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -59,6 +59,7 @@ type ApplicationSnapshotImage struct { Evaluators []evaluator.Evaluator files map[string]json.RawMessage component app.SnapshotComponent + snapshot app.SnapshotSpec } func (a ApplicationSnapshotImage) GetReference() name.Reference { @@ -66,7 +67,7 @@ func (a ApplicationSnapshotImage) GetReference() name.Reference { } // NewApplicationSnapshotImage returns an ApplicationSnapshotImage struct with reference, checkOpts, and evaluator ready to use. -func NewApplicationSnapshotImage(ctx context.Context, component app.SnapshotComponent, p policy.Policy) (*ApplicationSnapshotImage, error) { +func NewApplicationSnapshotImage(ctx context.Context, component app.SnapshotComponent, p policy.Policy, snap app.SnapshotSpec) (*ApplicationSnapshotImage, error) { opts, err := p.CheckOpts() if err != nil { return nil, err @@ -74,6 +75,7 @@ func NewApplicationSnapshotImage(ctx context.Context, component app.SnapshotComp a := &ApplicationSnapshotImage{ checkOpts: *opts, component: component, + snapshot: snap, } if err := a.SetImageURL(component.ContainerImage); err != nil { @@ -326,6 +328,7 @@ type image struct { type Input struct { Attestations []attestationData `json:"attestations"` Image image `json:"image"` + AppSnapshot app.SnapshotSpec `json:"snapshot"` } // WriteInputFile writes the JSON from the attestations to input.json in a random temp dir @@ -349,6 +352,7 @@ func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, Files: a.files, Source: a.component.Source, }, + AppSnapshot: a.snapshot, } if a.parentRef != nil { diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go index 909b4fa18..9ba15d0c9 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go @@ -50,6 +50,7 @@ import ( "github.com/stretchr/testify/require" "github.com/enterprise-contract/ec-cli/internal/attestation" + "github.com/enterprise-contract/ec-cli/internal/policy" "github.com/enterprise-contract/ec-cli/internal/signature" "github.com/enterprise-contract/ec-cli/internal/utils" o "github.com/enterprise-contract/ec-cli/internal/utils/oci" @@ -214,6 +215,16 @@ func TestWriteInputFile(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fs := afero.NewMemMapFs() ctx := utils.WithFS(context.Background(), fs) + tt.snapshot.snapshot = app.SnapshotSpec{ + Components: []app.SnapshotComponent{ + { + ContainerImage: "registry.io/repository/image:tag", + }, + { + ContainerImage: "registry.io/other-repository/image2:tag", + }, + }, + } inputPath, inputJSON, err := tt.snapshot.WriteInputFile(ctx) assert.NoError(t, err) @@ -234,9 +245,20 @@ func TestWriteInputFile(t *testing.T) { func TestWriteInputFileMultipleAttestations(t *testing.T) { att := createSimpleAttestation(nil) + snapshot := app.SnapshotSpec{ + Components: []app.SnapshotComponent{ + { + ContainerImage: "registry.io/repository/image:tag", + }, + { + ContainerImage: "registry.io/other-repository/image2:tag", + }, + }, + } a := ApplicationSnapshotImage{ reference: name.MustParseReference("registry.io/repository/image:tag"), attestations: []attestation.Attestation{att}, + snapshot: snapshot, } fs := afero.NewMemMapFs() @@ -257,6 +279,34 @@ func TestWriteInputFileMultipleAttestations(t *testing.T) { assert.JSONEq(t, string(inputJSON), string(bytes)) } +func TestNewApplicationSnapshotImage(t *testing.T) { + ctx := context.Background() + + component := app.SnapshotComponent{ + ContainerImage: "registry.io/repository/image:tag", + } + policy, err := policy.NewOfflinePolicy(ctx, policy.Now) + require.NoError(t, err) + + snapshot := app.SnapshotSpec{ + Components: []app.SnapshotComponent{ + { + ContainerImage: "registry.io/repository/image:tag", + }, + { + ContainerImage: "registry.io/other-repository/image2:tag", + }, + }, + } + actual, err := NewApplicationSnapshotImage(ctx, component, policy, snapshot) + assert.NoError(t, err) + + assert.Equal(t, len(actual.snapshot.Components), 2) + assert.Equal(t, actual.component.ContainerImage, component.ContainerImage) + assert.Equal(t, actual.snapshot.Components[0].ContainerImage, snapshot.Components[0].ContainerImage) + assert.Equal(t, actual.snapshot.Components[1].ContainerImage, snapshot.Components[1].ContainerImage) +} + func TestSyntaxValidationWithoutAttestations(t *testing.T) { noAttestations := ApplicationSnapshotImage{} diff --git a/internal/image/validate.go b/internal/image/validate.go index 3d77589a9..986ab3be1 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -35,11 +35,11 @@ import ( // ValidateImage executes the required method calls to evaluate a given policy // against a given image url. -func ValidateImage(ctx context.Context, comp app.SnapshotComponent, p policy.Policy, evaluators []evaluator.Evaluator, detailed bool) (*output.Output, error) { +func ValidateImage(ctx context.Context, comp app.SnapshotComponent, snap *app.SnapshotSpec, p policy.Policy, evaluators []evaluator.Evaluator, detailed bool) (*output.Output, error) { log.Debugf("Validating image %s", comp.ContainerImage) out := &output.Output{ImageURL: comp.ContainerImage, Detailed: detailed, Policy: p} - a, err := application_snapshot_image.NewApplicationSnapshotImage(ctx, comp, p) + a, err := application_snapshot_image.NewApplicationSnapshotImage(ctx, comp, p, *snap) if err != nil { log.Debug("Failed to create application snapshot image!") return nil, err diff --git a/internal/image/validate_test.go b/internal/image/validate_test.go index 9bce55356..78f22870c 100644 --- a/internal/image/validate_test.go +++ b/internal/image/validate_test.go @@ -23,6 +23,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "strings" "testing" "time" @@ -141,14 +142,27 @@ func TestBuiltinChecks(t *testing.T) { assert.NoError(t, err) evaluators := []evaluator.Evaluator{} + snap := app.SnapshotSpec{ + Components: []app.SnapshotComponent{ + { + ContainerImage: "registry.io/repository/image:tag", + }, + { + ContainerImage: "registry.io/other-repository/image2:tag", + }, + }, + } ctx = withImageConfig(ctx, c.component.ContainerImage) client := ecoci.NewClient(ctx) c.setup(client.(*fake.FakeClient)) - actual, err := ValidateImage(ctx, c.component, p, evaluators, false) + actual, err := ValidateImage(ctx, c.component, &snap, p, evaluators, false) assert.NoError(t, err) + // Verify application snapshot was a part of input + strings.Contains(string(actual.PolicyInput), "snapshot\":{\"application\":\"\",\"components\":[{\"name\":\"\",\"containerImage\":\"registry.io/repository/image:tag\",\"source\":{}},{\"name\":\"\",\"containerImage\":\"registry.io/other-repository/image2:tag\",\"source\":{}}],\"artifacts\":{}}") + assert.Equal(t, c.expectedWarnings, actual.Warnings()) assert.Equal(t, c.expectedViolations, actual.Violations()) assert.Equal(t, c.expectedImageURL, actual.ImageURL) @@ -316,7 +330,18 @@ func TestEvaluatorLifecycle(t *testing.T) { evaluators := []evaluator.Evaluator{e} - _, err = ValidateImage(ctx, component, policy, evaluators, false) + snap := app.SnapshotSpec{ + Components: []app.SnapshotComponent{ + { + ContainerImage: "registry.io/repository/image:tag", + }, + { + ContainerImage: "registry.io/other-repository/image2:tag", + }, + }, + } + + _, err = ValidateImage(ctx, component, &snap, policy, evaluators, false) require.NoError(t, err) }