diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json b/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json index c27d8527c5..72a8be0100 100644 --- a/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-affected.json @@ -36,7 +36,7 @@ }, "affects": [ { - "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@", + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#product-ABC", "versions": [ { "version": "2.4", diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index f5feaee027..0b7ed5f544 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -18,6 +18,7 @@ package testdata import ( _ "embed" "encoding/base64" + "fmt" "time" jsoniter "github.com/json-iterator/go" @@ -154,7 +155,7 @@ var ( 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", + StatusNotes: fmt.Sprintf("%s:%s", generated.VexStatusNotAffected, generated.VexJustificationVulnerableCodeNotInExecutePath), KnownSince: parseUTCTime("2020-12-03T00:00:00.000Z"), }, }, @@ -185,51 +186,43 @@ var ( }, }, } + CycloneDXUnAffectedPredicates = assembler.IngestPredicates{ + VulnMetadata: CycloneDXUnAffectedVulnMetadata, + Vex: CycloneDXUnAffectedVexIngest, + } - // 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{ + // CycloneDX VEX testdata affected packages. + 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, - }, + VexDataAffected = &generated.VexStatementInputSpec{ + Status: generated.VexStatusAffected, + VexJustification: generated.VexJustificationNotProvided, + Statement: "Versions of Product ABC are affected by the vulnerability. Customers are advised to upgrade to the latest release.", + StatusNotes: fmt.Sprintf("%s:%s", generated.VexStatusAffected, generated.VexJustificationNotProvided), + KnownSince: time.Unix(0, 0), } CycloneDXAffectedVulnMetadata = []assembler.VulnMetadataIngest{ { - Vulnerability: vulnSpecAffected, + Vulnerability: VulnSpecAffected, VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ ScoreType: generated.VulnerabilityScoreTypeCvssv31, ScoreValue: 10, + Timestamp: time.Unix(0, 0), }, }, } - CycloneDXAffectedCertifyVuln = []assembler.CertifyVulnIngest{ - { - Pkg: pkg1, - Vulnerability: vulnSpecAffected, - VulnData: &generated.ScanMetadataInput{}, - }, + + topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC") + HasSBOMVexAffected = []assembler.HasSBOMIngest{ { - Pkg: pkg2, - Vulnerability: vulnSpecAffected, - VulnData: &generated.ScanMetadataInput{}, + Pkg: topLevelPkg, + HasSBOM: &model.HasSBOMInputSpec{ + Algorithm: "sha256", + Digest: "eb62836ed6339a2d57f66d2e42509718fd480a1befea83f925e918444c369114", + KnownSince: parseRfc3339("2022-03-03T00:00:00Z"), + }, }, } diff --git a/pkg/handler/processor/cdx_vex/cdx_vex.go b/pkg/handler/processor/cdx_vex/cdx_vex.go deleted file mode 100644 index 7c2bf8b770..0000000000 --- a/pkg/handler/processor/cdx_vex/cdx_vex.go +++ /dev/null @@ -1,62 +0,0 @@ -// -// 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" - "fmt" - - jsoniter "github.com/json-iterator/go" - - cdx "github.com/CycloneDX/cyclonedx-go" - - "github.com/guacsec/guac/pkg/handler/processor" -) - -var json = jsoniter.ConfigCompatibleWithStandardLibrary - -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 deleted file mode 100644 index 34ae3646aa..0000000000 --- a/pkg/handler/processor/cdx_vex/cdx_vex_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// -// 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 b0cdb4e6e2..edacfaa923 100644 --- a/pkg/handler/processor/guesser/guesser_test.go +++ b/pkg/handler/processor/guesser/guesser_test.go @@ -209,26 +209,6 @@ 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/type_cyclonedx.go b/pkg/handler/processor/guesser/type_cyclonedx.go index 9b6586b76f..9c1dd94e35 100644 --- a/pkg/handler/processor/guesser/type_cyclonedx.go +++ b/pkg/handler/processor/guesser/type_cyclonedx.go @@ -38,9 +38,6 @@ 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: @@ -48,9 +45,6 @@ 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 index 9cfefb55ce..25222339a3 100644 --- a/pkg/handler/processor/guesser/type_cyclonedx_test.go +++ b/pkg/handler/processor/guesser/type_cyclonedx_test.go @@ -72,18 +72,6 @@ func Test_cyclonedxTypeGuesser_GuessDocumentType(t *testing.T) { 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) { diff --git a/pkg/handler/processor/process/process.go b/pkg/handler/processor/process/process.go index 4f0500a966..8af4d6f433 100644 --- a/pkg/handler/processor/process/process.go +++ b/pkg/handler/processor/process/process.go @@ -27,7 +27,6 @@ import ( "github.com/guacsec/guac/pkg/emitter" "github.com/guacsec/guac/pkg/handler/collector" "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" @@ -58,7 +57,6 @@ 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 ffc9cc8398..6a4eb0aba4 100644 --- a/pkg/handler/processor/processor.go +++ b/pkg/handler/processor/processor.go @@ -63,7 +63,6 @@ const ( DocumentCycloneDX DocumentType = "CycloneDX" DocumentDepsDev DocumentType = "DEPS_DEV" DocumentCsaf DocumentType = "CSAF" - DocumentCdxVex DocumentType = "CDX_VEX" DocumentOpenVEX DocumentType = "OPEN_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 deleted file mode 100644 index 84272fac80..0000000000 --- a/pkg/ingestor/parser/cdx_vex/parser_cdx_vex.go +++ /dev/null @@ -1,219 +0,0 @@ -// -// 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 deleted file mode 100644 index af5cc88ab7..0000000000 --- a/pkg/ingestor/parser/cdx_vex/parser_cdx_vex_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// -// 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/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index 346b5113d6..f4e8bc3bb7 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -23,9 +23,9 @@ import ( "strings" "time" + cdx "github.com/CycloneDX/cyclonedx-go" jsoniter "github.com/json-iterator/go" - cdx "github.com/CycloneDX/cyclonedx-go" "github.com/guacsec/guac/pkg/assembler" model "github.com/guacsec/guac/pkg/assembler/clients/generated" asmhelpers "github.com/guacsec/guac/pkg/assembler/helpers" @@ -40,12 +40,31 @@ const topCdxPurlGuac string = "pkg:guac/cdx/" var zeroTime = time.Unix(0, 0) +var vexStatusMap = map[cdx.ImpactAnalysisState]model.VexStatus{ + cdx.IASResolved: model.VexStatusFixed, + cdx.IASExploitable: model.VexStatusAffected, + cdx.IASInTriage: model.VexStatusUnderInvestigation, + cdx.IASNotAffected: model.VexStatusNotAffected, +} + +var justificationsMap = map[cdx.ImpactAnalysisJustification]model.VexJustification{ + cdx.IAJCodeNotPresent: model.VexJustificationVulnerableCodeNotPresent, + cdx.IAJCodeNotReachable: model.VexJustificationVulnerableCodeNotInExecutePath, +} + type cyclonedxParser struct { doc *processor.Document packagePackages map[string][]*model.PkgInputSpec packageArtifacts map[string][]*model.ArtifactInputSpec identifierStrings *common.IdentifierStrings cdxBom *cdx.BOM + vulnData vulnData +} + +type vulnData struct { + vulnMetadata []assembler.VulnMetadataIngest + certifyVuln []assembler.CertifyVulnIngest + vex []assembler.VexIngest } func NewCycloneDXParser() common.DocumentParser { @@ -64,10 +83,13 @@ func (c *cyclonedxParser) Parse(ctx context.Context, doc *processor.Document) er return fmt.Errorf("failed to parse cyclonedx BOM: %w", err) } c.cdxBom = cdxBom - if err := c.getTopLevelPackage(cdxBom); err != nil { + if err := c.getTopLevelPackage(); err != nil { + return err + } + if err := c.getPackages(); err != nil { return err } - if err := c.getPackages(cdxBom); err != nil { + if err := c.getVulnerabilities(ctx); err != nil { return err } @@ -79,17 +101,21 @@ func (c *cyclonedxParser) GetIdentities(ctx context.Context) []common.TrustInfor return nil } -func (c *cyclonedxParser) getTopLevelPackage(cdxBom *cdx.BOM) error { - if cdxBom.Metadata.Component != nil { - purl := cdxBom.Metadata.Component.PackageURL - if cdxBom.Metadata.Component.PackageURL == "" { - if cdxBom.Metadata.Component.Type == cdx.ComponentTypeContainer { - purl = parseContainerType(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true) - } else if cdxBom.Metadata.Component.Type == cdx.ComponentTypeFile { +func (c *cyclonedxParser) getTopLevelPackage() error { + if c.cdxBom.Metadata == nil { + return nil + } + + if c.cdxBom.Metadata.Component != nil { + purl := c.cdxBom.Metadata.Component.PackageURL + if c.cdxBom.Metadata.Component.PackageURL == "" { + if c.cdxBom.Metadata.Component.Type == cdx.ComponentTypeContainer { + purl = parseContainerType(c.cdxBom.Metadata.Component.Name, c.cdxBom.Metadata.Component.Version, true) + } else if c.cdxBom.Metadata.Component.Type == cdx.ComponentTypeFile { // example: file type ("/home/work/test/build/webserver") - purl = guacCDXFilePurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true) + purl = guacCDXFilePurl(c.cdxBom.Metadata.Component.Name, c.cdxBom.Metadata.Component.Version, true) } else { - purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, "", true) + purl = guacCDXPkgPurl(c.cdxBom.Metadata.Component.Name, c.cdxBom.Metadata.Component.Version, "", true) } } @@ -99,16 +125,16 @@ func (c *cyclonedxParser) getTopLevelPackage(cdxBom *cdx.BOM) error { } c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, purl) - c.packagePackages[string(cdxBom.Metadata.Component.BOMRef)] = append(c.packagePackages[string(cdxBom.Metadata.Component.BOMRef)], topPackage) + c.packagePackages[c.cdxBom.Metadata.Component.BOMRef] = append(c.packagePackages[c.cdxBom.Metadata.Component.BOMRef], topPackage) // if checksums exists create an artifact for each of them - if cdxBom.Metadata.Component.Hashes != nil { - for _, checksum := range *cdxBom.Metadata.Component.Hashes { + if c.cdxBom.Metadata.Component.Hashes != nil { + for _, checksum := range *c.cdxBom.Metadata.Component.Hashes { artifact := &model.ArtifactInputSpec{ Algorithm: strings.ToLower(string(checksum.Algorithm)), Digest: checksum.Value, } - c.packageArtifacts[string(cdxBom.Metadata.Component.BOMRef)] = append(c.packageArtifacts[string(cdxBom.Metadata.Component.BOMRef)], artifact) + c.packageArtifacts[c.cdxBom.Metadata.Component.BOMRef] = append(c.packageArtifacts[c.cdxBom.Metadata.Component.BOMRef], artifact) } } return nil @@ -146,9 +172,9 @@ func parseContainerType(name string, version string, topLevel bool) string { } } -func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error { - if cdxBom.Components != nil { - for _, comp := range *cdxBom.Components { +func (c *cyclonedxParser) getPackages() error { + if c.cdxBom.Components != nil { + for _, comp := range *c.cdxBom.Components { // skipping over the "operating-system" type as it does not contain // the required purl for package node. Currently there is no use-case // to capture OS for GUAC. @@ -167,7 +193,7 @@ func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error { if err != nil { return err } - c.packagePackages[string(comp.BOMRef)] = append(c.packagePackages[string(comp.BOMRef)], pkg) + c.packagePackages[comp.BOMRef] = append(c.packagePackages[comp.BOMRef], pkg) c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, comp.PackageURL) // if checksums exists create an artifact for each of them @@ -177,7 +203,7 @@ func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error { Algorithm: strings.ToLower(string(checksum.Algorithm)), Digest: checksum.Value, } - c.packageArtifacts[string(comp.BOMRef)] = append(c.packageArtifacts[string(comp.BOMRef)], artifact) + c.packageArtifacts[comp.BOMRef] = append(c.packageArtifacts[comp.BOMRef], artifact) } } } @@ -209,10 +235,13 @@ func (c *cyclonedxParser) GetIdentifiers(ctx context.Context) (*common.Identifie func (c *cyclonedxParser) GetPredicates(ctx context.Context) *assembler.IngestPredicates { logger := logging.FromContext(ctx) - preds := &assembler.IngestPredicates{} + var toplevel []*model.PkgInputSpec + + if c.cdxBom.Metadata != nil && c.cdxBom.Metadata.Component != nil { + toplevel = c.getPackageElement(c.cdxBom.Metadata.Component.BOMRef) + } - toplevel := c.getPackageElement(string(c.cdxBom.Metadata.Component.BOMRef)) // adding top level package edge manually for all depends on package // TODO: This is not based on the relationship so that can be inaccurate (can capture both direct and in-direct)...Remove this and be done below by the *c.cdxBom.Dependencies? // see https://github.com/CycloneDX/specification/issues/33 @@ -248,6 +277,9 @@ func (c *cyclonedxParser) GetPredicates(ctx context.Context) *assembler.IngestPr } } + preds.Vex = c.vulnData.vex + preds.VulnMetadata = c.vulnData.vulnMetadata + preds.CertifyVuln = c.vulnData.certifyVuln if c.cdxBom.Dependencies == nil { return preds } @@ -281,8 +313,151 @@ func (c *cyclonedxParser) GetPredicates(ctx context.Context) *assembler.IngestPr return preds } -func (s *cyclonedxParser) getPackageElement(elementID string) []*model.PkgInputSpec { - if packNode, ok := s.packagePackages[string(elementID)]; ok { +func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { + logger := logging.FromContext(ctx) + if c.cdxBom.Vulnerabilities == nil { + logger.Debugf("no vulnerabilities found in CycloneDX BOM") + return nil + } + + var status model.VexStatus + var justification model.VexJustification + var publishedTime time.Time + for _, vulnerability := range *c.cdxBom.Vulnerabilities { + vuln, err := asmhelpers.CreateVulnInput(vulnerability.ID) + if err != nil { + return fmt.Errorf("failed to create vuln input spec %v", err) + } + + if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { + status = vexStatus + } else { + return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + } + + if vexJustification, ok := justificationsMap[vulnerability.Analysis.Justification]; ok { + justification = vexJustification + } else { + justification = model.VexJustificationNotProvided + } + + if vulnerability.Published != "" { + publishedTime, _ = time.Parse(time.RFC3339, vulnerability.Published) + } else { + publishedTime = time.Unix(0, 0) + } + + vd := model.VexStatementInputSpec{ + Status: status, + VexJustification: justification, + KnownSince: publishedTime, + StatusNotes: fmt.Sprintf("%s:%s", string(status), string(justification)), + } + + if vulnerability.Analysis.Detail != "" { + vd.Statement = vulnerability.Analysis.Detail + } else if vulnerability.Analysis.Response != nil { + var response []string + for _, res := range *vulnerability.Analysis.Response { + response = append(response, string(res)) + } + vd.Statement = strings.Join(response, ",") + } + + for _, affect := range *vulnerability.Affects { + vi, err := c.getAffectedPackages(ctx, vuln, vd, affect) + if vi == nil || err != nil { + return fmt.Errorf("failed to get affected packages for vulnerability %s - %v", vulnerability.ID, err) + } + c.vulnData.vex = append(c.vulnData.vex, *vi...) + + for _, v := range *vi { + if status == model.VexStatusAffected || status == model.VexStatusUnderInvestigation { + cv := assembler.CertifyVulnIngest{ + Vulnerability: vuln, + VulnData: &model.ScanMetadataInput{ + TimeScanned: publishedTime, + }, + Pkg: v.Pkg, + } + c.vulnData.certifyVuln = append(c.vulnData.certifyVuln, cv) + } + } + } + + for _, vulnRating := range *vulnerability.Ratings { + vm := assembler.VulnMetadataIngest{ + Vulnerability: vuln, + VulnMetadata: &model.VulnerabilityMetadataInputSpec{ + ScoreType: model.VulnerabilityScoreType(vulnRating.Method), + ScoreValue: *vulnRating.Score, + Timestamp: publishedTime, + }, + } + c.vulnData.vulnMetadata = append(c.vulnData.vulnMetadata, vm) + } + } + + return nil +} + +// Get package name and range versions to create package input spec for the affected packages. +func (c *cyclonedxParser) getAffectedPackages(ctx context.Context, vulnInput *model.VulnerabilityInputSpec, vexData model.VexStatementInputSpec, affectsObj cdx.Affects) (*[]assembler.VexIngest, error) { + logger := logging.FromContext(ctx) + pkgRef := affectsObj.Ref + + // split ref using # as delimiter. + pkgRefInfo := strings.Split(pkgRef, "#") + if len(pkgRefInfo) != 2 { + return nil, fmt.Errorf("malformed affected-package reference: %q", affectsObj.Ref) + } + pkdIdentifier := pkgRefInfo[1] + + // check whether the ref contains a purl + if strings.Contains(pkdIdentifier, "pkg:") { + pkg, err := asmhelpers.PurlToPkg(pkdIdentifier) + if err != nil { + return nil, fmt.Errorf("unable to create package input spec: %v", err) + } + c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, pkdIdentifier) + return &[]assembler.VexIngest{{VexData: &vexData, Vulnerability: vulnInput, Pkg: pkg}}, nil + } + + if affectsObj.Range == nil { + return nil, fmt.Errorf("no vulnerable components found for ref %q", affectsObj.Ref) + } + + 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.Range != "" { + logger.Debugf("[cdx vex] package range versions not supported yet: %q", affect.Range) + continue + } + if affect.Version == "" { + return nil, fmt.Errorf("no version found for package ref %q", pkgRef) + } + vi := &assembler.VexIngest{ + VexData: &vexData, + Vulnerability: vulnInput, + } + + // create guac specific identifier string using affected package name and version. + pkgID := guacCDXPkgPurl(pkdIdentifier, affect.Version, "", false) + pkg, err := asmhelpers.PurlToPkg(pkgID) + if err != nil { + return nil, fmt.Errorf("unable to create package input spec from guac pkg purl: %v", err) + } + vi.Pkg = pkg + viList = append(viList, *vi) + c.identifierStrings.PurlStrings = append(c.identifierStrings.PurlStrings, pkgID) + } + + return &viList, nil +} + +func (c *cyclonedxParser) getPackageElement(elementID string) []*model.PkgInputSpec { + if packNode, ok := c.packagePackages[elementID]; ok { return packNode } return nil diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 6ab3a37406..a528e7301e 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -18,6 +18,7 @@ package cyclonedx import ( "context" "testing" + "time" cdx "github.com/CycloneDX/cyclonedx-go" "github.com/google/go-cmp/cmp" @@ -99,6 +100,24 @@ func Test_cyclonedxParser(t *testing.T) { }, wantPredicates: nil, wantErr: true, + }, { + name: "valid CycloneDX VEX document with unaffected packages", + doc: &processor.Document{ + Blob: testdata.CycloneDXVEXUnAffected, + Format: processor.FormatJSON, + Type: processor.DocumentCycloneDX, + }, + wantPredicates: &testdata.CycloneDXUnAffectedPredicates, + wantErr: false, + }, { + name: "valid CycloneDX VEX document with affected packages", + doc: &processor.Document{ + Blob: testdata.CycloneDXVEXAffected, + Format: processor.FormatJSON, + Type: processor.DocumentCycloneDX, + }, + wantPredicates: affectedVexPredicates(), + wantErr: false, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -282,7 +301,7 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) { identifierStrings: &common.IdentifierStrings{}, } c.cdxBom = tt.cdxBom - if err := c.getTopLevelPackage(tt.cdxBom); err != nil { + if err := c.getTopLevelPackage(); err != nil { t.Errorf("Failed to getTopLevelPackage %s", err) } wantPackage, err := asmhelpers.PurlToPkg(tt.wantPurl) @@ -439,7 +458,7 @@ func Test_cyclonedxParser_getComponentPackages(t *testing.T) { identifierStrings: &common.IdentifierStrings{}, } c.cdxBom = tt.cdxBom - if err := c.getPackages(tt.cdxBom); err != nil { + if err := c.getPackages(); err != nil { t.Errorf("Failed to getTopLevelPackage %s", err) } wantPackage, err := asmhelpers.PurlToPkg(tt.wantPurl) @@ -456,3 +475,44 @@ func Test_cyclonedxParser_getComponentPackages(t *testing.T) { }) } } + +func guacPkgHelper(name string, version string) *model.PkgInputSpec { + pkgURL := guacCDXPkgPurl(name, version, "", false) + pkg, _ := asmhelpers.PurlToPkg(pkgURL) + return pkg +} + +func affectedVexPredicates() *assembler.IngestPredicates { + return &assembler.IngestPredicates{ + HasSBOM: testdata.HasSBOMVexAffected, + VulnMetadata: testdata.CycloneDXAffectedVulnMetadata, + Vex: []assembler.VexIngest{ + { + Pkg: guacPkgHelper("product-ABC", "2.4"), + Vulnerability: testdata.VulnSpecAffected, + VexData: testdata.VexDataAffected, + }, + { + Pkg: guacPkgHelper("product-ABC", "2.6"), + Vulnerability: testdata.VulnSpecAffected, + VexData: testdata.VexDataAffected, + }, + }, + CertifyVuln: []assembler.CertifyVulnIngest{ + { + Pkg: guacPkgHelper("product-ABC", "2.4"), + Vulnerability: testdata.VulnSpecAffected, + VulnData: &model.ScanMetadataInput{ + TimeScanned: time.Unix(0, 0), + }, + }, + { + Pkg: guacPkgHelper("product-ABC", "2.6"), + Vulnerability: testdata.VulnSpecAffected, + VulnData: &model.ScanMetadataInput{ + TimeScanned: time.Unix(0, 0), + }, + }, + }, + } +} diff --git a/pkg/ingestor/parser/parser.go b/pkg/ingestor/parser/parser.go index 86dce2f206..0e2b93cb7d 100644 --- a/pkg/ingestor/parser/parser.go +++ b/pkg/ingestor/parser/parser.go @@ -19,14 +19,12 @@ import ( "context" "fmt" + "github.com/gofrs/uuid" jsoniter "github.com/json-iterator/go" - 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" @@ -52,7 +50,6 @@ func init() { _ = RegisterDocumentParser(deps_dev.NewDepsDevParser, processor.DocumentDepsDev) _ = RegisterDocumentParser(csaf.NewCsafParser, processor.DocumentCsaf) _ = RegisterDocumentParser(open_vex.NewOpenVEXParser, processor.DocumentOpenVEX) - _ = RegisterDocumentParser(cdxVex.NewCdxVexParser, processor.DocumentCdxVex) } var (