Skip to content

Commit 08e99a5

Browse files
joejstuartOpenAI GPT-4.1
andcommitted
refactor: migrate application snapshot VSA to per-image implementation and enable signing
- Refactored ApplicationSnapshot VSA generation to use the per-image VSA generation logic - Simplified snapshot-level VSA flow by reusing component-level signing implementation - Added support for signing ApplicationSnapshot VSAs using the same key and process as per-image VSAs - Removed legacy snapshot-specific logic that is now redundant Co-authored-by: OpenAI GPT-4.1 <[email protected]>
1 parent da3ad6c commit 08e99a5

File tree

14 files changed

+586
-259
lines changed

14 files changed

+586
-259
lines changed

cmd/validate/image.go

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"strings"
2727

2828
hd "github.com/MakeNowJust/heredoc"
29+
"github.com/google/go-containerregistry/pkg/name"
2930
app "github.com/konflux-ci/application-api/api/v1alpha1"
3031
"github.com/sigstore/cosign/v2/pkg/cosign"
3132
log "github.com/sirupsen/logrus"
@@ -39,6 +40,7 @@ import (
3940
"github.com/conforma/cli/internal/policy"
4041
"github.com/conforma/cli/internal/policy/source"
4142
"github.com/conforma/cli/internal/utils"
43+
"github.com/conforma/cli/internal/utils/oci"
4244
validate_utils "github.com/conforma/cli/internal/validate"
4345
"github.com/conforma/cli/internal/validate/vsa"
4446
)
@@ -458,40 +460,45 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
458460
}
459461

