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