Skip to content

Commit 2c1b24b

Browse files
waveywavesclaude
andcommitted
fix(crafter): surface DNS-1123 validation errors instead of masking them
The material auto-discovery loop in AddMaterialContactFreeWithAutoDetectedKind silently swallowed DNS-1123 validation errors because: 1. Variable shadowing: `m, err :=` inside the for loop created a new `err` that never propagated to the outer scope, so the final error message was always nil when all kinds failed. 2. No early validation: invalid material names (uppercase, underscores, dots) were passed through the entire kind-probing loop before failing deep in proto validation, with the real cause buried. 3. Proto-validation errors not detected: the loop treated schema-level validation failures the same as kind mismatches, continuing to probe instead of stopping. Fixes: - Replace `:=` with `=` to avoid shadowing the outer `err` - Add DNS-1123 regex check at the top of AddMaterialContractFree - Detect protovalidate.ValidationError in the auto-discovery loop and break immediately instead of masking the real error Fixes #2394 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Vibhav Bobade <vibhav.bobde@gmail.com>
1 parent 7542585 commit 2c1b24b

File tree

2 files changed

+64
-7
lines changed

2 files changed

+64
-7
lines changed

pkg/attestation/crafter/crafter.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net/url"
2323
"os"
2424
"reflect"
25+
"regexp"
2526
"slices"
2627
"strings"
2728
"time"
@@ -87,6 +88,9 @@ type VersionedCraftingState struct {
8788

8889
var ErrAttestationStateNotLoaded = errors.New("crafting state not loaded")
8990

91+
// dns1123Regex matches valid DNS-1123 label names (same rule as the proto CEL constraint).
92+
var dns1123Regex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
93+
9094
type NewOpt func(c *Crafter) error
9195

9296
func WithAuthRawToken(token string) NewOpt {
@@ -578,6 +582,13 @@ func (c *Crafter) AddMaterialContractFree(ctx context.Context, attestationID, ki
578582
m.Name = fmt.Sprintf("material-%d", time.Now().UnixNano())
579583
}
580584

585+
// 2.2 - Validate the material name is DNS-1123 compliant before proceeding.
586+
// This catches invalid names early instead of letting them be masked by the
587+
// auto-discovery loop which swallows validation errors while probing kinds.
588+
if !dns1123Regex.MatchString(m.Name) {
589+
return nil, fmt.Errorf("invalid material name %q: must be DNS-1123 compliant (lowercase alphanumeric and hyphens only)", m.Name)
590+
}
591+
581592
// 3 - Craft resulting material
582593
return c.addMaterial(ctx, &m, attestationID, value, casBackend, runtimeAnnotations)
583594
}
@@ -628,9 +639,12 @@ func (c *Crafter) IsMaterialInContract(key string) bool {
628639
// AddMaterialContactFreeWithAutoDetectedKind adds a material to the crafting state checking the incoming material matches any of the
629640
// supported types in validation order. If the material is not found it will return an error.
630641
func (c *Crafter) AddMaterialContactFreeWithAutoDetectedKind(ctx context.Context, attestationID, name, value string, casBackend *casclient.CASBackend, runtimeAnnotations map[string]string) (*api.Attestation_Material, error) {
631-
var err error
642+
var (
643+
err error
644+
m *api.Attestation_Material
645+
)
632646
for _, kind := range schemaapi.CraftingMaterialInValidationOrder {
633-
m, err := c.AddMaterialContractFree(ctx, attestationID, kind.String(), name, value, casBackend, runtimeAnnotations)
647+
m, err = c.AddMaterialContractFree(ctx, attestationID, kind.String(), name, value, casBackend, runtimeAnnotations)
634648
if err == nil {
635649
// Successfully added material, return the kind
636650
return m, nil
@@ -639,7 +653,6 @@ func (c *Crafter) AddMaterialContactFreeWithAutoDetectedKind(ctx context.Context
639653
c.Logger.Debug().Err(err).Str("kind", kind.String()).Msg("failed to add material")
640654

641655
// Handle base error for upload and craft errors except the opening file error
642-
// TODO: have an error to detect validation error instead
643656
var policyError *policies.PolicyError
644657
if errors.Is(err, materials.ErrBaseUploadAndCraft) || errors.As(err, &policyError) {
645658
return nil, err
@@ -649,6 +662,14 @@ func (c *Crafter) AddMaterialContactFreeWithAutoDetectedKind(ctx context.Context
649662
if v1.IsAttestationStateErrorConflict(err) {
650663
return nil, err
651664
}
665+
666+
// Proto-validation errors (e.g. invalid material name) are schema-level
667+
// failures, not kind mismatches. Stop probing immediately so the real
668+
// error is surfaced to the user instead of being masked by the loop.
669+
var valErr *protovalidate.ValidationError
670+
if errors.As(err, &valErr) {
671+
return nil, err
672+
}
652673
}
653674

654675
// Return an error if no material could be added

pkg/attestation/crafter/crafter_test.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
539539
expectedType schemaapi.CraftingSchema_Material_MaterialType
540540
uploadArtifact bool
541541
wantErr bool
542+
wantErrMsg string
542543
}{
543544
{
544545
name: "sarif",
@@ -593,6 +594,36 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
593594
wantErr: true,
594595
uploadArtifact: true,
595596
},
597+
{
598+
name: "non-dns-1123 name surfaces clear error",
599+
materialName: "My_Invalid.Name",
600+
materialPath: "./materials/testdata/report.sarif",
601+
wantErr: true,
602+
wantErrMsg: "must be DNS-1123 compliant",
603+
uploadArtifact: true, // no uploader interaction expected
604+
},
605+
{
606+
name: "uppercase name surfaces dns-1123 error",
607+
materialName: "MyMaterial",
608+
materialPath: "random-string",
609+
wantErr: true,
610+
wantErrMsg: "must be DNS-1123 compliant",
611+
uploadArtifact: true,
612+
},
613+
{
614+
// This test exercises the auto-discovery loop with a valid DNS-1123 name
615+
// but content (empty string) that every material kind rejects during Craft.
616+
// File-based kinds fail because empty string is not a file path. The STRING kind
617+
// crafts successfully but fails proto validation (KeyVal.Value min_len=1).
618+
// This verifies the shadowing fix (= instead of :=) works: the error from
619+
// the loop is properly propagated instead of being nil.
620+
name: "valid dns name with empty value surfaces crafting error",
621+
materialName: "my-material",
622+
materialPath: "",
623+
wantErr: true,
624+
wantErrMsg: "validation error",
625+
uploadArtifact: true,
626+
},
596627
}
597628

598629
for _, tc := range testCases {
@@ -613,7 +644,7 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
613644

614645
// Establishing a maximum size for the artifact to be uploaded to the CAS causes an error
615646
// if the value is exceeded
616-
if tc.wantErr {
647+
if tc.wantErr && tc.wantErrMsg == "" {
617648
backend.MaxSize = 1
618649
}
619650

@@ -622,10 +653,15 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
622653

623654
m, err := c.AddMaterialContactFreeWithAutoDetectedKind(context.Background(), "random-id", tc.materialName, tc.materialPath, backend, nil)
624655
if tc.wantErr {
625-
assert.ErrorIs(s.T(), err, materials.ErrBaseUploadAndCraft)
626-
} else {
627-
require.NoError(s.T(), err)
656+
require.Error(s.T(), err)
657+
if tc.wantErrMsg != "" {
658+
assert.Contains(s.T(), err.Error(), tc.wantErrMsg)
659+
} else {
660+
assert.ErrorIs(s.T(), err, materials.ErrBaseUploadAndCraft)
661+
}
662+
return
628663
}
664+
require.NoError(s.T(), err)
629665
kind := m.GetMaterialType()
630666
assert.Equal(s.T(), tc.expectedType.String(), kind.String())
631667
if tc.materialName != "" {

0 commit comments

Comments
 (0)