diff --git a/CLAUDE.md b/CLAUDE.md index 59bddaeba..292b66887 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -279,6 +279,7 @@ See [AI_POLICY.md](AI_POLICY.md) for the full AI contribution policy. - do not add co-author signoff lines — use `Assisted-by:` trailers for AI disclosure instead - when the schema is changed, run make generate, do not create a migration explicitly - If you are writing go code, adhere to best practices such as the ones in effective-go, or others. This could include, error handling patterns, interface design, package organization, concurrency patterns, etc. +- avoid unmarshalling JSON/YAML into empty interfaces (`interface{}`/`any`) for data access — prefer typed structs, which give type safety and don't let unexpected fields pass through silently. The one accepted exception is feeding a JSON-Schema validator (e.g. `internal/schemavalidators`), which consumes generically-decoded JSON; decode into `any` only for that call and use a typed struct for everything else. - When writing tests, use table-driven tests whenever possible - When implementing new functionality, follow TDD: write failing tests first, then implement the code to make them pass - do not change previous migrations, they are immutable diff --git a/app/cli/cmd/attestation_add.go b/app/cli/cmd/attestation_add.go index 8507ee568..a94859c1c 100644 --- a/app/cli/cmd/attestation_add.go +++ b/app/cli/cmd/attestation_add.go @@ -145,7 +145,7 @@ func newAttestationAddCmd() *cobra.Command { cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value") flagAttestationID(cmd) cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material to be recorded: %q", schemaapi.ListAvailableMaterialKind())) - cmd.Flags().BoolVar(&noStrictValidation, "no-strict-validation", false, "skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC)") + cmd.Flags().BoolVar(&noStrictValidation, "no-strict-validation", false, "skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC, OSSF_SCORECARD_JSON)") // Optional OCI registry credentials cmd.Flags().StringVar(®istryServer, "registry-server", "", fmt.Sprintf("OCI repository server, ($%s)", registryServerEnvVarName)) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 142f1866b..441e80313 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -252,9 +252,9 @@ 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" "CERTCC_DRANZER" "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" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] +--kind string kind of the material to be recorded: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CERTCC_DRANZER" "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" "OSSF_SCORECARD_JSON" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "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) +--no-strict-validation skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC, OSSF_SCORECARD_JSON) --registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) --registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER) --registry-username string registry username, ($CHAINLOOP_REGISTRY_USERNAME) @@ -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" "CERTCC_DRANZER" "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" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] +--kind string Kind of the material: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CERTCC_DRANZER" "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" "OSSF_SCORECARD_JSON" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "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 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 f26309801..a651fd836 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -289,6 +289,11 @@ export enum CraftingSchema_Material_MaterialType { SYSINTERNALS_ACCESSCHK = 35, /** CERTCC_DRANZER - CERT/CC dranzer ActiveX/COM control test report (plain text) https://github.com/CERTCC/dranzer */ CERTCC_DRANZER = 36, + /** + * OSSF_SCORECARD_JSON - OpenSSF Scorecard result in JSON format + * https://github.com/ossf/scorecard + */ + OSSF_SCORECARD_JSON = 37, UNRECOGNIZED = -1, } @@ -405,6 +410,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 36: case "CERTCC_DRANZER": return CraftingSchema_Material_MaterialType.CERTCC_DRANZER; + case 37: + case "OSSF_SCORECARD_JSON": + return CraftingSchema_Material_MaterialType.OSSF_SCORECARD_JSON; case -1: case "UNRECOGNIZED": default: @@ -488,6 +496,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "SYSINTERNALS_ACCESSCHK"; case CraftingSchema_Material_MaterialType.CERTCC_DRANZER: return "CERTCC_DRANZER"; + case CraftingSchema_Material_MaterialType.OSSF_SCORECARD_JSON: + return "OSSF_SCORECARD_JSON"; 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 34185f496..028785b49 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 @@ -53,7 +53,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "title": "Material Type", "type": "string" @@ -142,7 +143,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 bfec35e2e..6438474d1 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 @@ -53,7 +53,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "title": "Material Type", "type": "string" @@ -142,7 +143,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 93df40ba7..0601c9bfb 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -150,7 +150,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 af30dca76..d300fb6fe 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -150,7 +150,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 13d2a82d9..0c1211b28 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 @@ -70,7 +70,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 6a1842417..aac3c6b33 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 @@ -70,7 +70,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 e7d706a97..8099ae814 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 @@ -58,7 +58,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 e51535c93..2546bef37 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 @@ -58,7 +58,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 e39aa01c8..f0646d2e7 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -72,7 +72,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 5e3cee67c..f93331e63 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -72,7 +72,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 64968a9ca..f6e63ebcb 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -93,7 +93,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 8cd7ffd0f..4b0b040cb 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -93,7 +93,8 @@ "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", "SYSINTERNALS_ACCESSCHK", - "CERTCC_DRANZER" + "CERTCC_DRANZER", + "OSSF_SCORECARD_JSON" ], "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 2e0ba2a58..5ec49ba85 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -231,6 +231,9 @@ const ( CraftingSchema_Material_SYSINTERNALS_ACCESSCHK CraftingSchema_Material_MaterialType = 35 // CERT/CC dranzer ActiveX/COM control test report (plain text) https://github.com/CERTCC/dranzer CraftingSchema_Material_CERTCC_DRANZER CraftingSchema_Material_MaterialType = 36 + // OpenSSF Scorecard result in JSON format + // https://github.com/ossf/scorecard + CraftingSchema_Material_OSSF_SCORECARD_JSON CraftingSchema_Material_MaterialType = 37 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -273,6 +276,7 @@ var ( 34: "SYSINTERNALS_SIGCHECK", 35: "SYSINTERNALS_ACCESSCHK", 36: "CERTCC_DRANZER", + 37: "OSSF_SCORECARD_JSON", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -312,6 +316,7 @@ var ( "SYSINTERNALS_SIGCHECK": 34, "SYSINTERNALS_ACCESSCHK": 35, "CERTCC_DRANZER": 36, + "OSSF_SCORECARD_JSON": 37, } ) @@ -1973,7 +1978,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\"\xa5\x10\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xbe\x10\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1996,7 +2001,8 @@ 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\xf0\t\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\x89\n" + + "\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" + @@ -2005,7 +2011,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\"\xc2\x06\n" + + "skipUpload\"\xdb\x06\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2047,7 +2053,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x1cYELP_DETECT_SECRETS_BASELINE\x10!\x12\x19\n" + "\x15SYSINTERNALS_SIGCHECK\x10\"\x12\x1a\n" + "\x16SYSINTERNALS_ACCESSCHK\x10#\x12\x12\n" + - "\x0eCERTCC_DRANZER\x10$:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x0eCERTCC_DRANZER\x10$\x12\x17\n" + + "\x13OSSF_SCORECARD_JSON\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 1a04f5005..65bba3ec9 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -172,6 +172,9 @@ message CraftingSchema { SYSINTERNALS_ACCESSCHK = 35; // CERT/CC dranzer ActiveX/COM control test report (plain text) https://github.com/CERTCC/dranzer CERTCC_DRANZER = 36; + // OpenSSF Scorecard result in JSON format + // https://github.com/ossf/scorecard + OSSF_SCORECARD_JSON = 37; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index 18114520a..6a5531475 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -42,6 +42,7 @@ var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ CraftingSchema_Material_GITLEAKS_JSON, CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE, CraftingSchema_Material_SYSINTERNALS_SIGCHECK, + CraftingSchema_Material_OSSF_SCORECARD_JSON, CraftingSchema_Material_OPENAPI_SPEC, CraftingSchema_Material_ASYNCAPI_SPEC, CraftingSchema_Material_GRAPHQL_SPEC, diff --git a/extras/dagger/main.go b/extras/dagger/main.go index 565234fd1..8a579105e 100644 --- a/extras/dagger/main.go +++ b/extras/dagger/main.go @@ -436,7 +436,7 @@ func (att *Attestation) AddRawEvidence( // "key1=value1,key2=value2" // +optional annotations []string, - // Skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC) + // Skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC, OSSF_SCORECARD_JSON) // +optional noStrictValidation bool, // Enable debug logging @@ -500,7 +500,7 @@ func (att *Attestation) AddFileEvidence( // "key1=value1,key2=value2" // +optional annotations []string, - // Skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC) + // Skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC, OSSF_SCORECARD_JSON) // +optional noStrictValidation bool, // Enable debug logging diff --git a/internal/schemavalidators/external_schemas/scorecard/scorecard-v2.schema.json b/internal/schemavalidators/external_schemas/scorecard/scorecard-v2.schema.json new file mode 100644 index 000000000..5733eed09 --- /dev/null +++ b/internal/schemavalidators/external_schemas/scorecard/scorecard-v2.schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://chainloop.dev/schemas/scorecard/v2.json", + "title": "OpenSSF Scorecard result (V2 JSON)", + "description": "Schema for the JSON output produced by `scorecard --format json`. Derived from https://github.com/ossf/scorecard/blob/main/pkg/scorecard/json.v2.schema with details/metadata relaxed to allow null, matching real-world output.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "details": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "documentation": { + "type": "object", + "properties": { + "short": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "url", + "short" + ] + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "score": { + "type": "integer" + } + }, + "required": [ + "score", + "reason", + "name", + "documentation" + ] + } + }, + "date": { + "type": "string" + }, + "metadata": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "repo": { + "type": "object", + "properties": { + "commit": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "score": { + "type": "number" + }, + "scorecard": { + "type": "object", + "properties": { + "commit": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "version", + "commit" + ] + } + }, + "required": [ + "date", + "repo", + "scorecard", + "score", + "checks" + ] +} diff --git a/internal/schemavalidators/schemavalidators.go b/internal/schemavalidators/schemavalidators.go index 072a7308c..3fd170c26 100644 --- a/internal/schemavalidators/schemavalidators.go +++ b/internal/schemavalidators/schemavalidators.go @@ -54,6 +54,8 @@ type AIAgentConfigVersion string // AICodingSessionVersion represents the version of AI Coding Session schema. type AICodingSessionVersion string +type ScorecardVersion string + const ( // RunnerContextVersion0_1 represents Runner Context version 0.1 schema. RunnerContextVersion0_1 RunnerContextVersion = "0.1" @@ -77,6 +79,8 @@ const ( AIAgentConfigVersion0_1 AIAgentConfigVersion = "0.1" // AICodingSessionVersion0_1 represents AI Coding Session version 0.1 schema. AICodingSessionVersion0_1 AICodingSessionVersion = "0.1" + // ScorecardVersionV2 represents the OpenSSF Scorecard V2 JSON result schema. + ScorecardVersionV2 ScorecardVersion = "v2" // OpenAPIVersion2_0 represents Swagger/OpenAPI version 2.0 schema. OpenAPIVersion2_0 OpenAPIVersion = "2.0" // OpenAPIVersion3_0 represents OpenAPI version 3.0 schema. @@ -155,6 +159,10 @@ var ( asyncapiSpecVersion2_6 string //go:embed external_schemas/asyncapi/asyncapi-3.0.0.schema.json asyncapiSpecVersion3_0 string + + // OpenSSF Scorecard schemas + //go:embed external_schemas/scorecard/scorecard-v2.schema.json + scorecardSpecVersionV2 string ) var ( @@ -174,6 +182,8 @@ var ( openapiOnce sync.Once compiledAsyncAPISchemas map[AsyncAPIVersion]*jsonschema.Schema asyncapiOnce sync.Once + compiledScorecardSchemas map[ScorecardVersion]*jsonschema.Schema + scorecardOnce sync.Once ) func initCycloneDxSchemas() { @@ -449,6 +459,41 @@ func ValidateAICodingSession(data any, version AICodingSessionVersion) error { return nil } +func initScorecardSchemas() { + compiler := jsonschema.NewCompiler() + if err := compiler.AddResource("https://chainloop.dev/schemas/scorecard/v2.json", strings.NewReader(scorecardSpecVersionV2)); err != nil { + panic(fmt.Sprintf("schemavalidators: failed to add resource %s: %v", "https://chainloop.dev/schemas/scorecard/v2.json", err)) + } + + compiledScorecardSchemas = map[ScorecardVersion]*jsonschema.Schema{ + ScorecardVersionV2: compiler.MustCompile("https://chainloop.dev/schemas/scorecard/v2.json"), + } +} + +// ValidateOSSFScorecard validates the given object against the OpenSSF Scorecard V2 JSON schema. +func ValidateOSSFScorecard(data any, version ScorecardVersion) error { + scorecardOnce.Do(initScorecardSchemas) + + if version == "" { + version = ScorecardVersionV2 + } + + schema, ok := compiledScorecardSchemas[version] + if !ok { + return errors.New("invalid OpenSSF Scorecard schema version") + } + + if err := schema.Validate(data); err != nil { + var invalidJSONTypeError jsonschema.InvalidJSONTypeError + if errors.As(err, &invalidJSONTypeError) { + return ErrInvalidJSONPayload + } + return err + } + + return nil +} + func initOpenAPISchemas() { compiler := jsonschema.NewCompiler() if err := compiler.AddResource("http://json-schema.org/draft-04/schema", strings.NewReader(jsonSchemaDraft04)); err != nil { diff --git a/internal/schemavalidators/schemavalidators_test.go b/internal/schemavalidators/schemavalidators_test.go index 503a5472f..23b4cf222 100644 --- a/internal/schemavalidators/schemavalidators_test.go +++ b/internal/schemavalidators/schemavalidators_test.go @@ -469,3 +469,39 @@ func TestValidateAIAgentConfig(t *testing.T) { }) } } + +func TestValidateOSSFScorecard(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + }{ + { + name: "valid scorecard report", + filePath: "./testdata/scorecard_valid.json", + }, + { + name: "completely wrong format", + filePath: "./testdata/sbom-spdx.json", + wantErr: "missing properties", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + f, err := os.ReadFile(tc.filePath) + require.NoError(t, err) + + var v any + require.NoError(t, json.Unmarshal(f, &v)) + + err = schemavalidators.ValidateOSSFScorecard(v, "") + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + }) + } +} diff --git a/internal/schemavalidators/testdata/scorecard_valid.json b/internal/schemavalidators/testdata/scorecard_valid.json new file mode 100644 index 000000000..d6f8230aa --- /dev/null +++ b/internal/schemavalidators/testdata/scorecard_valid.json @@ -0,0 +1,45 @@ +{ + "date": "2026-06-15T14:27:17+02:00", + "repo": { + "name": "github.com/chainloop-dev/chainloop", + "commit": "892e78cfeb1af1600a818b537f84beb3692ea5ba" + }, + "scorecard": { + "version": "v5.5.0", + "commit": "c395761df6afe1a69e476bc60a013a94bcbc153f" + }, + "score": 8.2, + "checks": [ + { + "details": null, + "score": 10, + "reason": "no binaries found in the repo", + "name": "Binary-Artifacts", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts", + "short": "Determines if the project has generated executable (binary) artifacts in the source repository." + } + }, + { + "details": null, + "score": 10, + "reason": "license file detected", + "name": "License", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license", + "short": "Determines if the project has defined a license." + } + }, + { + "details": null, + "score": 0, + "reason": "22 existing vulnerabilities detected", + "name": "Vulnerabilities", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities", + "short": "Determines if the project has open, known unfixed vulnerabilities." + } + } + ], + "metadata": null +} diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index 6011360b8..b47864cee 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -305,6 +305,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewAccessChkCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CERTCC_DRANZER: crafter, err = NewDranzerCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_OSSF_SCORECARD_JSON: + crafter, err = NewOSSFScorecardCrafter(materialSchema, casBackend, logger, WithOSSFScorecardNoStrictValidation(opts.NoStrictValidation)) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) } diff --git a/pkg/attestation/crafter/materials/scorecard.go b/pkg/attestation/crafter/materials/scorecard.go new file mode 100644 index 000000000..d784f4f38 --- /dev/null +++ b/pkg/attestation/crafter/materials/scorecard.go @@ -0,0 +1,150 @@ +// +// 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" + "strconv" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/schemavalidators" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +type OSSFScorecardCrafter struct { + backend *casclient.CASBackend + noStrictValidation bool + *crafterCommon +} + +// scorecardReport is the typed subset of the OpenSSF Scorecard V2 JSON output +// (https://github.com/ossf/scorecard) that this crafter needs for material-type +// detection and annotations. Deep structural validation is delegated to the +// embedded JSON Schema via schemavalidators.ValidateOSSFScorecard. +type scorecardReport struct { + Date string `json:"date"` + // Score is a pointer so an absent aggregate score is distinguishable from a + // legitimate 0.0, avoiding misrepresenting a nonconformant report as score 0. + Score *float64 `json:"score"` + Repo struct { + Name string `json:"name"` + Commit string `json:"commit"` + } `json:"repo"` + Scorecard struct { + Version string `json:"version"` + Commit string `json:"commit"` + } `json:"scorecard"` + Checks []struct { + Name string `json:"name"` + Score int `json:"score"` + } `json:"checks"` +} + +type OSSFScorecardCraftOpt func(*OSSFScorecardCrafter) + +func WithOSSFScorecardNoStrictValidation(noStrict bool) OSSFScorecardCraftOpt { + return func(c *OSSFScorecardCrafter) { + c.noStrictValidation = noStrict + } +} + +func NewOSSFScorecardCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger, opts ...OSSFScorecardCraftOpt) (*OSSFScorecardCrafter, error) { + if materialSchema.Type != schemaapi.CraftingSchema_Material_OSSF_SCORECARD_JSON { + return nil, fmt.Errorf("material type is not OSSF_SCORECARD_JSON") + } + + c := &OSSFScorecardCrafter{ + backend: backend, + crafterCommon: &crafterCommon{logger: l, input: materialSchema}, + } + + for _, opt := range opts { + opt(c) + } + + return c, nil +} + +func (i *OSSFScorecardCrafter) Craft(ctx context.Context, filepath string) (*api.Attestation_Material, error) { + i.logger.Debug().Str("path", filepath).Msg("decoding OpenSSF Scorecard report") + + data, err := os.ReadFile(filepath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + + var report scorecardReport + if err := json.Unmarshal(data, &report); err != nil { + i.logger.Debug().Err(err).Msg("error decoding file") + return nil, fmt.Errorf("invalid OpenSSF Scorecard report file: %w", ErrInvalidMaterialType) + } + + // Discriminating field: a Scorecard report always carries the tool stamp + // under "scorecard.version". This guard is enforced even when strict schema + // validation is disabled, so arbitrary JSON is never misclassified as a + // Scorecard report. + if report.Scorecard.Version == "" { + return nil, fmt.Errorf("invalid OpenSSF Scorecard report file: %w", ErrInvalidMaterialType) + } + + // Deep structural validation against the embedded JSON Schema. The shared + // validator consumes generically-decoded JSON, so decode into `any` solely + // to feed it (the typed report above is used for everything else). + var generic any + if err := json.Unmarshal(data, &generic); err != nil { + return nil, fmt.Errorf("invalid OpenSSF Scorecard report file: %w", ErrInvalidMaterialType) + } + + if err := schemavalidators.ValidateOSSFScorecard(generic, schemavalidators.ScorecardVersionV2); err != nil { + if i.noStrictValidation { + i.logger.Warn().Err(err).Msg("error validating OpenSSF Scorecard report, strict validation disabled, continuing") + } else { + i.logger.Debug().Err(err).Msg("error validating OpenSSF Scorecard report") + i.logger.Info().Msg("you can disable strict validation to skip schema validation") + return nil, fmt.Errorf("invalid OpenSSF Scorecard report file: %w", ErrInvalidMaterialType) + } + } + + m, err := uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, &report) + + return m, nil +} + +func (i *OSSFScorecardCrafter) injectAnnotations(m *api.Attestation_Material, report *scorecardReport) { + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } + + m.Annotations[AnnotationToolNameKey] = "scorecard" + + if report.Scorecard.Version != "" { + m.Annotations[AnnotationToolVersionKey] = report.Scorecard.Version + } + + if report.Score != nil { + m.Annotations["chainloop.material.scorecard.score"] = strconv.FormatFloat(*report.Score, 'f', -1, 64) + } +} diff --git a/pkg/attestation/crafter/materials/scorecard_test.go b/pkg/attestation/crafter/materials/scorecard_test.go new file mode 100644 index 000000000..c161027d1 --- /dev/null +++ b/pkg/attestation/crafter/materials/scorecard_test.go @@ -0,0 +1,207 @@ +// +// 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 TestNewOSSFScorecardCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_OSSF_SCORECARD_JSON, + }, + }, + { + 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.NewOSSFScorecardCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestOSSFScorecardCrafter_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 OpenSSF Scorecard report file", + }, + { + name: "wrong content", + filePath: "./testdata/scorecard-invalid.json", + wantErr: "invalid OpenSSF Scorecard report file", + }, + { + name: "non-scorecard json (sbom)", + filePath: "./testdata/sbom-spdx.json", + wantErr: "invalid OpenSSF Scorecard report file", + }, + { + name: "valid report with high score", + filePath: "./testdata/scorecard-chainloop.json", + annotations: map[string]string{ + "chainloop.material.tool.name": "scorecard", + "chainloop.material.tool.version": "v5.5.0", + "chainloop.material.scorecard.score": "8.2", + }, + }, + { + name: "valid report with low score and inconclusive checks", + filePath: "./testdata/scorecard-low.json", + annotations: map[string]string{ + "chainloop.material.tool.name": "scorecard", + "chainloop.material.tool.version": "v5.5.0", + "chainloop.material.scorecard.score": "2.9", + }, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_OSSF_SCORECARD_JSON, + } + + 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.NewOSSFScorecardCrafter(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_OSSF_SCORECARD_JSON.String(), got.MaterialType.String()) + assert.True(t, got.UploadedToCas) + + for k, v := range tc.annotations { + assert.Equal(t, v, got.Annotations[k]) + } + }) + } +} + +// TestOSSFScorecardCrafter_Craft_NoStrictValidation ensures that even with +// strict schema validation disabled, the discriminating-field guard still +// rejects arbitrary JSON, so non-Scorecard files are not misclassified. +func TestOSSFScorecardCrafter_Craft_NoStrictValidation(t *testing.T) { + testCases := []struct { + name string + // noScoreAnnotation asserts the score annotation is absent (report had no score). + filePath string + wantErr string + noScoreAnnotation bool + }{ + { + name: "non-scorecard json still rejected", + filePath: "./testdata/sbom-spdx.json", + wantErr: "invalid OpenSSF Scorecard report file", + }, + { + name: "valid scorecard accepted", + filePath: "./testdata/scorecard-chainloop.json", + }, + { + name: "report without score is not annotated as score 0", + filePath: "./testdata/scorecard-no-score.json", + noScoreAnnotation: true, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_OSSF_SCORECARD_JSON, + } + + l := zerolog.Nop() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + 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.NewOSSFScorecardCrafter(schema, backend, &l, materials.WithOSSFScorecardNoStrictValidation(true)) + 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) + if tc.noScoreAnnotation { + _, ok := got.Annotations["chainloop.material.scorecard.score"] + assert.False(t, ok, "score annotation should be absent when report has no score") + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/testdata/scorecard-chainloop.json b/pkg/attestation/crafter/materials/testdata/scorecard-chainloop.json new file mode 100644 index 000000000..d6f8230aa --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/scorecard-chainloop.json @@ -0,0 +1,45 @@ +{ + "date": "2026-06-15T14:27:17+02:00", + "repo": { + "name": "github.com/chainloop-dev/chainloop", + "commit": "892e78cfeb1af1600a818b537f84beb3692ea5ba" + }, + "scorecard": { + "version": "v5.5.0", + "commit": "c395761df6afe1a69e476bc60a013a94bcbc153f" + }, + "score": 8.2, + "checks": [ + { + "details": null, + "score": 10, + "reason": "no binaries found in the repo", + "name": "Binary-Artifacts", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts", + "short": "Determines if the project has generated executable (binary) artifacts in the source repository." + } + }, + { + "details": null, + "score": 10, + "reason": "license file detected", + "name": "License", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license", + "short": "Determines if the project has defined a license." + } + }, + { + "details": null, + "score": 0, + "reason": "22 existing vulnerabilities detected", + "name": "Vulnerabilities", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities", + "short": "Determines if the project has open, known unfixed vulnerabilities." + } + } + ], + "metadata": null +} diff --git a/pkg/attestation/crafter/materials/testdata/scorecard-invalid.json b/pkg/attestation/crafter/materials/testdata/scorecard-invalid.json new file mode 100644 index 000000000..eff9d1638 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/scorecard-invalid.json @@ -0,0 +1 @@ +{"this": "is", "not": "a scorecard report"} diff --git a/pkg/attestation/crafter/materials/testdata/scorecard-low.json b/pkg/attestation/crafter/materials/testdata/scorecard-low.json new file mode 100644 index 000000000..b4650a957 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/scorecard-low.json @@ -0,0 +1,25 @@ +{ + "date": "2026-06-15T14:24:56+02:00", + "repo": { + "name": "github.com/ossf-tests/scorecard-check-branch-protection-e2e", + "commit": "12ae42962014ee9aeb01d991ee2cd799ad6de659" + }, + "scorecard": { + "version": "v5.5.0", + "commit": "c395761df6afe1a69e476bc60a013a94bcbc153f" + }, + "score": 2.9, + "checks": [ + { + "details": null, + "score": -1, + "reason": "internal error: error during branchesHandler.setup", + "name": "Branch-Protection", + "documentation": { + "url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection", + "short": "Determines if the default and release branches are protected with GitHub's branch protection settings." + } + } + ], + "metadata": null +} diff --git a/pkg/attestation/crafter/materials/testdata/scorecard-no-score.json b/pkg/attestation/crafter/materials/testdata/scorecard-no-score.json new file mode 100644 index 000000000..81f7047b7 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/scorecard-no-score.json @@ -0,0 +1,10 @@ +{ + "date": "2026-06-15T14:27:17+02:00", + "repo": { "name": "github.com/chainloop-dev/chainloop", "commit": "892e78c" }, + "scorecard": { "version": "v5.5.0", "commit": "c395761" }, + "checks": [ + { "details": null, "score": 10, "reason": "license file detected", "name": "License", + "documentation": { "url": "https://example.com", "short": "x" } } + ], + "metadata": null +}