From d0503f7aaec6010b3d40c9f2327472c289469576 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 15 Jun 2026 12:18:07 +0200 Subject: [PATCH] feat: add CERTCC dranzer output as a material type Add support for CERT/CC dranzer ActiveX/COM control test reports as a first-class material type (CERTCC_DRANZER). The raw plain-text report is attested verbatim while a structured JSON projection (run summary, per-object metadata and error/access-violation findings) is generated on the fly for the policy engine, following the SYSINTERNALS_ACCESSCHK and SYSINTERNALS_SIGCHECK pattern. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: f09b8b6c-a7ce-43df-b75b-6e4e3687bdd6 --- app/cli/documentation/cli-reference.mdx | 4 +- .../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 + .../api/attestation/v1/crafting_state.go | 95 +++--- .../api/attestation/v1/crafting_state_test.go | 14 + pkg/attestation/crafter/materials/dranzer.go | 84 +++++ .../crafter/materials/dranzer/dranzer.go | 316 ++++++++++++++++++ .../crafter/materials/dranzer/dranzer_test.go | 157 +++++++++ .../dranzer/testdata/dranzer-crash.txt | 27 ++ .../dranzer/testdata/dranzer-report.txt | 35 ++ .../dranzer/testdata/dranzer-summary.txt | 11 + .../materials/dranzer/testdata/garbage.txt | 4 + .../crafter/materials/dranzer_test.go | 120 +++++++ .../crafter/materials/materials.go | 2 + .../materials/testdata/dranzer-report.txt | 35 ++ 28 files changed, 906 insertions(+), 62 deletions(-) create mode 100644 pkg/attestation/crafter/materials/dranzer.go create mode 100644 pkg/attestation/crafter/materials/dranzer/dranzer.go create mode 100644 pkg/attestation/crafter/materials/dranzer/dranzer_test.go create mode 100644 pkg/attestation/crafter/materials/dranzer/testdata/dranzer-crash.txt create mode 100644 pkg/attestation/crafter/materials/dranzer/testdata/dranzer-report.txt create mode 100644 pkg/attestation/crafter/materials/dranzer/testdata/dranzer-summary.txt create mode 100644 pkg/attestation/crafter/materials/dranzer/testdata/garbage.txt create mode 100644 pkg/attestation/crafter/materials/dranzer_test.go create mode 100644 pkg/attestation/crafter/materials/testdata/dranzer-report.txt diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 4198c0f84..142f1866b 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" "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" "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) --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" "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" "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 e31cac21c..f26309801 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -287,6 +287,8 @@ export enum CraftingSchema_Material_MaterialType { SYSINTERNALS_SIGCHECK = 34, /** SYSINTERNALS_ACCESSCHK - Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk */ SYSINTERNALS_ACCESSCHK = 35, + /** CERTCC_DRANZER - CERT/CC dranzer ActiveX/COM control test report (plain text) https://github.com/CERTCC/dranzer */ + CERTCC_DRANZER = 36, UNRECOGNIZED = -1, } @@ -400,6 +402,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 35: case "SYSINTERNALS_ACCESSCHK": return CraftingSchema_Material_MaterialType.SYSINTERNALS_ACCESSCHK; + case 36: + case "CERTCC_DRANZER": + return CraftingSchema_Material_MaterialType.CERTCC_DRANZER; case -1: case "UNRECOGNIZED": default: @@ -481,6 +486,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "SYSINTERNALS_SIGCHECK"; case CraftingSchema_Material_MaterialType.SYSINTERNALS_ACCESSCHK: return "SYSINTERNALS_ACCESSCHK"; + case CraftingSchema_Material_MaterialType.CERTCC_DRANZER: + return "CERTCC_DRANZER"; 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 8b4bb3404..34185f496 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 @@ -52,7 +52,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "title": "Material Type", "type": "string" @@ -140,7 +141,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 d3afb88d5..bfec35e2e 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 @@ -52,7 +52,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "title": "Material Type", "type": "string" @@ -140,7 +141,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 c34c506d9..93df40ba7 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -149,7 +149,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 5b530f697..af30dca76 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -149,7 +149,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 91dcb1255..13d2a82d9 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 @@ -69,7 +69,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 bd08a002a..6a1842417 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 @@ -69,7 +69,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 5274ebca5..e7d706a97 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 @@ -57,7 +57,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 1ed300a2b..e51535c93 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 @@ -57,7 +57,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 81fe80e71..e39aa01c8 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -71,7 +71,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 b3452c1d5..5e3cee67c 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -71,7 +71,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 69960c6e3..64968a9ca 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -92,7 +92,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 aa98bd142..8cd7ffd0f 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -92,7 +92,8 @@ "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", "SYSINTERNALS_SIGCHECK", - "SYSINTERNALS_ACCESSCHK" + "SYSINTERNALS_ACCESSCHK", + "CERTCC_DRANZER" ], "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 c52129985..2e0ba2a58 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -229,6 +229,8 @@ const ( CraftingSchema_Material_SYSINTERNALS_SIGCHECK CraftingSchema_Material_MaterialType = 34 // Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk 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 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -270,6 +272,7 @@ var ( 33: "YELP_DETECT_SECRETS_BASELINE", 34: "SYSINTERNALS_SIGCHECK", 35: "SYSINTERNALS_ACCESSCHK", + 36: "CERTCC_DRANZER", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -308,6 +311,7 @@ var ( "YELP_DETECT_SECRETS_BASELINE": 33, "SYSINTERNALS_SIGCHECK": 34, "SYSINTERNALS_ACCESSCHK": 35, + "CERTCC_DRANZER": 36, } ) @@ -1969,7 +1973,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\"\x91\x10\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xa5\x10\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1992,7 +1996,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\xdc\t\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xf0\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" + @@ -2001,7 +2005,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\"\xae\x06\n" + + "skipUpload\"\xc2\x06\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2042,7 +2046,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\fGRAPHQL_SPEC\x10 \x12 \n" + "\x1cYELP_DETECT_SECRETS_BASELINE\x10!\x12\x19\n" + "\x15SYSINTERNALS_SIGCHECK\x10\"\x12\x1a\n" + - "\x16SYSINTERNALS_ACCESSCHK\x10#:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x16SYSINTERNALS_ACCESSCHK\x10#\x12\x12\n" + + "\x0eCERTCC_DRANZER\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 efbc50c80..1a04f5005 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -170,6 +170,8 @@ message CraftingSchema { SYSINTERNALS_SIGCHECK = 34; // Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk SYSINTERNALS_ACCESSCHK = 35; + // CERT/CC dranzer ActiveX/COM control test report (plain text) https://github.com/CERTCC/dranzer + CERTCC_DRANZER = 36; } } } diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index fe4a9a281..0a8b52196 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go @@ -27,6 +27,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/accesschk" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/attestation" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/dranzer" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/jacoco" materialsjunit "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/junit" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/sigcheck" @@ -124,48 +125,11 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) } } - // For XML based materials, we need to ingest them and read as json-like structure - switch m.MaterialType { - case v1.CraftingSchema_Material_JUNIT_XML: - suites, err := materialsjunit.Ingest(value) - if err != nil { - return nil, fmt.Errorf("failed to ingest junit xml: %w", err) - } - // this will render a json array - rawMaterial, err = json.Marshal(suites) - if err != nil { - return nil, fmt.Errorf("failed to marshal junit xml: %w", err) - } - case v1.CraftingSchema_Material_JACOCO_XML: - var report jacoco.Report - if err := xml.Unmarshal(rawMaterial, &report); err != nil { - return nil, fmt.Errorf("invalid Jacoco report file: %w", err) - } - rawMaterial, err = json.Marshal(&report) - if err != nil { - return nil, fmt.Errorf("failed to marshal to json Jacoco report file: %w", err) - } - case v1.CraftingSchema_Material_SYSINTERNALS_SIGCHECK: - report, ierr := sigcheck.Parse(rawMaterial) - if ierr != nil { - return nil, fmt.Errorf("failed to ingest sigcheck report: %w", ierr) - } - rawMaterial, err = report.JSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal sigcheck report: %w", err) - } - case v1.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK: - // AccessChk emits plain text; project it to JSON so the policy engine, - // which only consumes JSON, can evaluate it. The raw text is preserved - // in the projection's "raw" field for string-matching fallbacks. - report, perr := accesschk.Parse(rawMaterial) - if perr != nil { - return nil, fmt.Errorf("invalid accesschk material: %w", perr) - } - rawMaterial, err = json.Marshal(report) - if err != nil { - return nil, fmt.Errorf("failed to marshal accesschk material: %w", err) - } + // Non-JSON materials (XML- or text-based formats) are projected to the JSON + // the policy engine consumes; JSON-native materials pass through unchanged. + rawMaterial, err = m.ingestMaterialToJSON(rawMaterial, value) + if err != nil { + return nil, err } // if raw material is empty (container images, for example), let's create an empty json @@ -206,6 +170,53 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) return result, nil } +// ingestMaterialToJSON projects materials that are not natively JSON (XML- or +// text-based formats) into the JSON structure the policy engine consumes. +// Materials that are already JSON are returned unchanged. +func (m *Attestation_Material) ingestMaterialToJSON(rawMaterial []byte, value string) ([]byte, error) { + switch m.MaterialType { + case v1.CraftingSchema_Material_JUNIT_XML: + suites, err := materialsjunit.Ingest(value) + if err != nil { + return nil, fmt.Errorf("failed to ingest junit xml: %w", err) + } + // this will render a json array + return json.Marshal(suites) + case v1.CraftingSchema_Material_JACOCO_XML: + var report jacoco.Report + if err := xml.Unmarshal(rawMaterial, &report); err != nil { + return nil, fmt.Errorf("invalid Jacoco report file: %w", err) + } + return json.Marshal(&report) + case v1.CraftingSchema_Material_SYSINTERNALS_SIGCHECK: + report, err := sigcheck.Parse(rawMaterial) + if err != nil { + return nil, fmt.Errorf("failed to ingest sigcheck report: %w", err) + } + return report.JSON() + case v1.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK: + // AccessChk emits plain text; project it to JSON so the policy engine, + // which only consumes JSON, can evaluate it. The raw text is preserved + // in the projection's "raw" field for string-matching fallbacks. + report, err := accesschk.Parse(rawMaterial) + if err != nil { + return nil, fmt.Errorf("invalid accesschk material: %w", err) + } + return json.Marshal(report) + case v1.CraftingSchema_Material_CERTCC_DRANZER: + // dranzer emits plain text; project it to JSON so the policy engine, + // which only consumes JSON, can evaluate it. The raw text is preserved + // in the projection's "raw" field for string-matching fallbacks. + report, err := dranzer.Parse(rawMaterial) + if err != nil { + return nil, fmt.Errorf("invalid dranzer material: %w", err) + } + return json.Marshal(report) + } + + return rawMaterial, nil +} + // CraftingStateToIntotoDescriptor creates an intoto descriptor from a material in crafting state func (m *Attestation_Material) CraftingStateToIntotoDescriptor(name string) (*intoto.ResourceDescriptor, error) { material := &intoto.ResourceDescriptor{} diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go index 537feef97..69bf1cbd7 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go @@ -226,6 +226,20 @@ func TestGetEvaluableContentWithMetadata(t *testing.T) { }, testField: "objects", }, + { + name: "dranzer text material projected to json", + material: &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_CERTCC_DRANZER, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "name", Digest: "sha256:deadbeef", IsSubject: true, + Content: []byte("Testing COM Object - {11111111-2222-3333-4444-555555555555} Example.WidgetControl\nCOM Object Filename : example.ocx\n"), + }, + }, + InlineCas: true, + }, + testField: "objects", + }, } for _, tc := range cases { diff --git a/pkg/attestation/crafter/materials/dranzer.go b/pkg/attestation/crafter/materials/dranzer.go new file mode 100644 index 000000000..ad4c48999 --- /dev/null +++ b/pkg/attestation/crafter/materials/dranzer.go @@ -0,0 +1,84 @@ +// +// 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" + "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/attestation/crafter/materials/dranzer" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +// DranzerCrafter stores the text report of the CERT/CC dranzer ActiveX/COM +// control tester as supply-chain evidence. The raw text is stored as-is; the +// text-to-JSON projection used by the policy engine happens later, at +// evaluation time. +type DranzerCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +func NewDranzerCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*DranzerCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_CERTCC_DRANZER { + return nil, fmt.Errorf("material type is not a dranzer report") + } + craftCommon := &crafterCommon{logger: l, input: schema} + return &DranzerCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +func (i *DranzerCrafter) 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) + } + + // Soft fingerprint: dranzer emits free-form text, so we only require that + // the input is valid text that resembles dranzer output (a test-object + // banner or the test-engine version line). The raw text is stored unchanged; + // it is projected to JSON later for policy evaluation. + report, err := dranzer.Parse(data) + if err != nil { + return nil, fmt.Errorf("invalid dranzer output: %w", ErrInvalidMaterialType) + } + + if !report.LooksLikeDranzer() { + return nil, fmt.Errorf("input does not look like dranzer output: %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 *DranzerCrafter) injectAnnotations(m *api.Attestation_Material, report *dranzer.Report) { + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } + m.Annotations[AnnotationToolNameKey] = report.Tool.Name + if report.Tool.Version != "" { + m.Annotations[AnnotationToolVersionKey] = report.Tool.Version + } +} diff --git a/pkg/attestation/crafter/materials/dranzer/dranzer.go b/pkg/attestation/crafter/materials/dranzer/dranzer.go new file mode 100644 index 000000000..cca4dca79 --- /dev/null +++ b/pkg/attestation/crafter/materials/dranzer/dranzer.go @@ -0,0 +1,316 @@ +// +// 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 dranzer parses the plain-text report produced by the CERT/CC dranzer +// tool (https://github.com/CERTCC/dranzer), which fuzz-tests ActiveX/COM +// controls. Dranzer has no machine-readable output mode and its format is +// undocumented, so the parser is intentionally tolerant: it extracts the +// structure it recognizes (the run summary, per-object metadata and error +// findings) and always preserves the full original text in Raw so a policy can +// fall back to string matching. +// +// Real dranzer reports are emitted in the system's ANSI code page rather than +// UTF-8, so the parser sanitizes invalid byte sequences instead of rejecting +// them. +package dranzer + +import ( + "encoding/json" + "regexp" + "strings" +) + +// ToolName is the canonical tool name recorded for dranzer materials. +const ToolName = "dranzer" + +var ( + // objectRe matches a per-object banner such as + // "Testing COM Object - {GUID} ". The CLSID may or may not be + // wrapped in braces; the optional trailing text is the control description. + objectRe = regexp.MustCompile(`^Testing COM Object - (\{[^}]+\}|[0-9A-Fa-f-]+)\s*(.*)$`) + // failureHeaderRe matches the "{GUID}-" line that introduces a + // failed-object block in the report header, ahead of its "ERROR - ..." line. + // dranzer emits "%ws-%s", so the description may be empty ("{GUID}-"). + failureHeaderRe = regexp.MustCompile(`^(\{[^}]+\})-(.*)$`) + // errorRe matches an error line "ERROR - (0x)". + errorRe = regexp.MustCompile(`^ERROR - (.*?)\s*\((0x[0-9A-Fa-f]+)\)\s*$`) + // methodRe matches the "Invoking|Invoked - Interface::Method" lines, + // where is a space-padded token (Method / Property Get / Property Put + // / Property Put Reference). The captured signature gives context to the + // finding emitted by the access-violation/exception handler that follows. + methodRe = regexp.MustCompile(`^Invok(?:ing|ed) (?:Method|Property Get|Property Put Reference|Property Put)\s+-\s+(.+)$`) + // avRe matches the access-violation detail line + // "Access violation at 0x :Bad on 0x". + avRe = regexp.MustCompile(`^(.+?) at (0x[0-9A-Fa-f]+) :Bad (read|write) on (0x[0-9A-Fa-f]+)$`) + // win32Re matches the Win32-exception detail line + // " (code ) at 0x". + win32Re = regexp.MustCompile(`^(.+?) \(code ([0-9A-Fa-f]+)\) at (0x[0-9A-Fa-f]+)$`) + // metadataRe matches a "Key : value" / "Key : value" metadata line; dranzer + // pads the key with a variable number of spaces before the colon. + metadataRe = regexp.MustCompile(`^([A-Za-z][A-Za-z .]+?)\s*:\s*(.*)$`) + // counterRe matches a summary counter "Number of