From 6743da425d2380c789de01b44450cf8595d4fa74 Mon Sep 17 00:00:00 2001 From: pxp928 Date: Sun, 21 Apr 2024 19:50:12 -0400 Subject: [PATCH 1/2] implement fixes based on parsing and querying errors Signed-off-by: pxp928 --- cmd/guacone/cmd/vulnerability.go | 40 ++----- pkg/assembler/helpers/purl.go | 2 +- .../parser/cyclonedx/parser_cyclonedx.go | 104 +++++++++++------- pkg/ingestor/parser/spdx/parse_spdx.go | 2 +- 4 files changed, 80 insertions(+), 68 deletions(-) diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 37380fd576..0d08fd352e 100644 --- a/cmd/guacone/cmd/vulnerability.go +++ b/cmd/guacone/cmd/vulnerability.go @@ -22,7 +22,6 @@ import ( "net/http" "os" "strings" - "sync" "github.com/Khan/genqlient/graphql" model "github.com/guacsec/guac/pkg/assembler/clients/generated" @@ -433,24 +432,17 @@ func searchDependencyPackagesReverse(ctx context.Context, gqlclient graphql.Clie } } -func concurrentVulnAndVexNeighbors(ctx context.Context, gqlclient graphql.Client, pkgID string, isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency, resultChan chan<- struct { +type pkgVersionNeighborQueryResults struct { pkgVersionNeighborResponse *model.NeighborsResponse isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency -}, wg *sync.WaitGroup) { - defer wg.Done() +} - logger := logging.FromContext(ctx) +func getVulnAndVexNeighbors(ctx context.Context, gqlclient graphql.Client, pkgID string, isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency) (*pkgVersionNeighborQueryResults, error) { pkgVersionNeighborResponse, err := model.Neighbors(ctx, gqlclient, pkgID, []model.Edge{model.EdgePackageCertifyVuln, model.EdgePackageCertifyVexStatement}) if err != nil { - logger.Errorf("error querying neighbor for vulnerability: %w", err) - return + return nil, fmt.Errorf("failed to get neighbors for pkgID: %s with error %w", pkgID, err) } - - // Send the results to the resultChan - resultChan <- struct { - pkgVersionNeighborResponse *model.NeighborsResponse - isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency - }{pkgVersionNeighborResponse, isDep} + return &pkgVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isDep: isDep}, nil } // searchPkgViaHasSBOM takes in either a purl or URI for the initial value to find the hasSBOM node. @@ -460,7 +452,7 @@ func searchPkgViaHasSBOM(ctx context.Context, gqlclient graphql.Client, searchSt var path []string var tableRows []table.Row checkedPkgIDs := make(map[string]bool) - var wg sync.WaitGroup + var collectedPkgVersionResults []*pkgVersionNeighborQueryResults queue := make([]string, 0) // the queue of nodes in bfs type dfsNode struct { @@ -474,11 +466,6 @@ func searchPkgViaHasSBOM(ctx context.Context, gqlclient graphql.Client, searchSt nodeMap[searchString] = dfsNode{} queue = append(queue, searchString) - resultChan := make(chan struct { - pkgVersionNeighborResponse *model.NeighborsResponse - isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency - }) - for len(queue) > 0 { now := queue[0] queue = queue[1:] @@ -560,8 +547,11 @@ func searchPkgViaHasSBOM(ctx context.Context, gqlclient graphql.Client, searchSt if !dfsN.expanded { queue = append(queue, pkgID) } - wg.Add(1) - go concurrentVulnAndVexNeighbors(ctx, gqlclient, pkgID, isDep, resultChan, &wg) + pkgVersionNeighbors, err := getVulnAndVexNeighbors(ctx, gqlclient, pkgID, isDep) + if err != nil { + return nil, nil, fmt.Errorf("getVulnAndVexNeighbors failed with error: %w", err) + } + collectedPkgVersionResults = append(collectedPkgVersionResults, pkgVersionNeighbors) checkedPkgIDs[pkgID] = true } } @@ -570,16 +560,10 @@ func searchPkgViaHasSBOM(ctx context.Context, gqlclient graphql.Client, searchSt nodeMap[now] = nowNode } - // Close the result channel once all goroutines are done - go func() { - wg.Wait() - close(resultChan) - }() - checkedCertifyVulnIDs := make(map[string]bool) // Collect results from the channel - for result := range resultChan { + for _, result := range collectedPkgVersionResults { for _, neighbor := range result.pkgVersionNeighborResponse.Neighbors { if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { if !checkedCertifyVulnIDs[certifyVuln.Id] { diff --git a/pkg/assembler/helpers/purl.go b/pkg/assembler/helpers/purl.go index ae79695789..b8327322d2 100644 --- a/pkg/assembler/helpers/purl.go +++ b/pkg/assembler/helpers/purl.go @@ -137,7 +137,7 @@ func purlConvert(p purl.PackageURL) (*model.PkgInputSpec, error) { purl.TypeDebian, purl.TypeGem, purl.TypeGithub, purl.TypeGolang, purl.TypeHackage, purl.TypeHex, purl.TypeMaven, purl.TypeNPM, purl.TypeNuget, purl.TypePyPi, purl.TypeRPM, purl.TypeSwift, - purl.TypeGeneric: + purl.TypeGeneric, purl.TypeYocto, purl.TypeCpan: // some code r := pkg(p.Type, p.Namespace, p.Name, p.Version, p.Subpath, p.Qualifiers.Map()) return r, nil diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index cfba980fa8..9d3e6e17f7 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -326,40 +326,53 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to create vuln input spec %v", err) } + var vd model.VexStatementInputSpec + if vulnerability.Analysis != nil { + if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { + status = vexStatus + } else { + return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + } - 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 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) - } + 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)), - } + vd = model.VexStatementInputSpec{ + Status: status, + VexJustification: justification, + KnownSince: publishedTime, + StatusNotes: vulnerability.Description, + } - 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)) + 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, ",") + } else { + vd.Statement = vulnerability.Detail + } + } else { + vd = model.VexStatementInputSpec{ + // if status not specified, assume affected + Status: model.VexStatusAffected, + VexJustification: model.VexJustificationNotProvided, + KnownSince: time.Unix(0, 0), + StatusNotes: vulnerability.Description, + Statement: vulnerability.Detail, } - vd.Statement = strings.Join(response, ",") } for _, affect := range *vulnerability.Affects { @@ -384,18 +397,21 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { } for _, vulnRating := range *vulnerability.Ratings { - vm := assembler.VulnMetadataIngest{ - Vulnerability: vuln, - VulnMetadata: &model.VulnerabilityMetadataInputSpec{ - ScoreType: model.VulnerabilityScoreType(vulnRating.Method), - ScoreValue: *vulnRating.Score, - Timestamp: publishedTime, - }, + if vulnRating.Method != "" { + 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) + } else { + logger.Debugf("vulnerability method not specified in cdx sbom: %s, skipping", c.doc.SourceInformation.DocumentRef) } - c.vulnData.vulnMetadata = append(c.vulnData.vulnMetadata, vm) } } - return nil } @@ -404,6 +420,18 @@ func (c *cyclonedxParser) getAffectedPackages(ctx context.Context, vulnInput *mo logger := logging.FromContext(ctx) pkgRef := affectsObj.Ref + var foundVexIngest []assembler.VexIngest + + foundPkgElements := c.getPackageElement(affectsObj.Ref) + + for _, foundPkgElement := range foundPkgElements { + foundVexIngest = append(foundVexIngest, assembler.VexIngest{VexData: &vexData, Vulnerability: vulnInput, Pkg: foundPkgElement}) + } + + if len(foundVexIngest) > 0 { + return &foundVexIngest, nil + } + // split ref using # as delimiter. pkgRefInfo := strings.Split(pkgRef, "#") if len(pkgRefInfo) != 2 { diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index 71e6ac8b64..fb2a96e912 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -420,7 +420,7 @@ func fixLicense(ctx context.Context, l *generated.LicenseInputSpec, ol []*spdx.O } } if !found { - logger.Error("License identifier %q not found in OtherLicenses", l.Name) + logger.Error("License identifier %s not found in OtherLicenses", l.Name) s := "Not found" l.Inline = &s } From 633dcaed2f38015cb2124a32f99af506c1bf457a Mon Sep 17 00:00:00 2001 From: pxp928 Date: Sun, 21 Apr 2024 20:53:38 -0400 Subject: [PATCH 2/2] add unit test for cdx vex use case Signed-off-by: pxp928 --- .../cyclonedx-vex-no-analysis.json | 50 +++++++++++++++++++ internal/testing/testdata/testdata.go | 39 +++++++++++++-- pkg/assembler/helpers/purl_test.go | 8 +++ .../parser/cyclonedx/parser_cyclonedx.go | 43 +++++++--------- .../parser/cyclonedx/parser_cyclonedx_test.go | 44 ++++++++++++++++ 5 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 internal/testing/testdata/exampledata/cyclonedx-vex-no-analysis.json diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-no-analysis.json b/internal/testing/testdata/exampledata/cyclonedx-vex-no-analysis.json new file mode 100644 index 0000000000..5bb4c7584d --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-no-analysis.json @@ -0,0 +1,50 @@ +{ + "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" + } + ], + "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.", + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#product-ABC", + "versions": [ + { + "version": "2.4", + "status": "affected" + }, + { + "version": "2.6", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index f34c09e7eb..25e35d7f95 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -18,7 +18,6 @@ package testdata import ( _ "embed" "encoding/base64" - "fmt" "time" "github.com/google/go-cmp/cmp" @@ -111,6 +110,9 @@ var ( //go:embed exampledata/cyclonedx-vex-affected.json CycloneDXVEXAffected []byte + //go:embed exampledata/cyclonedx-vex-no-analysis.json + CycloneDXVEXWithoutAnalysis []byte + //go:embed exampledata/cyclonedx-vex.xml CyloneDXVEXExampleXML []byte @@ -186,8 +188,8 @@ var ( 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: fmt.Sprintf("%s:%s", generated.VexStatusNotAffected, generated.VexJustificationVulnerableCodeNotInExecutePath), + Statement: "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.", + StatusNotes: "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly.", KnownSince: parseUTCTime("2020-12-03T00:00:00.000Z"), }, }, @@ -231,8 +233,15 @@ var ( 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), + Statement: "", + StatusNotes: "Versions of Product ABC are affected by the vulnerability. Customers are advised to upgrade to the latest release.", + KnownSince: time.Unix(0, 0), + } + VexDataNoAnalysis = &generated.VexStatementInputSpec{ + Status: generated.VexStatusAffected, + VexJustification: generated.VexJustificationNotProvided, + Statement: "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.", + StatusNotes: "", KnownSince: time.Unix(0, 0), } CycloneDXAffectedVulnMetadata = []assembler.VulnMetadataIngest{ @@ -245,6 +254,16 @@ var ( }, }, } + CycloneDXNoAnalysisVulnMetadata = []assembler.VulnMetadataIngest{ + { + Vulnerability: VulnSpecAffected, + VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ + ScoreType: generated.VulnerabilityScoreTypeCvssv31, + ScoreValue: 10, + Timestamp: time.Unix(0, 0), + }, + }, + } topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC") HasSBOMVexAffected = []assembler.HasSBOMIngest{ @@ -257,6 +276,16 @@ var ( }, }, } + HasSBOMVexNoAnalysis = []assembler.HasSBOMIngest{ + { + Pkg: topLevelPkg, + HasSBOM: &model.HasSBOMInputSpec{ + Algorithm: "sha256", + Digest: "265c99f1f9a09b7fc10c14c97ca1a07fc52ae470f5cbcddd9baf5585fb28221c", + KnownSince: parseRfc3339("2022-03-03T00:00:00Z"), + }, + }, + } // DSSE/SLSA Testdata diff --git a/pkg/assembler/helpers/purl_test.go b/pkg/assembler/helpers/purl_test.go index 5623e7c525..cff4c24f5e 100644 --- a/pkg/assembler/helpers/purl_test.go +++ b/pkg/assembler/helpers/purl_test.go @@ -208,6 +208,14 @@ func TestPurlConvert(t *testing.T) { expected: pkg("oci", "registry.redhat.io/ubi9", "ubi9-container", "sha256:8614ce95268b970880a1eca97dddfce5154fab35418d839c5f75012cccaca0d9", "", map[string]string{ "tag": "9.2-489", }), + }, { + purlUri: "pkg:yocto/dmidecode@2.12-r0?arch=core2-32", + expected: pkg("yocto", "", "dmidecode", "2.12-r0", "", map[string]string{ + "arch": "core2-32", + }), + }, { + purlUri: "pkg:cpan/Pod-Perldoc@3.20", + expected: pkg("cpan", "", "Pod-Perldoc", "3.20", "", map[string]string{}), }, } diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index 9d3e6e17f7..4dfd90113a 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -317,61 +317,54 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { 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) } var vd model.VexStatementInputSpec + publishedTime := time.Unix(0, 0) if vulnerability.Analysis != nil { if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { - status = vexStatus + vd.Status = vexStatus } else { return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) } if vexJustification, ok := justificationsMap[vulnerability.Analysis.Justification]; ok { - justification = vexJustification + vd.VexJustification = vexJustification } else { - justification = model.VexJustificationNotProvided + vd.VexJustification = 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: vulnerability.Description, + publishedTime, err = time.Parse(time.RFC3339, vulnerability.Published) + if err != nil { + return fmt.Errorf("failed to pase time: %s, with error: %w", vulnerability.Published, err) + } } + vd.KnownSince = publishedTime + vd.Statement = vulnerability.Description if vulnerability.Analysis.Detail != "" { - vd.Statement = vulnerability.Analysis.Detail + vd.StatusNotes = 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, ",") + vd.StatusNotes = strings.Join(response, ",") } else { - vd.Statement = vulnerability.Detail + vd.StatusNotes = vulnerability.Detail } } else { vd = model.VexStatementInputSpec{ // if status not specified, assume affected Status: model.VexStatusAffected, VexJustification: model.VexJustificationNotProvided, - KnownSince: time.Unix(0, 0), - StatusNotes: vulnerability.Description, - Statement: vulnerability.Detail, + KnownSince: publishedTime, + StatusNotes: vulnerability.Detail, + Statement: vulnerability.Description, } } @@ -383,11 +376,11 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { c.vulnData.vex = append(c.vulnData.vex, *vi...) for _, v := range *vi { - if status == model.VexStatusAffected || status == model.VexStatusUnderInvestigation { + if v.VexData.Status == model.VexStatusAffected || v.VexData.Status == model.VexStatusUnderInvestigation { cv := assembler.CertifyVulnIngest{ Vulnerability: vuln, VulnData: &model.ScanMetadataInput{ - TimeScanned: publishedTime, + TimeScanned: v.VexData.KnownSince, }, Pkg: v.Pkg, } diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 9e84ee6abf..1ad93d88a0 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -118,6 +118,15 @@ func Test_cyclonedxParser(t *testing.T) { }, wantPredicates: affectedVexPredicates(), wantErr: false, + }, { + name: "valid CycloneDX VEX document with no analysis", + doc: &processor.Document{ + Blob: testdata.CycloneDXVEXWithoutAnalysis, + Format: processor.FormatJSON, + Type: processor.DocumentCycloneDX, + }, + wantPredicates: noAnalysisVexPredicates(), + wantErr: false, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -516,3 +525,38 @@ func affectedVexPredicates() *assembler.IngestPredicates { }, } } + +func noAnalysisVexPredicates() *assembler.IngestPredicates { + return &assembler.IngestPredicates{ + HasSBOM: testdata.HasSBOMVexNoAnalysis, + VulnMetadata: testdata.CycloneDXNoAnalysisVulnMetadata, + Vex: []assembler.VexIngest{ + { + Pkg: guacPkgHelper("product-ABC", "2.4"), + Vulnerability: testdata.VulnSpecAffected, + VexData: testdata.VexDataNoAnalysis, + }, + { + Pkg: guacPkgHelper("product-ABC", "2.6"), + Vulnerability: testdata.VulnSpecAffected, + VexData: testdata.VexDataNoAnalysis, + }, + }, + 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), + }, + }, + }, + } +}