From e1b9dc52033fb2c604024bb172e88b00fa4e63a8 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 6 Dec 2024 11:52:25 +0100 Subject: [PATCH] feat(materials): New JACOCO_XML material type and support for policies (#1624) Signed-off-by: Jose I. Paris --- README.md | 1 + .../workflowcontract/v1/crafting_schema.ts | 7 ++ ...on.v1.Attestation.Material.jsonschema.json | 6 +- ...tation.v1.Attestation.Material.schema.json | 6 +- ...tation.v1.PolicyEvaluation.jsonschema.json | 3 +- ...ttestation.v1.PolicyEvaluation.schema.json | 3 +- ...v1.CraftingSchema.Material.jsonschema.json | 3 +- ...act.v1.CraftingSchema.Material.schema.json | 3 +- ...ct.v1.PolicyGroup.Material.jsonschema.json | 3 +- ...ntract.v1.PolicyGroup.Material.schema.json | 3 +- ...flowcontract.v1.PolicySpec.jsonschema.json | 3 +- ...workflowcontract.v1.PolicySpec.schema.json | 3 +- ...owcontract.v1.PolicySpecV2.jsonschema.json | 3 +- ...rkflowcontract.v1.PolicySpecV2.schema.json | 3 +- .../workflowcontract/v1/crafting_schema.pb.go | 13 ++- .../workflowcontract/v1/crafting_schema.proto | 2 + .../v1/crafting_schema_validations.go | 1 + docs/docs/partials/materials.json | 5 + .../api/attestation/v1/crafting_state.go | 26 +++++ pkg/attestation/crafter/materials/jacoco.go | 74 ++++++++++++++ .../crafter/materials/jacoco/jacoco.go | 26 +++++ .../crafter/materials/jacoco_test.go | 96 +++++++++++++++++++ .../crafter/materials/junit_xml.go | 27 ++---- .../crafter/materials/materials.go | 2 + .../crafter/materials/testdata/jacoco.xml | 80 ++++++++++++++++ 25 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 pkg/attestation/crafter/materials/jacoco.go create mode 100644 pkg/attestation/crafter/materials/jacoco/jacoco.go create mode 100644 pkg/attestation/crafter/materials/jacoco_test.go create mode 100644 pkg/attestation/crafter/materials/testdata/jacoco.xml diff --git a/README.md b/README.md index 328099a36..a76af8cb1 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Chainloop supports the collection of the following list of evidence types. For t - [CSAF VEX](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#45-profile-5-vex) - [Gitlab Security report](https://docs.gitlab.com/ee/user/application_security/) - [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format) +- [JaCoCo XML Coverage Reports](https://www.jacoco.org/jacoco/trunk/doc/) - Attestation: existing Chainloop attestations. - Artifact Type: It represents a software artifact. - Custom Evidence Type: Custom piece of evidence that doesn't fit in any other category, for instance, an approval report in json format, etc. 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 ed8987634..4c64a6729 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -148,6 +148,8 @@ export enum CraftingSchema_Material_MaterialType { GHAS_SECRET_SCAN = 21, /** GHAS_DEPENDENCY_SCAN - https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28 */ GHAS_DEPENDENCY_SCAN = 22, + /** JACOCO_XML - Jacoco coverage reports https://www.jacoco.org/userdoc/importexport.html */ + JACOCO_XML = 23, UNRECOGNIZED = -1, } @@ -222,6 +224,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 22: case "GHAS_DEPENDENCY_SCAN": return CraftingSchema_Material_MaterialType.GHAS_DEPENDENCY_SCAN; + case 23: + case "JACOCO_XML": + return CraftingSchema_Material_MaterialType.JACOCO_XML; case -1: case "UNRECOGNIZED": default: @@ -277,6 +282,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "GHAS_SECRET_SCAN"; case CraftingSchema_Material_MaterialType.GHAS_DEPENDENCY_SCAN: return "GHAS_DEPENDENCY_SCAN"; + case CraftingSchema_Material_MaterialType.JACOCO_XML: + return "JACOCO_XML"; 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 11ffc2566..91bb27622 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 @@ -39,7 +39,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "title": "Material Type", "type": "string" @@ -106,7 +107,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 51e401520..e7f4676c8 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 @@ -39,7 +39,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "title": "Material Type", "type": "string" @@ -106,7 +107,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 fbd61c714..f61995488 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -118,7 +118,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 50b43ff87..e8bc096bd 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -118,7 +118,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 488c8032d..d01a28b7f 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 @@ -46,7 +46,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 827fe49da..f9d60cf81 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 @@ -46,7 +46,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 21a90a0a9..f3ae8150c 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 @@ -44,7 +44,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 6e43c03b9..eb4014342 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 @@ -44,7 +44,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 66ad707fe..701bc3fee 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -50,7 +50,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 f1b7448a1..273b34e43 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -50,7 +50,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 faeb5d805..e287cb403 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -33,7 +33,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 75d3c3bc9..8371a4e72 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -33,7 +33,8 @@ "TWISTCLI_SCAN_JSON", "GHAS_CODE_SCAN", "GHAS_SECRET_SCAN", - "GHAS_DEPENDENCY_SCAN" + "GHAS_DEPENDENCY_SCAN", + "JACOCO_XML" ], "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 51b912177..454fa72e5 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -136,6 +136,8 @@ const ( CraftingSchema_Material_GHAS_SECRET_SCAN CraftingSchema_Material_MaterialType = 21 // https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28 CraftingSchema_Material_GHAS_DEPENDENCY_SCAN CraftingSchema_Material_MaterialType = 22 + // Jacoco coverage reports https://www.jacoco.org/userdoc/importexport.html + CraftingSchema_Material_JACOCO_XML CraftingSchema_Material_MaterialType = 23 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -164,6 +166,7 @@ var ( 20: "GHAS_CODE_SCAN", 21: "GHAS_SECRET_SCAN", 22: "GHAS_DEPENDENCY_SCAN", + 23: "JACOCO_XML", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -189,6 +192,7 @@ var ( "GHAS_CODE_SCAN": 20, "GHAS_SECRET_SCAN": 21, "GHAS_DEPENDENCY_SCAN": 22, + "JACOCO_XML": 23, } ) @@ -1485,7 +1489,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xba, 0x0c, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x0c, 0x0a, 0x0e, 0x43, 0x72, 0x61, 0x66, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x30, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xba, 0x48, 0x06, 0x72, 0x04, 0x0a, @@ -1530,7 +1534,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x4a, 0x45, 0x4e, 0x4b, 0x49, 0x4e, 0x53, 0x5f, 0x4a, 0x4f, 0x42, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x49, 0x52, 0x43, 0x4c, 0x45, 0x43, 0x49, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x10, 0x05, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x47, 0x47, 0x45, - 0x52, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x06, 0x1a, 0xeb, 0x06, 0x0a, + 0x52, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x06, 0x1a, 0xfb, 0x06, 0x0a, 0x08, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x5a, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x39, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, @@ -1554,7 +1558,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xf1, 0x03, 0x0a, 0x0c, 0x4d, 0x61, 0x74, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x81, 0x04, 0x0a, 0x0c, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x41, 0x54, 0x45, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, @@ -1585,7 +1589,8 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x10, 0x14, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x48, 0x41, 0x53, 0x5f, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x10, 0x15, 0x12, 0x18, 0x0a, 0x14, 0x47, 0x48, 0x41, 0x53, 0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, - 0x4e, 0x43, 0x59, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x10, 0x16, 0x22, 0x46, 0x0a, 0x0a, 0x41, 0x6e, + 0x4e, 0x43, 0x59, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x10, 0x16, 0x12, 0x0e, 0x0a, 0x0a, 0x4a, 0x41, + 0x43, 0x4f, 0x43, 0x4f, 0x5f, 0x58, 0x4d, 0x4c, 0x10, 0x17, 0x22, 0x46, 0x0a, 0x0a, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x72, 0x09, 0x32, 0x07, 0x5e, 0x5b, 0x5c, 0x77, 0x5d, 0x2b, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index fd0a5c971..d73068d0f 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -121,6 +121,8 @@ message CraftingSchema { GHAS_SECRET_SCAN = 21; // https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28 GHAS_DEPENDENCY_SCAN = 22; + // Jacoco coverage reports https://www.jacoco.org/userdoc/importexport.html + JACOCO_XML = 23; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index f11428403..98ec1e171 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -39,6 +39,7 @@ var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ CraftingSchema_Material_CSAF_SECURITY_INCIDENT_RESPONSE, CraftingSchema_Material_GITLAB_SECURITY_REPORT, CraftingSchema_Material_JUNIT_XML, + CraftingSchema_Material_JACOCO_XML, CraftingSchema_Material_HELM_CHART, CraftingSchema_Material_SARIF, CraftingSchema_Material_BLACKDUCK_SCA_JSON, diff --git a/docs/docs/partials/materials.json b/docs/docs/partials/materials.json index b33a90038..652c05a77 100644 --- a/docs/docs/partials/materials.json +++ b/docs/docs/partials/materials.json @@ -33,6 +33,11 @@ "link": "https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format", "id": "JUNIT_XML" }, + { + "name": "JaCoCo XML Report", + "link": "https://www.jacoco.org/jacoco/trunk/doc/", + "id": "JACOCO_XML" + }, { "name": "Helm Chart", "link": "https://helm.sh/docs/topics/charts/", diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index 85f778c7b..620509a6f 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go @@ -18,13 +18,16 @@ package v1 import ( "bytes" "encoding/json" + "encoding/xml" "errors" "fmt" "os" "strings" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/jacoco" intoto "github.com/in-toto/attestation/go/v1" + "github.com/joshdk/go-junit" "github.com/secure-systems-lab/go-securesystemslib/dsse" "google.golang.org/protobuf/types/known/structpb" ) @@ -104,6 +107,29 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) } } + // For XML based materials, we need to ingest them and read as json-like structure + switch m.MaterialType { + case v1.CraftingSchema_Material_JUNIT_XML: + suites, err := junit.Ingest(rawMaterial) + if err != nil { + return nil, fmt.Errorf("failed to ingest junit xml: %w", err) + } + // this will render a json array + rawMaterial, err = json.Marshal(suites) + if err != nil { + return nil, fmt.Errorf("failed to marshal junit xml: %w", err) + } + case v1.CraftingSchema_Material_JACOCO_XML: + var report jacoco.Report + if err := xml.Unmarshal(rawMaterial, &report); err != nil { + return nil, fmt.Errorf("invalid Jacoco report file: %w", err) + } + rawMaterial, err = json.Marshal(&report) + if err != nil { + return nil, fmt.Errorf("failed to marshal to json Jacoco report file: %w", err) + } + } + // if raw material is empty (container images, for example), let's create an empty json if len(rawMaterial) == 0 { rawMaterial = []byte(`{}`) diff --git a/pkg/attestation/crafter/materials/jacoco.go b/pkg/attestation/crafter/materials/jacoco.go new file mode 100644 index 000000000..54197be8a --- /dev/null +++ b/pkg/attestation/crafter/materials/jacoco.go @@ -0,0 +1,74 @@ +// +// Copyright 2024 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "os" + "slices" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/casclient" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/jacoco" + "github.com/rs/zerolog" +) + +type JacocoCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +func NewJacocoCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) *JacocoCrafter { + return &JacocoCrafter{ + crafterCommon: &crafterCommon{logger: l, input: schema}, + backend: backend, + } +} + +func (c *JacocoCrafter) Craft(ctx context.Context, filePath string) (*api.Attestation_Material, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + defer f.Close() + + bytes, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("can't read the file: %w", err) + } + + var report jacoco.Report + + if err := xml.Unmarshal(bytes, &report); err != nil { + return nil, fmt.Errorf("invalid Jacoco report file: %w", ErrInvalidMaterialType) + } + + if len(report.Counters) == 0 { + return nil, fmt.Errorf("invalid Jacoco report file, no counters found: %w", ErrInvalidMaterialType) + } + // At least "instruction" counter should be available according to the documentation + // https://www.eclemma.org/jacoco/trunk/doc/counters.html + if !slices.ContainsFunc(report.Counters, func(counter *jacoco.Counter) bool { + return counter.Type == "INSTRUCTION" + }) { + return nil, fmt.Errorf("invalid Jacoco report file: %w", ErrInvalidMaterialType) + } + return uploadAndCraft(ctx, c.input, c.backend, filePath, c.logger) +} diff --git a/pkg/attestation/crafter/materials/jacoco/jacoco.go b/pkg/attestation/crafter/materials/jacoco/jacoco.go new file mode 100644 index 000000000..2600fc808 --- /dev/null +++ b/pkg/attestation/crafter/materials/jacoco/jacoco.go @@ -0,0 +1,26 @@ +// +// Copyright 2024 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 jacoco + +type Counter struct { + Type string `xml:"type,attr" json:"type"` + Missed int `xml:"missed,attr" json:"missed"` + Covered int `xml:"covered,attr" json:"covered"` +} + +type Report struct { + Counters []*Counter `xml:"counter" json:"counters"` +} diff --git a/pkg/attestation/crafter/materials/jacoco_test.go b/pkg/attestation/crafter/materials/jacoco_test.go new file mode 100644 index 000000000..fe0681391 --- /dev/null +++ b/pkg/attestation/crafter/materials/jacoco_test.go @@ -0,0 +1,96 @@ +// +// Copyright 2023 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. + +//nolint:dupl +package materials_test + +import ( + "context" + "testing" + + contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/casclient" + mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks" + attestationApi "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJacocoCraft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + }{ + { + name: "invalid path", + filePath: "./testdata/non-existing.xml", + wantErr: "no such file or directory", + }, + { + name: "invalid artifact type", + filePath: "./testdata/simple.txt", + wantErr: "unexpected material type", + }, + { + name: "invalid Jacoco file", + filePath: "./testdata/junit.xml", + wantErr: "unexpected material type", + }, + { + name: "valid artifact type", + filePath: "./testdata/jacoco.xml", + }, + } + + assert := assert.New(t) + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_JACOCO_XML, + } + l := zerolog.Nop() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Mock uploader + uploader := mUploader.NewUploader(t) + if tc.wantErr == "" { + uploader.On("UploadFile", context.TODO(), tc.filePath). + Return(&casclient.UpDownStatus{ + Digest: "deadbeef", + Filename: "jacoco.xml", + }, nil) + } + backend := &casclient.CASBackend{Uploader: uploader} + crafter := materials.NewJacocoCrafter(schema, backend, &l) + + got, err := crafter.Craft(context.TODO(), tc.filePath) + if tc.wantErr != "" { + assert.ErrorContains(err, tc.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(contractAPI.CraftingSchema_Material_JACOCO_XML.String(), got.MaterialType.String()) + assert.True(got.UploadedToCas) + + // The result includes the digest reference + assert.Equal(&attestationApi.Attestation_Material_Artifact{ + Id: "test", Digest: "sha256:d622d9dbebbd6f2bd7bf9a867e91bbd2662ebd4ea500d9f518e76f5f3d86c8e0", Name: "jacoco.xml", + }, got.GetArtifact()) + }) + } +} diff --git a/pkg/attestation/crafter/materials/junit_xml.go b/pkg/attestation/crafter/materials/junit_xml.go index 676c12591..814223ff4 100644 --- a/pkg/attestation/crafter/materials/junit_xml.go +++ b/pkg/attestation/crafter/materials/junit_xml.go @@ -17,10 +17,9 @@ package materials import ( "context" - "encoding/xml" + "errors" "fmt" - "io" - "os" + "io/fs" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/internal/casclient" @@ -51,25 +50,17 @@ func (i *JUnitXMLCrafter) Craft(ctx context.Context, filePath string) (*api.Atte } func (i *JUnitXMLCrafter) validate(filePath string) error { - f, err := os.Open(filePath) + suites, err := junit.IngestFile(filePath) if err != nil { - return fmt.Errorf("can't open the file: %w", err) - } - defer f.Close() - - bytes, err := io.ReadAll(f) - if err != nil { - return fmt.Errorf("can't read the file: %w", err) - } - - if err := xml.Unmarshal(bytes, &junit.Suite{}); err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("invalid file path: %w", err) + } + i.logger.Debug().Err(err).Msgf("error decoding file: %s", filePath) return fmt.Errorf("invalid JUnit XML file: %w", ErrInvalidMaterialType) } - _, err = junit.IngestReader(f) - if err != nil { - i.logger.Debug().Err(err).Msgf("error decoding file: %s", filePath) - return fmt.Errorf("invalid JUnit XML file: %w", ErrInvalidMaterialType) + if len(suites) == 0 { + return fmt.Errorf("invalid JUnit XML file, no suites found: %w", ErrInvalidMaterialType) } return nil diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index 45f841244..02a8ac406 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -171,6 +171,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewSPDXJSONCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_JUNIT_XML: crafter, err = NewJUnitXMLCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_JACOCO_XML: + crafter = NewJacocoCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_OPENVEX: crafter, err = NewOpenVEXCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CSAF_VEX: diff --git a/pkg/attestation/crafter/materials/testdata/jacoco.xml b/pkg/attestation/crafter/materials/testdata/jacoco.xml new file mode 100644 index 000000000..423562ffb --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/jacoco.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +