Skip to content

Commit c4f5a2a

Browse files
authored
Merge pull request #2622 from joejstuart/appsnapshot-refactor
refactor: migrate application snapshot VSA to per-image implementatio…
2 parents 9d68393 + e3a15cd commit c4f5a2a

File tree

14 files changed

+602
-259
lines changed

14 files changed

+602
-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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright The Conforma Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package applicationsnapshot
18+
19+
import (
20+
"crypto/sha256"
21+
"fmt"
22+
23+
"github.com/spf13/afero"
24+
)
25+
26+
// GetVSAPredicateDigest calculates the sha256 digest of the given file path.
27+
func GetVSAPredicateDigest(fs afero.Fs, path string) (string, error) {
28+
data, err := afero.ReadFile(fs, path)
29+
if err != nil {
30+
return "", err
31+
}
32+
return fmt.Sprintf("sha256:%x", sha256.Sum256(data)), nil
33+
}

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 (s *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(s.FS, "", s.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(s.FS, filepath, data, s.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 (s *SnapshotVSAGenerator) GeneratePredicate(ctx context.Context) (Report, error) {
87+
log.Infof("Generating application snapshot VSA predicate with %d components", len(s.Report.Components))
88+
89+
return s.Report, nil
6190
}

0 commit comments

Comments
 (0)