Skip to content

Commit 3eccecf

Browse files
authored
Merge pull request #3004 from joejstuart/image-fallback
Fallback to validate image
2 parents 6fea6d3 + eb5926b commit 3eccecf

File tree

14 files changed

+4686
-454
lines changed

14 files changed

+4686
-454
lines changed

cmd/validate/vsa.go

Lines changed: 796 additions & 125 deletions
Large diffs are not rendered by default.

cmd/validate/vsa_test.go

Lines changed: 781 additions & 11 deletions
Large diffs are not rendered by default.

docs/modules/ROOT/pages/ec_validate_vsa.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Validate VSA by comparing the embedded policy against a supplied policy configur
99
By default, VSA signature verification is enabled and requires a public key.
1010
Use --ignore-signature-verification to disable signature verification.
1111

12+
By default, fallback to image validation is enabled when VSA validation fails.
13+
Use --no-fallback to disable this behavior.
14+
1215
Supports validation of:
1316
- Single VSA by identifier (image digest, file path)
1417
- Multiple VSAs from application snapshot
@@ -26,17 +29,18 @@ ec validate vsa <vsa-identifier> [flags]
2629

2730
--color:: Enable color when using text output even when the current terminal does not support it (Default: false)
2831
--effective-time:: Effective time for comparison (Default: now)
32+
--fallback-public-key:: Public key to use for fallback image validation (different from VSA verification key)
2933
-h, --help:: help for vsa (Default: false)
3034
--ignore-signature-verification:: Ignore VSA signature verification (signature verification is enabled by default) (Default: false)
3135
--images:: Application snapshot file
3236
--no-color:: Disable color when using text output even when the current terminal supports it (Default: false)
33-
--output:: Output formats (Default: [])
34-
-o, --output-file:: Output file
37+
--no-fallback:: Disable fallback to image validation when VSA validation fails (fallback is enabled by default) (Default: false)
38+
--output:: Output formats (e.g., json, yaml, text) (Default: [])
3539
-p, --policy:: Policy configuration
36-
--public-key:: Path to public key for signature verification (required by default)
3740
--strict:: Exit with non-zero code if validation fails (Default: true)
3841
-v, --vsa:: VSA identifier (image digest, file path)
3942
--vsa-expiration:: VSA expiration threshold (e.g., 24h, 7d, 1w, 1m) (Default: 168h)
43+
--vsa-public-key:: Path to public key for VSA signature verification (required by default)
4044
--vsa-retrieval:: VSA retrieval backends (rekor@, file@) (Default: [])
4145
--workers:: Number of worker threads for parallel processing (Default: 5)
4246

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ replace github.com/google/go-containerregistry => github.com/conforma/go-contain
6565
require (
6666
github.com/go-openapi/runtime v0.28.0
6767
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
68+
golang.org/x/text v0.28.0
6869
gopkg.in/yaml.v3 v3.0.1
6970
k8s.io/api v0.32.3
7071
)
@@ -372,7 +373,6 @@ require (
372373
golang.org/x/oauth2 v0.30.0 // indirect
373374
golang.org/x/sys v0.35.0 // indirect
374375
golang.org/x/term v0.34.0 // indirect
375-
golang.org/x/text v0.28.0 // indirect
376376
golang.org/x/time v0.11.0 // indirect
377377
golang.org/x/tools v0.35.0 // indirect
378378
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect

internal/validate/vsa/errors.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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 vsa
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"time"
23+
24+
"golang.org/x/text/cases"
25+
"golang.org/x/text/language"
26+
)
27+
28+
// ValidationError represents a structured validation error with causes
29+
type ValidationError struct {
30+
Message string `json:"message"`
31+
Causes []Cause `json:"causes"`
32+
}
33+
34+
// Cause represents a specific cause of validation failure
35+
type Cause struct {
36+
Type string `json:"type"` // "vsa", "fallback", "network", "policy", etc.
37+
Message string `json:"message"`
38+
Details string `json:"details,omitempty"`
39+
SubCauses []Cause `json:"sub_causes,omitempty"`
40+
Timestamp string `json:"timestamp,omitempty"`
41+
Severity string `json:"severity,omitempty"` // "error", "warning", "info"
42+
}
43+
44+
// ErrorType constants
45+
const (
46+
ErrorTypeVSA = "vsa"
47+
ErrorTypeFallback = "fallback"
48+
ErrorTypeNetwork = "network"
49+
ErrorTypePolicy = "policy"
50+
ErrorTypeSignature = "signature"
51+
ErrorTypeTimeout = "timeout"
52+
ErrorTypeRetrieval = "retrieval"
53+
)
54+
55+
// Severity constants
56+
const (
57+
SeverityError = "error"
58+
SeverityWarning = "warning"
59+
SeverityInfo = "info"
60+
)
61+
62+
// Error implements the error interface
63+
func (ve *ValidationError) Error() string {
64+
if ve == nil {
65+
return ""
66+
}
67+
68+
var parts []string
69+
parts = append(parts, ve.Message)
70+
71+
for _, cause := range ve.Causes {
72+
parts = append(parts, fmt.Sprintf("- %s: %s", cause.Type, cause.Message))
73+
if cause.Details != "" {
74+
parts = append(parts, fmt.Sprintf(" Details: %s", cause.Details))
75+
}
76+
}
77+
78+
return strings.Join(parts, "\n")
79+
}
80+
81+
// HumanReadable returns a formatted human-readable version of the error
82+
func (ve *ValidationError) HumanReadable() string {
83+
if ve == nil {
84+
return ""
85+
}
86+
87+
var builder strings.Builder
88+
builder.WriteString(fmt.Sprintf("❌ %s\n", ve.Message))
89+
90+
for _, cause := range ve.Causes {
91+
builder.WriteString(fmt.Sprintf("\n%s Failure\n", cases.Title(language.English).String(cause.Type)))
92+
builder.WriteString(fmt.Sprintf(" Message: %s\n", cause.Message))
93+
94+
if cause.Details != "" {
95+
builder.WriteString(fmt.Sprintf(" Details: %s\n", cause.Details))
96+
}
97+
98+
if cause.Timestamp != "" {
99+
builder.WriteString(fmt.Sprintf(" Time: %s\n", cause.Timestamp))
100+
}
101+
102+
if len(cause.SubCauses) > 0 {
103+
// Determine label based on severity
104+
label := "Violations"
105+
if len(cause.SubCauses) > 0 && cause.SubCauses[0].Severity == SeverityWarning {
106+
label = "Warnings"
107+
}
108+
builder.WriteString(fmt.Sprintf(" %s:\n", label))
109+
for _, subCause := range cause.SubCauses {
110+
builder.WriteString(fmt.Sprintf(" - %s\n", subCause.Message))
111+
}
112+
}
113+
}
114+
115+
return builder.String()
116+
}
117+
118+
// BuildValidationError creates a structured error from VSA and fallback results
119+
func BuildValidationError(vsaResult *ValidationResult, fallbackResult *ImageValidationResult, vsaErr, fallbackErr error) *ValidationError {
120+
var causes []Cause
121+
122+
// Add VSA failure cause
123+
if vsaErr != nil || (vsaResult != nil && !vsaResult.Passed) {
124+
vsaCause := buildVSACause(vsaResult, vsaErr)
125+
causes = append(causes, vsaCause)
126+
}
127+
128+
// Add fallback failure cause
129+
if fallbackErr != nil || (fallbackResult != nil && !fallbackResult.Passed) {
130+
fallbackCause := buildFallbackCause(fallbackResult, fallbackErr)
131+
causes = append(causes, fallbackCause)
132+
}
133+
134+
if len(causes) == 0 {
135+
return nil
136+
}
137+
138+
message := "Validation failed"
139+
if len(causes) > 1 {
140+
message = "Both VSA and fallback validation failed"
141+
} else if len(causes) == 1 {
142+
message = fmt.Sprintf("%s validation failed", cases.Title(language.English).String(causes[0].Type))
143+
}
144+
145+
return &ValidationError{
146+
Message: message,
147+
Causes: causes,
148+
}
149+
}
150+
151+
// buildVSACause creates a cause for VSA validation failure
152+
func buildVSACause(vsaResult *ValidationResult, vsaErr error) Cause {
153+
cause := Cause{
154+
Type: ErrorTypeVSA,
155+
Message: "VSA validation failed",
156+
Timestamp: time.Now().Format(time.RFC3339),
157+
Severity: SeverityError,
158+
}
159+
160+
if vsaErr != nil {
161+
cause.Details = vsaErr.Error()
162+
// Try to categorize the error
163+
if strings.Contains(vsaErr.Error(), "signature") {
164+
cause.SubCauses = append(cause.SubCauses, Cause{
165+
Type: ErrorTypeSignature,
166+
Message: "Signature verification failed",
167+
Details: vsaErr.Error(),
168+
})
169+
} else if strings.Contains(vsaErr.Error(), "timeout") {
170+
cause.SubCauses = append(cause.SubCauses, Cause{
171+
Type: ErrorTypeTimeout,
172+
Message: "VSA retrieval timeout",
173+
Details: vsaErr.Error(),
174+
})
175+
} else if strings.Contains(vsaErr.Error(), "network") || strings.Contains(vsaErr.Error(), "connection") {
176+
cause.SubCauses = append(cause.SubCauses, Cause{
177+
Type: ErrorTypeNetwork,
178+
Message: "Network error during VSA retrieval",
179+
Details: vsaErr.Error(),
180+
})
181+
}
182+
} else if vsaResult != nil {
183+
cause.Details = vsaResult.Message
184+
if vsaResult.PredicateOutcome != "" && vsaResult.PredicateOutcome != "passed" {
185+
cause.SubCauses = append(cause.SubCauses, Cause{
186+
Type: ErrorTypePolicy,
187+
Message: fmt.Sprintf("Predicate status: %s", vsaResult.PredicateOutcome),
188+
Details: vsaResult.Message,
189+
})
190+
}
191+
}
192+
193+
return cause
194+
}
195+
196+
// buildFallbackCause creates a cause for fallback validation failure
197+
func buildFallbackCause(fallbackResult *ImageValidationResult, fallbackErr error) Cause {
198+
cause := Cause{
199+
Type: ErrorTypeFallback,
200+
Message: "Fallback validation failed",
201+
Timestamp: time.Now().Format(time.RFC3339),
202+
Severity: SeverityError,
203+
}
204+
205+
if fallbackErr != nil {
206+
cause.Details = fallbackErr.Error()
207+
} else if fallbackResult != nil {
208+
// Extract details from violations
209+
if len(fallbackResult.Violations) > 0 {
210+
cause.Details = fmt.Sprintf("%d policy violations found", len(fallbackResult.Violations))
211+
212+
// Add sub-causes for each violation
213+
for _, violation := range fallbackResult.Violations {
214+
subCause := Cause{
215+
Type: ErrorTypePolicy,
216+
Message: violation.Message,
217+
Details: "Policy violation",
218+
Severity: SeverityError,
219+
}
220+
cause.SubCauses = append(cause.SubCauses, subCause)
221+
}
222+
}
223+
224+
// Add warnings as sub-causes with lower severity
225+
for _, warning := range fallbackResult.Warnings {
226+
subCause := Cause{
227+
Type: ErrorTypePolicy,
228+
Message: warning.Message,
229+
Details: "Policy warning",
230+
Severity: SeverityWarning,
231+
}
232+
cause.SubCauses = append(cause.SubCauses, subCause)
233+
}
234+
}
235+
236+
return cause
237+
}
238+
239+
// BuildNetworkError creates a structured error for network-related failures
240+
func BuildNetworkError(operation string, err error) *ValidationError {
241+
return &ValidationError{
242+
Message: fmt.Sprintf("Network operation failed: %s", operation),
243+
Causes: []Cause{
244+
{
245+
Type: ErrorTypeNetwork,
246+
Message: fmt.Sprintf("Failed to %s", operation),
247+
Details: err.Error(),
248+
Timestamp: time.Now().Format(time.RFC3339),
249+
Severity: SeverityError,
250+
},
251+
},
252+
}
253+
}
254+
255+
// BuildTimeoutError creates a structured error for timeout failures
256+
func BuildTimeoutError(operation string, timeout time.Duration) *ValidationError {
257+
return &ValidationError{
258+
Message: fmt.Sprintf("Operation timed out: %s", operation),
259+
Causes: []Cause{
260+
{
261+
Type: ErrorTypeTimeout,
262+
Message: fmt.Sprintf("Timeout after %v", timeout),
263+
Details: fmt.Sprintf("Operation '%s' exceeded timeout of %v", operation, timeout),
264+
Timestamp: time.Now().Format(time.RFC3339),
265+
Severity: SeverityError,
266+
},
267+
},
268+
}
269+
}

0 commit comments

Comments
 (0)