From dbbd503c4a00f29a6816eb06ad6ad9d9d702ff9e Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 10 Jun 2026 13:18:04 +0200 Subject: [PATCH 1/2] feat: add Yelp detect-secrets baseline as a material type Adds the YELP_DETECT_SECRETS_BASELINE material type so detect-secrets baseline files can be attested as a first-class evidence type. The crafter validates the baseline structure (version, plugins_used and results fields) and records the tool name and version as material annotations, which are available to the policy engine at evaluation time. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: 9b2ede11-4681-46dc-bf3c-e7b9a8bc7e3f --- .../workflowcontract/v1/crafting_schema.ts | 7 + ...on.v1.Attestation.Material.jsonschema.json | 6 +- ...tation.v1.Attestation.Material.schema.json | 6 +- ...tation.v1.PolicyEvaluation.jsonschema.json | 3 +- ...ttestation.v1.PolicyEvaluation.schema.json | 3 +- ...v1.CraftingSchema.Material.jsonschema.json | 3 +- ...act.v1.CraftingSchema.Material.schema.json | 3 +- ...ct.v1.PolicyGroup.Material.jsonschema.json | 3 +- ...ntract.v1.PolicyGroup.Material.schema.json | 3 +- ...flowcontract.v1.PolicySpec.jsonschema.json | 3 +- ...workflowcontract.v1.PolicySpec.schema.json | 3 +- ...owcontract.v1.PolicySpecV2.jsonschema.json | 3 +- ...rkflowcontract.v1.PolicySpecV2.schema.json | 3 +- .../workflowcontract/v1/crafting_schema.pb.go | 13 +- .../workflowcontract/v1/crafting_schema.proto | 2 + .../v1/crafting_schema_validations.go | 1 + .../crafter/materials/detect_secrets.go | 92 ++++++++++++ .../crafter/materials/detect_secrets_test.go | 142 ++++++++++++++++++ .../crafter/materials/materials.go | 2 + .../detect-secrets-baseline-clean.json | 25 +++ .../detect-secrets-baseline-violations.json | 38 +++++ 21 files changed, 346 insertions(+), 18 deletions(-) create mode 100644 pkg/attestation/crafter/materials/detect_secrets.go create mode 100644 pkg/attestation/crafter/materials/detect_secrets_test.go create mode 100644 pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-clean.json create mode 100644 pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-violations.json 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 e6ed9bf7a..acd4fb016 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -278,6 +278,8 @@ export enum CraftingSchema_Material_MaterialType { * https://spec.graphql.org/ */ GRAPHQL_SPEC = 32, + /** YELP_DETECT_SECRETS_BASELINE - detect-secrets baseline file https://github.com/Yelp/detect-secrets */ + YELP_DETECT_SECRETS_BASELINE = 33, UNRECOGNIZED = -1, } @@ -382,6 +384,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 32: case "GRAPHQL_SPEC": return CraftingSchema_Material_MaterialType.GRAPHQL_SPEC; + case 33: + case "YELP_DETECT_SECRETS_BASELINE": + return CraftingSchema_Material_MaterialType.YELP_DETECT_SECRETS_BASELINE; case -1: case "UNRECOGNIZED": default: @@ -457,6 +462,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "ASYNCAPI_SPEC"; case CraftingSchema_Material_MaterialType.GRAPHQL_SPEC: return "GRAPHQL_SPEC"; + case CraftingSchema_Material_MaterialType.YELP_DETECT_SECRETS_BASELINE: + return "YELP_DETECT_SECRETS_BASELINE"; case CraftingSchema_Material_MaterialType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json index 5e2c60dcf..790f48b85 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json @@ -49,7 +49,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" @@ -134,7 +135,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json index cb62b431c..b27e430b3 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json @@ -49,7 +49,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" @@ -134,7 +135,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json index 20f240b2b..66af05b87 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -146,7 +146,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json index 095c0618e..268e51b0d 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -146,7 +146,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json index f11c5b08b..abc686ded 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json @@ -66,7 +66,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json index 52ebae460..ee3171258 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json @@ -66,7 +66,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json index c1202f3a6..dccfb83bd 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json @@ -54,7 +54,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json index 35747f08a..6d44ce5f0 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json @@ -54,7 +54,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json index 18f0b7540..8c6ef6e45 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -68,7 +68,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json index 59e485e55..05726bcf4 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -68,7 +68,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json index 086c87fc9..32384c902 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -89,7 +89,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json index 280b223f0..53871fbf0 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -89,7 +89,8 @@ "CHAINLOOP_AI_CODING_SESSION", "OPENAPI_SPEC", "ASYNCAPI_SPEC", - "GRAPHQL_SPEC" + "GRAPHQL_SPEC", + "YELP_DETECT_SECRETS_BASELINE" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index b5b457566..66fcaa196 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -222,6 +222,8 @@ const ( // GraphQL SDL schema // https://spec.graphql.org/ CraftingSchema_Material_GRAPHQL_SPEC CraftingSchema_Material_MaterialType = 32 + // detect-secrets baseline file https://github.com/Yelp/detect-secrets + CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE CraftingSchema_Material_MaterialType = 33 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -260,6 +262,7 @@ var ( 30: "OPENAPI_SPEC", 31: "ASYNCAPI_SPEC", 32: "GRAPHQL_SPEC", + 33: "YELP_DETECT_SECRETS_BASELINE", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -295,6 +298,7 @@ var ( "OPENAPI_SPEC": 30, "ASYNCAPI_SPEC": 31, "GRAPHQL_SPEC": 32, + "YELP_DETECT_SECRETS_BASELINE": 33, } ) @@ -1956,7 +1960,7 @@ var File_workflowcontract_v1_crafting_schema_proto protoreflect.FileDescriptor const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\n" + - ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xb8\x0f\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xda\x0f\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1979,7 +1983,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x0fDAGGER_PIPELINE\x10\x06\x12\x15\n" + "\x11TEAMCITY_PIPELINE\x10\a\x12\x13\n" + "\x0fTEKTON_PIPELINE\x10\b\x12\x15\n" + - "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\x83\t\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xa5\t\n" + "\bMaterial\x12[\n" + "\x04type\x18\x01 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\x12\x99\x01\n" + "\x04name\x18\x02 \x01(\tB\x84\x01\xbaH\x7f\xba\x01|\n" + @@ -1988,7 +1992,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x06output\x18\x04 \x01(\bB\x02\x18\x01R\x06output\x12E\n" + "\vannotations\x18\x05 \x03(\v2\x1f.workflowcontract.v1.AnnotationB\x02\x18\x01R\vannotations\x12\x1f\n" + "\vskip_upload\x18\x06 \x01(\bR\n" + - "skipUpload\"\xd5\x05\n" + + "skipUpload\"\xf7\x05\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2026,7 +2030,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x1bCHAINLOOP_AI_CODING_SESSION\x10\x1d\x12\x10\n" + "\fOPENAPI_SPEC\x10\x1e\x12\x11\n" + "\rASYNCAPI_SPEC\x10\x1f\x12\x10\n" + - "\fGRAPHQL_SPEC\x10 :\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\fGRAPHQL_SPEC\x10 \x12 \n" + + "\x1cYELP_DETECT_SECRETS_BASELINE\x10!:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + "\x10CraftingSchemaV2\x128\n" + "\vapi_version\x18\x01 \x01(\tB\x17\xbaH\x14r\x12\n" + "\x10chainloop.dev/v1R\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 180395658..6360aa067 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -163,6 +163,8 @@ message CraftingSchema { // GraphQL SDL schema // https://spec.graphql.org/ GRAPHQL_SPEC = 32; + // detect-secrets baseline file https://github.com/Yelp/detect-secrets + YELP_DETECT_SECRETS_BASELINE = 33; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index 7bafe8bca..bd4b02656 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -40,6 +40,7 @@ var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ CraftingSchema_Material_CSAF_SECURITY_INCIDENT_RESPONSE, CraftingSchema_Material_GITLAB_SECURITY_REPORT, CraftingSchema_Material_GITLEAKS_JSON, + CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE, CraftingSchema_Material_OPENAPI_SPEC, CraftingSchema_Material_ASYNCAPI_SPEC, CraftingSchema_Material_GRAPHQL_SPEC, diff --git a/pkg/attestation/crafter/materials/detect_secrets.go b/pkg/attestation/crafter/materials/detect_secrets.go new file mode 100644 index 000000000..9fa3e958b --- /dev/null +++ b/pkg/attestation/crafter/materials/detect_secrets.go @@ -0,0 +1,92 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "context" + "encoding/json" + "fmt" + "os" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +type DetectSecretsCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +// detectSecretsBaseline represents the subset of the detect-secrets baseline +// file used to validate its structure and extract metadata. +// https://github.com/Yelp/detect-secrets +type detectSecretsBaseline struct { + Version string `json:"version"` + PluginsUsed []map[string]any `json:"plugins_used"` + Results map[string][]map[string]any `json:"results"` +} + +func NewDetectSecretsCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*DetectSecretsCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE { + return nil, fmt.Errorf("material type is not a detect-secrets baseline") + } + craftCommon := &crafterCommon{logger: l, input: schema} + return &DetectSecretsCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +func (i *DetectSecretsCrafter) Craft(ctx context.Context, filePath string) (*api.Attestation_Material, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + + var baseline detectSecretsBaseline + if err = json.Unmarshal(data, &baseline); err != nil { + return nil, fmt.Errorf("invalid detect-secrets baseline file: %w", ErrInvalidMaterialType) + } + + // Structural fingerprint check. A detect-secrets baseline always carries a + // version, the list of plugins it ran, and a results map (which may be empty + // when no secrets were found). Reject anything missing these fields. + if baseline.Version == "" || baseline.PluginsUsed == nil || baseline.Results == nil { + return nil, fmt.Errorf("missing required detect-secrets baseline fields: %w", ErrInvalidMaterialType) + } + + // An empty results map means a clean scan. It's ambiguous, but we accept it. + if len(baseline.Results) == 0 { + i.logger.Debug().Msg("Accepting an empty report.") + } + + // Call uploadAndCraft with the path of the JSON baseline file + m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, &baseline) + + return m, nil +} + +func (i *DetectSecretsCrafter) injectAnnotations(m *api.Attestation_Material, baseline *detectSecretsBaseline) { + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } + m.Annotations[AnnotationToolNameKey] = "detect-secrets" + m.Annotations[AnnotationToolVersionKey] = baseline.Version +} diff --git a/pkg/attestation/crafter/materials/detect_secrets_test.go b/pkg/attestation/crafter/materials/detect_secrets_test.go new file mode 100644 index 000000000..39668f8cf --- /dev/null +++ b/pkg/attestation/crafter/materials/detect_secrets_test.go @@ -0,0 +1,142 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials_test + +import ( + "context" + "testing" + + contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials" + "github.com/chainloop-dev/chainloop/pkg/casclient" + mUploader "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewDetectSecretsCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE, + }, + }, + { + name: "wrong type", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := materials.NewDetectSecretsCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDetectSecretsCrafter_Craft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + annotations map[string]string + }{ + { + name: "invalid path", + filePath: "./testdata/non-existing.json", + wantErr: "no such file or directory", + }, + { + name: "empty file", + filePath: "./testdata/empty.txt", + wantErr: "invalid detect-secrets baseline file", + }, + { + name: "wrong content", + filePath: "./testdata/sbom-spdx.json", + wantErr: "missing required detect-secrets baseline fields", + }, + { + name: "clean baseline (no secrets)", + filePath: "./testdata/detect-secrets-baseline-clean.json", + annotations: map[string]string{ + "chainloop.material.tool.name": "detect-secrets", + "chainloop.material.tool.version": "1.5.0", + }, + }, + { + name: "baseline with violations", + filePath: "./testdata/detect-secrets-baseline-violations.json", + annotations: map[string]string{ + "chainloop.material.tool.name": "detect-secrets", + "chainloop.material.tool.version": "1.5.0", + }, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE, + } + + l := zerolog.Nop() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Mock uploader + uploader := mUploader.NewUploader(t) + if tc.wantErr == "" { + uploader.On("Upload", context.TODO(), mock.Anything, mock.Anything, mock.Anything). + Return(&casclient.UpDownStatus{}, nil) + } + + backend := &casclient.CASBackend{Uploader: uploader} + crafter, err := materials.NewDetectSecretsCrafter(schema, backend, &l) + require.NoError(t, err) + + got, err := crafter.Craft(context.TODO(), tc.filePath) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, contractAPI.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE.String(), got.MaterialType.String()) + assert.True(t, got.UploadedToCas) + + if tc.annotations != nil { + for k, v := range tc.annotations { + assert.Equal(t, v, got.Annotations[k]) + } + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index fe8c864d7..eb605c677 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -287,6 +287,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewChainloopPRInfoCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_GITLEAKS_JSON: crafter, err = NewGitleaksReportCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE: + crafter, err = NewDetectSecretsCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_AGENT_CONFIG: crafter, err = NewChainloopAIAgentConfigCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION: diff --git a/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-clean.json b/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-clean.json new file mode 100644 index 000000000..7f0ff1658 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-clean.json @@ -0,0 +1,25 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + } + ], + "results": {}, + "generated_at": "2024-01-01T00:00:00Z" +} diff --git a/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-violations.json b/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-violations.json new file mode 100644 index 000000000..f1fa6373f --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/detect-secrets-baseline-violations.json @@ -0,0 +1,38 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "AWSKeyDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + } + ], + "results": { + "config/settings.yml": [ + { + "type": "Secret Keyword", + "filename": "config/settings.yml", + "hashed_secret": "d4e0e04792fec3eccc4a2b659e25b5e1d8d6f53e", + "is_verified": false, + "line_number": 10 + } + ], + "src/aws.py": [ + { + "type": "AWS Access Key", + "filename": "src/aws.py", + "hashed_secret": "2ace62c1befa19e3ea37dd52be9f6d508c5163e6", + "is_verified": false, + "line_number": 42 + } + ] + }, + "generated_at": "2024-01-01T00:00:00Z" +} From c4ccf5e7dbea77d60d84ffa74233a5c878cd8b47 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 10 Jun 2026 13:29:27 +0200 Subject: [PATCH 2/2] docs: regenerate CLI reference for new material kind Assisted-by: Claude Code Signed-off-by: Javier Rodriguez --- app/cli/documentation/cli-reference.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index ea41b92a2..93205f939 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -252,7 +252,7 @@ Options --annotation strings additional annotation in the format of key=value --attestation-id string Unique identifier of the in-progress attestation -h, --help help for add ---kind string kind of the material to be recorded: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] +--kind string kind of the material to be recorded: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] --name string name of the material as shown in the contract --no-strict-validation skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC) --registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) @@ -3025,7 +3025,7 @@ Options --annotation strings Key-value pairs of material annotations (key=value) -h, --help help for eval --input stringArray Key-value pairs of policy inputs (key=value) ---kind string Kind of the material: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] +--kind string Kind of the material: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] --material string Path to material or attestation file -p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml") --project string Project name to use as engine context for chainloop.* built-ins