460462
if data.vsaEnabled {
461-
generator := vsa.NewGenerator(report)
462-
writer := vsa.NewWriter()
463-
for _, comp := range components {
464-
writtenPath, err := vsa.GenerateAndWriteVSA(cmd.Context(), generator, writer, comp)
465-
if err != nil {
466-
log.Error(err)
467-
continue
468-
}
463+
signer, err := vsa.NewSigner(data.vsaSigningKey, utils.FS(cmd.Context()))
464+
if err != nil {
465+
log.Error(err)
466+
return err
467+
}
469468

470-
signer, err := vsa.NewSigner(data.vsaSigningKey, utils.FS(cmd.Context()))
471-
if err != nil {
472-
log.Error(err)
473-
continue
474-
}
469+
// Create VSA service
470+
vsaService := vsa.NewServiceWithFS(signer, utils.FS(cmd.Context()))
475471

476-
// Get the git URL safely, defaulting to empty string if GitSource is nil
477-
var gitURL string
472+
// Define helper functions for getting git URL and digest
473+
getGitURL := func(comp applicationsnapshot.Component) string {
478474
if comp.Source.GitSource != nil {
479-
gitURL = comp.Source.GitSource.URL
475+
return comp.Source.GitSource.URL
480476
}
477+
return ""
478+
}
481479

482-
attestor, err := vsa.NewAttestor(writtenPath, gitURL, comp.ContainerImage, signer)
480+
getDigest := func(comp applicationsnapshot.Component) (string, error) {
481+
imageRef, err := name.ParseReference(comp.ContainerImage)
483482
if err != nil {
484-
log.Error(err)
485-
continue
483+
return "", fmt.Errorf("failed to parse image reference %s: %v", comp.ContainerImage, err)
486484
}
487-
envelope, err := vsa.AttestVSA(cmd.Context(), attestor, comp)
485+
486+
digest, err := oci.NewClient(cmd.Context()).ResolveDigest(imageRef)
488487
if err != nil {
489-
log.Error(err)
490-
continue
488+
return "", fmt.Errorf("failed to resolve digest for image %s: %v", comp.ContainerImage, err)
491489
}
492-
log.Infof("[VSA] VSA attested and envelope written to %s", envelope)
490+
491+
return digest, nil
492+
}
493+
494+
// Process all VSAs using the service
495+
err = vsaService.ProcessAllVSAs(cmd.Context(), report, getGitURL, getDigest)
496+
if err != nil {
497+
log.Errorf("Failed to process VSAs: %v", err)
498+
// Don't return error here, continue with the rest of the command
493499
}
494500
}
501+
495502
if data.strict && !report.Success {
496503
return errors.New("success criteria not met")
497504
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package applicationsnapshot
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
7+
"github.com/spf13/afero"
8+
)
9+
10+
// GetVSAPredicateDigest calculates the sha256 digest of the given file path.
11+
func GetVSAPredicateDigest(fs afero.Fs, path string) (string, error) {
12+
data, err := afero.ReadFile(fs, path)
13+
if err != nil {
14+
return "", err
15+
}
16+
return fmt.Sprintf("sha256:%x", sha256.Sum256(data)), nil
17+
}

internal/applicationsnapshot/report.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package applicationsnapshot
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"embed"
2223
"encoding/json"
2324
"encoding/xml"
@@ -219,11 +220,12 @@ func (r *Report) toFormat(format string) (data []byte, err error) {
219220
}
220221

221222
func (r *Report) toVSA() ([]byte, error) {
222-
vsa, err := NewVSA(*r)
223+
generator := NewSnapshotVSAGenerator(*r)
224+
predicate, err := generator.GeneratePredicate(context.Background())
223225
if err != nil {
224226
return []byte{}, err
225227
}
226-
return json.Marshal(vsa)
228+
return json.Marshal(predicate)
227229
}
228230

229231
// toSummary returns a condensed version of the report.

internal/applicationsnapshot/vsa.go

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,74 @@
1717
package applicationsnapshot
1818

1919
import (
20-
"github.com/in-toto/in-toto-golang/in_toto"
21-
)
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"os"
24+
"path/filepath"
2225

23-
const (
24-
// Make it visible elsewhere
25-
PredicateVSAProvenance = "https://conforma.dev/verification_summary/v1"
26-
StatmentVSA = "https://in-toto.io/Statement/v1"
26+
log "github.com/sirupsen/logrus"
27+
"github.com/spf13/afero"
2728
)
2829

29-
type ProvenanceStatementVSA struct {
30-
in_toto.StatementHeader
31-
Predicate Report `json:"predicate"`
30+
// SnapshotVSAWriter handles writing application snapshot VSA predicates to files
31+
type SnapshotVSAWriter struct {
32+
FS afero.Fs // defaults to afero.NewOsFs()
33+
TempDirPrefix string // defaults to "snapshot-vsa-"
34+
FilePerm os.FileMode // defaults to 0600
35+
}
36+
37+
// NewSnapshotVSAWriter creates a new application snapshot VSA file writer
38+
func NewSnapshotVSAWriter() *SnapshotVSAWriter {
39+
return &SnapshotVSAWriter{
40+
FS: afero.NewOsFs(),
41+
TempDirPrefix: "snapshot-vsa-",
42+
FilePerm: 0o600,
43+
}
3244
}
3345

34-
func NewVSA(report Report) (ProvenanceStatementVSA, error) {
35-
subjects, err := getSubjects(report)
46+
// WritePredicate writes the Report as a VSA predicate to a file
47+
func (w *SnapshotVSAWriter) WritePredicate(report Report) (string, error) {
48+
log.Infof("Writing application snapshot VSA")
49+
50+
// Serialize with indent
51+
data, err := json.MarshalIndent(report, "", " ")
3652
if err != nil {
37-
return ProvenanceStatementVSA{}, err
53+
return "", fmt.Errorf("failed to marshal application snapshot VSA: %w", err)
3854
}
3955

40-
return ProvenanceStatementVSA{
41-
StatementHeader: in_toto.StatementHeader{
42-
Type: StatmentVSA,
43-
PredicateType: PredicateVSAProvenance,
44-
Subject: subjects,
45-
},
46-
Predicate: report,
47-
}, nil
48-
}
56+
// Create temp directory
57+
tempDir, err := afero.TempDir(w.FS, "", w.TempDirPrefix)
58+
if err != nil {
59+
return "", fmt.Errorf("failed to create temp directory: %w", err)
60+
}
4961

50-
func getSubjects(report Report) ([]in_toto.Subject, error) {
51-
statements, err := report.attestations()
62+
// Write to file
63+
filename := "application-snapshot-vsa.json"
64+
filepath := filepath.Join(tempDir, filename)
65+
err = afero.WriteFile(w.FS, filepath, data, w.FilePerm)
5266
if err != nil {
53-
return []in_toto.Subject{}, err
67+
return "", fmt.Errorf("failed to write application snapshot VSA to file: %w", err)
5468
}
5569

56-
var subjects []in_toto.Subject
57-
for _, stmt := range statements {
58-
subjects = append(subjects, stmt.Subject...)
70+
log.Infof("Application snapshot VSA written to: %s", filepath)
71+
return filepath, nil
72+
}
73+
74+
type SnapshotVSAGenerator struct {
75+
Report Report
76+
}
77+
78+
// NewSnapshotVSAGenerator creates a new VSA predicate generator for application snapshots
79+
func NewSnapshotVSAGenerator(report Report) *SnapshotVSAGenerator {
80+
return &SnapshotVSAGenerator{
81+
Report: report,
5982
}
60-
return subjects, nil
83+
}
84+
85+
// GeneratePredicate creates a VSA predicate for the entire application snapshot
86+
func (g *SnapshotVSAGenerator) GeneratePredicate(ctx context.Context) (Report, error) {
87+
log.Infof("Generating application snapshot VSA predicate with %d components", len(g.Report.Components))
88+
89+
return g.Report, nil
6190
}

internal/applicationsnapshot/vsa_test.go

Lines changed: 45 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,104 +20,72 @@ package applicationsnapshot
2020

2121
import (
2222
"context"
23-
"encoding/json"
24-
"fmt"
23+
"os"
24+
"path/filepath"
2525
"testing"
2626

2727
ecc "github.com/enterprise-contract/enterprise-contract-controller/api/v1alpha1"
28-
"github.com/in-toto/in-toto-golang/in_toto"
2928
app "github.com/konflux-ci/application-api/api/v1alpha1"
3029
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
3131

3232
"github.com/conforma/cli/internal/evaluator"
33-
"github.com/conforma/cli/internal/policy"
34-
"github.com/conforma/cli/internal/utils"
3533
)
3634

37-
func TestNewVSA(t *testing.T) {
38-
components := []Component{
39-
{
40-
SnapshotComponent: app.SnapshotComponent{Name: "component1"},
41-
Violations: []evaluator.Result{
42-
{
43-
Message: "violation1",
44-
},
45-
},
46-
Attestations: []AttestationResult{
47-
{
48-
Statement: []byte{},
35+
func TestSnapshotVSAGenerator_GeneratePredicate(t *testing.T) {
36+
ctx := context.Background()
37+
38+
// Create a test report
39+
report := Report{
40+
Components: []Component{
41+
{
42+
SnapshotComponent: app.SnapshotComponent{
43+
Name: "test-component",
44+
ContainerImage: "test-image:latest",
4945
},
46+
Success: true,
47+
Violations: []evaluator.Result{},
48+
Warnings: []evaluator.Result{},
49+
Successes: []evaluator.Result{},
5050
},
5151
},
52+
Policy: ecc.EnterpriseContractPolicySpec{
53+
Name: "test-policy",
54+
},
5255
}
5356

54-
utils.SetTestRekorPublicKey(t)
55-
pkey := utils.TestPublicKey
56-
testPolicy, err := policy.NewPolicy(context.Background(), policy.Options{
57-
PublicKey: pkey,
58-
EffectiveTime: policy.Now,
59-
PolicyRef: toJson(&ecc.EnterpriseContractPolicySpec{PublicKey: pkey}),
60-
})
61-
assert.NoError(t, err)
57+
generator := NewSnapshotVSAGenerator(report)
6258

63-
report, err := NewReport("snappy", components, testPolicy, nil, true)
64-
assert.NoError(t, err)
59+
predicate, err := generator.GeneratePredicate(ctx)
60+
require.NoError(t, err)
6561

66-
expected := ProvenanceStatementVSA{
67-
StatementHeader: in_toto.StatementHeader{
68-
Type: "https://in-toto.io/Statement/v1",
69-
PredicateType: "https://conforma.dev/verification_summary/v1",
70-
Subject: nil,
71-
},
72-
Predicate: report,
73-
}
74-
vsa, err := NewVSA(report)
75-
assert.NoError(t, err)
76-
assert.Equal(t, expected, vsa)
62+
// Verify the predicate is the same as the report
63+
assert.Equal(t, report, predicate)
7764
}
7865

79-
func TestSubjects(t *testing.T) {
80-
expected := []in_toto.Subject{
81-
{
82-
Name: "my-subject",
83-
Digest: nil,
84-
},
85-
}
86-
87-
statement := in_toto.Statement{
88-
StatementHeader: in_toto.StatementHeader{
89-
Subject: expected,
90-
},
91-
}
92-
data, err := json.Marshal(statement)
93-
assert.NoError(t, err)
94-
95-
components := []Component{
96-
{
97-
SnapshotComponent: app.SnapshotComponent{Name: "component1"},
98-
Violations: []evaluator.Result{
99-
{
100-
Message: "violation1",
101-
},
102-
},
103-
Attestations: []AttestationResult{
104-
{
105-
Statement: data,
66+
func TestSnapshotVSAWriter_WritePredicate(t *testing.T) {
67+
// Create a test report
68+
report := Report{
69+
Components: []Component{
70+
{
71+
SnapshotComponent: app.SnapshotComponent{
72+
Name: "test-component",
73+
ContainerImage: "test-image:latest",
10674
},
75+
Success: true,
10776
},
10877
},
10978
}
11079

111-
report := Report{Components: components}
112-
subjects, err := getSubjects(report)
113-
assert.NoError(t, err)
114-
assert.Equal(t, expected, subjects)
115-
}
80+
writer := NewSnapshotVSAWriter()
11681

117-
func toJson(policy any) string {
118-
newInline, err := json.Marshal(policy)
119-
if err != nil {
120-
panic(fmt.Errorf("invalid JSON: %w", err))
121-
}
122-
return string(newInline)
82+
path, err := writer.WritePredicate(report)
83+
require.NoError(t, err)
84+
85+
// Verify the file was created and contains valid JSON
86+
assert.Contains(t, path, "snapshot-vsa-")
87+
assert.Contains(t, path, "application-snapshot-vsa.json")
88+
89+
// Clean up
90+
os.RemoveAll(filepath.Dir(path))
12391
}

0 commit comments

Comments
 (0)