diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index 337dc2c52..6e924df09 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -623,7 +623,7 @@ export interface CraftingSchemaV2Spec { } export interface Annotation { - /** Single word optionally separated with _ */ + /** Single word optionally separated with _ or - */ name: string; /** This value can be set in the contract or provided during the attestation */ value: string; diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.jsonschema.json index 0eb5d803c..be78f8b67 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.jsonschema.json @@ -4,7 +4,7 @@ "additionalProperties": false, "properties": { "name": { - "pattern": "^[\\w]+$", + "pattern": "^[\\w-]+$", "type": "string" }, "value": { diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.schema.json index 38f209e2b..1fbbf459d 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Annotation.schema.json @@ -4,7 +4,7 @@ "additionalProperties": false, "properties": { "name": { - "pattern": "^[\\w]+$", + "pattern": "^[\\w-]+$", "type": "string" }, "value": { diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 5b2c4c0ac..4dfea60fb 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -726,7 +726,7 @@ func (x *CraftingSchemaV2Spec) GetAnnotations() []*Annotation { type Annotation struct { state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Single word optionally separated with _ + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Single word optionally separated with _ or - // This value can be set in the contract or provided during the attestation Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields @@ -2209,10 +2209,11 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x06runner\x18\x03 \x01(\v2*.workflowcontract.v1.CraftingSchema.RunnerR\x06runner\x129\n" + "\bpolicies\x18\x04 \x01(\v2\x1d.workflowcontract.v1.PoliciesR\bpolicies\x12O\n" + "\rpolicy_groups\x18\x05 \x03(\v2*.workflowcontract.v1.PolicyGroupAttachmentR\fpolicyGroups\x12A\n" + - "\vannotations\x18\x06 \x03(\v2\x1f.workflowcontract.v1.AnnotationR\vannotations\"F\n" + + "\vannotations\x18\x06 \x03(\v2\x1f.workflowcontract.v1.AnnotationR\vannotations\"G\n" + "\n" + - "Annotation\x12\"\n" + - "\x04name\x18\x01 \x01(\tB\x0e\xbaH\vr\t2\a^[\\w]+$R\x04name\x12\x14\n" + + "Annotation\x12#\n" + + "\x04name\x18\x01 \x01(\tB\x0f\xbaH\fr\n" + + "2\b^[\\w-]+$R\x04name\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\"\x98\x01\n" + "\bPolicies\x12C\n" + "\tmaterials\x18\x01 \x03(\v2%.workflowcontract.v1.PolicyAttachmentR\tmaterials\x12G\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index aedf88077..46f879052 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -241,7 +241,7 @@ message CraftingSchemaV2Spec { } message Annotation { - string name = 1 [(buf.validate.field).string.pattern = "^[\\w]+$"]; // Single word optionally separated with _ + string name = 1 [(buf.validate.field).string.pattern = "^[\\w-]+$"]; // Single word optionally separated with _ or - // This value can be set in the contract or provided during the attestation string value = 2; } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go index d0b3734e2..e1d3809d9 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go @@ -13,18 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -// limitations under the License. - package v1_test import ( "errors" "testing" + validate "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "buf.build/go/protovalidate" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" ) func TestValidateAnnotations(t *testing.T) { @@ -50,10 +51,9 @@ func TestValidateAnnotations(t *testing.T) { value: "hello_world", }, { - desc: "invalid key hyphen", - name: "hello-world", - value: "hello-world", - wantErr: true, + desc: "valid key hyphen", + name: "hello-world", + value: "hello-world", }, { desc: "invalid key space", @@ -61,6 +61,18 @@ func TestValidateAnnotations(t *testing.T) { value: "hello-world", wantErr: true, }, + { + desc: "invalid key punctuation", + name: "hello.world", + value: "hello", + wantErr: true, + }, + { + desc: "invalid key slash", + name: "hello/world", + value: "hello", + wantErr: true, + }, { desc: "valid key camel case", name: "helloWorld", @@ -94,6 +106,19 @@ func TestValidateAnnotations(t *testing.T) { } } +func TestAnnotationNameConstraintAllowsHyphen(t *testing.T) { + field := (&v1.Annotation{}).ProtoReflect().Descriptor().Fields().ByName("name") + require.NotNil(t, field) + + opts, ok := field.Options().(*descriptorpb.FieldOptions) + require.True(t, ok) + + constraints, ok := proto.GetExtension(opts, validate.E_Field).(*validate.FieldRules) + require.True(t, ok) + require.NotNil(t, constraints.GetString()) + assert.Equal(t, `^[\w-]+$`, constraints.GetString().GetPattern()) +} + func TestPolicyAttachment(t *testing.T) { testCases := []struct { desc string diff --git a/app/controlplane/plugins/core/dependency-track/v1/extension_test.go b/app/controlplane/plugins/core/dependency-track/v1/extension_test.go index 94b972cca..16fae6c96 100644 --- a/app/controlplane/plugins/core/dependency-track/v1/extension_test.go +++ b/app/controlplane/plugins/core/dependency-track/v1/extension_test.go @@ -146,11 +146,17 @@ func TestResolveProjectName(t *testing.T) { projectName: "{{.Attestation.Annotations.Version}}", want: "1.2.3", }, + { + name: "hyphenated annotation interpolates via index", + projectName: `{{ index .Material.Annotations "my-annotation" }}`, + want: "hyphen-ok", + }, } sbomAnnotation := map[string]string{ - "hello": "hola", - "world": "mundo", + "hello": "hola", + "world": "mundo", + "my-annotation": "hyphen-ok", } attAnnotation := map[string]string{ @@ -348,8 +354,9 @@ func TestVerifyAllFilters(t *testing.T) { } materialAnnotations := map[string]string{ - "environment": "staging", - "critical": "true", + "environment": "staging", + "critical": "true", + "release-stage": "ga", } testCases := []struct { @@ -398,6 +405,10 @@ func TestVerifyAllFilters(t *testing.T) { filter: "environment", errMsg: "invalid filter segment", }, + { + name: "hyphenated annotation name matches", + filter: "release-stage=ga", + }, } for _, tc := range testCases {