Skip to content

Commit 9241f71

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 9241f71

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

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: 26 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,22 @@ 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+
},
596613
}
597614

598615
for _, tc := range testCases {
@@ -613,7 +630,7 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
613630

614631
// Establishing a maximum size for the artifact to be uploaded to the CAS causes an error
615632
// if the value is exceeded
616-
if tc.wantErr {
633+
if tc.wantErr && tc.wantErrMsg == "" {
617634
backend.MaxSize = 1
618635
}
619636

@@ -622,10 +639,15 @@ func (s *crafterSuite) TestAddMaterialsAutomatic() {
622639

623640
m, err := c.AddMaterialContactFreeWithAutoDetectedKind(context.Background(), "random-id", tc.materialName, tc.materialPath, backend, nil)
624641
if tc.wantErr {
625-
assert.ErrorIs(s.T(), err, materials.ErrBaseUploadAndCraft)
626-
} else {
627-
require.NoError(s.T(), err)
642+
require.Error(s.T(), err)
643+
if tc.wantErrMsg != "" {
644+
assert.Contains(s.T(), err.Error(), tc.wantErrMsg)
645+
} else {
646+
assert.ErrorIs(s.T(), err, materials.ErrBaseUploadAndCraft)
647+
}
648+
return
628649
}
650+
require.NoError(s.T(), err)
629651
kind := m.GetMaterialType()
630652
assert.Equal(s.T(), tc.expectedType.String(), kind.String())
631653
if tc.materialName != "" {

0 commit comments

Comments
 (0)