Skip to content

Commit 95a19b5

Browse files
authored
Merge pull request #2955 from joejstuart/EC-1488
feat(vsa): introduce stable VSA schema and switch to minimal payload
2 parents 8442a8c + 7ae5575 commit 95a19b5

File tree

11 files changed

+947
-401
lines changed

11 files changed

+947
-401
lines changed

cmd/validate/image.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
6969
outputFile string
7070
policy policy.Policy
7171
policyConfiguration string
72+
policySource string
7273
publicKey string
7374
rekorURL string
7475
snapshot string
@@ -221,6 +222,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
221222
data.expansion = exp
222223
}
223224

225+
// Store policy source before resolution
226+
data.policySource = data.policyConfiguration
227+
224228
policyConfiguration, err := validate_utils.GetPolicyConfig(ctx, data.policyConfiguration)
225229
if err != nil {
226230
allErrors = errors.Join(allErrors, err)
@@ -500,7 +504,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
500504
}
501505

502506
// Create VSA service
503-
vsaService := vsa.NewServiceWithFS(signer, utils.FS(cmd.Context()))
507+
vsaService := vsa.NewServiceWithFS(signer, utils.FS(cmd.Context()), data.policySource, data.policy)
504508

505509
// Define helper functions for getting git URL and digest
506510
getGitURL := func(comp applicationsnapshot.Component) string {

internal/applicationsnapshot/report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ func (r *Report) toFormat(format string) (data []byte, err error) {
224224
}
225225

226226
func (r *Report) toVSA() ([]byte, error) {
227-
generator := NewSnapshotVSAGenerator(*r)
227+
generator := NewSnapshotPredicateGenerator(*r)
228228
predicate, err := generator.GeneratePredicate(context.Background())
229229
if err != nil {
230230
return []byte{}, err

internal/applicationsnapshot/vsa.go

Lines changed: 167 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,69 @@ import (
2222
"fmt"
2323
"os"
2424
"path/filepath"
25+
"time"
2526

27+
ecc "github.com/enterprise-contract/enterprise-contract-controller/api/v1alpha1"
2628
log "github.com/sirupsen/logrus"
2729
"github.com/spf13/afero"
2830
)
2931

30-
// SnapshotVSAWriter handles writing application snapshot VSA predicates to files
31-
type SnapshotVSAWriter struct {
32+
// SnapshotComponentDetail represents detailed information about a component in the snapshot summary
33+
type SnapshotComponentDetail struct {
34+
Name string `json:"name"`
35+
ContainerImage string `json:"containerImage"`
36+
Success bool `json:"success"`
37+
Violations int `json:"violations"`
38+
Warnings int `json:"warnings"`
39+
Successes int `json:"successes"`
40+
}
41+
42+
// SnapshotSummary represents the summary information for a snapshot predicate
43+
type SnapshotSummary struct {
44+
Snapshot string `json:"snapshot"`
45+
Components int `json:"components"`
46+
Success bool `json:"success"`
47+
Key string `json:"key"`
48+
EcVersion string `json:"ec_version"`
49+
ComponentDetails []SnapshotComponentDetail `json:"component_details"`
50+
Violations int `json:"Violations"`
51+
Warnings int `json:"Warnings"`
52+
}
53+
54+
// SnapshotPredicate represents a predicate for an entire application snapshot
55+
type SnapshotPredicate struct {
56+
Policy ecc.EnterpriseContractPolicySpec `json:"policy"`
57+
ImageRefs []string `json:"imageRefs"`
58+
Timestamp string `json:"timestamp"`
59+
Status string `json:"status"`
60+
Verifier string `json:"verifier"`
61+
Summary SnapshotSummary `json:"summary"`
62+
}
63+
64+
// SnapshotPredicateWriter handles writing application snapshot predicates to files
65+
type SnapshotPredicateWriter struct {
3266
FS afero.Fs // defaults to afero.NewOsFs()
33-
TempDirPrefix string // defaults to "snapshot-vsa-"
67+
TempDirPrefix string // defaults to "snapshot-predicate-"
3468
FilePerm os.FileMode // defaults to 0600
3569
}
3670

37-
// NewSnapshotVSAWriter creates a new application snapshot VSA file writer
38-
func NewSnapshotVSAWriter() *SnapshotVSAWriter {
39-
return &SnapshotVSAWriter{
71+
// NewSnapshotPredicateWriter creates a new application snapshot predicate file writer
72+
func NewSnapshotPredicateWriter() *SnapshotPredicateWriter {
73+
return &SnapshotPredicateWriter{
4074
FS: afero.NewOsFs(),
41-
TempDirPrefix: "snapshot-vsa-",
75+
TempDirPrefix: "snapshot-predicate-",
4276
FilePerm: 0o600,
4377
}
4478
}
4579

46-
// WritePredicate writes the Report as a VSA predicate to a file
47-
func (s *SnapshotVSAWriter) WritePredicate(report Report) (string, error) {
80+
// WritePredicate writes the SnapshotPredicate as a JSON file to a temp directory and returns the path
81+
func (s *SnapshotPredicateWriter) WritePredicate(predicate *SnapshotPredicate) (string, error) {
4882
log.Infof("Writing application snapshot VSA")
4983

5084
// Serialize with indent
51-
data, err := json.MarshalIndent(report, "", " ")
85+
data, err := json.MarshalIndent(predicate, "", " ")
5286
if err != nil {
53-
return "", fmt.Errorf("failed to marshal application snapshot VSA: %w", err)
87+
return "", fmt.Errorf("failed to marshal application snapshot VSA predicate: %w", err)
5488
}
5589

5690
// Create temp directory
@@ -59,32 +93,144 @@ func (s *SnapshotVSAWriter) WritePredicate(report Report) (string, error) {
5993
return "", fmt.Errorf("failed to create temp directory: %w", err)
6094
}
6195

62-
// Write to file
96+
// Write to file with same naming convention as old VSA
6397
filename := "application-snapshot-vsa.json"
6498
filepath := filepath.Join(tempDir, filename)
6599
err = afero.WriteFile(s.FS, filepath, data, s.FilePerm)
66100
if err != nil {
67-
return "", fmt.Errorf("failed to write application snapshot VSA to file: %w", err)
101+
return "", fmt.Errorf("failed to write application snapshot VSA predicate to file: %w", err)
68102
}
69103

70104
log.Infof("Application snapshot VSA written to: %s", filepath)
71105
return filepath, nil
72106
}
73107

74-
type SnapshotVSAGenerator struct {
108+
// SnapshotPredicateGenerator generates predicates for application snapshots
109+
type SnapshotPredicateGenerator struct {
75110
Report Report
76111
}
77112

78-
// NewSnapshotVSAGenerator creates a new VSA predicate generator for application snapshots
79-
func NewSnapshotVSAGenerator(report Report) *SnapshotVSAGenerator {
80-
return &SnapshotVSAGenerator{
113+
// NewSnapshotPredicateGenerator creates a new predicate generator for application snapshots
114+
func NewSnapshotPredicateGenerator(report Report) *SnapshotPredicateGenerator {
115+
return &SnapshotPredicateGenerator{
81116
Report: report,
82117
}
83118
}
84119

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))
120+
// GeneratePredicate creates a predicate for the entire application snapshot
121+
func (s *SnapshotPredicateGenerator) GeneratePredicate(ctx context.Context) (*SnapshotPredicate, error) {
122+
log.Infof("Generating application snapshot EC predicate with %d components", len(s.Report.Components))
123+
124+
// Collect all image references from all components
125+
imageRefs := s.getAllImageRefs()
126+
127+
// Determine overall status
128+
status := "failed"
129+
if s.Report.Success {
130+
status = "passed"
131+
}
132+
133+
// Add detailed component breakdown and calculate totals
134+
components := make([]SnapshotComponentDetail, 0, len(s.Report.Components))
135+
totalViolations := 0
136+
totalWarnings := 0
137+
138+
for _, comp := range s.Report.Components {
139+
compViolations := len(comp.Violations)
140+
compWarnings := len(comp.Warnings)
141+
142+
compDetails := SnapshotComponentDetail{
143+
Name: comp.Name,
144+
ContainerImage: comp.ContainerImage,
145+
Success: comp.Success,
146+
Violations: compViolations,
147+
Warnings: compWarnings,
148+
Successes: len(comp.Successes),
149+
}
150+
components = append(components, compDetails)
151+
152+
// Add to totals
153+
totalViolations += compViolations
154+
totalWarnings += compWarnings
155+
}
156+
157+
// Create summary with component information
158+
summary := SnapshotSummary{
159+
Snapshot: s.Report.Snapshot,
160+
Components: len(s.Report.Components),
161+
Success: s.Report.Success,
162+
Key: s.Report.Key,
163+
EcVersion: s.Report.EcVersion,
164+
ComponentDetails: components,
165+
Violations: totalViolations,
166+
Warnings: totalWarnings,
167+
}
168+
169+
return &SnapshotPredicate{
170+
Policy: s.Report.Policy, // This contains the resolved policy with pinned URLs
171+
ImageRefs: imageRefs,
172+
Timestamp: time.Now().UTC().Format(time.RFC3339),
173+
Status: status,
174+
Verifier: "conforma",
175+
Summary: summary,
176+
}, nil
177+
}
178+
179+
// getAllImageRefs returns all image references from all components including expansion info
180+
func (s *SnapshotPredicateGenerator) getAllImageRefs() []string {
181+
var allImageRefs []string
182+
183+
for _, comp := range s.Report.Components {
184+
// Add the main component image reference
185+
allImageRefs = append(allImageRefs, comp.ContainerImage)
186+
187+
// If we have expansion info, add the index and all architecture-specific images
188+
if s.Report.Expansion != nil {
189+
// Get the normalized index reference
190+
normalizedRef := normalizeIndexRef(comp.ContainerImage, s.Report.Expansion)
191+
192+
// Add the index reference if it's different from the component image
193+
if normalizedRef != comp.ContainerImage {
194+
allImageRefs = append(allImageRefs, normalizedRef)
195+
}
196+
197+
// Get all child images for this index
198+
if children, ok := s.Report.Expansion.GetChildrenByIndex(normalizedRef); ok {
199+
allImageRefs = append(allImageRefs, children...)
200+
}
201+
}
202+
}
203+
204+
// Remove duplicates and return
205+
return removeDuplicateStrings(allImageRefs)
206+
}
207+
208+
// normalizeIndexRef normalizes an image reference to its pinned digest form if it's an index
209+
func normalizeIndexRef(ref string, exp *ExpansionInfo) string {
210+
if exp == nil {
211+
return ref
212+
}
213+
if pinned, ok := exp.GetIndexAlias(ref); ok {
214+
return pinned
215+
}
216+
return ref
217+
}
218+
219+
// removeDuplicateStrings removes duplicate strings from a slice
220+
func removeDuplicateStrings(slice []string) []string {
221+
if len(slice) == 0 {
222+
return []string{}
223+
}
224+
225+
keys := make(map[string]bool)
226+
var result []string
227+
228+
for _, item := range slice {
229+
if !keys[item] {
230+
keys[item] = true
231+
result = append(result, item)
232+
}
233+
}
88234

89-
return s.Report, nil
235+
return result
90236
}

0 commit comments

Comments
 (0)