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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 55 additions & 36 deletions cmd/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,11 +458,38 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
}

if data.vsaEnabled {
generator := vsa.NewGenerator(report)
writer := vsa.NewWriter()
for _, comp := range components {
if err := processVSA(cmd.Context(), report, comp); err != nil {
log.Errorf("[VSA] Error processing VSA for image %s: %v", comp.ContainerImage, err)
writtenPath, err := generateAndWrite(cmd.Context(), generator, writer, comp)
if err != nil {
log.Error(err)
continue
}

signer, err := vsa.NewSigner(data.vsaSigningKey, utils.FS(cmd.Context()))
if err != nil {
log.Error(err)
continue
}

// Get the git URL safely, defaulting to empty string if GitSource is nil
var gitURL string
if comp.Source.GitSource != nil {
gitURL = comp.Source.GitSource.URL
}

attestor, err := vsa.NewAttestOptions(writtenPath, gitURL, comp.ContainerImage, signer)
if err != nil {
log.Error(err)
continue
}
envelope, err := attestVSA(cmd.Context(), attestor, comp)
if err != nil {
log.Error(err)
continue
}
log.Infof("[VSA] VSA attested and envelope written to %s", envelope)
}
}
if data.strict && !report.Success {
Expand Down Expand Up @@ -581,50 +608,42 @@ func containsOutput(data []string, value string) bool {
return false
}

// PredicateGenerator defines the interface for generating VSA predicates
type PredicateGenerator interface {
GeneratePredicate(ctx context.Context, report applicationsnapshot.Report, comp applicationsnapshot.Component) (*vsa.Predicate, error)
// Create interfaces for the VSA components for easier testing
type Generator interface {
GeneratePredicate(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error)
}

// VSAWriter defines the interface for writing VSA files
type VSAWriter interface {
WriteVSA(predicate *vsa.Predicate) (string, error)
type Writer interface {
WriteVSA(pred *vsa.Predicate) (string, error)
}

// generateAndWriteVSA generates a VSA predicate and writes it to a file
func generateAndWriteVSA(
ctx context.Context,
report applicationsnapshot.Report,
comp applicationsnapshot.Component,
generator PredicateGenerator,
writer VSAWriter,
) (string, error) {
log.Debugf("[VSA] Generating predicate for image: %s", comp.ContainerImage)
pred, err := generator.GeneratePredicate(ctx, report, comp)
type Attestor interface {
AttestPredicate(ctx context.Context) ([]byte, error)
WriteEnvelope(data []byte) (string, error)
}

// attestVSA handles VSA attestation and envelope writing for a single component.
func attestVSA(ctx context.Context, attestor Attestor, comp applicationsnapshot.Component) (string, error) {
env, err := attestor.AttestPredicate(ctx)
if err != nil {
return "", fmt.Errorf("failed to generate predicate for image %s: %w", comp.ContainerImage, err)
return "", fmt.Errorf("[VSA] Error attesting VSA for image %s: %w", comp.ContainerImage, err)
}
log.Debugf("[VSA] Predicate generated for image: %s", comp.ContainerImage)

log.Debugf("[VSA] Writing VSA for image: %s", comp.ContainerImage)
writtenPath, err := writer.WriteVSA(pred)
envelopePath, err := attestor.WriteEnvelope(env)
if err != nil {
return "", fmt.Errorf("failed to write VSA for image %s: %w", comp.ContainerImage, err)
return "", fmt.Errorf("[VSA] Error writing envelope for image %s: %w", comp.ContainerImage, err)
}
log.Debugf("[VSA] VSA written to %s", writtenPath)

return writtenPath, nil
return envelopePath, nil
}

// processVSA handles the complete VSA generation, signing and upload process for a component
func processVSA(ctx context.Context, report applicationsnapshot.Report, comp applicationsnapshot.Component) error {
generator := vsa.NewGenerator()
writer := vsa.NewWriter()

vsaPath, err := generateAndWriteVSA(ctx, report, comp, generator, writer)
log.Infof("[VSA] VSA written to %s", vsaPath)
// generateAndWrite generates a VSA predicate and writes it to a file, returning the written path.
func generateAndWrite(ctx context.Context, generator Generator, writer Writer, comp applicationsnapshot.Component) (string, error) {
pred, err := generator.GeneratePredicate(ctx, comp)
if err != nil {
return err
return "", err
}
return nil
writtenPath, err := writer.WriteVSA(pred)
if err != nil {
return "", err
}
return writtenPath, nil
}
163 changes: 163 additions & 0 deletions cmd/validate/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/conforma/cli/internal/utils"
"github.com/conforma/cli/internal/utils/oci"
"github.com/conforma/cli/internal/utils/oci/fake"
"github.com/conforma/cli/internal/validate/vsa"
)

type data struct {
Expand Down Expand Up @@ -1362,3 +1363,165 @@ func TestContainsAttestation(t *testing.T) {
assert.Equal(t, test.expected, result, test.name)
}
}

// --- Mocks and tests for processVSAForComponent ---

type mockGenerator struct {
GeneratePredicateFunc func(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error)
}

func (m *mockGenerator) GeneratePredicate(ctx context.Context, comp applicationsnapshot.Component) (*vsa.Predicate, error) {
return m.GeneratePredicateFunc(ctx, comp)
}

type mockWriter struct {
WriteVSAFunc func(pred *vsa.Predicate) (string, error)
}

func (m *mockWriter) WriteVSA(pred *vsa.Predicate) (string, error) {
return m.WriteVSAFunc(pred)
}

type mockAttestor struct {
AttestPredicateFunc func(ctx context.Context) ([]byte, error)
WriteEnvelopeFunc func(data []byte) (string, error)
}

func (m *mockAttestor) AttestPredicate(ctx context.Context) ([]byte, error) {
return m.AttestPredicateFunc(ctx)
}

func (m *mockAttestor) WriteEnvelope(data []byte) (string, error) {
return m.WriteEnvelopeFunc(data)
}

func Test_attestVSA_success(t *testing.T) {
ctx := context.Background()
comp := applicationsnapshot.Component{
SnapshotComponent: app.SnapshotComponent{
ContainerImage: "test-image",
},
}

attestor := &mockAttestor{
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
return []byte("envelope"), nil
},
WriteEnvelopeFunc: func(data []byte) (string, error) {
if string(data) != "envelope" {
t.Errorf("unexpected data passed to WriteEnvelope")
}
return "/tmp/envelope.json", nil
},
}

path, err := attestVSA(ctx, attestor, comp)
assert.NoError(t, err)
assert.Equal(t, "/tmp/envelope.json", path)
}

func Test_attestVSA_errors(t *testing.T) {
ctx := context.Background()
comp := applicationsnapshot.Component{
SnapshotComponent: app.SnapshotComponent{
ContainerImage: "test-image",
},
}

t.Run("attest predicate fails", func(t *testing.T) {
attestor := &mockAttestor{
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
return nil, errors.New("attest error")
},
}
path, err := attestVSA(ctx, attestor, comp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Error attesting VSA")
assert.Empty(t, path)
})

t.Run("write envelope fails", func(t *testing.T) {
attestor := &mockAttestor{
AttestPredicateFunc: func(ctx context.Context) ([]byte, error) {
return []byte("envelope"), nil
},
WriteEnvelopeFunc: func(data []byte) (string, error) {
return "", errors.New("envelope error")
},
}
path, err := attestVSA(ctx, attestor, comp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Error writing envelope")
assert.Empty(t, path)
})
}

func Test_generateAndWrite_success(t *testing.T) {
ctx := context.Background()
comp := applicationsnapshot.Component{
SnapshotComponent: app.SnapshotComponent{
ContainerImage: "test-image",
},
}
pred := &vsa.Predicate{ImageRef: "test-image"}

gen := &mockGenerator{
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
return pred, nil
},
}
writer := &mockWriter{
WriteVSAFunc: func(p *vsa.Predicate) (string, error) {
if p != pred {
t.Errorf("unexpected predicate passed to WriteVSA")
}
return "/tmp/vsa.json", nil
},
}

path, err := generateAndWrite(ctx, gen, writer, comp)
assert.NoError(t, err)
assert.Equal(t, "/tmp/vsa.json", path)
}

func Test_generateAndWrite_errors(t *testing.T) {
ctx := context.Background()
comp := applicationsnapshot.Component{
SnapshotComponent: app.SnapshotComponent{
ContainerImage: "test-image",
},
}
pred := &vsa.Predicate{ImageRef: "test-image"}

t.Run("predicate generation fails", func(t *testing.T) {
gen := &mockGenerator{
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
return nil, errors.New("predicate generation error")
},
}
writer := &mockWriter{}

path, err := generateAndWrite(ctx, gen, writer, comp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "predicate generation error")
assert.Empty(t, path)
})

t.Run("write VSA fails", func(t *testing.T) {
gen := &mockGenerator{
GeneratePredicateFunc: func(ctx context.Context, c applicationsnapshot.Component) (*vsa.Predicate, error) {
return pred, nil
},
}
writer := &mockWriter{
WriteVSAFunc: func(p *vsa.Predicate) (string, error) {
return "", errors.New("write VSA error")
},
}

path, err := generateAndWrite(ctx, gen, writer, comp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "write VSA error")
assert.Empty(t, path)
})
}
Loading
Loading