From 02424f6486dee8ddd12f1d4fb682eb22879b6794 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 10 Jun 2026 17:54:32 +0200 Subject: [PATCH] feat: add Sysinternals sigcheck output as a material type Adds a SYSINTERNALS_SIGCHECK material type that ingests Sysinternals sigcheck CSV output. Because the policy engine evaluates JSON only, the CSV is transformed into a JSON array of records at evaluation time (delimiter auto-detected, BOM-aware), consistent with the existing JUnit and Jacoco handling. The crafter validates the report and stores the original CSV in the CAS. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: 8fed5172-740a-4468-9572-6e160777444d --- app/cli/documentation/cli-reference.mdx | 4 +- .../workflowcontract/v1/crafting_schema.ts | 10 ++ ...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 | 14 +- .../workflowcontract/v1/crafting_schema.proto | 3 + .../v1/crafting_schema_validations.go | 1 + go.mod | 2 +- .../api/attestation/v1/crafting_state.go | 12 +- .../api/attestation/v1/crafting_state_test.go | 15 +- .../v1/testdata/sigcheck-report.csv | 2 + .../crafter/materials/materials.go | 2 + pkg/attestation/crafter/materials/sigcheck.go | 80 +++++++++ .../crafter/materials/sigcheck/sigcheck.go | 146 +++++++++++++++++ .../materials/sigcheck/sigcheck_test.go | 155 ++++++++++++++++++ .../crafter/materials/sigcheck_test.go | 133 +++++++++++++++ .../testdata/sigcheck-report-empty.csv | 1 + .../testdata/sigcheck-report-tab.csv | 2 + .../materials/testdata/sigcheck-report.csv | 3 + 29 files changed, 604 insertions(+), 23 deletions(-) create mode 100644 pkg/attestation/crafter/api/attestation/v1/testdata/sigcheck-report.csv create mode 100644 pkg/attestation/crafter/materials/sigcheck.go create mode 100644 pkg/attestation/crafter/materials/sigcheck/sigcheck.go create mode 100644 pkg/attestation/crafter/materials/sigcheck/sigcheck_test.go create mode 100644 pkg/attestation/crafter/materials/sigcheck_test.go create mode 100644 pkg/attestation/crafter/materials/testdata/sigcheck-report-empty.csv create mode 100644 pkg/attestation/crafter/materials/testdata/sigcheck-report-tab.csv create mode 100644 pkg/attestation/crafter/materials/testdata/sigcheck-report.csv diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 93205f939..a23346ade 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" "YELP_DETECT_SECRETS_BASELINE" "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" "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" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "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" "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 acd4fb016..e03898df9 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -280,6 +280,11 @@ export enum CraftingSchema_Material_MaterialType { GRAPHQL_SPEC = 32, /** YELP_DETECT_SECRETS_BASELINE - detect-secrets baseline file https://github.com/Yelp/detect-secrets */ YELP_DETECT_SECRETS_BASELINE = 33, + /** + * SYSINTERNALS_SIGCHECK - Sysinternals sigcheck output in CSV format + * https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck + */ + SYSINTERNALS_SIGCHECK = 34, UNRECOGNIZED = -1, } @@ -387,6 +392,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 33: case "YELP_DETECT_SECRETS_BASELINE": return CraftingSchema_Material_MaterialType.YELP_DETECT_SECRETS_BASELINE; + case 34: + case "SYSINTERNALS_SIGCHECK": + return CraftingSchema_Material_MaterialType.SYSINTERNALS_SIGCHECK; case -1: case "UNRECOGNIZED": default: @@ -464,6 +472,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "GRAPHQL_SPEC"; case CraftingSchema_Material_MaterialType.YELP_DETECT_SECRETS_BASELINE: return "YELP_DETECT_SECRETS_BASELINE"; + case CraftingSchema_Material_MaterialType.SYSINTERNALS_SIGCHECK: + return "SYSINTERNALS_SIGCHECK"; 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 790f48b85..f0b068b8f 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 @@ -50,7 +50,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "title": "Material Type", "type": "string" @@ -136,7 +137,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 b27e430b3..5ef0f1676 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 @@ -50,7 +50,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "title": "Material Type", "type": "string" @@ -136,7 +137,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 66af05b87..769851142 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -147,7 +147,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 268e51b0d..32c9991ff 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -147,7 +147,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 abc686ded..1f0002a1f 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 @@ -67,7 +67,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 ee3171258..a6aed6d6f 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 @@ -67,7 +67,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 dccfb83bd..997d140ad 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 @@ -55,7 +55,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 6d44ce5f0..a4136862a 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 @@ -55,7 +55,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 8c6ef6e45..75aa9fbad 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -69,7 +69,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 05726bcf4..190ce82ac 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -69,7 +69,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 32384c902..d2d76eff4 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -90,7 +90,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 53871fbf0..27346d5ed 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -90,7 +90,8 @@ "OPENAPI_SPEC", "ASYNCAPI_SPEC", "GRAPHQL_SPEC", - "YELP_DETECT_SECRETS_BASELINE" + "YELP_DETECT_SECRETS_BASELINE", + "SYSINTERNALS_SIGCHECK" ], "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 66fcaa196..19b2de6e4 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -224,6 +224,9 @@ const ( 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 + // Sysinternals sigcheck output in CSV format + // https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck + CraftingSchema_Material_SYSINTERNALS_SIGCHECK CraftingSchema_Material_MaterialType = 34 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -263,6 +266,7 @@ var ( 31: "ASYNCAPI_SPEC", 32: "GRAPHQL_SPEC", 33: "YELP_DETECT_SECRETS_BASELINE", + 34: "SYSINTERNALS_SIGCHECK", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -299,6 +303,7 @@ var ( "ASYNCAPI_SPEC": 31, "GRAPHQL_SPEC": 32, "YELP_DETECT_SECRETS_BASELINE": 33, + "SYSINTERNALS_SIGCHECK": 34, } ) @@ -1960,7 +1965,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\"\xda\x0f\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xf5\x0f\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1983,7 +1988,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\xa5\t\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xc0\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" + @@ -1992,7 +1997,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\"\xf7\x05\n" + + "skipUpload\"\x92\x06\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2031,7 +2036,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\fOPENAPI_SPEC\x10\x1e\x12\x11\n" + "\rASYNCAPI_SPEC\x10\x1f\x12\x10\n" + "\fGRAPHQL_SPEC\x10 \x12 \n" + - "\x1cYELP_DETECT_SECRETS_BASELINE\x10!:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x1cYELP_DETECT_SECRETS_BASELINE\x10!\x12\x19\n" + + "\x15SYSINTERNALS_SIGCHECK\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 6360aa067..1374e7807 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -165,6 +165,9 @@ message CraftingSchema { GRAPHQL_SPEC = 32; // detect-secrets baseline file https://github.com/Yelp/detect-secrets YELP_DETECT_SECRETS_BASELINE = 33; + // Sysinternals sigcheck output in CSV format + // https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck + SYSINTERNALS_SIGCHECK = 34; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index bd4b02656..18114520a 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -41,6 +41,7 @@ var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ CraftingSchema_Material_GITLAB_SECURITY_REPORT, CraftingSchema_Material_GITLEAKS_JSON, CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE, + CraftingSchema_Material_SYSINTERNALS_SIGCHECK, CraftingSchema_Material_OPENAPI_SPEC, CraftingSchema_Material_ASYNCAPI_SPEC, CraftingSchema_Material_GRAPHQL_SPEC, diff --git a/go.mod b/go.mod index ee6699a2c..02032ce81 100644 --- a/go.mod +++ b/go.mod @@ -458,7 +458,7 @@ require ( golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 golang.org/x/sys v0.44.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/text v0.37.0 golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index 33bcc3db4..8a2f2e5c2 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-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. @@ -28,6 +28,7 @@ import ( "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/attestation" "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" intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/types/known/structpb" ) @@ -143,6 +144,15 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) 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) + } } // if raw material is empty (container images, for example), let's create an empty json 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 a6176e925..b4d88cbea 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-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. @@ -199,6 +199,19 @@ func TestGetEvaluableContentWithMetadata(t *testing.T) { filename: "testdata/sbom.cyclonedx.json", testField: "bomFormat", }, + { + name: "sigcheck csv material", + material: &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_SYSINTERNALS_SIGCHECK, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "name", Digest: "sha256:deadbeef", IsSubject: true, + }, + }, + }, + filename: "testdata/sigcheck-report.csv", + testField: "elements", + }, } for _, tc := range cases { diff --git a/pkg/attestation/crafter/api/attestation/v1/testdata/sigcheck-report.csv b/pkg/attestation/crafter/api/attestation/v1/testdata/sigcheck-report.csv new file mode 100644 index 000000000..a8c2e5d66 --- /dev/null +++ b/pkg/attestation/crafter/api/attestation/v1/testdata/sigcheck-report.csv @@ -0,0 +1,2 @@ +"Path","Verified","Date","Publisher","Company","Description","Product","Product Version","File Version","Machine Type" +"c:\windows\system32\ntdll.dll","Signed","10:00 AM 1/1/2026","Microsoft Windows","Microsoft Corporation","NT Layer DLL","Microsoft Windows Operating System","10.0.19041.1","10.0.19041.1 (WinBuild.160101.0800)","64-bit" diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index eb605c677..28e8698df 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -289,6 +289,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewGitleaksReportCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_YELP_DETECT_SECRETS_BASELINE: crafter, err = NewDetectSecretsCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_SYSINTERNALS_SIGCHECK: + crafter, err = NewSigcheckCrafter(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/sigcheck.go b/pkg/attestation/crafter/materials/sigcheck.go new file mode 100644 index 000000000..be37dce5a --- /dev/null +++ b/pkg/attestation/crafter/materials/sigcheck.go @@ -0,0 +1,80 @@ +// +// 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/sigcheck" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +type SigcheckCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +func NewSigcheckCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*SigcheckCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_SYSINTERNALS_SIGCHECK { + return nil, fmt.Errorf("material type is not a sigcheck report") + } + craftCommon := &crafterCommon{logger: l, input: schema} + return &SigcheckCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +func (i *SigcheckCrafter) 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) + } + + report, err := sigcheck.Parse(data) + if err != nil { + return nil, fmt.Errorf("invalid sigcheck report: %w", ErrInvalidMaterialType) + } + + // Structural fingerprint: sigcheck always emits at least Path and Verified + // columns. This is heuristic but rejects unrelated CSV/JSON files. + if !report.HasColumns("Path", "Verified") { + return nil, fmt.Errorf("missing required sigcheck columns (Path, Verified): %w", ErrInvalidMaterialType) + } + + // A header-only report means a clean scan with no files listed. Accept it. + if len(report.Rows) == 0 { + i.logger.Debug().Msg("Accepting an empty sigcheck report.") + } + + m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m) + + return m, nil +} + +func (i *SigcheckCrafter) injectAnnotations(m *api.Attestation_Material) { + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } + m.Annotations[AnnotationToolNameKey] = "sigcheck" +} diff --git a/pkg/attestation/crafter/materials/sigcheck/sigcheck.go b/pkg/attestation/crafter/materials/sigcheck/sigcheck.go new file mode 100644 index 000000000..5c56de94a --- /dev/null +++ b/pkg/attestation/crafter/materials/sigcheck/sigcheck.go @@ -0,0 +1,146 @@ +// +// 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 sigcheck parses Sysinternals sigcheck CSV/TSV output into a +// JSON-friendly structure for policy evaluation. +// https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck +package sigcheck + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "strings" + + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +// Report is a parsed sigcheck report: the CSV header columns and one map per +// data row, keyed by header column. +type Report struct { + Header []string + // Rows holds one map per data row, keyed by header column. If the header + // contains duplicate column names, the last column wins. + Rows []map[string]string +} + +// Parse decodes sigcheck CSV/TSV output. It strips/decodes UTF-8 and UTF-16 +// byte-order marks and auto-detects whether the delimiter is a comma or a tab. +func Parse(raw []byte) (*Report, error) { + data, err := decode(raw) + if err != nil { + return nil, fmt.Errorf("decoding sigcheck output: %w", err) + } + + if len(bytes.TrimSpace(data)) == 0 { + return nil, errors.New("empty sigcheck report") + } + + r := csv.NewReader(bytes.NewReader(data)) + r.Comma = detectDelimiter(data) + r.LazyQuotes = true + r.FieldsPerRecord = -1 // tolerate ragged rows + + records, err := r.ReadAll() + if err != nil { + return nil, fmt.Errorf("parsing sigcheck CSV: %w", err) + } + if len(records) == 0 { + return nil, errors.New("sigcheck report has no header row") + } + + header := records[0] + report := &Report{Header: header, Rows: make([]map[string]string, 0, len(records)-1)} + for _, rec := range records[1:] { + row := make(map[string]string, len(header)) + for i, col := range header { + if i < len(rec) { + row[col] = rec[i] + } else { + row[col] = "" + } + } + report.Rows = append(report.Rows, row) + } + + return report, nil +} + +// JSON marshals the report rows as a JSON array. A header-only report marshals +// to "[]". +func (r *Report) JSON() ([]byte, error) { + if r.Rows == nil { + return []byte("[]"), nil + } + return json.Marshal(r.Rows) +} + +// HasColumns reports whether every named column is present in the header. +func (r *Report) HasColumns(cols ...string) bool { + set := make(map[string]struct{}, len(r.Header)) + for _, h := range r.Header { + set[strings.TrimSpace(h)] = struct{}{} + } + for _, c := range cols { + if _, ok := set[strings.TrimSpace(c)]; !ok { + return false + } + } + return true +} + +// decode normalizes the input to UTF-8, using the BOM (if any) to detect +// UTF-16; defaults to UTF-8 when no BOM is present, stripping a UTF-8 BOM. +func decode(raw []byte) ([]byte, error) { + dec := unicode.BOMOverride(unicode.UTF8.NewDecoder()) + out, _, err := transform.Bytes(dec, raw) + return out, err +} + +// detectDelimiter inspects the header line and picks the delimiter (comma or +// tab) that appears more often outside of quoted fields. sigcheck's comma +// output quotes every field, so a tab inside a quoted path or description must +// not be mistaken for a TSV separator. Defaults to comma. +func detectDelimiter(data []byte) rune { + line := data + if i := bytes.IndexByte(data, '\n'); i >= 0 { + line = data[:i] + } + + var commas, tabs int + inQuotes := false + for _, b := range line { + switch b { + case '"': + inQuotes = !inQuotes + case ',': + if !inQuotes { + commas++ + } + case '\t': + if !inQuotes { + tabs++ + } + } + } + + if tabs > commas { + return '\t' + } + return ',' +} diff --git a/pkg/attestation/crafter/materials/sigcheck/sigcheck_test.go b/pkg/attestation/crafter/materials/sigcheck/sigcheck_test.go new file mode 100644 index 000000000..da1d71529 --- /dev/null +++ b/pkg/attestation/crafter/materials/sigcheck/sigcheck_test.go @@ -0,0 +1,155 @@ +// +// 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 sigcheck_test + +import ( + "encoding/json" + "testing" + + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/sigcheck" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + colPath = "Path" + colVerified = "Verified" + pathADLL = "c:\\a.dll" +) + +func TestParse(t *testing.T) { + testCases := []struct { + name string + raw []byte + wantErr bool + wantHeader []string + wantRows int + wantFirst map[string]string // subset asserted on rows[0] + }{ + { + name: "comma csv", + raw: []byte("\"Path\",\"Verified\"\n\"c:\\a.dll\",\"Signed\"\n\"c:\\b.exe\",\"Unsigned\"\n"), + wantHeader: []string{colPath, colVerified}, + wantRows: 2, + wantFirst: map[string]string{colPath: pathADLL, colVerified: "Signed"}, + }, + { + name: "tab separated", + raw: []byte("Path\tVerified\tCompany\nc:\\a.dll\tSigned\tMicrosoft\n"), + wantHeader: []string{colPath, colVerified, "Company"}, + wantRows: 1, + wantFirst: map[string]string{colPath: pathADLL, "Company": "Microsoft"}, + }, + { + name: "utf-8 BOM stripped", + raw: append([]byte{0xEF, 0xBB, 0xBF}, []byte("Path,Verified\nc:\\a.dll,Signed\n")...), + wantHeader: []string{colPath, colVerified}, + wantRows: 1, + wantFirst: map[string]string{colPath: pathADLL}, + }, + { + name: "utf-16 LE BOM decoded", + raw: utf16LE("Path,Verified\nc:\\a.dll,Signed\n"), + wantHeader: []string{colPath, colVerified}, + wantRows: 1, + wantFirst: map[string]string{colPath: pathADLL}, + }, + { + name: "utf-16 BE BOM decoded", + raw: utf16BE("Path,Verified\nc:\\a.dll,Signed\n"), + wantHeader: []string{colPath, colVerified}, + wantRows: 1, + wantFirst: map[string]string{colPath: pathADLL}, + }, + { + name: "header only is a clean scan", + raw: []byte("Path,Verified\n"), + wantHeader: []string{colPath, colVerified}, + wantRows: 0, + }, + { + name: "empty input", + raw: []byte(" \n"), + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + report, err := sigcheck.Parse(tc.raw) + if tc.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.wantHeader, report.Header) + assert.Len(t, report.Rows, tc.wantRows) + for k, v := range tc.wantFirst { + assert.Equal(t, v, report.Rows[0][k]) + } + }) + } +} + +func TestReportJSON(t *testing.T) { + report, err := sigcheck.Parse([]byte("Path,Verified\nc:\\a.dll,Signed\n")) + require.NoError(t, err) + + out, err := report.JSON() + require.NoError(t, err) + + var rows []map[string]string + require.NoError(t, json.Unmarshal(out, &rows)) + require.Len(t, rows, 1) + assert.Equal(t, "Signed", rows[0][colVerified]) +} + +func TestReportJSONEmptyIsArray(t *testing.T) { + report, err := sigcheck.Parse([]byte("Path,Verified\n")) + require.NoError(t, err) + + out, err := report.JSON() + require.NoError(t, err) + assert.Equal(t, "[]", string(out)) +} + +func TestHasColumns(t *testing.T) { + report, err := sigcheck.Parse([]byte("Path,Verified,Company\nc:\\a.dll,Signed,MS\n")) + require.NoError(t, err) + + assert.True(t, report.HasColumns(colPath, colVerified)) + assert.False(t, report.HasColumns(colPath, "Nonexistent")) +} + +// utf16LE encodes s as UTF-16 little-endian with a BOM, mimicking PowerShell redirection. +func utf16LE(s string) []byte { + out := make([]byte, 0, 2+2*len(s)) + out = append(out, 0xFF, 0xFE) // LE BOM + for _, r := range s { + out = append(out, byte(r), byte(r>>8)) + } + return out +} + +// utf16BE encodes s as UTF-16 big-endian with a BOM. +func utf16BE(s string) []byte { + out := make([]byte, 0, 2+2*len(s)) + out = append(out, 0xFE, 0xFF) // BE BOM + for _, r := range s { + out = append(out, byte(r>>8), byte(r)) + } + return out +} diff --git a/pkg/attestation/crafter/materials/sigcheck_test.go b/pkg/attestation/crafter/materials/sigcheck_test.go new file mode 100644 index 000000000..cfbf821fb --- /dev/null +++ b/pkg/attestation/crafter/materials/sigcheck_test.go @@ -0,0 +1,133 @@ +// +// 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 TestNewSigcheckCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{Type: contractAPI.CraftingSchema_Material_SYSINTERNALS_SIGCHECK}, + }, + { + 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.NewSigcheckCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func TestSigcheckCrafter_Craft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + annotations map[string]string + }{ + { + name: "invalid path", + filePath: "./testdata/non-existing.csv", + wantErr: "no such file or directory", + }, + { + name: "wrong content", + filePath: "./testdata/sbom-spdx.json", + wantErr: "missing required sigcheck columns", + }, + { + name: "valid report", + filePath: "./testdata/sigcheck-report.csv", + annotations: map[string]string{ + "chainloop.material.tool.name": "sigcheck", + }, + }, + { + name: "tab separated report", + filePath: "./testdata/sigcheck-report-tab.csv", + annotations: map[string]string{ + "chainloop.material.tool.name": "sigcheck", + }, + }, + { + name: "header-only report accepted", + filePath: "./testdata/sigcheck-report-empty.csv", + annotations: map[string]string{ + "chainloop.material.tool.name": "sigcheck", + }, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_SYSINTERNALS_SIGCHECK, + } + + 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.NewSigcheckCrafter(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_SYSINTERNALS_SIGCHECK.String(), got.MaterialType.String()) + assert.True(t, got.UploadedToCas) + for k, v := range tc.annotations { + assert.Equal(t, v, got.Annotations[k]) + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/testdata/sigcheck-report-empty.csv b/pkg/attestation/crafter/materials/testdata/sigcheck-report-empty.csv new file mode 100644 index 000000000..596d7e4b3 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sigcheck-report-empty.csv @@ -0,0 +1 @@ +"Path","Verified","Date","Publisher","Company","Description","Product","Product Version","File Version","Machine Type" diff --git a/pkg/attestation/crafter/materials/testdata/sigcheck-report-tab.csv b/pkg/attestation/crafter/materials/testdata/sigcheck-report-tab.csv new file mode 100644 index 000000000..d34848012 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sigcheck-report-tab.csv @@ -0,0 +1,2 @@ +Path Verified Company +c:\windows\system32\ntdll.dll Signed Microsoft Corporation diff --git a/pkg/attestation/crafter/materials/testdata/sigcheck-report.csv b/pkg/attestation/crafter/materials/testdata/sigcheck-report.csv new file mode 100644 index 000000000..029947dce --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sigcheck-report.csv @@ -0,0 +1,3 @@ +"Path","Verified","Date","Publisher","Company","Description","Product","Product Version","File Version","Machine Type" +"c:\windows\system32\ntdll.dll","Signed","10:00 AM 1/1/2026","Microsoft Windows","Microsoft Corporation","NT Layer DLL","Microsoft Windows Operating System","10.0.19041.1","10.0.19041.1 (WinBuild.160101.0800)","64-bit" +"c:\app\unsigned.exe","Unsigned","11:00 AM 1/1/2026","n/a","n/a","Sample App","n/a","n/a","n/a","32-bit"