diff --git a/internal/testing/testdata/exampledata/cyclonedx-unaffected-vex.json b/internal/testing/testdata/exampledata/cyclonedx-unaffected-vex.json new file mode 100644 index 0000000000..aca23e9549 --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-unaffected-vex.json @@ -0,0 +1,96 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "vulnerabilities": [ + { + "id": "CVE-2020-25649", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25649" + }, + "references": [ + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302", + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + } + } + ], + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&version=3.1" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + }, + "score": 8.2, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "Acme Inc", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "cwes": [ + 611 + ], + "description": "com.fasterxml.jackson.core:jackson-databind is a library which contains the general-purpose data-binding functionality and tree-model for Jackson Data Processor.\n\nAffected versions of this package are vulnerable to XML External Entity (XXE) Injection. A flaw was found in FasterXML Jackson Databind, where it does not have entity expansion secured properly in the DOMDeserializer class. The highest threat from this vulnerability is data integrity.", + "detail": "XXE Injection is a type of attack against an application that parses XML input. XML is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. By default, many XML processors allow specification of an external entity, a URI that is dereferenced and evaluated during XML processing. When an XML document is being parsed, the parser can make a request and include the content at the specified URI inside of the XML document.\n\nAttacks can include disclosing local files, which may contain sensitive data such as passwords or private user data, using file: schemes or relative paths in the system identifier.", + "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.6.7.4, 2.9.10.7, 2.10.5.1 or higher.", + "advisories": [ + { + "title": "GitHub Commit", + "url": "https://github.com/FasterXML/jackson-databind/commit/612f971b78c60202e9cd75a299050c8f2d724a59" + }, + { + "title": "GitHub Issue", + "url": "https://github.com/FasterXML/jackson-databind/issues/2589" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1887664" + } + ], + "created": "2020-12-03T00:00:00.000Z", + "published": "2020-12-03T00:00:00.000Z", + "updated": "2021-10-26T00:00:00.000Z", + "credits": { + "individuals": [ + { + "name": "Bartosz Baranowski" + } + ] + }, + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable", + "response": ["will_not_fix", "update"], + "detail": "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly." + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.10.0?type=jar" + } + ] + } + ] +} diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json b/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json new file mode 100644 index 0000000000..c27d8527c5 --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json @@ -0,0 +1,54 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata" : { + "timestamp" : "2022-03-03T00:00:00Z", + "component" : { + "name" : "ABC", + "type" : "application", + "bom-ref" : "product-ABC" + } + }, + "vulnerabilities": [ + { + "id": "CVE-2021-44228", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H&version=3.1" + }, + "score": 10.0, + "severity": "critical", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" + } + ], + "analysis": { + "state": "exploitable", + "response": ["will_not_fix", "update"], + "detail": "Versions of Product ABC are affected by the vulnerability. Customers are advised to upgrade to the latest release." + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@", + "versions": [ + { + "version": "2.4", + "status": "affected" + }, + { + "version": "2.6", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex.xml b/internal/testing/testdata/exampledata/cyclonedx-vex.xml new file mode 100644 index 0000000000..3ff3b8c897 --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-vex.xml @@ -0,0 +1,57 @@ + + + + + CVE-2018-7489 + + NVD + https://nvd.nist.gov/vuln/detail/CVE-2019-9997 + + + + + NVD + https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&version=3.0 + + 9.8 + critical + CVSSv3 + AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + + + 184 + 502 + + FasterXML jackson-databind before 2.7.9.3, 2.8.x before 2.8.11.1 and 2.9.x before 2.9.5 allows unauthenticated remote code execution because of an incomplete fix for the CVE-2017-7525 deserialization flaw. This is exploitable by sending maliciously crafted JSON input to the readValue method of the ObjectMapper, bypassing a blacklist that is ineffective if the c3p0 libraries are available in the classpath. + Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.6.7.5, 2.8.11.1, 2.9.5 or higher. + + + GitHub Commit + https://github.com/FasterXML/jackson-databind/commit/6799f8f10cc78e9af6d443ed6982d00a13f2e7d2 + + + GitHub Issue + https://github.com/FasterXML/jackson-databind/issues/1931 + + + 2021-01-01T00:00:00.000Z + 2021-01-01T00:00:00.000Z + 2021-01-01T00:00:00.000Z + + not_affected + code_not_reachable + + will_not_fix + update + + An optional explanation of why the application is not affected by the vulnerable component. + + + + urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#jackson-databind-2.8.0 + + + + + diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index 7fe8883787..5bb896c107 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -98,6 +98,15 @@ var ( //go:embed exampledata/cyclonedx-no-top-level.json CycloneDXExampleNoTopLevelComp []byte + //go:embed exampledata/cyclonedx-unaffected-vex.json + CycloneDXVEXUnAffected []byte + + //go:embed exampledata/cyclonedx-vex-affected.json + CycloneDXVEXAffected []byte + + //go:embed exampledata/cyclonedx-vex.xml + CyloneDXVEXExampleXML []byte + //go:embed exampledata/crev-review.json ITE6CREVExample []byte @@ -128,6 +137,99 @@ var ( //go:embed exampledata/ingest_predicates.json IngestPredicatesExample []byte + // CycloneDX VEX testdata unaffected + pkg, _ = asmhelpers.PurlToPkg("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.10.0?type=jar") + vulnSpec = &generated.VulnerabilityInputSpec{ + Type: "cve", + VulnerabilityID: "cve-2020-25649", + } + CycloneDXUnAffectedVexIngest = []assembler.VexIngest{ + { + Pkg: pkg, + Vulnerability: vulnSpec, + VexData: &generated.VexStatementInputSpec{ + Status: generated.VexStatusNotAffected, + VexJustification: generated.VexJustificationVulnerableCodeNotInExecutePath, + Statement: "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly.", + StatusNotes: "not_affected:code_not_reachable", + KnownSince: parseUTCTime("2020-12-03T00:00:00.000Z"), + }, + }, + } + CycloneDXUnAffectedVulnMetadata = []assembler.VulnMetadataIngest{ + { + Vulnerability: vulnSpec, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreTypeCvssv31, + ScoreValue: 7.5, + Timestamp: parseUTCTime("2020-12-03T00:00:00.000Z"), + }, + }, + { + Vulnerability: vulnSpec, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreTypeCvssv31, + ScoreValue: 8.2, + Timestamp: parseUTCTime("2020-12-03T00:00:00.000Z"), + }, + }, + { + Vulnerability: vulnSpec, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreTypeCvssv31, + ScoreValue: 0.0, + Timestamp: parseUTCTime("2020-12-03T00:00:00.000Z"), + }, + }, + } + + // CycloneDX VEX testdata in triage + pkg1, _ = asmhelpers.PurlToPkg("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.4") + pkg2, _ = asmhelpers.PurlToPkg("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.6") + + vulnSpecAffected = &generated.VulnerabilityInputSpec{ + Type: "cve", + VulnerabilityID: "cve-2021-44228", + } + vexDataAffected = &generated.VexStatementInputSpec{ + Status: generated.VexStatusAffected, + Statement: "Versions of Product ABC are affected by the vulnerability. Customers are advised to upgrade to the latest release.", + StatusNotes: "exploitable:", + } + CycloneDXAffectedVexIngest = []assembler.VexIngest{ + { + Pkg: pkg1, + Vulnerability: vulnSpecAffected, + VexData: vexDataAffected, + }, + { + Pkg: pkg2, + Vulnerability: vulnSpecAffected, + VexData: vexDataAffected, + }, + } + CycloneDXAffectedVulnMetadata = []assembler.VulnMetadataIngest{ + { + Vulnerability: vulnSpecAffected, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreTypeCvssv31, + ScoreValue: 10, + }, + }, + } + CycloneDXAffectedCertifyVuln = []assembler.CertifyVulnIngest{ + { + Pkg: pkg1, + Vulnerability: vulnSpecAffected, + VulnData: &generated.ScanMetadataInput{}, + }, + { + Pkg: pkg2, + Vulnerability: vulnSpecAffected, + VulnData: &generated.ScanMetadataInput{}, + }, + } + // DSSE/SLSA Testdata // Taken from: https://slsa.dev/provenance/v0.2#example @@ -1925,8 +2027,8 @@ var ( }, Vulnerability: &model.VulnerabilityInputSpec{Type: "cve", VulnerabilityID: "cve-2023-0286"}, VexData: &model.VexStatementInputSpec{ - Status: "AFFECTED", - VexJustification: "NOT_PROVIDED", + Status: generated.VexStatusAffected, + VexJustification: generated.VexJustificationNotProvided, Statement: `For details on how to apply this update, which includes the changes described in this advisory, refer to: https://access.redhat.com/articles/11258 @@ -2500,3 +2602,11 @@ func toTime(s string) time.Time { } return timeScanned } + +func parseUTCTime(s string) time.Time { + timeScanned, err := time.Parse("2006-01-02T15:04:05Z", s) + if err != nil { + panic(err) + } + return timeScanned +} diff --git a/pkg/assembler/clients/generated/operations.go b/pkg/assembler/clients/generated/operations.go index 782c2152f5..f2e9ab735b 100644 --- a/pkg/assembler/clients/generated/operations.go +++ b/pkg/assembler/clients/generated/operations.go @@ -23366,10 +23366,14 @@ func (v *VulnerabilityMetadataInputSpec) GetCollector() string { return v.Collec type VulnerabilityScoreType string const ( - VulnerabilityScoreTypeCvssv2 VulnerabilityScoreType = "CVSSv2" - VulnerabilityScoreTypeCvssv3 VulnerabilityScoreType = "CVSSv3" - VulnerabilityScoreTypeEpssv1 VulnerabilityScoreType = "EPSSv1" - VulnerabilityScoreTypeEpssv2 VulnerabilityScoreType = "EPSSv2" + VulnerabilityScoreTypeCvssv2 VulnerabilityScoreType = "CVSSv2" + VulnerabilityScoreTypeCvssv3 VulnerabilityScoreType = "CVSSv3" + VulnerabilityScoreTypeEpssv1 VulnerabilityScoreType = "EPSSv1" + VulnerabilityScoreTypeEpssv2 VulnerabilityScoreType = "EPSSv2" + VulnerabilityScoreTypeCvssv31 VulnerabilityScoreType = "CVSSv31" + VulnerabilityScoreTypeCvssv4 VulnerabilityScoreType = "CVSSv4" + VulnerabilityScoreTypeOwasp VulnerabilityScoreType = "OWASP" + VulnerabilityScoreTypeSsvc VulnerabilityScoreType = "SSVC" ) // VulnerabilitySpec allows filtering the list of vulnerabilities to return in a query. diff --git a/pkg/assembler/graphql/generated/root_.generated.go b/pkg/assembler/graphql/generated/root_.generated.go index ab24454976..c66d4c8908 100644 --- a/pkg/assembler/graphql/generated/root_.generated.go +++ b/pkg/assembler/graphql/generated/root_.generated.go @@ -4507,6 +4507,10 @@ enum VulnerabilityScoreType { CVSSv3 EPSSv1 EPSSv2 + CVSSv31 + CVSSv4 + OWASP + SSVC } "The Comparator is used by the vulnerability score filter on ranges" diff --git a/pkg/assembler/graphql/model/nodes.go b/pkg/assembler/graphql/model/nodes.go index c6b6be8e3d..6ca6dfc737 100644 --- a/pkg/assembler/graphql/model/nodes.go +++ b/pkg/assembler/graphql/model/nodes.go @@ -1762,10 +1762,14 @@ func (e VexStatus) MarshalGQL(w io.Writer) { type VulnerabilityScoreType string const ( - VulnerabilityScoreTypeCVSSv2 VulnerabilityScoreType = "CVSSv2" - VulnerabilityScoreTypeCVSSv3 VulnerabilityScoreType = "CVSSv3" - VulnerabilityScoreTypeEPSSv1 VulnerabilityScoreType = "EPSSv1" - VulnerabilityScoreTypeEPSSv2 VulnerabilityScoreType = "EPSSv2" + VulnerabilityScoreTypeCVSSv2 VulnerabilityScoreType = "CVSSv2" + VulnerabilityScoreTypeCVSSv3 VulnerabilityScoreType = "CVSSv3" + VulnerabilityScoreTypeEPSSv1 VulnerabilityScoreType = "EPSSv1" + VulnerabilityScoreTypeEPSSv2 VulnerabilityScoreType = "EPSSv2" + VulnerabilityScoreTypeCVSSv31 VulnerabilityScoreType = "CVSSv31" + VulnerabilityScoreTypeCVSSv4 VulnerabilityScoreType = "CVSSv4" + VulnerabilityScoreTypeOwasp VulnerabilityScoreType = "OWASP" + VulnerabilityScoreTypeSsvc VulnerabilityScoreType = "SSVC" ) var AllVulnerabilityScoreType = []VulnerabilityScoreType{ @@ -1773,11 +1777,15 @@ var AllVulnerabilityScoreType = []VulnerabilityScoreType{ VulnerabilityScoreTypeCVSSv3, VulnerabilityScoreTypeEPSSv1, VulnerabilityScoreTypeEPSSv2, + VulnerabilityScoreTypeCVSSv31, + VulnerabilityScoreTypeCVSSv4, + VulnerabilityScoreTypeOwasp, + VulnerabilityScoreTypeSsvc, } func (e VulnerabilityScoreType) IsValid() bool { switch e { - case VulnerabilityScoreTypeCVSSv2, VulnerabilityScoreTypeCVSSv3, VulnerabilityScoreTypeEPSSv1, VulnerabilityScoreTypeEPSSv2: + case VulnerabilityScoreTypeCVSSv2, VulnerabilityScoreTypeCVSSv3, VulnerabilityScoreTypeEPSSv1, VulnerabilityScoreTypeEPSSv2, VulnerabilityScoreTypeCVSSv31, VulnerabilityScoreTypeCVSSv4, VulnerabilityScoreTypeOwasp, VulnerabilityScoreTypeSsvc: return true } return false diff --git a/pkg/assembler/graphql/schema/vulnMetadata.graphql b/pkg/assembler/graphql/schema/vulnMetadata.graphql index 9a659e2c41..05a963a5c4 100644 --- a/pkg/assembler/graphql/schema/vulnMetadata.graphql +++ b/pkg/assembler/graphql/schema/vulnMetadata.graphql @@ -23,6 +23,10 @@ enum VulnerabilityScoreType { CVSSv3 EPSSv1 EPSSv2 + CVSSv31 + CVSSv4 + OWASP + SSVC } "The Comparator is used by the vulnerability score filter on ranges" diff --git a/pkg/handler/processor/cdx_vex/cdx_vex.go b/pkg/handler/processor/cdx_vex/cdx_vex.go new file mode 100644 index 0000000000..7654d6acbe --- /dev/null +++ b/pkg/handler/processor/cdx_vex/cdx_vex.go @@ -0,0 +1,59 @@ +// +// Copyright 2023 The GUAC 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 cdx_vex + +import ( + "bytes" + "encoding/json" + "fmt" + + cdx "github.com/CycloneDX/cyclonedx-go" + + "github.com/guacsec/guac/pkg/handler/processor" +) + +type CdxVexProcessor struct{} + +func (p *CdxVexProcessor) ValidateSchema(d *processor.Document) error { + if d.Type != processor.DocumentCdxVex { + return fmt.Errorf("expected document type: %v, actual document type: %v", processor.DocumentCdxVex, d.Type) + } + + switch d.Format { + case processor.FormatJSON: + var decoded cdx.BOM + err := json.Unmarshal(d.Blob, &decoded) + if err == nil && decoded.Vulnerabilities != nil { + return nil + } + return err + case processor.FormatXML: + reader := bytes.NewReader(d.Blob) + bom := new(cdx.BOM) + decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatXML) + return decoder.Decode(bom) + } + + return fmt.Errorf("unable to support parsing of cdx-vex document format: %v", d.Format) +} + +func (p *CdxVexProcessor) Unpack(d *processor.Document) ([]*processor.Document, error) { + if d.Type != processor.DocumentCdxVex { + return nil, fmt.Errorf("expected document type: %v, actual document type: %v", processor.DocumentCdxVex, d.Type) + } + + return []*processor.Document{}, nil +} diff --git a/pkg/handler/processor/cdx_vex/cdx_vex_test.go b/pkg/handler/processor/cdx_vex/cdx_vex_test.go new file mode 100644 index 0000000000..34ae3646aa --- /dev/null +++ b/pkg/handler/processor/cdx_vex/cdx_vex_test.go @@ -0,0 +1,117 @@ +// +// Copyright 2023 The GUAC 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 cdx_vex + +import ( + "fmt" + "reflect" + "testing" + + "github.com/guacsec/guac/internal/testing/testdata" + "github.com/guacsec/guac/pkg/handler/processor" +) + +func Test_ValidateSchema(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + doc processor.Document + expectedErr error + }{ + { + name: "Incorrect document type", + doc: processor.Document{ + Type: processor.DocumentCsaf, + }, + expectedErr: fmt.Errorf("expected document type: %v, actual document type: %v", processor.DocumentCdxVex, processor.DocumentCsaf), + }, + { + name: "Successful validation of cdx-vex json document", + doc: processor.Document{ + Type: processor.DocumentCdxVex, + Format: processor.FormatJSON, + Blob: testdata.CycloneDXVEXUnAffected, + }, + }, + { + name: "Successful validation of cdx-vex xml document", + doc: processor.Document{ + Type: processor.DocumentCdxVex, + Format: processor.FormatXML, + Blob: testdata.CyloneDXVEXExampleXML, + }, + }, + { + name: "Invalid format for cdx-vex document", + doc: processor.Document{ + Type: processor.DocumentCdxVex, + Format: processor.FormatUnknown, + Blob: testdata.CycloneDXVEXUnAffected, + }, + expectedErr: fmt.Errorf("unable to support parsing of cdx-vex document format: %v", processor.FormatUnknown), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + c := CdxVexProcessor{} + err := c.ValidateSchema(&tt.doc) + if err != nil && err.Error() != tt.expectedErr.Error() { + t.Errorf("ValidateSchema() actual error = %v, expected error %v", err, tt.expectedErr) + } + }) + } +} + +func Test_Unpack(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + doc processor.Document + expectedRes []*processor.Document + expectedErr error + }{ + { + name: "Invalid document type", + doc: processor.Document{ + Type: processor.DocumentCycloneDX, + }, + expectedRes: nil, + expectedErr: fmt.Errorf("expected document type: %v, actual document type: %v", processor.DocumentCdxVex, processor.DocumentCycloneDX), + }, + { + name: "Successful unpacked cdx-vex document", + doc: processor.Document{ + Type: processor.DocumentCdxVex, + }, + expectedRes: []*processor.Document{}, + expectedErr: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + c := CdxVexProcessor{} + res, err := c.Unpack(&tt.doc) + if err != nil && err.Error() != tt.expectedErr.Error() { + t.Errorf("Unpack() actual error = %v, expected error %v", err, tt.expectedErr) + } + if !reflect.DeepEqual(res, tt.expectedRes) { + t.Errorf("Unpack() actual result = %v, expected result %v", res, tt.expectedRes) + } + }) + } +} diff --git a/pkg/handler/processor/guesser/guesser_test.go b/pkg/handler/processor/guesser/guesser_test.go index edacfaa923..b0cdb4e6e2 100644 --- a/pkg/handler/processor/guesser/guesser_test.go +++ b/pkg/handler/processor/guesser/guesser_test.go @@ -209,6 +209,26 @@ func Test_GuessDocument(t *testing.T) { }, expectedType: processor.DocumentCsaf, expectedFormat: processor.FormatJSON, + }, { + name: "valid cdx vex json Document", + document: &processor.Document{ + Blob: testdata.CycloneDXVEXUnAffected, + Type: processor.DocumentUnknown, + Format: processor.FormatUnknown, + SourceInformation: processor.SourceInformation{}, + }, + expectedType: processor.DocumentCdxVex, + expectedFormat: processor.FormatJSON, + }, { + name: "valid cdx vex xml Document", + document: &processor.Document{ + Blob: testdata.CyloneDXVEXExampleXML, + Type: processor.DocumentUnknown, + Format: processor.FormatUnknown, + SourceInformation: processor.SourceInformation{}, + }, + expectedType: processor.DocumentCdxVex, + expectedFormat: processor.FormatXML, }} for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/handler/processor/guesser/test_cyclonedx_test.go b/pkg/handler/processor/guesser/test_cyclonedx_test.go deleted file mode 100644 index 0ff5b1fcf4..0000000000 --- a/pkg/handler/processor/guesser/test_cyclonedx_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright 2022 The GUAC 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 guesser - -import ( - "testing" - - "github.com/guacsec/guac/internal/testing/testdata" - "github.com/guacsec/guac/pkg/handler/processor" -) - -func Test_cyclonedxTypeGuesser_GuessDocumentType(t *testing.T) { - testCases := []struct { - name string - blob []byte - format processor.FormatType - expected processor.DocumentType - }{{ - name: "invalid cyclonedx Document", - blob: []byte(`{ - "abc": "def" - }`), - format: processor.FormatJSON, - expected: processor.DocumentUnknown, - }, { - name: "invalid cyclonedx Document", - blob: testdata.CycloneDXInvalidExample, - format: processor.FormatJSON, - expected: processor.DocumentUnknown, - }, { - name: "invalid xml cyclonedx Document", - blob: testdata.CycloneDXInvalidExampleXML, - format: processor.FormatXML, - expected: processor.DocumentUnknown, - }, { - name: "valid small cyclonedx Document", - blob: testdata.CycloneDXBusyboxExample, - format: processor.FormatJSON, - expected: processor.DocumentCycloneDX, - }, { - name: "valid distroless cyclonedx Document", - blob: testdata.CycloneDXDistrolessExample, - format: processor.FormatJSON, - expected: processor.DocumentCycloneDX, - }, { - name: "valid alpine cyclonedx Document", - blob: testdata.CycloneDXExampleAlpine, - format: processor.FormatJSON, - expected: processor.DocumentCycloneDX, - }, { - name: "valid xml cyclonedx Document", - blob: testdata.CycloneDXExampleLaravelXML, - format: processor.FormatXML, - expected: processor.DocumentCycloneDX, - }} - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - guesser := &cycloneDXTypeGuesser{} - f := guesser.GuessDocumentType(tt.blob, tt.format) - if f != tt.expected { - t.Errorf("got the wrong format, got %v, expected %v", f, tt.expected) - } - }) - } -} diff --git a/pkg/handler/processor/guesser/type_cyclonedx.go b/pkg/handler/processor/guesser/type_cyclonedx.go index 9c1dd94e35..9b6586b76f 100644 --- a/pkg/handler/processor/guesser/type_cyclonedx.go +++ b/pkg/handler/processor/guesser/type_cyclonedx.go @@ -38,6 +38,9 @@ func (_ *cycloneDXTypeGuesser) GuessDocumentType(blob []byte, format processor.F decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatJSON) err := decoder.Decode(bom) if err == nil && bom.BOMFormat == cycloneDXFormat { + if bom.Vulnerabilities != nil { + return processor.DocumentCdxVex + } return processor.DocumentCycloneDX } case processor.FormatXML: @@ -45,6 +48,9 @@ func (_ *cycloneDXTypeGuesser) GuessDocumentType(blob []byte, format processor.F decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatXML) err := decoder.Decode(bom) if err == nil && strings.HasPrefix(bom.XMLNS, "http://cyclonedx.org/schema/bom/") { + if bom.Vulnerabilities != nil { + return processor.DocumentCdxVex + } return processor.DocumentCycloneDX } } diff --git a/pkg/handler/processor/guesser/type_cyclonedx_test.go b/pkg/handler/processor/guesser/type_cyclonedx_test.go new file mode 100644 index 0000000000..9cfefb55ce --- /dev/null +++ b/pkg/handler/processor/guesser/type_cyclonedx_test.go @@ -0,0 +1,97 @@ +// +// Copyright 2022 The GUAC 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 guesser + +import ( + "testing" + + "github.com/guacsec/guac/internal/testing/testdata" + "github.com/guacsec/guac/pkg/handler/processor" +) + +func Test_cyclonedxTypeGuesser_GuessDocumentType(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + blob []byte + format processor.FormatType + expected processor.DocumentType + }{ + { + name: "invalid cyclonedx Document", + blob: []byte(`{"abc": "def"}`), + format: processor.FormatJSON, + expected: processor.DocumentUnknown, + }, + { + name: "invalid cyclonedx Document", + blob: testdata.CycloneDXInvalidExample, + format: processor.FormatJSON, + expected: processor.DocumentUnknown, + }, + { + name: "invalid xml cyclonedx Document", + blob: testdata.CycloneDXInvalidExampleXML, + format: processor.FormatXML, + expected: processor.DocumentUnknown, + }, + { + name: "valid small cyclonedx Document", + blob: testdata.CycloneDXBusyboxExample, + format: processor.FormatJSON, + expected: processor.DocumentCycloneDX, + }, + { + name: "valid distroless cyclonedx Document", + blob: testdata.CycloneDXDistrolessExample, + format: processor.FormatJSON, + expected: processor.DocumentCycloneDX, + }, + { + name: "valid alpine cyclonedx Document", + blob: testdata.CycloneDXExampleAlpine, + format: processor.FormatJSON, + expected: processor.DocumentCycloneDX, + }, + { + name: "valid xml cyclonedx Document", + blob: testdata.CycloneDXExampleLaravelXML, + format: processor.FormatXML, + expected: processor.DocumentCycloneDX, + }, + { + name: "valid cyclonedx vex json Document", + blob: testdata.CycloneDXVEXUnAffected, + format: processor.FormatJSON, + expected: processor.DocumentCdxVex, + }, + { + name: "valid cyclonedx vex xml Document", + blob: testdata.CyloneDXVEXExampleXML, + format: processor.FormatXML, + expected: processor.DocumentCdxVex, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + guesser := &cycloneDXTypeGuesser{} + f := guesser.GuessDocumentType(tt.blob, tt.format) + if f != tt.expected { + t.Errorf("got the wrong format, got %v, expected %v", f, tt.expected) + } + }) + } +} diff --git a/pkg/handler/processor/process/process.go b/pkg/handler/processor/process/process.go index d8fd17fb3a..6cee6d5076 100644 --- a/pkg/handler/processor/process/process.go +++ b/pkg/handler/processor/process/process.go @@ -24,6 +24,7 @@ import ( uuid "github.com/gofrs/uuid" "github.com/guacsec/guac/pkg/emitter" "github.com/guacsec/guac/pkg/handler/processor" + "github.com/guacsec/guac/pkg/handler/processor/cdx_vex" "github.com/guacsec/guac/pkg/handler/processor/csaf" "github.com/guacsec/guac/pkg/handler/processor/cyclonedx" "github.com/guacsec/guac/pkg/handler/processor/deps_dev" @@ -49,6 +50,7 @@ func init() { _ = RegisterDocumentProcessor(&scorecard.ScorecardProcessor{}, processor.DocumentScorecard) _ = RegisterDocumentProcessor(&cyclonedx.CycloneDXProcessor{}, processor.DocumentCycloneDX) _ = RegisterDocumentProcessor(&deps_dev.DepsDev{}, processor.DocumentDepsDev) + _ = RegisterDocumentProcessor(&cdx_vex.CdxVexProcessor{}, processor.DocumentCdxVex) } func RegisterDocumentProcessor(p processor.DocumentProcessor, d processor.DocumentType) error { diff --git a/pkg/handler/processor/processor.go b/pkg/handler/processor/processor.go index af8c205a42..07f237c31a 100644 --- a/pkg/handler/processor/processor.go +++ b/pkg/handler/processor/processor.go @@ -62,6 +62,7 @@ const ( DocumentCycloneDX DocumentType = "CycloneDX" DocumentDepsDev DocumentType = "DEPS_DEV" DocumentCsaf DocumentType = "CSAF" + DocumentCdxVex DocumentType = "CDX_VEX" DocumentIngestPredicates DocumentType = "INGEST_PREDICATES" DocumentUnknown DocumentType = "UNKNOWN" ) diff --git a/pkg/ingestor/parser/cdx_vex/parser_cdx_vex.go b/pkg/ingestor/parser/cdx_vex/parser_cdx_vex.go new file mode 100644 index 0000000000..84272fac80 --- /dev/null +++ b/pkg/ingestor/parser/cdx_vex/parser_cdx_vex.go @@ -0,0 +1,219 @@ +// +// Copyright 2023 The GUAC 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 cdx_vex + +import ( + "context" + "fmt" + "strings" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" + + "github.com/guacsec/guac/pkg/assembler" + "github.com/guacsec/guac/pkg/assembler/clients/generated" + "github.com/guacsec/guac/pkg/assembler/helpers" + "github.com/guacsec/guac/pkg/handler/processor" + "github.com/guacsec/guac/pkg/ingestor/parser/common" + "github.com/guacsec/guac/pkg/ingestor/parser/cyclonedx" + "github.com/guacsec/guac/pkg/logging" +) + +var vexStatusMap = map[cdx.ImpactAnalysisState]generated.VexStatus{ + "resolved": generated.VexStatusFixed, + "exploitable": generated.VexStatusAffected, + "in_triage": generated.VexStatusUnderInvestigation, + "not_affected": generated.VexStatusNotAffected, +} + +var justificationsMap = map[cdx.ImpactAnalysisJustification]generated.VexJustification{ + "code_not_present": generated.VexJustificationVulnerableCodeNotPresent, + "code_not_reachable": generated.VexJustificationVulnerableCodeNotInExecutePath, +} + +type cdxVexParser struct { + doc *processor.Document + identifierStrings *common.IdentifierStrings + cdxBom *cdx.BOM +} + +func NewCdxVexParser() common.DocumentParser { + return &cdxVexParser{ + identifierStrings: &common.IdentifierStrings{}, + } +} + +// Parse breaks out the document into the graph components +func (c *cdxVexParser) Parse(ctx context.Context, doc *processor.Document) error { + c.doc = doc + bom, err := cyclonedx.ParseCycloneDXBOM(doc) + if err != nil { + return fmt.Errorf("unable to parse cdx-vex document: %w", err) + } + c.cdxBom = bom + return nil +} + +// GetIdentities gets the identity node from the document if they exist +func (c *cdxVexParser) GetIdentities(ctx context.Context) []common.TrustInformation { + return nil +} + +func (c *cdxVexParser) GetIdentifiers(ctx context.Context) (*common.IdentifierStrings, error) { + return c.identifierStrings, nil +} + +// Get package name and range versions to create package input spec for the affected packages. +func (c *cdxVexParser) getAffectedPackages(ctx context.Context, vulnInput *generated.VulnerabilityInputSpec, vexData generated.VexStatementInputSpec, affectsObj cdx.Affects) *[]assembler.VexIngest { + logger := logging.FromContext(ctx) + var pkgRef string + // TODO: retrieve purl from metadata if present - https://github.com/guacsec/guac/blob/main/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go#L76 + if affectsObj.Ref != "" { + pkgRef = affectsObj.Ref + } else { + logger.Warnf("[cdx vex] package reference not found") + return nil + } + + // split ref using # as delimiter. + pkgRefInfo := strings.Split(pkgRef, "#") + if len(pkgRefInfo) != 2 { + logger.Warnf("[cdx vex] malformed package reference: %q", affectsObj.Ref) + return nil + } + pkgURL := pkgRefInfo[1] + + // multiple package versions do not exist, resolve to using ref directly. + if affectsObj.Range == nil { + pkg, err := helpers.PurlToPkg(pkgURL) + if err != nil { + logger.Warnf("[cdx vex] unable to create package input spec: %v", err) + return nil + } + + c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, pkgURL) + return &[]assembler.VexIngest{{VexData: &vexData, Vulnerability: vulnInput, Pkg: pkg}} + } + + // split pkgURL using @ as delimiter. + pkgURLInfo := strings.Split(pkgURL, "@") + if len(pkgURLInfo) != 2 { + logger.Warnf("[cdx vex] malformed package url info: %q", pkgURL) + return nil + } + + pkgName := pkgURLInfo[0] + var viList []assembler.VexIngest + for _, affect := range *affectsObj.Range { + // TODO: Handle package range versions (see - https://github.com/CycloneDX/bom-examples/blob/master/VEX/CISA-Use-Cases/Case-8/vex.json#L42) + if affect.Version == "" { + continue + } + vi := &assembler.VexIngest{ + VexData: &vexData, + Vulnerability: vulnInput, + } + + pkg, err := helpers.PurlToPkg(fmt.Sprintf("%s@%s", pkgName, affect.Version)) + if err != nil { + logger.Warnf("[cdx vex] unable to create package input spec from purl: %v", err) + return nil + } + vi.Pkg = pkg + viList = append(viList, *vi) + c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, pkgURL) + } + + return &viList +} + +func (c *cdxVexParser) GetPredicates(ctx context.Context) *assembler.IngestPredicates { + layout := "2006-01-02T15:04:05.000Z" + pred := &assembler.IngestPredicates{} + + var vex []assembler.VexIngest + var vulnMetadata []assembler.VulnMetadataIngest + var certifyVuln []assembler.CertifyVulnIngest + var status generated.VexStatus + var justification generated.VexJustification + var publishedTime time.Time + + for _, vulnerability := range *c.cdxBom.Vulnerabilities { + vuln, err := helpers.CreateVulnInput(vulnerability.ID) + if err != nil { + return nil + } + + if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { + status = vexStatus + } + + if vexJustification, ok := justificationsMap[vulnerability.Analysis.Justification]; ok { + justification = vexJustification + } + + time, err := time.Parse(layout, vulnerability.Published) + if err == nil { + publishedTime = time + } + + vd := generated.VexStatementInputSpec{ + Status: status, + VexJustification: justification, + KnownSince: publishedTime, + Statement: vulnerability.Analysis.Detail, + StatusNotes: fmt.Sprintf("%s:%s", string(vulnerability.Analysis.State), string(vulnerability.Analysis.Justification)), + } + + for _, affect := range *vulnerability.Affects { + vi := c.getAffectedPackages(ctx, vuln, vd, affect) + if vi == nil { + continue + } + vex = append(vex, *vi...) + + for _, v := range *vi { + if status == generated.VexStatusAffected || status == generated.VexStatusUnderInvestigation { + cv := assembler.CertifyVulnIngest{ + Vulnerability: vuln, + VulnData: &generated.ScanMetadataInput{ + TimeScanned: publishedTime, + }, + Pkg: v.Pkg, + } + certifyVuln = append(certifyVuln, cv) + } + } + } + + for _, vulnRating := range *vulnerability.Ratings { + vm := assembler.VulnMetadataIngest{ + Vulnerability: vuln, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreType(vulnRating.Method), + ScoreValue: *vulnRating.Score, + Timestamp: publishedTime, + }, + } + vulnMetadata = append(vulnMetadata, vm) + } + } + + pred.Vex = vex + pred.CertifyVuln = certifyVuln + pred.VulnMetadata = vulnMetadata + return pred +} diff --git a/pkg/ingestor/parser/cdx_vex/parser_cdx_vex_test.go b/pkg/ingestor/parser/cdx_vex/parser_cdx_vex_test.go new file mode 100644 index 0000000000..af5cc88ab7 --- /dev/null +++ b/pkg/ingestor/parser/cdx_vex/parser_cdx_vex_test.go @@ -0,0 +1,77 @@ +// +// Copyright 2023 The GUAC 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 cdx_vex + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/guacsec/guac/internal/testing/testdata" + "github.com/guacsec/guac/pkg/assembler" + "github.com/guacsec/guac/pkg/handler/processor" + "github.com/guacsec/guac/pkg/logging" +) + +// Test and assert predicates +func Test_CdxVexParser(t *testing.T) { + t.Parallel() + ctx := logging.WithLogger(context.Background()) + tests := []struct { + name string + doc *processor.Document + wantPredicates *assembler.IngestPredicates + }{ + { + name: "successfully parsed a cdx_vex document containing unaffected package", + doc: &processor.Document{ + Blob: testdata.CycloneDXVEXUnAffected, + Format: processor.FormatJSON, + }, + wantPredicates: &assembler.IngestPredicates{ + Vex: testdata.CycloneDXUnAffectedVexIngest, + VulnMetadata: testdata.CycloneDXUnAffectedVulnMetadata, + }, + }, + { + name: "successfully parsed a cdx_vex document containing affected package", + doc: &processor.Document{ + Blob: testdata.CycloneDXVEXAffected, + Format: processor.FormatJSON, + }, + wantPredicates: &assembler.IngestPredicates{ + Vex: testdata.CycloneDXAffectedVexIngest, + VulnMetadata: testdata.CycloneDXAffectedVulnMetadata, + CertifyVuln: testdata.CycloneDXAffectedCertifyVuln, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewCdxVexParser() + if err := c.Parse(ctx, tt.doc); err != nil { + t.Errorf("CdxVexParser.Parse() error = %v", err) + return + } + + preds := c.GetPredicates(ctx) + if d := cmp.Diff(tt.wantPredicates, preds, testdata.IngestPredicatesCmpOpts...); len(d) != 0 { + t.Errorf("cdxVex.GetPredicate mismatch values (+got, -expected): %s", d) + } + }) + } +} diff --git a/pkg/ingestor/parser/common/graph_builder.go b/pkg/ingestor/parser/common/graph_builder.go index ef6d725ee2..3d59599218 100644 --- a/pkg/ingestor/parser/common/graph_builder.go +++ b/pkg/ingestor/parser/common/graph_builder.go @@ -97,4 +97,14 @@ func addMetadata(predicates *assembler.IngestPredicates, foundIdentities []Trust v.HasSourceAt.Collector = srcInfo.Collector v.HasSourceAt.Origin = srcInfo.Source } + + for _, v := range predicates.VulnMetadata { + v.VulnMetadata.Collector = srcInfo.Collector + v.VulnMetadata.Origin = srcInfo.Source + } + + for _, v := range predicates.Vex { + v.VexData.Collector = srcInfo.Collector + v.VexData.Origin = srcInfo.Source + } } diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index f025454f61..9bab80de0b 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -53,7 +53,7 @@ func NewCycloneDXParser() common.DocumentParser { // Parse breaks out the document into the graph components func (c *cyclonedxParser) Parse(ctx context.Context, doc *processor.Document) error { c.doc = doc - cdxBom, err := parseCycloneDXBOM(doc) + cdxBom, err := ParseCycloneDXBOM(doc) if err != nil { return fmt.Errorf("failed to parse cyclonedx BOM: %w", err) } @@ -180,7 +180,7 @@ func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error { return nil } -func parseCycloneDXBOM(doc *processor.Document) (*cdx.BOM, error) { +func ParseCycloneDXBOM(doc *processor.Document) (*cdx.BOM, error) { bom := cdx.BOM{} switch doc.Format { case processor.FormatJSON: diff --git a/pkg/ingestor/parser/parser.go b/pkg/ingestor/parser/parser.go index 0641276e2b..1798107669 100644 --- a/pkg/ingestor/parser/parser.go +++ b/pkg/ingestor/parser/parser.go @@ -21,9 +21,11 @@ import ( "fmt" uuid "github.com/gofrs/uuid" + "github.com/guacsec/guac/pkg/assembler" "github.com/guacsec/guac/pkg/emitter" "github.com/guacsec/guac/pkg/handler/processor" + cdxVex "github.com/guacsec/guac/pkg/ingestor/parser/cdx_vex" "github.com/guacsec/guac/pkg/ingestor/parser/common" "github.com/guacsec/guac/pkg/ingestor/parser/csaf" "github.com/guacsec/guac/pkg/ingestor/parser/cyclonedx" @@ -45,6 +47,7 @@ func init() { _ = RegisterDocumentParser(scorecard.NewScorecardParser, processor.DocumentScorecard) _ = RegisterDocumentParser(deps_dev.NewDepsDevParser, processor.DocumentDepsDev) _ = RegisterDocumentParser(csaf.NewCsafParser, processor.DocumentCsaf) + _ = RegisterDocumentParser(cdxVex.NewCdxVexParser, processor.DocumentCdxVex) } var (