From e4c3bc62cb7a15075d3c01db555677162c63aedc Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 14:01:59 +0200 Subject: [PATCH 1/3] feat: OpenVEX support Signed-off-by: Miguel Martinez Trivino --- .../workflowcontract/v1/crafting_schema.ts | 8 +- .../workflowcontract/v1/crafting_schema.pb.go | 35 +++-- .../workflowcontract/v1/crafting_schema.proto | 90 ++++++------ go.mod | 2 + go.sum | 4 + .../crafter/materials/materials.go | 2 + .../attestation/crafter/materials/openvex.go | 64 +++++++++ .../crafter/materials/openvex_test.go | 132 ++++++++++++++++++ .../materials/testdata/openvex_v0.2.0.json | 89 ++++++++++++ 9 files changed, 368 insertions(+), 58 deletions(-) create mode 100644 internal/attestation/crafter/materials/openvex.go create mode 100644 internal/attestation/crafter/materials/openvex_test.go create mode 100644 internal/attestation/crafter/materials/testdata/openvex_v0.2.0.json 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 063330617..d3bbf3a61 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -91,8 +91,9 @@ export enum CraftingSchema_Material_MaterialType { ARTIFACT = 3, SBOM_CYCLONEDX_JSON = 4, SBOM_SPDX_JSON = 5, - /** JUNIT_XML - SARIF = 5; */ JUNIT_XML = 6, + /** OPENVEX - https://github.com/openvex/spec */ + OPENVEX = 7, UNRECOGNIZED = -1, } @@ -119,6 +120,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 6: case "JUNIT_XML": return CraftingSchema_Material_MaterialType.JUNIT_XML; + case 7: + case "OPENVEX": + return CraftingSchema_Material_MaterialType.OPENVEX; case -1: case "UNRECOGNIZED": default: @@ -142,6 +146,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "SBOM_SPDX_JSON"; case CraftingSchema_Material_MaterialType.JUNIT_XML: return "JUNIT_XML"; + case CraftingSchema_Material_MaterialType.OPENVEX: + return "OPENVEX"; case CraftingSchema_Material_MaterialType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index a7d06b313..7c4169143 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -97,7 +97,9 @@ const ( CraftingSchema_Material_ARTIFACT CraftingSchema_Material_MaterialType = 3 CraftingSchema_Material_SBOM_CYCLONEDX_JSON CraftingSchema_Material_MaterialType = 4 CraftingSchema_Material_SBOM_SPDX_JSON CraftingSchema_Material_MaterialType = 5 - CraftingSchema_Material_JUNIT_XML CraftingSchema_Material_MaterialType = 6 // SARIF = 5; + CraftingSchema_Material_JUNIT_XML CraftingSchema_Material_MaterialType = 6 + // https://github.com/openvex/spec + CraftingSchema_Material_OPENVEX CraftingSchema_Material_MaterialType = 7 // SARIF = 5; ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -110,6 +112,7 @@ var ( 4: "SBOM_CYCLONEDX_JSON", 5: "SBOM_SPDX_JSON", 6: "JUNIT_XML", + 7: "OPENVEX", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -119,6 +122,7 @@ var ( "SBOM_CYCLONEDX_JSON": 4, "SBOM_SPDX_JSON": 5, "JUNIT_XML": 6, + "OPENVEX": 7, } ) @@ -427,7 +431,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, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x07, 0x0a, 0x0e, 0x43, 0x72, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x07, 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, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x0a, 0x02, 0x76, 0x31, 0x52, @@ -459,7 +463,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x47, 0x49, 0x54, 0x4c, 0x41, 0x42, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x5a, - 0x55, 0x52, 0x45, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x03, 0x1a, 0x9b, + 0x55, 0x52, 0x45, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x03, 0x1a, 0xa8, 0x03, 0x0a, 0x08, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x57, 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, @@ -476,7 +480,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 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, 0x98, 0x01, 0x0a, 0x0c, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x79, + 0x73, 0x22, 0xa5, 0x01, 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, 0x10, 0x01, 0x12, 0x13, 0x0a, @@ -485,17 +489,18 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x12, 0x17, 0x0a, 0x13, 0x53, 0x42, 0x4f, 0x4d, 0x5f, 0x43, 0x59, 0x43, 0x4c, 0x4f, 0x4e, 0x45, 0x44, 0x58, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x42, 0x4f, 0x4d, 0x5f, 0x53, 0x50, 0x44, 0x58, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x0d, 0x0a, - 0x09, 0x4a, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x58, 0x4d, 0x4c, 0x10, 0x06, 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, 0xfa, 0x42, 0x0b, 0x72, 0x09, 0x32, - 0x07, 0x5e, 0x5b, 0x5c, 0x77, 0x5d, 0x2b, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, - 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x4a, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x58, 0x4d, 0x4c, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, + 0x4f, 0x50, 0x45, 0x4e, 0x56, 0x45, 0x58, 0x10, 0x07, 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, 0xfa, 0x42, 0x0b, 0x72, 0x09, 0x32, 0x07, 0x5e, 0x5b, + 0x5c, 0x77, 0x5d, 0x2b, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2f, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 38dd755a1..d782fe9a2 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -17,59 +17,65 @@ syntax = "proto3"; package workflowcontract.v1; -option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"; - import "validate/validate.proto"; +option go_package = "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"; + // Schema definition provided by the user to the tool // that defines the schema of the workflowRun message CraftingSchema { - // Version of the schema, do not confuse with the revision of the content - string schema_version = 1 [(validate.rules).string.const = "v1"]; - repeated Material materials = 2; - repeated string env_allow_list = 3; - Runner runner = 4; - // List of annotations that can be used to add metadata to the attestation - // this metadata can be used later on by the integrations engine to filter and interpolate data - // It works in addition to the annotations defined in the materials and the runner - repeated Annotation annotations = 5; + // Version of the schema, do not confuse with the revision of the content + string schema_version = 1 [(validate.rules).string.const = "v1"]; + repeated Material materials = 2; + repeated string env_allow_list = 3; + Runner runner = 4; + // List of annotations that can be used to add metadata to the attestation + // this metadata can be used later on by the integrations engine to filter and interpolate data + // It works in addition to the annotations defined in the materials and the runner + repeated Annotation annotations = 5; - message Runner { - RunnerType type = 1 [(validate.rules).enum = {not_in: [0]}]; + message Runner { + RunnerType type = 1 [(validate.rules).enum = { + not_in: [0] + }]; - enum RunnerType { - RUNNER_TYPE_UNSPECIFIED = 0; - GITHUB_ACTION = 1; - GITLAB_PIPELINE = 2; - AZURE_PIPELINE = 3; - } + enum RunnerType { + RUNNER_TYPE_UNSPECIFIED = 0; + GITHUB_ACTION = 1; + GITLAB_PIPELINE = 2; + AZURE_PIPELINE = 3; } + } - message Material { - MaterialType type = 1 [(validate.rules).enum = {not_in: [0]}]; - string name = 2 [(validate.rules).string.pattern = "^[\\w|-]+$"]; // Single word optionally separated with _ or - - bool optional = 3; - // If a material is set as output it will get added to the subject in the statement - bool output = 4; - // List of annotations that can be used to add metadata to the material - // this metadata can be used later on by the integrations engine to filter and interpolate data - repeated Annotation annotations = 5; + message Material { + MaterialType type = 1 [(validate.rules).enum = { + not_in: [0] + }]; + string name = 2 [(validate.rules).string.pattern = "^[\\w|-]+$"]; // Single word optionally separated with _ or - + bool optional = 3; + // If a material is set as output it will get added to the subject in the statement + bool output = 4; + // List of annotations that can be used to add metadata to the material + // this metadata can be used later on by the integrations engine to filter and interpolate data + repeated Annotation annotations = 5; - enum MaterialType { - MATERIAL_TYPE_UNSPECIFIED = 0; - STRING = 1; - CONTAINER_IMAGE = 2; - ARTIFACT = 3; - SBOM_CYCLONEDX_JSON = 4; - SBOM_SPDX_JSON = 5; - JUNIT_XML = 6; - // SARIF = 5; - } + enum MaterialType { + MATERIAL_TYPE_UNSPECIFIED = 0; + STRING = 1; + CONTAINER_IMAGE = 2; + ARTIFACT = 3; + SBOM_CYCLONEDX_JSON = 4; + SBOM_SPDX_JSON = 5; + JUNIT_XML = 6; + // https://github.com/openvex/spec + OPENVEX = 7; + // SARIF = 5; } + } } message Annotation { - string name = 1 [(validate.rules).string.pattern = "^[\\w]+$"]; // Single word optionally separated with _ - // This value can be set in the contract or provided during the attestation - string value = 2; -} \ No newline at end of file + string name = 1 [(validate.rules).string.pattern = "^[\\w]+$"]; // Single word optionally separated with _ + // This value can be set in the contract or provided during the attestation + string value = 2; +} diff --git a/go.mod b/go.mod index 280ec4164..c7f483ac8 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,8 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/openvex/go-vex v0.2.5 // indirect + github.com/package-url/packageurl-go v0.1.1 // indirect github.com/pkg/xattr v0.4.9 // indirect go.opentelemetry.io/otel/metric v1.17.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect diff --git a/go.sum b/go.sum index 738bb2341..86b619579 100644 --- a/go.sum +++ b/go.sum @@ -951,10 +951,14 @@ github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ= +github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= +github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/internal/attestation/crafter/materials/materials.go b/internal/attestation/crafter/materials/materials.go index 0cdbbe72d..1394011fe 100644 --- a/internal/attestation/crafter/materials/materials.go +++ b/internal/attestation/crafter/materials/materials.go @@ -156,6 +156,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_OPENVEX: + crafter, err = NewOpenVEXCrafter(materialSchema, casBackend, logger) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) } diff --git a/internal/attestation/crafter/materials/openvex.go b/internal/attestation/crafter/materials/openvex.go new file mode 100644 index 000000000..c32a9c684 --- /dev/null +++ b/internal/attestation/crafter/materials/openvex.go @@ -0,0 +1,64 @@ +// +// 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. + +package materials + +import ( + "context" + "fmt" + "os" + + api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/casclient" + "github.com/openvex/go-vex/pkg/vex" + "github.com/rs/zerolog" +) + +type OpenVEXCrafter struct { + backend *casclient.CASBackend + *crafterCommon +} + +func NewOpenVEXCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*OpenVEXCrafter, error) { + if materialSchema.Type != schemaapi.CraftingSchema_Material_OPENVEX { + return nil, fmt.Errorf("material type is not OpenVEX format") + } + + return &OpenVEXCrafter{ + backend: backend, + crafterCommon: &crafterCommon{logger: l, input: materialSchema}, + }, nil +} + +func (i *OpenVEXCrafter) Craft(ctx context.Context, filePath string) (*api.Attestation_Material, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + + i.logger.Debug().Str("path", filePath).Msg("decoding OpenVex file") + doc, err := vex.Parse(data) + // parse doesn't fail if the provided file is a valid JSON, but not a valid OpenVEX file + if err != nil || doc.ID == "" { + if err != nil { + i.logger.Debug().Err(err).Msg("error decoding file") + } + + return nil, fmt.Errorf("invalid OpenVEX file: %w", ErrInvalidMaterialType) + } + + return uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) +} diff --git a/internal/attestation/crafter/materials/openvex_test.go b/internal/attestation/crafter/materials/openvex_test.go new file mode 100644 index 000000000..62a7a643e --- /dev/null +++ b/internal/attestation/crafter/materials/openvex_test.go @@ -0,0 +1,132 @@ +// +// 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" + + attestationApi "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials" + "github.com/chainloop-dev/chainloop/internal/casclient" + mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewOpenVEXCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_OPENVEX, + }, + }, + { + name: "wrong type", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := materials.NewOpenVEXCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestOpenVEXCraft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + }{ + { + name: "invalid sbom format", + filePath: "./testdata/sbom.cyclonedx.json", + wantErr: "unexpected material type", + }, + { + name: "invalid path", + filePath: "./testdata/non-existing.json", + wantErr: "no such file or directory", + }, + { + name: "invalid artifact type", + filePath: "./testdata/simple.txt", + wantErr: "unexpected material type", + }, + { + name: "valid artifact type", + filePath: "./testdata/openvex_v0.2.0.json", + }, + } + + assert := assert.New(t) + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_OPENVEX, + } + 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: "openvex.json", + }, nil) + } + + backend := &casclient.CASBackend{Uploader: uploader} + crafter, err := materials.NewOpenVEXCrafter(schema, backend, &l) + require.NoError(t, err) + + 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_OPENVEX.String(), got.MaterialType.String()) + assert.True(got.UploadedToCas) + + // // The result includes the digest reference + assert.Equal(&attestationApi.Attestation_Material_Artifact{ + Id: "test", Digest: "sha256:b4bd86d5855f94bcac0a92d3100ae7b85d050bd2e5fb9037a200e5f5f0b073a2", Name: "openvex_v0.2.0.json", + }, got.GetArtifact()) + }) + } +} diff --git a/internal/attestation/crafter/materials/testdata/openvex_v0.2.0.json b/internal/attestation/crafter/materials/testdata/openvex_v0.2.0.json new file mode 100644 index 000000000..637d09078 --- /dev/null +++ b/internal/attestation/crafter/materials/testdata/openvex_v0.2.0.json @@ -0,0 +1,89 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", + "author": "The OpenVEX Project ", + "role": "Demo Writer", + "timestamp": "2023-07-17T18:28:47.696004345-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-1255" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-2650" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-2975" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "fixed" + }, + { + "vulnerability": { + "name": "CVE-2023-3446" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "affected functions were removed before packaging" + }, + { + "vulnerability": { + "name": "CVE-2023-3817" + }, + "products": [ + { + "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + "subcomponents": [ + { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, + { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "affected functions were removed before packaging" + } + ] +} From e28103ea49fa2f2d008d3c9e8db7b9f61f2fb0fb Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 14:24:06 +0200 Subject: [PATCH 2/3] feat: add CSAF VEX support Signed-off-by: Miguel Martinez Trivino --- .../workflowcontract/v1/crafting_schema.ts | 7 + .../workflowcontract/v1/crafting_schema.pb.go | 35 +++-- .../workflowcontract/v1/crafting_schema.proto | 3 +- go.mod | 2 +- .../attestation/crafter/materials/csafvex.go | 58 ++++++++ .../crafter/materials/csafvex_test.go | 132 ++++++++++++++++++ .../crafter/materials/openvex_test.go | 2 +- .../materials/testdata/csaf_vex_v0.2.0.json | 119 ++++++++++++++++ 8 files changed, 340 insertions(+), 18 deletions(-) create mode 100644 internal/attestation/crafter/materials/csafvex.go create mode 100644 internal/attestation/crafter/materials/csafvex_test.go create mode 100644 internal/attestation/crafter/materials/testdata/csaf_vex_v0.2.0.json 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 d3bbf3a61..3a0718797 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -94,6 +94,8 @@ export enum CraftingSchema_Material_MaterialType { JUNIT_XML = 6, /** OPENVEX - https://github.com/openvex/spec */ OPENVEX = 7, + /** CSAF_VEX - https://docs.oasis-open.org/csaf/csaf/v2.0/cs03/csaf-v2.0-cs03.html */ + CSAF_VEX = 8, UNRECOGNIZED = -1, } @@ -123,6 +125,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 7: case "OPENVEX": return CraftingSchema_Material_MaterialType.OPENVEX; + case 8: + case "CSAF_VEX": + return CraftingSchema_Material_MaterialType.CSAF_VEX; case -1: case "UNRECOGNIZED": default: @@ -148,6 +153,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "JUNIT_XML"; case CraftingSchema_Material_MaterialType.OPENVEX: return "OPENVEX"; + case CraftingSchema_Material_MaterialType.CSAF_VEX: + return "CSAF_VEX"; case CraftingSchema_Material_MaterialType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 7c4169143..077f15cfc 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -99,7 +99,9 @@ const ( CraftingSchema_Material_SBOM_SPDX_JSON CraftingSchema_Material_MaterialType = 5 CraftingSchema_Material_JUNIT_XML CraftingSchema_Material_MaterialType = 6 // https://github.com/openvex/spec - CraftingSchema_Material_OPENVEX CraftingSchema_Material_MaterialType = 7 // SARIF = 5; + CraftingSchema_Material_OPENVEX CraftingSchema_Material_MaterialType = 7 + // https://docs.oasis-open.org/csaf/csaf/v2.0/cs03/csaf-v2.0-cs03.html + CraftingSchema_Material_CSAF_VEX CraftingSchema_Material_MaterialType = 8 // SARIF = 5; ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -113,6 +115,7 @@ var ( 5: "SBOM_SPDX_JSON", 6: "JUNIT_XML", 7: "OPENVEX", + 8: "CSAF_VEX", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -123,6 +126,7 @@ var ( "SBOM_SPDX_JSON": 5, "JUNIT_XML": 6, "OPENVEX": 7, + "CSAF_VEX": 8, } ) @@ -431,7 +435,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, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad, 0x07, 0x0a, 0x0e, 0x43, 0x72, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x07, 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, 0xfa, 0x42, 0x06, 0x72, 0x04, 0x0a, 0x02, 0x76, 0x31, 0x52, @@ -463,7 +467,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x47, 0x49, 0x54, 0x4c, 0x41, 0x42, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x41, 0x5a, - 0x55, 0x52, 0x45, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x03, 0x1a, 0xa8, + 0x55, 0x52, 0x45, 0x5f, 0x50, 0x49, 0x50, 0x45, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x03, 0x1a, 0xb6, 0x03, 0x0a, 0x08, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x57, 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, @@ -480,7 +484,7 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 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, 0xa5, 0x01, 0x0a, 0x0c, 0x4d, 0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x79, + 0x73, 0x22, 0xb3, 0x01, 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, 0x10, 0x01, 0x12, 0x13, 0x0a, @@ -490,17 +494,18 @@ var file_workflowcontract_v1_crafting_schema_proto_rawDesc = []byte{ 0x44, 0x58, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x42, 0x4f, 0x4d, 0x5f, 0x53, 0x50, 0x44, 0x58, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x4a, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x58, 0x4d, 0x4c, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, - 0x4f, 0x50, 0x45, 0x4e, 0x56, 0x45, 0x58, 0x10, 0x07, 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, 0xfa, 0x42, 0x0b, 0x72, 0x09, 0x32, 0x07, 0x5e, 0x5b, - 0x5c, 0x77, 0x5d, 0x2b, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2f, 0x76, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x50, 0x45, 0x4e, 0x56, 0x45, 0x58, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x53, 0x41, + 0x46, 0x5f, 0x56, 0x45, 0x58, 0x10, 0x08, 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, 0xfa, 0x42, 0x0b, 0x72, 0x09, 0x32, 0x07, 0x5e, 0x5b, 0x5c, 0x77, + 0x5d, 0x2b, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, + 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x66, + 0x6c, 0x6f, 0x77, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2f, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index d782fe9a2..2eddf6cdd 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -69,7 +69,8 @@ message CraftingSchema { JUNIT_XML = 6; // https://github.com/openvex/spec OPENVEX = 7; - // SARIF = 5; + // https://docs.oasis-open.org/csaf/csaf/v2.0/cs03/csaf-v2.0-cs03.html + CSAF_VEX = 8; } } } diff --git a/go.mod b/go.mod index c7f483ac8..13f8845ee 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/invopop/jsonschema v0.7.0 github.com/jackc/pgx/v5 v5.4.3 github.com/muesli/reflow v0.3.0 + github.com/openvex/go-vex v0.2.5 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d google.golang.org/genproto/googleapis/bytestream v0.0.0-20230822172742-b8732ec3820d @@ -91,7 +92,6 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/oklog/run v1.1.0 // indirect - github.com/openvex/go-vex v0.2.5 // indirect github.com/package-url/packageurl-go v0.1.1 // indirect github.com/pkg/xattr v0.4.9 // indirect go.opentelemetry.io/otel/metric v1.17.0 // indirect diff --git a/internal/attestation/crafter/materials/csafvex.go b/internal/attestation/crafter/materials/csafvex.go new file mode 100644 index 000000000..5c37e8456 --- /dev/null +++ b/internal/attestation/crafter/materials/csafvex.go @@ -0,0 +1,58 @@ +// +// 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. + +package materials + +import ( + "context" + "fmt" + + api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/casclient" + "github.com/openvex/go-vex/pkg/csaf" + "github.com/rs/zerolog" +) + +type CSAFVEXCrafter struct { + backend *casclient.CASBackend + *crafterCommon +} + +func NewCSAFVEXCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*CSAFVEXCrafter, error) { + if materialSchema.Type != schemaapi.CraftingSchema_Material_CSAF_VEX { + return nil, fmt.Errorf("material type is not CSAF_VEX format") + } + + return &CSAFVEXCrafter{ + backend: backend, + crafterCommon: &crafterCommon{logger: l, input: materialSchema}, + }, nil +} + +func (i *CSAFVEXCrafter) Craft(ctx context.Context, filepath string) (*api.Attestation_Material, error) { + i.logger.Debug().Str("path", filepath).Msg("decoding CSAF VEX file") + doc, err := csaf.Open(filepath) + // parse doesn't fail if the provided file is a valid JSON, but not a valid CSAF VEX file + if err != nil || doc.Document.Title == "" { + if err != nil { + i.logger.Debug().Err(err).Msg("error decoding file") + } + + return nil, fmt.Errorf("invalid CSAF VEX file: %w", ErrInvalidMaterialType) + } + + return uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger) +} diff --git a/internal/attestation/crafter/materials/csafvex_test.go b/internal/attestation/crafter/materials/csafvex_test.go new file mode 100644 index 000000000..0027d1402 --- /dev/null +++ b/internal/attestation/crafter/materials/csafvex_test.go @@ -0,0 +1,132 @@ +// +// 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" + + attestationApi "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" + contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials" + "github.com/chainloop-dev/chainloop/internal/casclient" + mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCSAFVEXCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_CSAF_VEX, + }, + }, + { + name: "wrong type", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := materials.NewCSAFVEXCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestCSAFVEXCraft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + }{ + { + name: "non-expected json file", + filePath: "./testdata/sbom.cyclonedx.json", + wantErr: "unexpected material type", + }, + { + name: "invalid path", + filePath: "./testdata/non-existing.json", + wantErr: "unexpected material type", + }, + { + name: "invalid artifact type", + filePath: "./testdata/simple.txt", + wantErr: "unexpected material type", + }, + { + name: "valid artifact type", + filePath: "./testdata/csaf_vex_v0.2.0.json", + }, + } + + assert := assert.New(t) + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_CSAF_VEX, + } + 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: "vex.json", + }, nil) + } + + backend := &casclient.CASBackend{Uploader: uploader} + crafter, err := materials.NewCSAFVEXCrafter(schema, backend, &l) + require.NoError(t, err) + + 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_CSAF_VEX.String(), got.MaterialType.String()) + assert.True(got.UploadedToCas) + + // // The result includes the digest reference + assert.Equal(&attestationApi.Attestation_Material_Artifact{ + Id: "test", Digest: "sha256:c27087147fa040909e0ef1b522386608af545b0a163c30c9f11c3d753676fa44", Name: "csaf_vex_v0.2.0.json", + }, got.GetArtifact()) + }) + } +} diff --git a/internal/attestation/crafter/materials/openvex_test.go b/internal/attestation/crafter/materials/openvex_test.go index 62a7a643e..e7a97203a 100644 --- a/internal/attestation/crafter/materials/openvex_test.go +++ b/internal/attestation/crafter/materials/openvex_test.go @@ -71,7 +71,7 @@ func TestOpenVEXCraft(t *testing.T) { wantErr string }{ { - name: "invalid sbom format", + name: "non-expected json file", filePath: "./testdata/sbom.cyclonedx.json", wantErr: "unexpected material type", }, diff --git a/internal/attestation/crafter/materials/testdata/csaf_vex_v0.2.0.json b/internal/attestation/crafter/materials/testdata/csaf_vex_v0.2.0.json new file mode 100644 index 000000000..38d248b10 --- /dev/null +++ b/internal/attestation/crafter/materials/testdata/csaf_vex_v0.2.0.json @@ -0,0 +1,119 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "notes": [ + { + "category": "summary", + "text": "Example VEX document.", + "title": "Document Title" + } + ], + "publisher": { + "category": "vendor", + "name": "Example Company", + "namespace": "https://psirt.example.com" + }, + "title": "Example VEX Document", + "tracking": { + "current_release_date": "2022-03-03T11:00:00.000Z", + "generator": { + "date": "2022-03-03T11:00:00.000Z", + "engine": { + "name": "Secvisogram", + "version": "1.11.0" + } + }, + "id": "2022-EVD-UC-01-NA-001", + "initial_release_date": "2022-03-03T11:00:00.000Z", + "revision_history": [ + { + "date": "2022-03-03T11:00:00.000Z", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "product": { + "name": "Example Company ABC 4.2", + "product_id": "CSAFPID-0001", + "product_identification_helper": { + "purl": "pkg:maven/@1.3.4" + } + }, + "branches": [ + { + "category": "product_version", + "name": "4.2", + "product": { + "name": "Example Company ABC 4.2", + "product_id": "INTERNAL-0001", + "product_identification_helper": { + "purl": "pkg:golang/github.com/go-homedir@v1.1.0" + } + } + }, + { + "category": "product_version", + "name": "2.2", + "product": { + "name": "Example Company ABC 2.2", + "product_id": "INTERNAL-0002", + "product_identification_helper": { + "purl": "pkg:golang/github.com/go-homedir@v1.0.0" + } + } + } + ], + "category": "product_name", + "name": "ABC" + } + ], + "category": "vendor", + "name": "Example Company" + } + ], + "relationships": [ + { + "category": "default_component_of", + "full_product_name": { + "name": "Example Company ABC 2.2", + "product_id": "ABC:INTERNAL-0002" + }, + "product_reference": "INTERNAL-0002", + "relates_to_product_reference": "ABC" + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2009-4487", + "notes": [ + { + "category": "description", + "text": "nginx 0.7.64 writes data to a log file without sanitizing non-printable characters, which might allow remote attackers to modify a window's title, or possibly execute arbitrary commands or overwrite files, via an HTTP request containing an escape sequence for a terminal emulator.", + "title": "CVE description" + } + ], + "product_status": { + "known_not_affected": ["CSAFPID-0001"], + "known_affected": ["ABC:CSAFPID-0002"] + }, + "threats": [ + { + "category": "impact", + "details": "Class with vulnerable code was removed before shipping.", + "product_ids": ["CSAFPID-0001"] + } + ] + } + ] +} From ce93ec518dc9b803fd11ca9cbaca705a1592f8ed Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 15:10:35 +0200 Subject: [PATCH 3/3] feat: add CSAF VEX support Signed-off-by: Miguel Martinez Trivino --- README.md | 15 +++++++++++++++ .../attestation/crafter/materials/materials.go | 2 ++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index df8998383..706386122 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,21 @@ SecOps are the ones in charge of defining the Workflow Contracts, setting up thi Development teams on the other hand, just need to integrate Chainloop's jargon-free [crafting tool](https://docs.chainloop.dev/getting-started/attestation-crafting) and follow the steps via a familiar DevExp to make sure they comply with the Workflow Contract defined by the SecOps team. No need to learn in-toto, signing, SLSA, OCI, APIs, nada :) +## Supported Pieces of Evidence / Materials + +Chainloop supports the collection of the following pieces of evidence types: + +- [Container Image Reference](https://github.com/opencontainers/image-spec) +- [CycloneDX SBOM](https://github.com/CycloneDX/specification) +- [SPDX SBOM](https://spdx.dev/specifications/) +- [CSAF VEX](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html) +- [OpenVEX](https://github.com/openvex) +- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format) +- Generic Artifact Types +- Key-Value metadata pairs + +During the attestation process, these pieces of evidence will get uploaded to the [Content Addressable Storage](https://docs.chainloop.dev/reference/operator/cas-backend/) (if applicable) and referenced in a [SLSA](https://slsa.dev) attestation. + ## Documentation To learn more, please visit the Chainloop project's documentation website, https://docs.chainloop.dev where you will find a getting started guide, FAQ, examples, and more. diff --git a/internal/attestation/crafter/materials/materials.go b/internal/attestation/crafter/materials/materials.go index 1394011fe..c0764f768 100644 --- a/internal/attestation/crafter/materials/materials.go +++ b/internal/attestation/crafter/materials/materials.go @@ -158,6 +158,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewJUnitXMLCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_OPENVEX: crafter, err = NewOpenVEXCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_CSAF_VEX: + crafter, err = NewCSAFVEXCrafter(materialSchema, casBackend, logger) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) }