diff --git a/app/cli/documentation/cli-reference.md b/app/cli/documentation/cli-reference.md index b6d08f84a..875069029 100644 --- a/app/cli/documentation/cli-reference.md +++ b/app/cli/documentation/cli-reference.md @@ -264,7 +264,7 @@ Options --append reserved for a future release: will control whether --policy-input and --policy-input-from-file append to (rather than replace) the contract-declared value; has no effect yet --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_AI_SECURITY_CONTEXT" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CHECKMARX_JSON" "COBERTURA_XML" "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" "OVERSECURED_JSON" "PITEST_XML" "RADAMSA_CRASHES" "RADAMSA_REPORT" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TRUFFLEHOG_JSON" "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_AI_SECURITY_CONTEXT" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CHECKMARX_JSON" "COBERTURA_XML" "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" "MSBUILD_LOG" "OPENAPI_SPEC" "OPENVEX" "OSSF_SCORECARD_JSON" "OVERSECURED_JSON" "PITEST_XML" "RADAMSA_CRASHES" "RADAMSA_REPORT" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TRUFFLEHOG_JSON" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] --max-extract-entries int max number of files to extract when --value is an archive (default 10000) --max-extract-size string max total uncompressed size to extract when --value is an archive (default "1GiB") --name string name of the material as shown in the contract @@ -2876,7 +2876,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_AI_SECURITY_CONTEXT" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CHECKMARX_JSON" "COBERTURA_XML" "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" "OVERSECURED_JSON" "PITEST_XML" "RADAMSA_CRASHES" "RADAMSA_REPORT" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TRUFFLEHOG_JSON" "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_AI_SECURITY_CONTEXT" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CHECKMARX_JSON" "COBERTURA_XML" "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" "MSBUILD_LOG" "OPENAPI_SPEC" "OPENVEX" "OSSF_SCORECARD_JSON" "OVERSECURED_JSON" "PITEST_XML" "RADAMSA_CRASHES" "RADAMSA_REPORT" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TRUFFLEHOG_JSON" "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 6e924df09..66b585d7c 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -342,6 +342,13 @@ export enum CraftingSchema_Material_MaterialType { * and byte-verifiable evidence anchors backing each claim */ CHAINLOOP_AI_SECURITY_CONTEXT = 45, + /** + * MSBUILD_LOG - Build log of Microsoft build tools: an MSBuild binary log (-bl), an + * MSBuild text log, a Visual C++ build log (BuildLog.htm, written by + * VCBuild), or an archive of them. Its compiler and build warnings and + * errors are evaluated. + */ + MSBUILD_LOG = 46, UNRECOGNIZED = -1, } @@ -485,6 +492,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 45: case "CHAINLOOP_AI_SECURITY_CONTEXT": return CraftingSchema_Material_MaterialType.CHAINLOOP_AI_SECURITY_CONTEXT; + case 46: + case "MSBUILD_LOG": + return CraftingSchema_Material_MaterialType.MSBUILD_LOG; case -1: case "UNRECOGNIZED": default: @@ -586,6 +596,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "PITEST_XML"; case CraftingSchema_Material_MaterialType.CHAINLOOP_AI_SECURITY_CONTEXT: return "CHAINLOOP_AI_SECURITY_CONTEXT"; + case CraftingSchema_Material_MaterialType.MSBUILD_LOG: + return "MSBUILD_LOG"; 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 1e9131d91..52a4d3b99 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 @@ -62,7 +62,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "title": "Material Type", "type": "string" @@ -160,7 +161,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 3661c9bf4..c62fd6372 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 @@ -62,7 +62,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "title": "Material Type", "type": "string" @@ -160,7 +161,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 78a2c9514..11aea81ba 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -173,7 +173,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 ed6228d36..b53938a6b 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -173,7 +173,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 3203a4ea4..40e442bc3 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 @@ -83,7 +83,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 0c0d8190c..215b47d86 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 @@ -83,7 +83,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 aaac50622..079b84634 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 @@ -107,7 +107,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 3ead8e336..0d0b7a013 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 @@ -107,7 +107,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 a96f72d35..fd3f11692 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -81,7 +81,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 cb2fa59ff..17ac102d1 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -81,7 +81,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 a14cea087..d9e4b8c16 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -102,7 +102,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 15dbc37f3..2f63c34c8 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -102,7 +102,8 @@ "CHECKMARX_JSON", "OVERSECURED_JSON", "PITEST_XML", - "CHAINLOOP_AI_SECURITY_CONTEXT" + "CHAINLOOP_AI_SECURITY_CONTEXT", + "MSBUILD_LOG" ], "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 d5e6b58ce..0381d74cf 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -260,6 +260,11 @@ const ( // repository's fix history, the attack surfaces they share, ranked risks, // and byte-verifiable evidence anchors backing each claim CraftingSchema_Material_CHAINLOOP_AI_SECURITY_CONTEXT CraftingSchema_Material_MaterialType = 45 + // Build log of Microsoft build tools: an MSBuild binary log (-bl), an + // MSBuild text log, a Visual C++ build log (BuildLog.htm, written by + // VCBuild), or an archive of them. Its compiler and build warnings and + // errors are evaluated. + CraftingSchema_Material_MSBUILD_LOG CraftingSchema_Material_MaterialType = 46 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -311,6 +316,7 @@ var ( 43: "OVERSECURED_JSON", 44: "PITEST_XML", 45: "CHAINLOOP_AI_SECURITY_CONTEXT", + 46: "MSBUILD_LOG", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -359,6 +365,7 @@ var ( "OVERSECURED_JSON": 43, "PITEST_XML": 44, "CHAINLOOP_AI_SECURITY_CONTEXT": 45, + "MSBUILD_LOG": 46, } ) @@ -2105,7 +2112,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\"\x99\x13\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xaa\x13\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -2128,7 +2135,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\xe4\f\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xf5\f\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" + @@ -2139,7 +2146,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\vskip_upload\x18\x06 \x01(\bR\n" + "skipUpload\x12\xaa\x01\n" + "\x05group\x18\a \x01(\tB\x93\x01\xbaH\x8f\x01\xba\x01\x8b\x01\n" + - "\x0egroup.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a=this == '' || this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x05group\"\x89\b\n" + + "\x0egroup.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a=this == '' || this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x05group\"\x9a\b\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2191,7 +2198,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x10OVERSECURED_JSON\x10+\x12\x0e\n" + "\n" + "PITEST_XML\x10,\x12!\n" + - "\x1dCHAINLOOP_AI_SECURITY_CONTEXT\x10-:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x1dCHAINLOOP_AI_SECURITY_CONTEXT\x10-\x12\x0f\n" + + "\vMSBUILD_LOG\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 46f879052..4124983c1 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -209,6 +209,11 @@ message CraftingSchema { // repository's fix history, the attack surfaces they share, ranked risks, // and byte-verifiable evidence anchors backing each claim CHAINLOOP_AI_SECURITY_CONTEXT = 45; + // Build log of Microsoft build tools: an MSBuild binary log (-bl), an + // MSBuild text log, a Visual C++ build log (BuildLog.htm, written by + // VCBuild), or an archive of them. Its compiler and build warnings and + // errors are evaluated. + MSBUILD_LOG = 46; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go index e1d3809d9..f1ef65ea2 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_test.go @@ -359,6 +359,10 @@ func TestMaterialKindsExcludedFromAutoDetection(t *testing.T) { name: "radamsa crashes accept almost any non-empty file", kind: v1.CraftingSchema_Material_RADAMSA_CRASHES, }, + { + name: "build log probing parses whole logs of hundreds of megabytes", + kind: v1.CraftingSchema_Material_MSBUILD_LOG, + }, } for _, tc := range testCases { diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index 99db20ae6..d4857e963 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -42,6 +42,9 @@ import ( // an app platform), but the export envelope is not published by the vendor, // so pinning it here would bet every other JSON kind on a shape inferred // from a sample. +// - MSBUILD_LOG: recognizing a log means parsing it, and diagnostic-verbosity +// text logs reach hundreds of megabytes; probing every file for it would +// slow down every auto-detected material. // // TestMaterialKindsExcludedFromAutoDetection pins these exclusions. var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index 07ea4766e..00410aef4 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/buildlog" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/cobertura" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/dranzer" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/jacoco" @@ -350,6 +351,16 @@ func (m *Attestation_Material) ingestMaterialToJSON(rawMaterial []byte, value st return nil, fmt.Errorf("invalid dranzer material: %w", err) } return json.Marshal(bundle) + case v1.CraftingSchema_Material_MSBUILD_LOG: + // Build logs are binary (MSBuild -bl), plain text or HTML, possibly + // several in an archive. They project to the normalized diagnostics + // only, with no raw text: diagnostic-verbosity text logs reach hundreds + // of megabytes and the policy engine holds its whole input in memory. + report, err := buildlog.Parse(rawMaterial) + if err != nil { + return nil, fmt.Errorf("invalid build log material: %w", err) + } + return json.Marshal(report) } return rawMaterial, nil 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 b0495130a..b7af12f62 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go @@ -372,6 +372,35 @@ func TestDranzerBundleIsEvaluable(t *testing.T) { assert.Len(t, decoded["reports"], 4) } +// TestMSBuildLogIsEvaluable guards that an MSBUILD_LOG material, which is stored as +// the raw (often gzip-compressed binary) log, projects to the normalized +// diagnostics policies evaluate. Handing the engine the raw bytes would fail +// to parse, or for a text log yield nothing to evaluate. +func TestMSBuildLogIsEvaluable(t *testing.T) { + m := &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_MSBUILD_LOG, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{Name: "build-log", Digest: "sha256:deadbeef"}, + }, + } + + for _, name := range []string{"sample.binlog", "msbuild.txt", "vcbuild.htm"} { + t.Run(name, func(t *testing.T) { + content, err := m.GetEvaluableContent("../../../materials/testdata/buildlog/"+name, nil) + require.NoError(t, err) + + var decoded map[string]any + require.NoError(t, json.NewDecoder(bytes.NewReader(content)).Decode(&decoded)) + + assert.NotEmpty(t, decoded["tool"].(map[string]any)["name"]) + assert.EqualValues(t, 4, decoded["summary"].(map[string]any)["warnings"]) + assert.Len(t, decoded["diagnostics"], 4) + assert.Len(t, decoded["logs"], 1) + assert.Contains(t, decoded, "chainloop_metadata") + }) + } +} + // TestRadamsaReportArchiveIsEvaluable guards that a RADAMSA_REPORT material whose // value is an archive of per-run -M logs projects to input.elements holding the // records merged across every entry. Recording the archive whole without merging diff --git a/pkg/attestation/crafter/materials/buildlog/binlog.go b/pkg/attestation/crafter/materials/buildlog/binlog.go new file mode 100644 index 000000000..566af253e --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/binlog.go @@ -0,0 +1,457 @@ +// +// 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 buildlog + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/binary" + "errors" + "fmt" + "io" + "regexp" + "strconv" +) + +// An MSBuild binary log (-bl) is a gzip stream: a header of two little-endian +// int32 (the file format version and the minimum reader version), then a +// sequence of records, each a 7-bit encoded kind followed by a 7-bit encoded +// length and the record body. Strings are written once as String records and +// referenced by index afterwards. +// +// The layout is MSBuild's BuildEventArgsWriter/BuildEventArgsReader. Only the +// records that carry diagnostics are decoded; every other record is skipped by +// its length, which is why logs older than the length-prefixed format are not +// supported. +const ( + recordEndOfFile = 0 + recordBuildFinished = 2 + recordProjectStarted = 3 + recordError = 9 + recordWarning = 10 + recordString = 24 + + // minBinlogVersion is the first format version whose records are all + // length-prefixed (MSBuild's ForwardCompatibilityMinimalVersion). + minBinlogVersion = 18 + // knownBinlogVersion is the newest format version whose diagnostic record + // layout this reader was written against. Newer versions may append fields + // to a record, which are skipped; older ones must match it exactly. + knownBinlogVersion = 25 + + // stringStartIndex is the index MSBuild assigns to the first String record; + // 0 is null, 1 is the empty string and the rest are reserved. + stringStartIndex = 10 +) + +// BuildEventArgsFieldFlags: which optional fields a record carries. +const ( + flagBuildEventContext = 1 << 0 + flagHelpKeyword = 1 << 1 + flagMessage = 1 << 2 + flagSenderName = 1 << 3 + flagThreadID = 1 << 4 + flagTimestamp = 1 << 5 + flagSubcategory = 1 << 6 + flagCode = 1 << 7 + flagFile = 1 << 8 + flagProjectFile = 1 << 9 + flagLineNumber = 1 << 10 + flagColumnNumber = 1 << 11 + flagEndLineNumber = 1 << 12 + flagEndColumnNumber = 1 << 13 + flagArguments = 1 << 14 + flagImportance = 1 << 15 + flagExtended = 1 << 16 +) + +// ErrUnsupportedBinlog is returned for a binary log whose format version this +// reader cannot decode. +var ErrUnsupportedBinlog = errors.New("unsupported MSBuild binary log version") + +// maxBinlogRecord bounds a single record body. Diagnostic records are a few +// hundred bytes; the bound only stops a corrupt length from allocating +// gigabytes. +const maxBinlogRecord = 64 << 20 + +// formatArgRe matches a .NET composite-format placeholder such as {0} or +// {1,-8:N2}, or an escaped brace, "{{" or "}}". +var formatArgRe = regexp.MustCompile(`\{\{|\}\}|\{(\d+)(?:,[^}:]*)?(?::[^}]*)?\}`) + +// isBinlog reports whether peeked bytes are the start of an MSBuild binary log: +// a gzip stream whose first decompressed int32 is a plausible format version. +// A tar.gz, the other gzip content a material may hold, starts with an entry +// name instead. +func isBinlog(peek []byte) bool { + if !bytes.HasPrefix(peek, []byte{0x1f, 0x8b}) { + return false + } + zr, err := gzip.NewReader(bytes.NewReader(peek)) + if err != nil { + return false + } + var version int32 + if err := binary.Read(zr, binary.LittleEndian, &version); err != nil { + return false + } + return version > 0 && version < 1<<16 +} + +type binlogReader struct { + r *bufio.Reader + version int32 + strings []string +} + +// parseBinlog reads the diagnostics of an MSBuild binary log. +func parseBinlog(r io.Reader) (*parsedLog, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, fmt.Errorf("opening binary log: %w", err) + } + defer zr.Close() + + br := &binlogReader{r: bufio.NewReader(zr)} + var header [2]int32 + if err := binary.Read(br.r, binary.LittleEndian, &header); err != nil { + return nil, fmt.Errorf("reading binary log header: %w", err) + } + br.version = header[0] + if br.version < minBinlogVersion { + return nil, fmt.Errorf("%w %d: produce the log with a newer MSBuild (format %d or later)", ErrUnsupportedBinlog, br.version, minBinlogVersion) + } + if header[1] > knownBinlogVersion { + return nil, fmt.Errorf("%w %d: it requires a reader for format %d or later", ErrUnsupportedBinlog, br.version, header[1]) + } + + log := &parsedLog{format: FormatMSBuildBinlog, tool: Tool{Name: ToolMSBuild}} + for n := 0; ; n++ { + kind, err := br.varint(br.r) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + if kind == recordEndOfFile { + return log, nil + } + length, err := br.varint(br.r) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + if length < 0 { + return nil, fmt.Errorf("binary log record %d has an invalid length %d", n, length) + } + // Only records read into memory are bounded; the rest are skipped + // whatever their size (an embedded project-imports archive can be large). + if kind != recordString && kind != recordWarning && kind != recordError && + kind != recordProjectStarted && kind != recordBuildFinished { + if _, err := br.r.Discard(length); err != nil { + return nil, fmt.Errorf("binary log is truncated: skipping record %d: %w", n, err) + } + continue + } + if length > maxBinlogRecord { + return nil, fmt.Errorf("binary log record %d has an invalid length %d", n, length) + } + + switch kind { + case recordBuildFinished: + body, err := br.body(length) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + if log.result, err = br.buildResult(body); err != nil { + return nil, fmt.Errorf("decoding binary log record %d: %w", n, err) + } + case recordProjectStarted: + body, err := br.body(length) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + if log.solution == "" { + // The first project started is the one the build was run for. + // Only a label, so a record this reader cannot decode is no error. + log.solution = br.projectFile(body) + } + case recordString: + // A String record's length is the string's own length prefix. + body, err := br.body(length) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + br.strings = append(br.strings, string(body)) + case recordWarning, recordError: + body, err := br.body(length) + if err != nil { + return nil, fmt.Errorf("binary log is truncated: reading record %d: %w", n, err) + } + d, err := br.diagnostic(body) + if err != nil { + return nil, fmt.Errorf("decoding binary log record %d: %w", n, err) + } + if kind == recordError { + d.Severity = SeverityError + } else { + d.Severity = SeverityWarning + } + log.diagnostics = append(log.diagnostics, d) + } + } +} + +func (br *binlogReader) body(length int) ([]byte, error) { + body := make([]byte, length) + if _, err := io.ReadFull(br.r, body); err != nil { + return nil, err + } + return body, nil +} + +// diagnostic decodes a Warning or Error record body: the shared event fields +// (driven by the flags), then the diagnostic fields that are always present. +func (br *binlogReader) diagnostic(body []byte) (Diagnostic, error) { + r := bytes.NewReader(body) + message, arguments, err := br.eventFields(r) + if err != nil { + return Diagnostic{}, err + } + d, err := br.diagnosticFields(r) + if err != nil { + return Diagnostic{}, err + } + + // Unread bytes mean the record does not have the layout this reader + // expects. That is fatal for the versions it knows, where it would mean a + // misread; newer versions may append fields, which are safe to skip. + if r.Len() != 0 && br.version <= knownBinlogVersion { + return Diagnostic{}, fmt.Errorf("record has %d unread bytes", r.Len()) + } + + d.Message = formatMessage(message, arguments) + return d, nil +} + +// eventFields reads the optional fields every build event may carry, in the +// order of BuildEventArgsReader.ReadBuildEventArgsFields, and returns the +// message with its format arguments. +func (br *binlogReader) eventFields(r *bytes.Reader) (string, []string, error) { + flags, err := br.varint(r) + if err != nil { + return "", nil, err + } + + var message string + var arguments []string + skipVarint := func() error { return br.skipVarints(r, 1) } + skipString := func() error { _, err := br.str(r); return err } + + fields := []struct { + flag int + read func() error + }{ + {flagMessage, func() (err error) { message, err = br.str(r); return err }}, + // nodeId, projectContextId, targetId, taskId, submissionId, + // projectInstanceId, evaluationId + {flagBuildEventContext, func() error { return br.skipVarints(r, 7) }}, + {flagThreadID, skipVarint}, + {flagHelpKeyword, skipString}, + {flagSenderName, skipString}, + // ticks, then the DateTimeKind + {flagTimestamp, func() error { + if _, err := r.Seek(8, io.SeekCurrent); err != nil { + return err + } + return skipVarint() + }}, + // type, metadata (an index into a name-value list), data + {flagExtended, func() error { + if err := skipString(); err != nil { + return err + } + if err := skipVarint(); err != nil { + return err + } + return skipString() + }}, + // Diagnostic events never set these, but the reader honors them. + {flagSubcategory, skipString}, + {flagCode, skipString}, + {flagFile, skipString}, + {flagProjectFile, skipString}, + {flagLineNumber, skipVarint}, + {flagColumnNumber, skipVarint}, + {flagEndLineNumber, skipVarint}, + {flagEndColumnNumber, skipVarint}, + {flagArguments, func() (err error) { arguments, err = br.arguments(r); return err }}, + {flagImportance, skipVarint}, + } + for _, f := range fields { + if flags&f.flag == 0 { + continue + } + if err := f.read(); err != nil { + return "", nil, err + } + } + return message, arguments, nil +} + +func (br *binlogReader) arguments(r *bytes.Reader) ([]string, error) { + count, err := br.varint(r) + if err != nil { + return nil, err + } + if count < 0 || count > r.Len() { + return nil, fmt.Errorf("invalid argument count %d", count) + } + arguments := make([]string, 0, count) + for range count { + a, err := br.str(r) + if err != nil { + return nil, err + } + arguments = append(arguments, a) + } + return arguments, nil +} + +// diagnosticFields reads what BuildEventArgsReader.ReadDiagnosticFields does: +// subcategory, code, file, project file, line, column, end line, end column. +func (br *binlogReader) diagnosticFields(r *bytes.Reader) (Diagnostic, error) { + var d Diagnostic + var err error + if _, err = br.str(r); err != nil { + return d, err + } + if d.Code, err = br.str(r); err != nil { + return d, err + } + if d.File, err = br.str(r); err != nil { + return d, err + } + if d.Project, err = br.str(r); err != nil { + return d, err + } + if d.Line, err = br.varint(r); err != nil { + return d, err + } + if d.Column, err = br.varint(r); err != nil { + return d, err + } + return d, br.skipVarints(r, 2) +} + +// buildResult reads a BuildFinished record body: the event fields, then +// whether the build succeeded. +func (br *binlogReader) buildResult(body []byte) (string, error) { + r := bytes.NewReader(body) + if _, _, err := br.eventFields(r); err != nil { + return "", err + } + succeeded, err := r.ReadByte() + if err != nil { + return "", err + } + if succeeded != 0 { + return ResultSucceeded, nil + } + return ResultFailed, nil +} + +// projectFile returns the project file of a ProjectStarted record body: the +// event fields, an optional parent build context, then the project file. It +// returns the empty string when the body cannot be decoded. +func (br *binlogReader) projectFile(body []byte) string { + r := bytes.NewReader(body) + if _, _, err := br.eventFields(r); err != nil { + return "" + } + hasParent, err := r.ReadByte() + if err != nil { + return "" + } + if hasParent != 0 { + if err := br.skipVarints(r, 7); err != nil { + return "" + } + } + project, err := br.str(r) + if err != nil { + return "" + } + return project +} + +// formatMessage expands a .NET composite format string with its arguments, +// which is how MSBuild stores lazily formatted task messages. +func formatMessage(format string, arguments []string) string { + if len(arguments) == 0 { + return format + } + return formatArgRe.ReplaceAllStringFunc(format, func(m string) string { + if m == "{{" || m == "}}" { + return m[:1] + } + i, err := strconv.Atoi(formatArgRe.FindStringSubmatch(m)[1]) + if err != nil || i >= len(arguments) { + return m + } + return arguments[i] + }) +} + +// str reads a deduplicated string reference. +func (br *binlogReader) str(r io.ByteReader) (string, error) { + i, err := br.varint(r) + if err != nil { + return "", err + } + switch { + case i == 0, i == 1: + return "", nil + case i < stringStartIndex || i-stringStartIndex >= len(br.strings): + return "", fmt.Errorf("invalid string reference %d", i) + } + return br.strings[i-stringStartIndex], nil +} + +func (br *binlogReader) skipVarints(r io.ByteReader, n int) error { + for range n { + if _, err := br.varint(r); err != nil { + return err + } + } + return nil +} + +// varint reads a .NET 7-bit encoded int32. +func (br *binlogReader) varint(r io.ByteReader) (int, error) { + var v uint32 + for shift := 0; shift < 35; shift += 7 { + b, err := r.ReadByte() + if err != nil { + if errors.Is(err, io.EOF) && shift > 0 { + err = io.ErrUnexpectedEOF + } + return 0, err + } + v |= uint32(b&0x7f) << shift + if b&0x80 == 0 { + return int(int32(v)), nil + } + } + return 0, errors.New("invalid 7-bit encoded integer") +} diff --git a/pkg/attestation/crafter/materials/buildlog/binlog_test.go b/pkg/attestation/crafter/materials/buildlog/binlog_test.go new file mode 100644 index 000000000..db4b11907 --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/binlog_test.go @@ -0,0 +1,255 @@ +// +// 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 buildlog + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// binlogWriter builds minimal binary logs for the record shapes the synthetic +// fixtures do not contain, following MSBuild's BuildEventArgsWriter layout. +type binlogWriter struct { + out bytes.Buffer + strings map[string]int +} + +func newBinlogWriter(version, minReaderVersion int32) *binlogWriter { + w := &binlogWriter{strings: map[string]int{}} + _ = binary.Write(&w.out, binary.LittleEndian, version) + _ = binary.Write(&w.out, binary.LittleEndian, minReaderVersion) + return w +} + +func putVarint(buf *bytes.Buffer, v int) { + u := uint32(int32(v)) + for u >= 0x80 { + buf.WriteByte(byte(u) | 0x80) + u >>= 7 + } + buf.WriteByte(byte(u)) +} + +// str returns the deduplicated index of s, writing its String record first +// when it is new. Index 0 is null, as in MSBuild. +func (w *binlogWriter) str(s *string) int { + if s == nil { + return 0 + } + if i, ok := w.strings[*s]; ok { + return i + } + putVarint(&w.out, recordString) + putVarint(&w.out, len(*s)) + w.out.WriteString(*s) + i := len(w.strings) + stringStartIndex + w.strings[*s] = i + return i +} + +type diagnosticRecord struct { + kind int + message string + arguments []string + extendedType string + code string + file string + project string + line, column int +} + +func ptr(s string) *string { return &s } + +const ( + codeVariadicClass = "C4840" + codeUnresolvedRef = "MSB3245" + sourceFile = `C:\src\a.cpp` + sourceProject = `C:\src\a.vcxproj` +) + +func (w *binlogWriter) diagnostic(d diagnosticRecord) { + // String records go out first, as the writer flushes them before the + // record that references them. + msg := w.str(ptr(d.message)) + sender := w.str(ptr("Task")) + var extType, extData int + if d.extendedType != "" { + extType = w.str(ptr(d.extendedType)) + extData = w.str(ptr(`{"ruleId":"` + d.code + `"}`)) + } + args := make([]int, 0, len(d.arguments)) + for _, a := range d.arguments { + args = append(args, w.str(ptr(a))) + } + code, file, project := w.str(ptr(d.code)), w.str(ptr(d.file)), w.str(ptr(d.project)) + + var body bytes.Buffer + flags := flagMessage | flagBuildEventContext | flagSenderName | flagTimestamp + if d.extendedType != "" { + flags |= flagExtended + } + if len(args) > 0 { + flags |= flagArguments + } + putVarint(&body, flags) + putVarint(&body, msg) + for _, v := range []int{1, 2, 3, 4, 5, 6, 7} { + putVarint(&body, v) + } + putVarint(&body, sender) + _ = binary.Write(&body, binary.LittleEndian, int64(638000000000000000)) + putVarint(&body, 1) + if d.extendedType != "" { + putVarint(&body, extType) + putVarint(&body, 0) // no metadata + putVarint(&body, extData) + } + if len(args) > 0 { + putVarint(&body, len(args)) + for _, a := range args { + putVarint(&body, a) + } + } + putVarint(&body, 0) // subcategory + putVarint(&body, code) + putVarint(&body, file) + putVarint(&body, project) + putVarint(&body, d.line) + putVarint(&body, d.column) + putVarint(&body, 0) + putVarint(&body, 0) + + putVarint(&w.out, d.kind) + putVarint(&w.out, body.Len()) + w.out.Write(body.Bytes()) +} + +func (w *binlogWriter) bytes(t *testing.T) []byte { + t.Helper() + putVarint(&w.out, recordEndOfFile) + return gzipOf(t, w.out.Bytes()) +} + +func gzipOf(t *testing.T, raw []byte) []byte { + t.Helper() + var gz bytes.Buffer + zw := gzip.NewWriter(&gz) + _, err := zw.Write(raw) + require.NoError(t, err) + require.NoError(t, zw.Close()) + return gz.Bytes() +} + +func TestParseBinlogRecordShapes(t *testing.T) { + w := newBinlogWriter(25, 18) + // MSVC reports its diagnostics as extended events carrying a SARIF result. + w.diagnostic(diagnosticRecord{ + kind: recordWarning, message: "non-portable use of class as an argument to a variadic function", + extendedType: "SarifBuildEvent", code: codeVariadicClass, file: sourceFile, project: sourceProject, line: 10, column: 4, + }) + // Task warnings may keep their message as a format string plus arguments. + w.diagnostic(diagnosticRecord{ + kind: recordWarning, message: "Reference {0} could not be resolved: {1}", arguments: []string{"Foo", "not found"}, + code: codeUnresolvedRef, file: `C:\sdk\Microsoft.Common.targets`, project: `C:\src\b.csproj`, line: 2400, column: 5, + }) + w.diagnostic(diagnosticRecord{ + kind: recordError, message: msgUndeclared, + code: codeUndeclared, file: sourceFile, project: sourceProject, line: 41, column: 7, + }) + + report, err := Parse(w.bytes(t)) + require.NoError(t, err) + + assert.Equal(t, Summary{ + Warnings: 2, + Errors: 1, + ByCode: map[string]int{codeUndeclared: 1, codeVariadicClass: 1, codeUnresolvedRef: 1}, + }, report.Summary) + assert.Equal(t, inLogs([]Diagnostic{ + {Severity: SeverityError, Code: codeUndeclared, Message: msgUndeclared, File: sourceFile, Line: 41, Column: 7, Project: sourceProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: codeVariadicClass, Message: "non-portable use of class as an argument to a variadic function", File: sourceFile, Line: 10, Column: 4, Project: sourceProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: codeUnresolvedRef, Message: "Reference Foo could not be resolved: not found", File: `C:\sdk\Microsoft.Common.targets`, Line: 2400, Column: 5, Project: `C:\src\b.csproj`, Occurrences: 1}, + }, 0), report.Diagnostics) +} + +func TestParseBinlogUnsupportedVersions(t *testing.T) { + testCases := []struct { + name string + version, minRead int32 + }{ + {name: "older than length-prefixed records", version: 17, minRead: 17}, + {name: "requires a newer reader", version: 40, minRead: 40}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse(newBinlogWriter(tc.version, tc.minRead).bytes(t)) + assert.ErrorIs(t, err, ErrUnsupportedBinlog) + }) + } +} + +func TestParseBinlogTruncatedFails(t *testing.T) { + w := newBinlogWriter(25, 18) + w.diagnostic(diagnosticRecord{kind: recordWarning, message: "m", code: codeUnusedLocal, file: "a.cs", project: csharpProject, line: 1, column: 1}) + complete := w.out.Bytes() + + // A log cut short, whether inside a record or before the end-of-file + // record, is incomplete evidence: the parse must fail rather than report + // only the warnings it got to. + for name, raw := range map[string][]byte{ + "inside a record": complete[:len(complete)-3], + "missing end-of-file": complete, + "inside the file header": complete[:5], + } { + t.Run(name, func(t *testing.T) { + _, err := Parse(gzipOf(t, raw)) + assert.Error(t, err) + assert.NotErrorIs(t, err, ErrNoBuildLog) + }) + } +} + +func TestParseBinlogMalformedRecordFails(t *testing.T) { + w := newBinlogWriter(25, 18) + // A record whose declared length leaves fields unread means the layout is + // not the one this reader knows. + putVarint(&w.out, recordWarning) + putVarint(&w.out, 40) + w.out.Write(make([]byte, 40)) + + _, err := Parse(w.bytes(t)) + assert.Error(t, err) +} + +func TestFormatMessage(t *testing.T) { + // .NET composite format: "{{" and "}}" are literal braces. + for format, want := range map[string]string{ + "{0} is unused": "x is unused", + "{{0}} is {0}": "{0} is x", + "{{{0}}}": "{x}", + "}} {1,-8:N2} {{": "} y {", + "{2} has no value": "{2} has no value", + "{{ and }} only": "{ and } only", + "unbalanced } and {": "unbalanced } and {", + } { + assert.Equal(t, want, formatMessage(format, []string{"x", "y"}), format) + } +} diff --git a/pkg/attestation/crafter/materials/buildlog/buildlog.go b/pkg/attestation/crafter/materials/buildlog/buildlog.go new file mode 100644 index 000000000..f4c5b3e7d --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/buildlog.go @@ -0,0 +1,501 @@ +// +// 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 buildlog extracts the compiler and build diagnostics (warnings and +// errors) from the logs Microsoft build tools write, and normalizes them into +// one Chainloop-defined structure that policies evaluate: +// +// - MSBuild binary logs (-bl), format version 18 or later +// - MSBuild text logs (console or file logger, any verbosity that keeps +// warnings), possibly several invocations appended to one file +// - Visual C++ build logs (BuildLog.htm, written by vcbuild), UTF-16 HTML +// - a zip or tar archive holding any mix of the above, in any folders, for +// builds that produce one log per solution or per project. Archives inside +// it (such as the binary logs zipped per solution) are read one level deep; +// entries that are not logs are skipped. +// +// The original log is what a material stores; this projection is recomputed +// from it whenever policies run. It keeps no raw log text: diagnostic-verbosity +// logs reach hundreds of megabytes, and the policy engine holds its whole +// input in memory. +package buildlog + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "slices" + "sort" + "strings" + + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/archiveio" +) + +const ( + FormatMSBuildBinlog = "msbuild-binlog" + FormatMSBuildText = "msbuild-text" + FormatVCBuildHTML = "vcbuild-html" + + ToolMSBuild = "msbuild" + ToolVCBuild = "vcbuild" + + SeverityWarning = "warning" + SeverityError = "error" + + ResultSucceeded = "succeeded" + ResultFailed = "failed" +) + +// ErrNoBuildLog is returned when a material holds no recognizable build log: a +// single file in another format, or an archive with no build log entry. +var ErrNoBuildLog = errors.New("no build log found") + +// Tool identifies the build tool that wrote a log. +type Tool struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` +} + +// Diagnostic is one distinct warning or error. Repeats of the same diagnostic +// (a header warning reported once per translation unit including it, or a +// project built by several solutions) are folded into one entry: Occurrences +// counts them, and Logs lists the indexes in Report.Logs of the logs they were +// seen in, so a folded diagnostic can still be traced to each build. +type Diagnostic struct { + Severity string `json:"severity"` + Code string `json:"code,omitempty"` + Message string `json:"message"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Column int `json:"column,omitempty"` + Project string `json:"project,omitempty"` + Occurrences int `json:"occurrences"` + Logs []int `json:"logs"` +} + +// Summary counts distinct diagnostics. +type Summary struct { + Warnings int `json:"warnings"` + Errors int `json:"errors"` + ByCode map[string]int `json:"by_code"` +} + +// Counts are the totals a log states about itself, such as MSBuild's +// "N Warning(s)" summary. They let a policy check that every diagnostic the +// tool reported was parsed. +type Counts struct { + Warnings int `json:"warnings"` + Errors int `json:"errors"` +} + +// Log describes one build recorded in the material: a binary log, one MSBuild +// run of a text log (a text log can hold several, one per solution, which then +// share a Source), or a Visual C++ build log. Source names the archive entry it +// was read from, and is empty for a single-file material. Solution is the +// project the build was run for (usually a .sln), when the log records it. +// +// Result is how the build ended, empty when the log does not say (a log cut +// short). Parsed counts the occurrences read from the log, comparable with the +// totals it Reported about itself: fewer parsed than reported means warnings +// the parser could not read. +type Log struct { + Source string `json:"source,omitempty"` + Solution string `json:"solution,omitempty"` + Format string `json:"format"` + Tool Tool `json:"tool"` + Result string `json:"result,omitempty"` + Summary Summary `json:"summary"` + Parsed Counts `json:"parsed"` + Reported *Counts `json:"reported,omitempty"` +} + +// Report is the policy projection of an MSBUILD_LOG material. Skipped lists the +// archive entries that were not read as logs, so a policy can tell a log that +// could not be read from a file that is not a log. +type Report struct { + Tool Tool `json:"tool"` + Logs []Log `json:"logs"` + Skipped []string `json:"skipped,omitempty"` + Summary Summary `json:"summary"` + Diagnostics []Diagnostic `json:"diagnostics"` +} + +// parsedLog is one log as read, before its diagnostics are folded. +type parsedLog struct { + source string + solution string + result string + format string + tool Tool + diagnostics []Diagnostic + reported *Counts +} + +// archiveLimits bounds archive expansion. Logs are streamed and only their +// diagnostics are kept, so the bound is on the work done, not on memory. +func archiveLimits() archiveio.Limits { + return archiveio.Limits{MaxEntries: 1024, MaxTotalSize: 4 << 30} +} + +// Parse projects an MSBUILD_LOG material held in memory. +func Parse(data []byte) (*Report, error) { + if isBinlog(data) { + return parseSingle(bytes.NewReader(data)) + } + if format := archiveio.DetectBytes(data); format != archiveio.None { + return parseArchive(func(yield func(string, io.Reader) error) error { + return archiveio.WalkBytes(data, format, archiveLimits(), yield) + }) + } + return parseSingle(bytes.NewReader(data)) +} + +// ParseFile projects an MSBUILD_LOG material on disk, streaming it. +func ParseFile(p string) (*Report, error) { + f, err := os.Open(p) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + defer f.Close() + + br := bufio.NewReader(f) + peek, _ := br.Peek(512) + if isBinlog(peek) { + return parseSingle(br) + } + format, err := archiveio.DetectFile(p) + if err != nil { + return nil, fmt.Errorf("inspecting %q: %w", p, err) + } + if format != archiveio.None { + return parseArchive(func(yield func(string, io.Reader) error) error { + return archiveio.WalkPath(p, format, archiveLimits(), yield) + }) + } + return parseSingle(br) +} + +func parseSingle(r io.Reader) (*Report, error) { + logs, err := parseLog(r) + if err != nil { + return nil, err + } + if len(logs) == 0 { + return nil, ErrNoBuildLog + } + return newReport(logs), nil +} + +// walker yields the entries of an archive. +type walker func(yield func(name string, r io.Reader) error) error + +// maxNestedArchive bounds an archive inside the material's archive, which has +// to be read into memory to be opened. +const maxNestedArchive = 512 << 20 + +// nestedArchiveExts are the entry names opened as archives. Packages such as +// .nupkg or .vsix are zips too, but not bundles of logs, so they are skipped +// like any other entry that is not a log. +var nestedArchiveExts = []string{".zip", ".tar", ".tar.gz", ".tgz"} + +func isArchiveName(name string) bool { + lower := strings.ToLower(name) + for _, ext := range nestedArchiveExts { + if strings.HasSuffix(lower, ext) { + return true + } + } + return false +} + +// parseArchive parses every archive entry that is a build log, including those +// of an archive one level down: a bundle of everything a release produced may +// hold the binary logs already zipped per solution. Other entries are skipped, +// but at least one log must be found. +func parseArchive(walk walker) (*Report, error) { + c := collected{budget: budget{limits: archiveLimits()}} + if err := c.walk(walk, "", 0); err != nil { + return nil, fmt.Errorf("reading build log archive: %w", err) + } + if len(c.logs) == 0 { + return nil, ErrNoBuildLog + } + // Deterministic order, so the projection does not depend on archive order. + // Stable, so the runs of one text log keep their order. + sort.SliceStable(c.logs, func(i, j int) bool { return c.logs[i].source < c.logs[j].source }) + sort.Strings(c.skipped) + report := newReport(c.logs) + report.Skipped = c.skipped + return report, nil +} + +// collected gathers the logs of an archive and the entries that are not logs. +type collected struct { + logs []*parsedLog + skipped []string + budget budget +} + +// budget counts the entries and bytes read from the material's archive and +// from the archives inside it. Each archive walk applies its limits to itself +// only, so without a shared count every nested archive could expand to the +// full limits again. +type budget struct { + limits archiveio.Limits + entries int + read int64 +} + +// entry counts an entry and returns a reader that counts its bytes. +func (b *budget) entry(r io.Reader) (io.Reader, error) { + b.entries++ + if b.entries > b.limits.MaxEntries { + return nil, archiveio.ErrTooManyEntries + } + return &budgetReader{r: r, b: b}, nil +} + +type budgetReader struct { + r io.Reader + b *budget +} + +func (br *budgetReader) Read(p []byte) (int, error) { + n, err := br.r.Read(p) + br.b.read += int64(n) + if br.b.read > br.b.limits.MaxTotalSize { + return n, archiveio.ErrArchiveTooLarge + } + return n, err +} + +// projectImportsSuffix names the archive of project files that +// -bl:ProjectImports=ZipFile writes next to a binary log. It never holds logs. +const projectImportsSuffix = ".projectimports.zip" + +// walk adds the build logs of an archive, naming each by its path from the +// material's archive. depth counts the archives the walk is in beyond the +// material's own. +func (c *collected) walk(walk walker, prefix string, depth int) error { + return walk(func(name string, r io.Reader) error { + source := prefix + name + r, err := c.budget.entry(r) + if err != nil { + return err + } + switch { + case strings.HasSuffix(strings.ToLower(name), projectImportsSuffix): + c.skipped = append(c.skipped, source) + return nil + case isArchiveName(name): + return c.nested(r, source, depth) + } + return c.add(r, source) + }) +} + +// add parses r and adds its logs, or records it as skipped when it holds none. +func (c *collected) add(r io.Reader, source string) error { + parsed, err := parseLog(r) + if err != nil { + return fmt.Errorf("parsing %q: %w", source, err) + } + if len(parsed) == 0 { + c.skipped = append(c.skipped, source) + return nil + } + for _, l := range parsed { + l.source = source + } + c.logs = append(c.logs, parsed...) + return nil +} + +// nested opens an archive found inside the material's archive. Skipping it +// would drop its logs from the evaluation without a trace, so anything that +// cannot be read fails instead. +func (c *collected) nested(r io.Reader, source string, depth int) error { + if depth > 0 { + return fmt.Errorf("archive %q is inside another archive within the material: move its logs up one level", source) + } + data, err := io.ReadAll(io.LimitReader(r, maxNestedArchive+1)) + if err != nil { + return fmt.Errorf("reading %q: %w", source, err) + } + if len(data) > maxNestedArchive { + return fmt.Errorf("archive %q is larger than %d MiB: add its logs to the material's archive directly", source, maxNestedArchive>>20) + } + + format := archiveio.DetectBytes(data) + if format == archiveio.None || isBinlog(data) { + // Named like an archive but is not one; it may still be a log. + return c.add(bytes.NewReader(data), source) + } + return c.walk(func(yield func(string, io.Reader) error) error { + return archiveio.WalkBytes(data, format, archiveLimits(), yield) + }, source+"/", depth+1) +} + +// sniffSize is how much of a file is read to recognize it. A CI console capture +// can hold the output of earlier steps before MSBuild starts. +const sniffSize = 64 << 10 + +// parseLog parses the builds recorded in one log file, or returns none when r +// is not a build log. +func parseLog(r io.Reader) ([]*parsedLog, error) { + br := bufio.NewReaderSize(r, sniffSize) + peek, err := br.Peek(sniffSize) + if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, bufio.ErrBufferFull) { + return nil, err + } + if isBinlog(peek) { + log, err := parseBinlog(br) + if err != nil { + return nil, err + } + return []*parsedLog{log}, nil + } + if format := sniffText(peek); format != "" { + return parseText(br, format) + } + return nil, nil +} + +// diagnosticKey identifies a diagnostic for folding repeats. Paths are compared +// without regard to case, as the Windows tools that write these logs spell the +// same file differently from one report to the next. +type diagnosticKey struct { + severity, code, message, file, project string + line, column int +} + +func keyOf(d Diagnostic) diagnosticKey { + return diagnosticKey{ + severity: d.Severity, + code: d.Code, + message: d.Message, + file: strings.ToLower(d.File), + project: strings.ToLower(d.Project), + line: d.Line, + column: d.Column, + } +} + +// fold merges repeated diagnostics, keeping the first spelling of each, and +// sorts the result. +func fold(diagnostics []Diagnostic) []Diagnostic { + index := make(map[diagnosticKey]int, len(diagnostics)) + out := make([]Diagnostic, 0, len(diagnostics)) + for _, d := range diagnostics { + k := keyOf(d) + if i, ok := index[k]; ok { + out[i].Occurrences += max(d.Occurrences, 1) + out[i].Logs = mergeIndexes(out[i].Logs, d.Logs) + continue + } + d.Occurrences = max(d.Occurrences, 1) + index[k] = len(out) + out = append(out, d) + } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + if a.Code != b.Code { + return a.Code < b.Code + } + if a.File != b.File { + return a.File < b.File + } + if a.Line != b.Line { + return a.Line < b.Line + } + if a.Column != b.Column { + return a.Column < b.Column + } + if a.Project != b.Project { + return a.Project < b.Project + } + if a.Severity != b.Severity { + return a.Severity < b.Severity + } + return a.Message < b.Message + }) + return out +} + +// occurrences counts the diagnostics read from a log, before folding. +func occurrences(diagnostics []Diagnostic) Counts { + var c Counts + for _, d := range diagnostics { + if d.Severity == SeverityError { + c.Errors += max(d.Occurrences, 1) + } else { + c.Warnings += max(d.Occurrences, 1) + } + } + return c +} + +// mergeIndexes returns the sorted union of two index lists. +func mergeIndexes(a, b []int) []int { + out := append(slices.Clone(a), b...) + slices.Sort(out) + return slices.Compact(out) +} + +func summarize(diagnostics []Diagnostic) Summary { + s := Summary{ByCode: map[string]int{}} + for _, d := range diagnostics { + if d.Severity == SeverityError { + s.Errors++ + } else { + s.Warnings++ + } + if d.Code != "" { + s.ByCode[d.Code]++ + } + } + return s +} + +func newReport(logs []*parsedLog) *Report { + report := &Report{Logs: make([]Log, 0, len(logs))} + var all []Diagnostic + for i, l := range logs { + folded := fold(l.diagnostics) + for j := range folded { + folded[j].Logs = []int{i} + } + if report.Tool.Name == "" { + report.Tool = l.tool + } + report.Logs = append(report.Logs, Log{ + Source: l.source, + Solution: l.solution, + Result: l.result, + Parsed: occurrences(l.diagnostics), + Format: l.format, + Tool: l.tool, + Summary: summarize(folded), + Reported: l.reported, + }) + all = append(all, folded...) + } + report.Diagnostics = fold(all) + report.Summary = summarize(report.Diagnostics) + return report +} diff --git a/pkg/attestation/crafter/materials/buildlog/buildlog_test.go b/pkg/attestation/crafter/materials/buildlog/buildlog_test.go new file mode 100644 index 000000000..8e23f15f7 --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/buildlog_test.go @@ -0,0 +1,419 @@ +// +// 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 buildlog + +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testdataDir = "../testdata/buildlog/" + sampleBinlog = "sample.binlog" + msbuildText = "msbuild.txt" + vcbuildHTML = "vcbuild.htm" + readmeEntry = "logs/README.txt" + csharpProject = "a.csproj" +) + +// Codes and names shared by several assertions. +const ( + codeBrokenCrypto = "CA5351" + codeConversion = "C4244" + codeUnsafeCRT = "C4996" + codeUnusedLocal = "CS0168" + codeNullConvert = "CS8600" + codeNullDeref = "CS8602" + codeUndeclared = "C2065" + msgUndeclared = "'buf': undeclared identifier" + appProject = "/src/App/App.csproj" + appSource = "/src/App/Program.cs" + vcProject = "demo" +) + +// sampleDiagnostics are the warnings the synthetic two-project solution in +// testdata produces, whichever log format recorded the build. +var sampleDiagnostics = []Diagnostic{ + {Severity: SeverityWarning, Code: codeBrokenCrypto, Message: "Weak uses a broken cryptographic algorithm MD5 (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351)", File: "/src/Lib/Hashing.cs", Line: 9, Column: 25, Project: "/src/Lib/Lib.csproj", Occurrences: 1}, + {Severity: SeverityWarning, Code: codeUnusedLocal, Message: "The variable 'unused' is declared but never used", File: appSource, Line: 3, Column: 5, Project: appProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: codeNullConvert, Message: "Converting null literal or possible null value to non-nullable type.", File: appSource, Line: 4, Column: 12, Project: appProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: codeNullDeref, Message: "Dereference of a possibly null reference.", File: appSource, Line: 5, Column: 19, Project: appProject, Occurrences: 1}, +} + +var sampleSummary = Summary{ + Warnings: 4, + ByCode: map[string]int{codeBrokenCrypto: 1, codeUnusedLocal: 1, codeNullConvert: 1, codeNullDeref: 1}, +} + +// inLogs returns diagnostics as seen in the given logs of a report. +func inLogs(ds []Diagnostic, logs ...int) []Diagnostic { + out := make([]Diagnostic, len(ds)) + for i, d := range ds { + d.Logs = logs + out[i] = d + } + return out +} + +func readTestdata(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(testdataDir + name) + require.NoError(t, err) + return data +} + +func zipOf(t *testing.T, files map[string][]byte) []byte { + t.Helper() + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for name, content := range files { + w, err := zw.Create(name) + require.NoError(t, err) + _, err = w.Write(content) + require.NoError(t, err) + } + require.NoError(t, zw.Close()) + return buf.Bytes() +} + +func TestParseMSBuildBinlog(t *testing.T) { + report, err := Parse(readTestdata(t, sampleBinlog)) + require.NoError(t, err) + + assert.Equal(t, Tool{Name: ToolMSBuild}, report.Tool) + require.Len(t, report.Logs, 1) + assert.Equal(t, FormatMSBuildBinlog, report.Logs[0].Format) + // The build's root project: the solution the binary log was produced for. + assert.Equal(t, "/src/Sample.sln", report.Logs[0].Solution) + assert.Equal(t, sampleSummary, report.Summary) + assert.Equal(t, inLogs(sampleDiagnostics, 0), report.Diagnostics) +} + +func TestParseMSBuildTextLog(t *testing.T) { + report, err := Parse(readTestdata(t, msbuildText)) + require.NoError(t, err) + + require.Len(t, report.Logs, 1) + assert.Equal(t, FormatMSBuildText, report.Logs[0].Format) + assert.Equal(t, "/src/Sample.sln", report.Logs[0].Solution) + // The warnings MSBuild repeats in its end-of-build summary must not be + // counted twice. + assert.Equal(t, sampleSummary, report.Summary) + assert.Equal(t, inLogs(sampleDiagnostics, 0), report.Diagnostics) + assert.Equal(t, &Counts{Warnings: 4}, report.Logs[0].Reported) +} + +func TestParseMSBuildTextLogOfSeveralSolutions(t *testing.T) { + // One text log can hold several MSBuild invocations, one per solution, + // appended to each other. Each repeats its warnings in its own summary; + // a project both solutions build reports its warning once per solution. + invocation := func(solution string, warnings ...string) string { + s := "MSBuild version 17.9.8+b34f75857 for .NET Framework\r\n" + + "Build started 1/2/2026 3:04:05 PM.\r\n" + + "Project \"" + solution + "\" on node 1 (default targets).\r\n" + for _, w := range warnings { + s += " 1>" + w + "\r\n" + } + s += "Build succeeded.\r\n\r\n" + for _, w := range warnings { + s += " " + w + "\r\n" + } + return s + fmt.Sprintf("\r\n %d Warning(s)\r\n 0 Error(s)\r\n\r\n", len(warnings)) + } + shared := "/src/Lib/Hashing.cs(9,25): warning CA5351: Weak uses a broken cryptographic algorithm MD5 (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351) [/src/Lib/Lib.csproj]" + app := "/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]" + log := invocation("/src/A.sln", shared, app) + invocation("/src/B.sln", shared) + + report, err := Parse([]byte(log)) + require.NoError(t, err) + + // Each MSBuild run is a log of its own, so the warnings stay attributed to + // the solution that produced them. + require.Len(t, report.Logs, 2) + assert.Equal(t, "/src/A.sln", report.Logs[0].Solution) + assert.Equal(t, &Counts{Warnings: 2}, report.Logs[0].Reported) + assert.Equal(t, "/src/B.sln", report.Logs[1].Solution) + assert.Equal(t, &Counts{Warnings: 1}, report.Logs[1].Reported) + assert.Equal(t, report.Logs[0].Source, report.Logs[1].Source) + + assert.Equal(t, Summary{Warnings: 2, ByCode: map[string]int{codeBrokenCrypto: 1, codeUnusedLocal: 1}}, report.Summary) + want := []Diagnostic{sampleDiagnostics[0], sampleDiagnostics[1]} + want[0].Occurrences = 2 + want[0].Logs = []int{0, 1} + want[1].Logs = []int{0} + assert.Equal(t, want, report.Diagnostics) +} + +func TestParseArchiveOfVCBuildLogs(t *testing.T) { + // Visual C++ writes one BuildLog.htm per project, so a solution's logs come + // zipped. Each entry is a log of its own. + htm := readTestdata(t, vcbuildHTML) + report, err := Parse(zipOf(t, map[string][]byte{ + "demo/Release/BuildLog.htm": htm, + "other/Release/BuildLog.htm": htm, + })) + require.NoError(t, err) + + require.Len(t, report.Logs, 2) + for i, source := range []string{"demo/Release/BuildLog.htm", "other/Release/BuildLog.htm"} { + assert.Equal(t, source, report.Logs[i].Source) + assert.Equal(t, FormatVCBuildHTML, report.Logs[i].Format) + assert.Equal(t, &Counts{Warnings: 5}, report.Logs[i].Reported) + } + assert.Equal(t, Tool{Name: ToolVCBuild}, report.Tool) + assert.Equal(t, 4, report.Summary.Warnings) + for _, d := range report.Diagnostics { + assert.Equal(t, vcProject, d.Project) + assert.Equal(t, []int{0, 1}, d.Logs) + } +} + +func TestParseMSBuildTextLogSummaryOnly(t *testing.T) { + // A console log captured with -clp:Summary only carries the warnings in + // the end-of-build summary; they must still be picked up. + log := "Build started 1/2/2026 3:04:05 PM.\r\n" + + "Build succeeded.\r\n" + + "\r\n" + + " \"/src/App/App.csproj\" (default target) (1) ->\r\n" + + " /src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]\r\n" + + "\r\n" + + " 1 Warning(s)\r\n" + + " 0 Error(s)\r\n" + + report, err := Parse([]byte(log)) + require.NoError(t, err) + assert.Equal(t, inLogs([]Diagnostic{sampleDiagnostics[1]}, 0), report.Diagnostics) +} + +func TestParseMSBuildTextLogWithBanner(t *testing.T) { + log := "MSBuild version 17.9.8+b34f75857 for .NET Framework\r\n" + + "Build started 1/2/2026 3:04:05 PM.\r\n" + + " 1>C:\\src\\lib\\io.cpp(41,7): error C2065: 'buf': undeclared identifier [C:\\src\\lib\\lib.vcxproj]\r\n" + + "Build FAILED.\r\n" + + " 0 Warning(s)\r\n" + + " 1 Error(s)\r\n" + + report, err := Parse([]byte(log)) + require.NoError(t, err) + + assert.Equal(t, Tool{Name: ToolMSBuild, Version: "17.9.8+b34f75857"}, report.Tool) + assert.Equal(t, Summary{Errors: 1, ByCode: map[string]int{codeUndeclared: 1}}, report.Summary) + assert.Equal(t, []Diagnostic{{ + Severity: SeverityError, Code: codeUndeclared, Message: msgUndeclared, + File: "C:\\src\\lib\\io.cpp", Line: 41, Column: 7, Project: "C:\\src\\lib\\lib.vcxproj", Occurrences: 1, Logs: []int{0}, + }}, report.Diagnostics) + assert.Equal(t, &Counts{Errors: 1}, report.Logs[0].Reported) +} + +func TestParseVCBuildHTMLLog(t *testing.T) { + report, err := Parse(readTestdata(t, vcbuildHTML)) + require.NoError(t, err) + + assert.Equal(t, Tool{Name: ToolVCBuild}, report.Tool) + require.Len(t, report.Logs, 1) + assert.Equal(t, FormatVCBuildHTML, report.Logs[0].Format) + assert.Equal(t, &Counts{Warnings: 5}, report.Logs[0].Reported) + + assert.Equal(t, Summary{ + Warnings: 4, + ByCode: map[string]int{codeConversion: 1, "C4706": 1, codeUnsafeCRT: 1, "LNK4099": 1}, + }, report.Summary) + assert.Equal(t, inLogs([]Diagnostic{ + // The header warning is reported once per translation unit including it, + // with the path spelled in different case; it is one diagnostic. + {Severity: SeverityWarning, Code: codeConversion, Message: "'initializing' : conversion from 'int' to 'char', possible loss of data", File: "C:\\SDK\\include\\vendor.h", Line: 7, Project: vcProject, Occurrences: 2}, + {Severity: SeverityWarning, Code: "C4706", Message: "assignment within conditional expression", File: ".\\util.cpp", Line: 30, Project: vcProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: codeUnsafeCRT, Message: "'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead.", File: ".\\main.cpp", Line: 12, Project: vcProject, Occurrences: 1}, + {Severity: SeverityWarning, Code: "LNK4099", Message: "PDB 'vc90.pdb' was not found with 'vendor.lib'; linking object as if no debug info", File: "vendor.lib(io.obj)", Project: vcProject, Occurrences: 1}, + }, 0), report.Diagnostics) +} + +func TestParseArchiveOfBinlogs(t *testing.T) { + // One binlog per solution, zipped together. The library is built by both + // solutions, so its warning is one diagnostic seen twice. + bundle := zipOf(t, map[string][]byte{ + "logs/sample.binlog": readTestdata(t, sampleBinlog), + "logs/lib.binlog": readTestdata(t, "lib.binlog"), + readmeEntry: []byte("not a build log"), + }) + + report, err := Parse(bundle) + require.NoError(t, err) + + require.Len(t, report.Logs, 2) + assert.Equal(t, "logs/lib.binlog", report.Logs[0].Source) + assert.Equal(t, "/src/Lib/Lib.csproj", report.Logs[0].Solution) + assert.Equal(t, Summary{Warnings: 1, ByCode: map[string]int{codeBrokenCrypto: 1}}, report.Logs[0].Summary) + assert.Equal(t, "logs/sample.binlog", report.Logs[1].Source) + assert.Equal(t, "/src/Sample.sln", report.Logs[1].Solution) + assert.Equal(t, sampleSummary, report.Logs[1].Summary) + + assert.Equal(t, sampleSummary, report.Summary) + // Each diagnostic names the logs it was seen in: the library's warning both. + want := inLogs(sampleDiagnostics, 1) + want[0].Occurrences = 2 + want[0].Logs = []int{0, 1} + assert.Equal(t, want, report.Diagnostics) +} + +// everythingZip is one archive with every log of a release in it, in folders, +// next to files that are not logs: the binary logs zipped per solution as +// their own archive, a text log of the same build, and per-project Visual C++ +// logs. +func everythingZip(t *testing.T) []byte { + t.Helper() + binlogs := zipOf(t, map[string][]byte{ + sampleBinlog: readTestdata(t, sampleBinlog), + "lib.binlog": readTestdata(t, "lib.binlog"), + }) + return zipOf(t, map[string][]byte{ + "release/binlogs.zip": binlogs, + "release/msbuild/build.log": readTestdata(t, msbuildText), + "release/ce/demo/Release/BuildLog.htm": readTestdata(t, vcbuildHTML), + "release/README.txt": []byte("build logs of the release"), + // A package is a zip too, but not a bundle of logs. + "release/packages/tool.nupkg": zipOf(t, map[string][]byte{"tool.nuspec": []byte("")}), + }) +} + +func TestParseArchiveOfEverything(t *testing.T) { + report, err := Parse(everythingZip(t)) + require.NoError(t, err) + + sources := make([]string, 0, len(report.Logs)) + formats := map[string]string{} + for _, l := range report.Logs { + sources = append(sources, l.Source) + formats[l.Source] = l.Format + } + assert.Equal(t, []string{ + "release/binlogs.zip/lib.binlog", + "release/binlogs.zip/sample.binlog", + "release/ce/demo/Release/BuildLog.htm", + "release/msbuild/build.log", + }, sources) + assert.Equal(t, FormatMSBuildBinlog, formats["release/binlogs.zip/lib.binlog"]) + assert.Equal(t, FormatVCBuildHTML, formats["release/ce/demo/Release/BuildLog.htm"]) + assert.Equal(t, FormatMSBuildText, formats["release/msbuild/build.log"]) + + // The C# warnings and the Visual C++ ones, each once: the text log and the + // binary log record the same build. + assert.Equal(t, 8, report.Summary.Warnings) + occurrences := map[string]int{} + for _, d := range report.Diagnostics { + occurrences[d.Code] += d.Occurrences + } + // Built by both solutions, and in the text log too. + assert.Equal(t, 3, occurrences[codeBrokenCrypto]) + assert.Equal(t, 2, occurrences[codeUnusedLocal]) +} + +func TestParseFileOfEverythingMatchesParse(t *testing.T) { + data := everythingZip(t) + p := filepath.Join(t.TempDir(), "release.zip") + require.NoError(t, os.WriteFile(p, data, 0o600)) + + fromFile, err := ParseFile(p) + require.NoError(t, err) + fromBytes, err := Parse(data) + require.NoError(t, err) + assert.Equal(t, fromBytes, fromFile) +} + +func TestParseArchiveNestedTooDeep(t *testing.T) { + // Logs two archives down are not read. That must fail, not pass as a + // bundle without them. + inner := zipOf(t, map[string][]byte{sampleBinlog: readTestdata(t, sampleBinlog)}) + middle := zipOf(t, map[string][]byte{"inner.zip": inner}) + outer := zipOf(t, map[string][]byte{ + "middle.zip": middle, + "vcbuild/demo.htm": readTestdata(t, vcbuildHTML), + }) + + _, err := Parse(outer) + require.Error(t, err) + assert.NotErrorIs(t, err, ErrNoBuildLog) + assert.ErrorContains(t, err, "middle.zip/inner.zip") +} + +func TestParseArchiveWithoutBuildLogs(t *testing.T) { + _, err := Parse(zipOf(t, map[string][]byte{"README.txt": []byte("hello")})) + assert.ErrorIs(t, err, ErrNoBuildLog) +} + +func TestParseRejectsOtherContent(t *testing.T) { + for name, data := range map[string][]byte{ + "empty": nil, + "plain text": []byte("hello world\n"), + "json": []byte(`{"runs": []}`), + } { + t.Run(name, func(t *testing.T) { + _, err := Parse(data) + assert.ErrorIs(t, err, ErrNoBuildLog) + }) + } +} + +func TestParseFileMatchesParse(t *testing.T) { + for _, name := range []string{sampleBinlog, msbuildText, vcbuildHTML} { + t.Run(name, func(t *testing.T) { + fromFile, err := ParseFile(testdataDir + name) + require.NoError(t, err) + fromBytes, err := Parse(readTestdata(t, name)) + require.NoError(t, err) + assert.Equal(t, fromBytes, fromFile) + }) + } +} + +func TestReportJSONShape(t *testing.T) { + report, err := Parse(readTestdata(t, sampleBinlog)) + require.NoError(t, err) + + raw, err := json.Marshal(report) + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, json.Unmarshal(raw, &doc)) + assert.Equal(t, map[string]any{"name": "msbuild"}, doc["tool"]) + assert.Equal(t, map[string]any{ + "warnings": float64(4), + "errors": float64(0), + "by_code": map[string]any{codeBrokenCrypto: float64(1), codeUnusedLocal: float64(1), codeNullConvert: float64(1), codeNullDeref: float64(1)}, + }, doc["summary"]) + diagnostics := doc["diagnostics"].([]any) + assert.Equal(t, map[string]any{ + "severity": "warning", + "code": codeBrokenCrypto, + "message": sampleDiagnostics[0].Message, + "file": "/src/Lib/Hashing.cs", + "line": float64(9), + "column": float64(25), + "project": "/src/Lib/Lib.csproj", + "occurrences": float64(1), + "logs": []any{float64(0)}, + }, diagnostics[0]) + logs := doc["logs"].([]any) + assert.Equal(t, "/src/Sample.sln", logs[0].(map[string]any)["solution"]) +} diff --git a/pkg/attestation/crafter/materials/buildlog/fidelity_test.go b/pkg/attestation/crafter/materials/buildlog/fidelity_test.go new file mode 100644 index 000000000..f947dcb43 --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/fidelity_test.go @@ -0,0 +1,238 @@ +// +// 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 buildlog + +// The projection feeds a compliance gate, so what it cannot read must show: +// these tests guard the facts a policy needs to fail rather than pass a log +// whose warnings were not all read. + +import ( + "bytes" + "io" + "testing" + + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/archiveio" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseRecordsParsedCounts(t *testing.T) { + // Parsed counts occurrences, like the totals a log states about itself, + // so a policy can compare the two. + for name, want := range map[string]Counts{ + sampleBinlog: {Warnings: 4}, + msbuildText: {Warnings: 4}, + vcbuildHTML: {Warnings: 5}, + } { + t.Run(name, func(t *testing.T) { + report, err := Parse(readTestdata(t, name)) + require.NoError(t, err) + assert.Equal(t, want, report.Logs[0].Parsed) + }) + } +} + +func TestParseRecordsBuildResult(t *testing.T) { + for name, want := range map[string]string{ + sampleBinlog: ResultSucceeded, + msbuildText: ResultSucceeded, + vcbuildHTML: ResultSucceeded, + } { + t.Run(name, func(t *testing.T) { + report, err := Parse(readTestdata(t, name)) + require.NoError(t, err) + assert.Equal(t, want, report.Logs[0].Result) + }) + } + + t.Run("failed msbuild run", func(t *testing.T) { + report, err := Parse([]byte("Build started 1/2/2026 3:04:05 PM.\r\n" + + " 1>C:\\src\\io.cpp(41,7): error C2065: 'buf': undeclared identifier [C:\\src\\lib.vcxproj]\r\n" + + "Build FAILED.\r\n 0 Warning(s)\r\n 1 Error(s)\r\n")) + require.NoError(t, err) + assert.Equal(t, ResultFailed, report.Logs[0].Result) + }) + + t.Run("text log cut short", func(t *testing.T) { + report, err := Parse([]byte("Build started 1/2/2026 3:04:05 PM.\r\n" + + " 1>C:\\src\\io.cpp(41,7): warning C4996: 'strcpy': unsafe [C:\\src\\lib.vcxproj]\r\n")) + require.NoError(t, err) + assert.Empty(t, report.Logs[0].Result) + }) + + t.Run("failed visual c++ build", func(t *testing.T) { + htm := bytes.ReplaceAll(readTestdata(t, vcbuildHTML), utf16le("0 error(s)"), utf16le("2 error(s)")) + report, err := Parse(htm) + require.NoError(t, err) + assert.Equal(t, ResultFailed, report.Logs[0].Result) + }) +} + +func utf16le(s string) []byte { + out := make([]byte, 0, 2*len(s)) + for _, c := range []byte(s) { + out = append(out, c, 0) + } + return out +} + +func TestParseMSBuildRunsWithoutBuildStarted(t *testing.T) { + // At minimal verbosity MSBuild writes no "Build started" line. Each run + // still ends with its counts, and starts with a banner unless -nologo. + run := func(banner bool, warning string) string { + s := "" + if banner { + s += "MSBuild version 17.9.8+b34f75857 for .NET Framework\r\n" + } + return s + " " + warning + "\r\n" + + "Build succeeded.\r\n\r\n " + warning + "\r\n\r\n 1 Warning(s)\r\n 0 Error(s)\r\n\r\nTime Elapsed 00:00:01.00\r\n" + } + first := "C:\\src\\a.cpp(1,1): warning C4244: conversion [C:\\src\\a.vcxproj]" + second := "C:\\src\\b.cpp(2,2): warning C4996: 'strcpy': unsafe [C:\\src\\b.vcxproj]" + + for name, log := range map[string]string{ + "each run with a banner": run(true, first) + run(true, second), + "second run without its banner": "Build started 1/2/2026 3:04:05 PM.\r\n" + run(false, first) + run(false, second), + "first run with a banner only": run(true, first) + run(false, second), + } { + t.Run(name, func(t *testing.T) { + report, err := Parse([]byte(log)) + require.NoError(t, err) + require.Len(t, report.Logs, 2) + assert.Equal(t, map[string]int{codeConversion: 1, codeUnsafeCRT: 1}, report.Summary.ByCode) + for _, l := range report.Logs { + assert.Equal(t, &Counts{Warnings: 1}, l.Reported) + assert.Equal(t, Counts{Warnings: 1}, l.Parsed) + } + }) + } +} + +func TestParseArchiveRecordsSkippedEntries(t *testing.T) { + report, err := Parse(zipOf(t, map[string][]byte{ + "logs/sample.binlog": readTestdata(t, sampleBinlog), + readmeEntry: []byte("build logs"), + "logs/broken.binlog": []byte("not gzip"), + })) + require.NoError(t, err) + assert.Equal(t, []string{readmeEntry, "logs/broken.binlog"}, report.Skipped) +} + +func TestParseArchiveSkipsProjectImports(t *testing.T) { + // -bl:ProjectImports=ZipFile writes the project files next to each binary + // log as .ProjectImports.zip: never logs, at any depth. + binlogs := zipOf(t, map[string][]byte{ + "sample.binlog": readTestdata(t, sampleBinlog), + "sample.ProjectImports.zip": zipOf(t, map[string][]byte{csharpProject: []byte("")}), + }) + report, err := Parse(zipOf(t, map[string][]byte{"release/binlogs.zip": binlogs})) + require.NoError(t, err) + require.Len(t, report.Logs, 1) + assert.Equal(t, []string{"release/binlogs.zip/sample.ProjectImports.zip"}, report.Skipped) +} + +func TestParseBinlogSkipsLargeRecordsItDoesNotRead(t *testing.T) { + // A large embedded project-imports archive is skipped, not read, so its + // size does not matter. + w := newBinlogWriter(25, 18) + const recordProjectImportArchive = 17 + putVarint(&w.out, recordProjectImportArchive) + putVarint(&w.out, maxBinlogRecord+1) + w.out.Write(make([]byte, maxBinlogRecord+1)) + w.diagnostic(diagnosticRecord{kind: recordWarning, message: "m", code: codeUnusedLocal, file: "a.cs", project: csharpProject, line: 1, column: 1}) + + report, err := Parse(w.bytes(t)) + require.NoError(t, err) + assert.Equal(t, 1, report.Summary.Warnings) +} + +func TestParseDiagnosticLineEdgeCases(t *testing.T) { + testCases := []struct { + line string + want Diagnostic + }{ + { + // A column range. + line: `C:\src\a.cpp(10,5-12): warning C4996: 'strcpy': unsafe [C:\src\a.vcxproj]`, + want: Diagnostic{Severity: SeverityWarning, Code: codeUnsafeCRT, Message: "'strcpy': unsafe", File: `C:\src\a.cpp`, Line: 10, Column: 5, Project: `C:\src\a.vcxproj`}, + }, + { + // clang-cl names the warning flag in brackets; it is not a project. + line: `C:\src\a.cpp(3,1): warning : unused variable 'x' [-Wunused-variable]`, + want: Diagnostic{Severity: SeverityWarning, Message: "unused variable 'x' [-Wunused-variable]", File: `C:\src\a.cpp`, Line: 3, Column: 1}, + }, + } + for _, tc := range testCases { + t.Run(tc.line, func(t *testing.T) { + got, ok := parseDiagnosticLine(tc.line) + require.True(t, ok) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestSniffOldMSBuildBanner(t *testing.T) { + assert.Equal(t, FormatMSBuildText, sniffText([]byte("Microsoft (R) Build Engine Version 3.5.30729.1\r\n[Microsoft .NET Framework, Version 2.0.50727.8806]\r\n"))) +} + +func TestParseRejectsResultLineWithoutBuild(t *testing.T) { + // Other tools print "Build succeeded." too. A result line with no sign + // of an MSBuild build would pass as a clean build. + for name, log := range map[string]string{ + "result line only": "Build succeeded.\n", + "inside other output": "Restoring tools\nBuild succeeded.\nDone in 3s\n", + "translated result only": "La génération a réussi.\n", + } { + t.Run(name, func(t *testing.T) { + _, err := Parse([]byte(log)) + assert.ErrorIs(t, err, ErrNoBuildLog) + }) + } + + t.Run("with its counts", func(t *testing.T) { + // A clean `dotnet build`: the counts after the result line are the + // build's own record that nothing was reported. + report, err := Parse([]byte("Build succeeded.\n 0 Warning(s)\n 0 Error(s)\n")) + require.NoError(t, err) + require.Len(t, report.Logs, 1) + assert.Equal(t, ResultSucceeded, report.Logs[0].Result) + assert.Equal(t, &Counts{}, report.Logs[0].Reported) + }) +} + +func TestParseArchiveBoundsNestedArchivesTogether(t *testing.T) { + // Each archive walk applies the limits to itself only, so the archives + // inside the material's archive must share what the material allows. + log := readTestdata(t, msbuildText) + nested := zipOf(t, map[string][]byte{"build.log": log}) + outer := zipOf(t, map[string][]byte{"a.zip": nested, "b.zip": nested}) + walk := func(yield func(string, io.Reader) error) error { + return archiveio.WalkBytes(outer, archiveio.DetectBytes(outer), archiveLimits(), yield) + } + + // Room for one nested archive and its log, not for two. + one := int64(len(nested) + len(log)) + c := collected{budget: budget{limits: archiveio.Limits{MaxEntries: 1024, MaxTotalSize: one + one/2}}} + require.ErrorIs(t, c.walk(walk, "", 0), archiveio.ErrArchiveTooLarge) + + // Four entries: two archives and a log in each. + c = collected{budget: budget{limits: archiveio.Limits{MaxEntries: 3, MaxTotalSize: 1 << 30}}} + require.ErrorIs(t, c.walk(walk, "", 0), archiveio.ErrTooManyEntries) + + c = collected{budget: budget{limits: archiveio.Limits{MaxEntries: 4, MaxTotalSize: 2 * one}}} + require.NoError(t, c.walk(walk, "", 0)) + assert.Len(t, c.logs, 2) +} diff --git a/pkg/attestation/crafter/materials/buildlog/localized_test.go b/pkg/attestation/crafter/materials/buildlog/localized_test.go new file mode 100644 index 000000000..8ea4ca874 --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/localized_test.go @@ -0,0 +1,221 @@ +// +// 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 buildlog + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// A localized Visual Studio translates MSBuild's own lines (build started, +// result, counts, project) but the compiler diagnostics keep the canonical +// English "warning CODE:" form. +func TestParseLocalizedMSBuildTextLogs(t *testing.T) { + const warning = `C:\src\a.cpp(5,3): warning C4996: 'strcpy': unsafe [C:\src\a.vcxproj]` + testCases := []struct { + lang, banner, start, project, result, warnings, errors string + wantResult string + wantReported *Counts + }{ + { + lang: "zh-CN", + banner: "适用于 .NET Framework MSBuild 版本 17.14.40+3e7442088", start: "生成启动时间为 2026/09/20 11:38:29。", + project: ` 1>项目“C:\src\a.sln”在节点 1 上(默认目标)。`, + result: "已成功生成。", warnings: " 1 个警告", errors: " 0 个错误", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + { + lang: "zh-CN failed", + start: "生成启动时间为 2018/3/13 星期二 20:57:50。", project: `1>项目“C:\src\a.sln”在节点 2 上(Build 个目标)。`, + result: "生成失败。", warnings: " 1 个警告", errors: " 1 个错误", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "zh-TW", + start: "已經開始建置於 2014/10/18 下午 02:46:20。", project: `1>節點 2 (Build 目標) 上的專案 "C:\src\a.sln"。`, + result: "建置成功。", + wantResult: ResultSucceeded, + }, + { + lang: "ja", + start: "2020/05/16 15:14:01 にビルドを開始しました。", project: `1>ノード 2 上のプロジェクト "C:\src\a.sln" (Build ターゲット)。`, + result: "ビルドに成功しました。", warnings: " 1 個の警告", errors: " 0 エラー", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + { + lang: "ko", + start: "빌드 시작: 2017-10-12 오전 2:11:06", project: `1>2 노드의 "C:\src\a.sln" 프로젝트(Build 대상)입니다.`, + result: "빌드하지 못했습니다.", + wantResult: ResultFailed, + }, + { + lang: "de", + start: "Der Buildvorgang wurde am 26.01.2015 01:09:39 gestartet.", project: `1>Projekt "C:\src\a.sln" auf Knoten "2", Rebuild Ziel(e).`, + result: "Build erfolgreich.", + wantResult: ResultSucceeded, + }, + { + lang: "fr", + start: "La génération a démarré 09/04/2015 11:37:23.", project: `1>Projet "C:\src\a.sln" sur le nœud 2 (Build cible(s)).`, + result: "ÉCHEC de la build.", warnings: " 1 Avertissement(s)", errors: " 1 Erreur(s)", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "ru", + start: "Сборка начата 04.04.2015 16:03:05.", project: `1>Проект "C:\src\a.sln" в узле 5 (целевые объекты Build).`, + result: "Сборка успешно завершена.", warnings: "Предупреждений: 1", errors: "Ошибок: 0", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + // The current translations in MSBuild's resources. + { + lang: "de current", + start: "Der Buildvorgang wurde am 28.10.2026 09:00:00 gestartet.", project: `1>Projekt "C:\src\a.sln" auf Knoten "1" (Standardziele).`, + result: "Der Buildvorgang wurde erfolgreich ausgeführt.", warnings: " 1 Warnung(en)", errors: " 0 Fehler", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + { + lang: "de failed", + start: "Der Buildvorgang wurde am 28.10.2026 09:00:00 gestartet.", project: `1>Projekt "C:\src\a.sln" auf Knoten "1" (Standardziele).`, + result: "Fehler beim Buildvorgang.", warnings: " 1 Warnung(en)", errors: " 1 Fehler", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "ko succeeded", + start: "빌드 시작: 2026-10-28 오전 9:00:00", project: `1>1 노드의 "C:\src\a.sln" 프로젝트(기본 대상)입니다.`, + result: "빌드했습니다.", warnings: " 경고 1개", errors: " 오류 0개", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + { + lang: "ko failed with counts", + start: "빌드 시작: 2026-10-28 오전 9:00:00", project: `1>1 노드의 "C:\src\a.sln" 프로젝트(기본 대상)입니다.`, + result: "빌드하지 못했습니다.", warnings: " 경고 1개", errors: " 오류 1개", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "zh-TW failed", + start: "已經開始建置於 2026/10/28 上午 09:00:00。", project: `1>節點 1 (預設目標) 上的專案 "C:\src\a.sln"。`, + result: "建置失敗。", warnings: " 1 個警告", errors: " 1 個錯誤", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "ja failed", + start: "2026/10/28 9:00:00 にビルドを開始しました。", project: `1>ノード 1 上のプロジェクト "C:\src\a.sln" (既定のターゲット)。`, + result: "ビルドに失敗しました。", warnings: " 1 個の警告", errors: " 1 エラー", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + { + lang: "fr succeeded", + start: "La génération a démarré 28/10/2026 09:00:00.", project: `1>Projet "C:\src\a.sln" sur le nœud 1 (cibles par défaut).`, + result: "La génération a réussi.", warnings: " 1 Avertissement(s)", errors: " 0 Erreur(s)", + wantResult: ResultSucceeded, wantReported: &Counts{Warnings: 1}, + }, + { + lang: "ru failed", + start: "Сборка начата 28.10.2026 9:00:00.", project: `1>Проект "C:\src\a.sln" в узле 1 (целевые объекты по умолчанию).`, + result: "Ошибка сборки.", warnings: "Предупреждений: 1", errors: "Ошибок: 1", + wantResult: ResultFailed, wantReported: &Counts{Warnings: 1, Errors: 1}, + }, + } + for _, tc := range testCases { + t.Run(tc.lang, func(t *testing.T) { + lines := []string{} + if tc.banner != "" { + lines = append(lines, tc.banner) + } + lines = append(lines, tc.start, tc.project, " 1>"+warning, tc.result) + if tc.warnings != "" { + lines = append(lines, "", " "+warning, "", tc.warnings, tc.errors) + } + report, err := Parse([]byte("\ufeff" + strings.Join(lines, "\r\n") + "\r\n")) + require.NoError(t, err) + + require.Len(t, report.Logs, 1) + l := report.Logs[0] + assert.Equal(t, FormatMSBuildText, l.Format) + assert.Equal(t, `C:\src\a.sln`, l.Solution) + assert.Equal(t, tc.wantResult, l.Result) + assert.Equal(t, tc.wantReported, l.Reported) + assert.Equal(t, Counts{Warnings: 1}, l.Parsed, "the summary repeats the warning: it is read once") + assert.Equal(t, map[string]int{codeUnsafeCRT: 1}, report.Summary.ByCode) + }) + } +} + +func TestParseCIPrefixedMSBuildLogs(t *testing.T) { + // CI systems prefix every console line, and the build starts after the + // output of earlier steps. + steps := strings.Repeat("##[group]Run some earlier step with a long output line to push MSBuild past the first kilobytes\n", 200) + body := []string{ + "MSBuild version 17.14.60+43b635718 for .NET Framework", + "Build started 10/20/2026 9:31:59 PM.", + ` 1>Project "C:\src\a.sln" on node 1 (default targets).`, + ` 1>C:\src\a.cpp(5,3): warning C4996: 'strcpy': unsafe [C:\src\a.vcxproj]`, + "Build succeeded.", + ` C:\src\a.cpp(5,3): warning C4996: 'strcpy': unsafe [C:\src\a.vcxproj]`, + " 1 Warning(s)", + " 0 Error(s)", + } + for name, prefix := range map[string]string{ + "GitHub Actions or Azure DevOps": "2026-10-20T21:31:58.4328243Z ", + "AppVeyor": "[00:01:12] ", + } { + t.Run(name, func(t *testing.T) { + var b strings.Builder + for _, l := range strings.Split(strings.TrimSuffix(steps, "\n"), "\n") { + b.WriteString(prefix + l + "\n") + } + for _, l := range body { + b.WriteString(prefix + l + "\n") + } + + report, err := Parse([]byte(b.String())) + require.NoError(t, err) + require.Len(t, report.Logs, 1) + l := report.Logs[0] + assert.Equal(t, `C:\src\a.sln`, l.Solution) + assert.Equal(t, ResultSucceeded, l.Result) + assert.Equal(t, &Counts{Warnings: 1}, l.Reported) + assert.Equal(t, Counts{Warnings: 1}, l.Parsed) + assert.Equal(t, `C:\src\a.cpp`, report.Diagnostics[0].File) + }) + } +} + +func TestParseDotnetBuildConsoleLog(t *testing.T) { + // `dotnet build` prints neither a banner nor "Build started": its result + // line is what identifies the log. + for lang, log := range map[string]string{ + "en": " Determining projects to restore...\n" + + `/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]` + "\n" + + "Build succeeded.\n" + `/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]` + "\n" + + " 1 Warning(s)\n 0 Error(s)\n", + "ru": `/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]` + "\n" + + "Сборка успешно завершена.\n" + `/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj]` + "\n" + + "Предупреждений: 1\nОшибок: 0\n", + } { + t.Run(lang, func(t *testing.T) { + report, err := Parse([]byte(log)) + require.NoError(t, err) + require.Len(t, report.Logs, 1) + assert.Equal(t, ResultSucceeded, report.Logs[0].Result) + assert.Equal(t, &Counts{Warnings: 1}, report.Logs[0].Reported) + assert.Equal(t, Counts{Warnings: 1}, report.Logs[0].Parsed) + }) + } +} diff --git a/pkg/attestation/crafter/materials/buildlog/text.go b/pkg/attestation/crafter/materials/buildlog/text.go new file mode 100644 index 000000000..4f5d2367f --- /dev/null +++ b/pkg/attestation/crafter/materials/buildlog/text.go @@ -0,0 +1,337 @@ +// +// 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 buildlog + +import ( + "bufio" + "bytes" + "fmt" + "html" + "regexp" + "strconv" + "strings" + + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +var ( + // diagnosticRe matches MSBuild's canonical error format, which both + // MSBuild and the Visual C++ tools write: + // origin(location) : [subcategory] category code : text [project] + // The origin is a file, or a tool name such as "LINK" or "lib.lib(obj.obj)"; + // the location is "(line)", "(line,col)", "(line-line)", "(line,col-col)" or + // "(line,col,line,col)". MSBuild prefixes a node id ("3>") when it builds in + // parallel and appends the project in brackets. A bracket starting with "-" + // is a clang-cl warning flag, not a project. + diagnosticRe = regexp.MustCompile(`^\s*(?:\d+(?::\d+)?>)?(.*?)(?:\((\d+)(?:([,-])(\d+))?(?:[,-]\d+)*\))?\s*:\s*(?:[^:]*?\s)?(fatal error|error|warning)(?:\s+([A-Za-z]+[0-9]+))?\s*:\s*(.*?)(?:\s+\[([^\]\-][^\]]*)\])?\s*$`) + // MSBuild's own lines are translated when Visual Studio runs in another + // language, while compiler diagnostics keep the canonical English form. The + // patterns below hold the English line, its zh-CN, zh-TW, ja, ko, de, fr and + // ru translations in MSBuild's resources, and older translations seen in + // real logs. A translation not listed leaves the fact unknown, which the + // policy treats as incomplete evidence. + + // msbuildBannerRe extracts the MSBuild version from the console banner. + msbuildBannerRe = regexp.MustCompile(`^(?:MSBuild (?i:version)|Microsoft \(R\) Build Engine (?i:version)|.*MSBuild 版本) ([0-9][^\s]*)`) + // msbuildStartRe and the result patterns delimit one MSBuild invocation: the + // diagnostics logged while it runs, then the end-of-build summary that + // repeats them. + msbuildStartRe = regexp.MustCompile(`^(?:Build started\b|生成启动时间为|已經開始建置於|빌드 시작:|Der Buildvorgang wurde am .* gestartet\.|La génération a démarré|Сборка начата)|にビルドを開始しました。\s*$`) + msbuildSucceededRe = regexp.MustCompile(`^(?:Build succeeded\.|已成功生成。|生成成功。|建置成功。|ビルドに成功しました。|빌드했습니다\.|Der Buildvorgang wurde erfolgreich ausgeführt\.|Build erfolgreich\.|La génération a réussi\.|Сборка успешно завершена\.)\s*$`) + msbuildFailedRe = regexp.MustCompile(`^(?:Build FAILED\.|生成失败。|建置失敗。|ビルドに失敗しました。|빌드하지 못했습니다\.|Fehler beim Buildvorgang\.|ÉCHEC de la build\.|Ошибка сборки\.)\s*$`) + // msbuildProjectRe matches the start of a project build, + // `Project "path" on node N`, which may follow other text on its line. + msbuildProjectRe = regexp.MustCompile(`\bProject "([^"]+)" on node \d+|项目“([^”]+)”在节点 \d+ 上|節點 \d+ .*?上的專案 "([^"]+)"|ノード \d+ 上のプロジェクト "([^"]+)"|\d+ 노드의 "([^"]+)" 프로젝트|Projekt "([^"]+)" auf Knoten|Projet "([^"]+)" sur le nœud|Проект "([^"]+)" в узле`) + // msbuildWarningsRe and msbuildErrorsRe match the summary counters + // "N Warning(s)" and "N Error(s)". + msbuildWarningsRe = regexp.MustCompile(`^\s*(?:(\d+) (?:Warning\(s\)|个警告|個警告|個の警告|Warnung\(en\)|Avertissement\(s\))|경고 (\d+)개|Предупреждений: (\d+))\s*$`) + msbuildErrorsRe = regexp.MustCompile(`^\s*(?:(\d+) (?:Error\(s\)|个错误|個錯誤|エラー|Fehler|Erreur\(s\))|오류 (\d+)개|Ошибок: (\d+))\s*$`) + // ciPrefixRe matches what CI systems put before each console line: an ISO + // timestamp (GitHub Actions, Azure DevOps) or "[hh:mm:ss] " (AppVeyor). + ciPrefixRe = regexp.MustCompile(`^(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z |\[\d{2}:\d{2}:\d{2}\] )`) + + // vcbuildProjectRe matches the per-project heading of a Visual C++ build log. + vcbuildProjectRe = regexp.MustCompile(`Build started: Project: ([^,]+),`) + // vcbuildResultRe matches the per-project result line "name - N error(s), M warning(s)". + vcbuildResultRe = regexp.MustCompile(`^.+ - (\d+) error\(s\), (\d+) warning\(s\)`) + // htmlTagRe matches the markup a Visual C++ build log wraps its text in. + htmlTagRe = regexp.MustCompile(`<[^>]*>`) +) + +// maxLineLength bounds one log line. Diagnostic-verbosity logs echo whole +// compiler command lines and item lists, so lines of tens of kilobytes are +// normal. +const maxLineLength = 64 << 20 + +// sniffText decides whether peeked bytes start a text build log, and which +// kind. It returns the empty string for anything else. An MSBuild log is +// recognized by its banner, "Build started" or result line anywhere in the +// peek: a CI console capture shows the output of earlier steps first, and +// `dotnet build` prints neither a banner nor "Build started". +func sniffText(peek []byte) string { + head, err := decodeText(peek) + if err != nil { + return "" + } + if strings.Contains(strings.ToLower(head), " 0 +} + +func (r *run) count() *Counts { + if r.reported == nil { + r.reported = &Counts{} + } + return r.reported +} + +// log returns the run as a parsed log. Its diagnostics are those logged while +// building, or the summary's when the log only kept the summary (-clp:Summary). +func (r *run) log() *parsedLog { + diagnostics := r.inline + if len(diagnostics) == 0 { + diagnostics = r.summary + } + return &parsedLog{ + format: FormatMSBuildText, + tool: Tool{Name: ToolMSBuild, Version: r.version}, + solution: r.solution, + result: r.result, + diagnostics: diagnostics, + reported: r.reported, + } +} + +// parseText reads the builds recorded in a text build log of the given format, +// streaming it line by line so that large diagnostic-verbosity logs are not +// held in memory. +func parseText(r *bufio.Reader, format string) ([]*parsedLog, error) { + peek, _ := r.Peek(4096) + scanner := bufio.NewScanner(transform.NewReader(r, textDecoder(peek))) + scanner.Buffer(make([]byte, 0, 64<<10), maxLineLength) + + var logs []*parsedLog + var err error + if format == FormatVCBuildHTML { + var log *parsedLog + if log, err = scanVCBuild(scanner); log != nil { + logs = []*parsedLog{log} + } + } else { + logs, err = scanMSBuild(scanner) + } + if err != nil { + return nil, fmt.Errorf("reading build log: %w", err) + } + return logs, nil +} + +// scanVCBuild reads a Visual C++ build log: one project's build. +func scanVCBuild(scanner *bufio.Scanner) (*parsedLog, error) { + log := &parsedLog{format: FormatVCBuildHTML, tool: Tool{Name: ToolVCBuild}} + var project string + for scanner.Scan() { + line := strings.TrimRight(scanner.Text(), "\r") + if m := vcbuildProjectRe.FindStringSubmatch(line); m != nil { + project = strings.TrimSpace(html.UnescapeString(m[1])) + continue + } + line = html.UnescapeString(htmlTagRe.ReplaceAllString(line, "")) + if m := vcbuildResultRe.FindStringSubmatch(line); m != nil { + if log.reported == nil { + log.reported = &Counts{} + } + errs, _ := strconv.Atoi(m[1]) + warns, _ := strconv.Atoi(m[2]) + log.reported.Errors += errs + log.reported.Warnings += warns + if log.reported.Errors > 0 { + log.result = ResultFailed + } else { + log.result = ResultSucceeded + } + continue + } + if d, ok := parseDiagnosticLine(line); ok { + if d.Project == "" { + d.Project = project + } + log.diagnostics = append(log.diagnostics, d) + } + } + return log, scanner.Err() +} + +// scanMSBuild reads an MSBuild text log. Several invocations may be appended to +// one file (one per solution); each is returned as a log of its own, so that +// their warnings stay attributed to the solution that produced them. A run +// starts at its banner or "Build started" line and ends with the counts of its +// summary; at minimal verbosity there is no "Build started", and with -nologo +// no banner, so any of them delimits it. +func scanMSBuild(scanner *bufio.Scanner) ([]*parsedLog, error) { + var logs []*parsedLog + cur := &run{} + next := func() { + if cur.build() { + logs = append(logs, cur.log()) + } + cur = &run{} + } + for scanner.Scan() { + line := ciPrefixRe.ReplaceAllString(strings.TrimRight(scanner.Text(), "\r"), "") + + switch { + case msbuildBannerRe.MatchString(line): + next() + cur.version, cur.opened = msbuildBannerRe.FindStringSubmatch(line)[1], true + continue + case msbuildStartRe.MatchString(line): + // Keep what came before it too: MSBuild reports some command-line + // errors before it starts building. + if cur.inSummary || cur.result != "" { + next() + } + cur.opened = true + continue + case msbuildSucceededRe.MatchString(line): + cur.inSummary, cur.result = true, ResultSucceeded + continue + case msbuildFailedRe.MatchString(line): + cur.inSummary, cur.result = true, ResultFailed + continue + } + if cur.solution == "" { + // The first project a run builds is the one it was started for. + if m := msbuildProjectRe.FindStringSubmatch(line); m != nil { + cur.solution = firstMatch(m) + } + } + if cur.inSummary { + if m := msbuildWarningsRe.FindStringSubmatch(line); m != nil { + n, _ := strconv.Atoi(firstMatch(m)) + cur.count().Warnings += n + continue + } + if m := msbuildErrorsRe.FindStringSubmatch(line); m != nil { + n, _ := strconv.Atoi(firstMatch(m)) + cur.count().Errors += n + // The error count closes the run's summary. + next() + continue + } + } + if d, ok := parseDiagnosticLine(line); ok { + if cur.inSummary { + cur.summary = append(cur.summary, d) + } else { + cur.inline = append(cur.inline, d) + } + } + } + next() + return logs, scanner.Err() +} + +// parseDiagnosticLine parses one line in canonical error format. +func parseDiagnosticLine(line string) (Diagnostic, bool) { + m := diagnosticRe.FindStringSubmatch(line) + if m == nil { + return Diagnostic{}, false + } + d := Diagnostic{ + File: strings.TrimSpace(m[1]), + Severity: SeverityWarning, + Code: m[6], + Message: m[7], + Project: m[8], + } + if m[5] != "warning" { + d.Severity = SeverityError + } + d.Line, _ = strconv.Atoi(m[2]) + // "(line,col)" carries a column; "(line-line)" is a range of lines. + if m[3] == "," { + d.Column, _ = strconv.Atoi(m[4]) + } + return d, true +} diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index d897cb4fd..d7dbc0151 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -491,6 +491,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewCheckmarxCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_OVERSECURED_JSON: crafter, err = NewOversecuredCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_MSBUILD_LOG: + crafter, err = NewMSBuildLogCrafter(materialSchema, casBackend, logger) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) } diff --git a/pkg/attestation/crafter/materials/msbuild_log.go b/pkg/attestation/crafter/materials/msbuild_log.go new file mode 100644 index 000000000..ee2d7bf0c --- /dev/null +++ b/pkg/attestation/crafter/materials/msbuild_log.go @@ -0,0 +1,105 @@ +// +// 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" + "errors" + "fmt" + "slices" + "strconv" + + 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/buildlog" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +const ( + // AnnotationMSBuildLogLogsCount is the number of build logs in an MSBUILD_LOG + // material: 1 for a single log, or the number of logs found in an archive. + AnnotationMSBuildLogLogsCount = "chainloop.material.msbuild_log.logs.count" + // AnnotationMSBuildLogWarningsCount and AnnotationMSBuildLogErrorsCount are the + // number of distinct warnings and errors in an MSBUILD_LOG material. + AnnotationMSBuildLogWarningsCount = "chainloop.material.msbuild_log.warnings.count" + AnnotationMSBuildLogErrorsCount = "chainloop.material.msbuild_log.errors.count" +) + +// MSBuildLogCrafter stores the log of a Microsoft build (MSBuild binary or text +// log, or a Visual C++ BuildLog.htm) as evidence of the warnings and errors the +// build produced. The log is stored as-is; its projection to the JSON the +// policy engine consumes happens later, at evaluation time. +// +// The value may also be an archive of logs, for builds that produce one binary +// log per solution. The archive is recorded whole and the projection +// aggregates its entries. +type MSBuildLogCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +func NewMSBuildLogCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*MSBuildLogCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_MSBUILD_LOG { + return nil, fmt.Errorf("material type is not a build log") + } + craftCommon := &crafterCommon{logger: l, input: schema} + return &MSBuildLogCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +func (i *MSBuildLogCrafter) Craft(ctx context.Context, filePath string) (*CraftResult, error) { + // Parse with the same code the policy projection uses, so a material + // accepted here is one the projection can read. Were the two to disagree, a + // log would be attested and then fail, or worse pass, the policy gate. + report, err := buildlog.ParseFile(filePath) + if err != nil { + if errors.Is(err, buildlog.ErrNoBuildLog) { + return nil, fmt.Errorf("input does not look like a build log: %w", ErrInvalidMaterialType) + } + return nil, err + } + + m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, report) + + return craftResult(m, nil) +} + +func (i *MSBuildLogCrafter) injectAnnotations(m *api.Attestation_Material, report *buildlog.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 + } + // An archive can hold the logs of several tools: list each of them. + var tools []Tool + for _, l := range report.Logs { + tool := Tool{Name: l.Tool.Name, Version: l.Tool.Version} + if !slices.Contains(tools, tool) { + tools = append(tools, tool) + } + } + SetToolsAnnotation(m, tools) + m.Annotations[AnnotationMSBuildLogLogsCount] = strconv.Itoa(len(report.Logs)) + m.Annotations[AnnotationMSBuildLogWarningsCount] = strconv.Itoa(report.Summary.Warnings) + m.Annotations[AnnotationMSBuildLogErrorsCount] = strconv.Itoa(report.Summary.Errors) +} diff --git a/pkg/attestation/crafter/materials/msbuild_log_test.go b/pkg/attestation/crafter/materials/msbuild_log_test.go new file mode 100644 index 000000000..8f0e778d7 --- /dev/null +++ b/pkg/attestation/crafter/materials/msbuild_log_test.go @@ -0,0 +1,215 @@ +// +// 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 ( + "bytes" + "compress/gzip" + "context" + "os" + "path/filepath" + "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/attestation/crafter/materials/buildlog" + "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" +) + +// Values that other tests of the package repeat. +const ( + buildLogMissingFile = "no such file or directory" + buildLogOtherMaterial = "./testdata/sbom-spdx.json" +) + +func TestNewMSBuildLogCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "build log material", + input: &contractAPI.CraftingSchema_Material{Type: contractAPI.CraftingSchema_Material_MSBUILD_LOG}, + }, + { + name: "other material type", + input: &contractAPI.CraftingSchema_Material{Type: contractAPI.CraftingSchema_Material_SARIF}, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := materials.NewMSBuildLogCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +// writeBuildLogZip zips the binary logs of two solutions, the form used when a +// build produces one binary log per solution. +func writeBuildLogZip(t *testing.T) string { + t.Helper() + entries := map[string][]byte{} + for _, n := range []string{"sample.binlog", "lib.binlog"} { + content, err := os.ReadFile(filepath.Join("./testdata/buildlog", n)) + require.NoError(t, err) + entries["logs/"+n] = content + } + p := filepath.Join(t.TempDir(), "logs.zip") + writeZip(t, p, entries) + return p +} + +// writeMixedBuildLogZip zips a binary log with a Visual C++ build log, as a +// product that builds with both tools does. +func writeMixedBuildLogZip(t *testing.T) string { + t.Helper() + entries := map[string][]byte{} + for _, n := range []string{"sample.binlog", "vcbuild.htm"} { + content, err := os.ReadFile(filepath.Join("./testdata/buildlog", n)) + require.NoError(t, err) + entries["logs/"+n] = content + } + p := filepath.Join(t.TempDir(), "logs.zip") + writeZip(t, p, entries) + return p +} + +// writeOldBinlog writes a binary log header in a format version that predates +// length-prefixed records. +func writeOldBinlog(t *testing.T) string { + t.Helper() + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + _, err := zw.Write([]byte{14, 0, 0, 0, 14, 0, 0, 0}) + require.NoError(t, err) + require.NoError(t, zw.Close()) + p := filepath.Join(t.TempDir(), "old.binlog") + require.NoError(t, os.WriteFile(p, buf.Bytes(), 0o600)) + return p +} + +func TestMSBuildLogCrafter_Craft(t *testing.T) { + testCases := []struct { + name string + filePath func(t *testing.T) string + wantErr string + annotations map[string]string + }{ + { + name: "missing file", + filePath: func(*testing.T) string { return "./testdata/non-existing.binlog" }, + wantErr: buildLogMissingFile, + }, + { + name: "not a build log", + filePath: func(*testing.T) string { return buildLogOtherMaterial }, + wantErr: "does not look like a build log", + }, + { + name: "unsupported binary log version", + filePath: writeOldBinlog, + wantErr: "unsupported MSBuild binary log version 14", + }, + { + name: "msbuild binary log", + filePath: func(*testing.T) string { return "./testdata/buildlog/sample.binlog" }, + annotations: map[string]string{ + materials.AnnotationToolNameKey: buildlog.ToolMSBuild, + materials.AnnotationMSBuildLogLogsCount: "1", + materials.AnnotationMSBuildLogWarningsCount: "4", + materials.AnnotationMSBuildLogErrorsCount: "0", + }, + }, + { + name: "msbuild text log", + filePath: func(*testing.T) string { return "./testdata/buildlog/msbuild.txt" }, + annotations: map[string]string{ + materials.AnnotationToolNameKey: buildlog.ToolMSBuild, + materials.AnnotationMSBuildLogWarningsCount: "4", + }, + }, + { + name: "visual c++ build log", + filePath: func(*testing.T) string { return "./testdata/buildlog/vcbuild.htm" }, + annotations: map[string]string{ + materials.AnnotationToolNameKey: buildlog.ToolVCBuild, + materials.AnnotationMSBuildLogWarningsCount: "4", + }, + }, + { + name: "archive of binary logs", + filePath: writeBuildLogZip, + annotations: map[string]string{ + materials.AnnotationToolNameKey: buildlog.ToolMSBuild, + materials.AnnotationMSBuildLogLogsCount: "2", + materials.AnnotationMSBuildLogWarningsCount: "4", + }, + }, + { + name: "archive of binary and Visual C++ build logs", + filePath: writeMixedBuildLogZip, + annotations: map[string]string{ + materials.AnnotationToolsKey: `["msbuild","vcbuild"]`, + materials.AnnotationMSBuildLogLogsCount: "2", + }, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "build-log", + Type: contractAPI.CraftingSchema_Material_MSBUILD_LOG, + } + + 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.NewMSBuildLogCrafter(schema, backend, &l) + require.NoError(t, err) + + got, err := craftedMaterial(crafter.Craft(context.TODO(), tc.filePath(t))) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, contractAPI.CraftingSchema_Material_MSBUILD_LOG.String(), got.MaterialType.String()) + assert.True(t, got.UploadedToCas) + for k, v := range tc.annotations { + assert.Equal(t, v, got.Annotations[k], k) + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/testdata/buildlog/lib.binlog b/pkg/attestation/crafter/materials/testdata/buildlog/lib.binlog new file mode 100644 index 000000000..5c6c38053 Binary files /dev/null and b/pkg/attestation/crafter/materials/testdata/buildlog/lib.binlog differ diff --git a/pkg/attestation/crafter/materials/testdata/buildlog/msbuild.txt b/pkg/attestation/crafter/materials/testdata/buildlog/msbuild.txt new file mode 100644 index 000000000..343bed75e --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/buildlog/msbuild.txt @@ -0,0 +1,179 @@ +Build started 10/28/2026 05:20:01. +Logging verbosity is set to: Normal. 1>Project "/src/Sample.sln" on node 1 (Restore target(s)). + 1>ValidateSolutionConfiguration: + Building solution configuration "Debug|Any CPU". + _GetAllRestoreProjectPathItems: + Determining projects to restore... + Restore: + X.509 certificate chain validation will use the fallback certificate bundle at '/usr/share/dotnet/sdk/9.0.320/trustedroots/codesignctl.pem'. + X.509 certificate chain validation will use the fallback certificate bundle at '/usr/share/dotnet/sdk/9.0.320/trustedroots/timestampctl.pem'. + Restoring packages for /src/Lib/Lib.csproj... + Restoring packages for /src/App/App.csproj... + Generating MSBuild file /src/Lib/obj/Lib.csproj.nuget.g.props. + Generating MSBuild file /src/Lib/obj/Lib.csproj.nuget.g.targets. + Writing assets file to disk. Path: /src/Lib/obj/project.assets.json + Generating MSBuild file /src/App/obj/App.csproj.nuget.g.props. + Generating MSBuild file /src/App/obj/App.csproj.nuget.g.targets. + Writing assets file to disk. Path: /src/App/obj/project.assets.json + Restored /src/App/App.csproj (in 45 ms). + Restored /src/Lib/Lib.csproj (in 41 ms). + + NuGet Config files used: + /root/.nuget/NuGet/NuGet.Config + + Feeds used: + https://api.nuget.org/v3/index.json + 1>Done Building Project "/src/Sample.sln" (Restore target(s)). + 1:2>Project "/src/Sample.sln" on node 1 (default targets). + 1>ValidateSolutionConfiguration: + Building solution configuration "Debug|Any CPU". + 1:2>Project "/src/Sample.sln" (1:2) is building "/src/Lib/Lib.csproj" (3:7) on node 2 (default targets). + 3>PrepareForBuild: + Creating directory "bin/Debug/net9.0/". + Creating directory "obj/Debug/net9.0/". + Creating directory "/src/Lib/obj/Debug/net9.0/refint/". + Creating directory "obj/Debug/net9.0/ref". + 1:2>Project "/src/Sample.sln" (1:2) is building "/src/App/App.csproj" (2:6) on node 1 (default targets). + 2>PrepareForBuild: + Creating directory "bin/Debug/net9.0/". + Creating directory "obj/Debug/net9.0/". + Creating directory "/src/App/obj/Debug/net9.0/refint/". + Creating directory "obj/Debug/net9.0/ref". + 3>_GenerateSourceLinkFile: + Source Link is empty, file 'obj/Debug/net9.0/Lib.sourcelink.json' does not exist. + CoreCompile: + /usr/share/dotnet/dotnet exec "/usr/share/dotnet/sdk/9.0.320/Roslyn/bincore/csc.dll" /noconfig /unsafe- /checked- /nowarn:1701,1702,1701,1702 /fullpaths /nostdlib+ /errorreport:prompt /warn:9 /define:TRACE;DEBUG;NET;NET9_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER /highentropyva+ /nullable:enable /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.CSharp.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.VisualBasic.Core.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.VisualBasic.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.Win32.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.Win32.Registry.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/mscorlib.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/netstandard.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.AppContext.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Buffers.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Concurrent.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Immutable.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.NonGeneric.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Specialized.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.Annotations.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.DataAnnotations.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.EventBasedAsync.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.TypeConverter.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Configuration.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Console.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Core.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.Common.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.DataSetExtensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Contracts.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Debug.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.DiagnosticSource.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.FileVersionInfo.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Process.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.StackTrace.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.TextWriterTraceListener.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Tools.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.TraceSource.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Tracing.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Drawing.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Drawing.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Dynamic.Runtime.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Formats.Asn1.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Formats.Tar.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.Calendars.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.Brotli.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.FileSystem.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.ZipFile.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.DriveInfo.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.Watcher.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.IsolatedStorage.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.MemoryMappedFiles.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipelines.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipes.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipes.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.UnmanagedMemoryStream.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Expressions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Parallel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Queryable.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Memory.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Http.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Http.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.HttpListener.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Mail.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.NameResolution.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.NetworkInformation.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Ping.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Quic.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Requests.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Security.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.ServicePoint.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Sockets.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebClient.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebHeaderCollection.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebProxy.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebSockets.Client.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebSockets.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Numerics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Numerics.Vectors.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ObjectModel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.DispatchProxy.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.ILGeneration.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.Lightweight.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Metadata.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.TypeExtensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.Reader.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.ResourceManager.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.Writer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.CompilerServices.Unsafe.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.CompilerServices.VisualC.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Handles.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.JavaScript.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.RuntimeInformation.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Intrinsics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Loader.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Numerics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Formatters.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Xml.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Claims.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Algorithms.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Cng.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Csp.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Encoding.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.OpenSsl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.X509Certificates.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Principal.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Principal.Windows.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.SecureString.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ServiceModel.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ServiceProcess.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.CodePages.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encodings.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.RegularExpressions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Channels.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Overlapped.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Dataflow.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Parallel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Thread.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.ThreadPool.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Timer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Transactions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Transactions.Local.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ValueTuple.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Web.HttpUtility.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Windows.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.Linq.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.ReaderWriter.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.Serialization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XmlDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XmlSerializer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XPath.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XPath.XDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/WindowsBase.dll /debug+ /debug:portable /filealign:512 /optimize- /out:obj/Debug/net9.0/Lib.dll /refout:obj/Debug/net9.0/refint/Lib.dll /target:library /warnaserror- /utf8output /deterministic+ /langversion:13.0 /analyzerconfig:/src/.editorconfig /analyzerconfig:obj/Debug/net9.0/Lib.GeneratedMSBuildEditorConfig.editorconfig /analyzerconfig:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_9_default.globalconfig /analyzer:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll /analyzer:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll Hashing.cs "obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs" obj/Debug/net9.0/Lib.AssemblyInfo.cs /warnaserror+:NU1605,SYSLIB0011 + 3>/src/Lib/Hashing.cs(9,25): warning CA5351: Weak uses a broken cryptographic algorithm MD5 (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351) [/src/Lib/Lib.csproj] + CompilerServer: server - server processed compilation - Lib (net9.0) + CopyFilesToOutputDirectory: + Copying file from "/src/Lib/obj/Debug/net9.0/Lib.dll" to "/src/Lib/bin/Debug/net9.0/Lib.dll". + Copying reference assembly from "obj/Debug/net9.0/refint/Lib.dll" to "/src/Lib/obj/Debug/net9.0/ref/Lib.dll". + Lib -> /src/Lib/bin/Debug/net9.0/Lib.dll + Copying file from "/src/Lib/obj/Debug/net9.0/Lib.pdb" to "/src/Lib/bin/Debug/net9.0/Lib.pdb". + 3>Done Building Project "/src/Lib/Lib.csproj" (default targets). + 2>_GenerateSourceLinkFile: + Source Link is empty, file 'obj/Debug/net9.0/App.sourcelink.json' does not exist. + CoreCompile: + /usr/share/dotnet/dotnet exec "/usr/share/dotnet/sdk/9.0.320/Roslyn/bincore/csc.dll" /noconfig /unsafe- /checked- /nowarn:1701,1702,1701,1702 /fullpaths /nostdlib+ /errorreport:prompt /warn:9 /define:TRACE;DEBUG;NET;NET9_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER /highentropyva+ /nullable:enable /reference:/src/Lib/obj/Debug/net9.0/ref/Lib.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.CSharp.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.VisualBasic.Core.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.VisualBasic.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.Win32.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/Microsoft.Win32.Registry.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/mscorlib.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/netstandard.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.AppContext.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Buffers.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Concurrent.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Immutable.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.NonGeneric.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Collections.Specialized.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.Annotations.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.DataAnnotations.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.EventBasedAsync.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ComponentModel.TypeConverter.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Configuration.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Console.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Core.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.Common.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.DataSetExtensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Data.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Contracts.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Debug.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.DiagnosticSource.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.FileVersionInfo.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Process.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.StackTrace.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.TextWriterTraceListener.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Tools.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.TraceSource.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Diagnostics.Tracing.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Drawing.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Drawing.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Dynamic.Runtime.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Formats.Asn1.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Formats.Tar.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.Calendars.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Globalization.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.Brotli.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.FileSystem.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Compression.ZipFile.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.DriveInfo.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.FileSystem.Watcher.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.IsolatedStorage.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.MemoryMappedFiles.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipelines.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipes.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.Pipes.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.IO.UnmanagedMemoryStream.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Expressions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Parallel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Linq.Queryable.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Memory.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Http.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Http.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.HttpListener.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Mail.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.NameResolution.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.NetworkInformation.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Ping.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Quic.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Requests.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Security.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.ServicePoint.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.Sockets.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebClient.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebHeaderCollection.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebProxy.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebSockets.Client.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Net.WebSockets.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Numerics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Numerics.Vectors.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ObjectModel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.DispatchProxy.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.ILGeneration.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Emit.Lightweight.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Metadata.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Reflection.TypeExtensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.Reader.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.ResourceManager.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Resources.Writer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.CompilerServices.Unsafe.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.CompilerServices.VisualC.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Handles.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.JavaScript.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.InteropServices.RuntimeInformation.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Intrinsics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Loader.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Numerics.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Formatters.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Runtime.Serialization.Xml.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.AccessControl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Claims.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Algorithms.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Cng.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Csp.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Encoding.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.OpenSsl.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.Primitives.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Cryptography.X509Certificates.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Principal.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.Principal.Windows.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Security.SecureString.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ServiceModel.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ServiceProcess.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.CodePages.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encoding.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Encodings.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.Json.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Text.RegularExpressions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Channels.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Overlapped.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Dataflow.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Extensions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Tasks.Parallel.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Thread.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.ThreadPool.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Threading.Timer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Transactions.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Transactions.Local.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.ValueTuple.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Web.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Web.HttpUtility.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Windows.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.Linq.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.ReaderWriter.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.Serialization.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XmlDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XmlSerializer.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XPath.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/System.Xml.XPath.XDocument.dll /reference:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/ref/net9.0/WindowsBase.dll /debug+ /debug:portable /filealign:512 /optimize- /out:obj/Debug/net9.0/App.dll /refout:obj/Debug/net9.0/refint/App.dll /target:exe /warnaserror- /utf8output /deterministic+ /langversion:13.0 /analyzerconfig:/src/.editorconfig /analyzerconfig:obj/Debug/net9.0/App.GeneratedMSBuildEditorConfig.editorconfig /analyzerconfig:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_9_default.globalconfig /analyzer:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll /analyzer:/usr/share/dotnet/sdk/9.0.320/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll /analyzer:/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/9.0.22/analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll Program.cs "obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs" obj/Debug/net9.0/App.AssemblyInfo.cs /warnaserror+:NU1605,SYSLIB0011 + 2>/src/App/Program.cs(4,12): warning CS8600: Converting null literal or possible null value to non-nullable type. [/src/App/App.csproj] + 2>/src/App/Program.cs(5,19): warning CS8602: Dereference of a possibly null reference. [/src/App/App.csproj] + 2>/src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj] + CompilerServer: server - server processed compilation - App (net9.0) + _CopyFilesMarkedCopyLocal: + Copying file from "/src/Lib/bin/Debug/net9.0/Lib.pdb" to "/src/App/bin/Debug/net9.0/Lib.pdb". + Copying file from "/src/Lib/bin/Debug/net9.0/Lib.dll" to "/src/App/bin/Debug/net9.0/Lib.dll". + Creating "/src/App/obj/Debug/net9.0/App.csproj.Up2Date" because "AlwaysCreate" was specified. + Touching "/src/App/obj/Debug/net9.0/App.csproj.Up2Date". + _CopyOutOfDateSourceItemsToOutputDirectory: + Copying file from "/src/App/obj/Debug/net9.0/apphost" to "/src/App/bin/Debug/net9.0/App". + CopyFilesToOutputDirectory: + Copying file from "/src/App/obj/Debug/net9.0/App.dll" to "/src/App/bin/Debug/net9.0/App.dll". + Copying reference assembly from "obj/Debug/net9.0/refint/App.dll" to "/src/App/obj/Debug/net9.0/ref/App.dll". + App -> /src/App/bin/Debug/net9.0/App.dll + Copying file from "/src/App/obj/Debug/net9.0/App.pdb" to "/src/App/bin/Debug/net9.0/App.pdb". + 2>Done Building Project "/src/App/App.csproj" (default targets). + 1>Done Building Project "/src/Sample.sln" (default targets). +Deferred Messages + + Detailed Build Summary + ====================== + + + ============================== Build Hierarchy (IDs represent configurations) ===================================================== + Id : Exclusive Time Total Time Path (Targets) + ----------------------------------------------------------------------------------------------------------------------------------- + 0 : 0.193s 0.480s /src/Sample.sln (Restore) + | 1 : 0.094s 0.094s /src/App/App.csproj (_IsProjectRestoreSupported) + | 2 : 0.145s 0.145s /src/Lib/Lib.csproj (_IsProjectRestoreSupported) + | 4 : 0.009s 0.009s /src/Lib/Lib.csproj (_GenerateRestoreProjectPathWalk) + | 3 : 0.004s 0.010s /src/App/App.csproj (_GenerateRestoreProjectPathWalk) + | . 4 : 0.000s 0.000s /src/Lib/Lib.csproj (_GenerateRestoreProjectPathWalk) + | 6 : 0.036s 0.036s /src/App/App.csproj (_GenerateRestoreGraphProjectEntry) + | 5 : 0.044s 0.044s /src/Lib/Lib.csproj (_GenerateRestoreGraphProjectEntry) + | 8 : 0.020s 0.020s /src/App/App.csproj (_GenerateProjectRestoreGraph) + . 7 : 0.020s 0.020s /src/Lib/Lib.csproj (_GenerateProjectRestoreGraph) + + ============================== Node Utilization (IDs represent configurations) ==================================================== + Timestamp: 1 2 Duration Cumulative + ----------------------------------------------------------------------------------------------------------------------------------- + 639287616012568408: 0 x 0.018s 0.018s + 639287616012745867: 1 x 0.068s 0.086s . + 639287616013424098: | 2 0.027s 0.112s + 639287616013689116: x | 0.119s 0.231s .. + 639287616014874759: 0 x 0.006s 0.236s + 639287616014932450: 3 4 0.003s 0.239s + 639287616014958322: x | 0.006s 0.245s + 639287616015019145: 3 x 0.002s 0.247s + 639287616015036614: 0 x 0.002s 0.249s + 639287616015059701: 6 5 0.036s 0.285s + 639287616015414960: x | 0.008s 0.293s + 639287616015497908: 0 x 0.001s 0.294s + 639287616015503473: 8 7 0.020s 0.313s + 639287616015700372: | x 0.000s 0.314s + 639287616015705287: 0 x 0.166s 0.480s ... + ----------------------------------------------------------------------------------------------------------------------------------- + Utilization: 72.3 55.1 Average Utilization: 63.7 + + Detailed Build Summary + ====================== + + + ============================== Build Hierarchy (IDs represent configurations) ===================================================== + Id : Exclusive Time Total Time Path (Targets) + ----------------------------------------------------------------------------------------------------------------------------------- + 9 : 0.003s 1.293s /src/Sample.sln () + | 11 : 1.106s 1.106s /src/Lib/Lib.csproj () + . 10 : 0.259s 1.291s /src/App/App.csproj () + | | 12 : 0.003s 0.003s /src/Lib/Lib.csproj (GetTargetFrameworks) + | | 13 : 0.001s 0.001s /src/Lib/Lib.csproj (GetNativeManifest) + | . 14 : 0.002s 0.002s /src/Lib/Lib.csproj (GetCopyToOutputDirectoryItems) + + ============================== Node Utilization (IDs represent configurations) ==================================================== + Timestamp: 1 2 Duration Cumulative + ----------------------------------------------------------------------------------------------------------------------------------- + 639287616012568408: 0 x 0.018s 0.018s + 639287616012745867: 1 x 0.068s 0.086s . + 639287616013424098: | 2 0.027s 0.112s + 639287616013689116: x | 0.119s 0.231s .. + 639287616014874759: 0 x 0.006s 0.236s + 639287616014932450: 3 4 0.003s 0.239s + 639287616014958322: x | 0.006s 0.245s + 639287616015019145: 3 x 0.002s 0.247s + 639287616015036614: 0 x 0.002s 0.249s + 639287616015059701: 6 5 0.036s 0.285s + 639287616015414960: x | 0.008s 0.293s + 639287616015497908: 0 x 0.001s 0.294s + 639287616015503473: 8 7 0.020s 0.313s + 639287616015700372: | x 0.000s 0.314s + 639287616015705287: 0 x 0.166s 0.480s ... + 639287616017483914: 9 x 0.002s 0.494s + 639287616017504833: 10 11 0.080s 0.573s . + 639287616018303047: x | 1.026s 1.600s .................... + 639287616028564226: x 12 0.004s 1.603s + 639287616028599628: 10 x 0.006s 1.609s + 639287616028661342: x 13 0.001s 1.610s + 639287616028671626: 10 x 0.143s 1.754s .. + 639287616030105598: x 14 0.002s 1.755s + 639287616030121837: 10 x 0.029s 1.784s + 639287616030413264: 9 x 0.001s 1.785s + ----------------------------------------------------------------------------------------------------------------------------------- + Utilization: 34.1 78.2 Average Utilization: 56.2 + +Build succeeded. + + "/src/Sample.sln" (default target) (1:2) -> + "/src/Lib/Lib.csproj" (default target) (3:7) -> + (CoreCompile target) -> + /src/Lib/Hashing.cs(9,25): warning CA5351: Weak uses a broken cryptographic algorithm MD5 (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351) [/src/Lib/Lib.csproj] + + + "/src/Sample.sln" (default target) (1:2) -> + "/src/App/App.csproj" (default target) (2:6) -> + /src/App/Program.cs(4,12): warning CS8600: Converting null literal or possible null value to non-nullable type. [/src/App/App.csproj] + /src/App/Program.cs(5,19): warning CS8602: Dereference of a possibly null reference. [/src/App/App.csproj] + /src/App/Program.cs(3,5): warning CS0168: The variable 'unused' is declared but never used [/src/App/App.csproj] + + 4 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:01.84 diff --git a/pkg/attestation/crafter/materials/testdata/buildlog/sample.binlog b/pkg/attestation/crafter/materials/testdata/buildlog/sample.binlog new file mode 100644 index 000000000..2590a5349 Binary files /dev/null and b/pkg/attestation/crafter/materials/testdata/buildlog/sample.binlog differ diff --git a/pkg/attestation/crafter/materials/testdata/buildlog/vcbuild.htm b/pkg/attestation/crafter/materials/testdata/buildlog/vcbuild.htm new file mode 100644 index 000000000..d6eb953ea Binary files /dev/null and b/pkg/attestation/crafter/materials/testdata/buildlog/vcbuild.htm differ