From b311f0a66102a184b61063e9cdd1e53ea9be17be Mon Sep 17 00:00:00 2001 From: pxp928 Date: Mon, 23 Sep 2024 15:02:45 -0400 Subject: [PATCH 1/5] update vuln cli to handle artifact input Signed-off-by: pxp928 --- cmd/guacone/cmd/vulnerability.go | 84 +++++++------ pkg/guacanalytics/searchForSBOM.go | 196 ++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 38 deletions(-) diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 0b220a3100..681ad73be9 100644 --- a/cmd/guacone/cmd/vulnerability.go +++ b/cmd/guacone/cmd/vulnerability.go @@ -127,35 +127,31 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer var path []string var tableRows []table.Row - // The primaryCall parameter in searchForSBOMViaPkg is there for us to know whether - // the searchString is expected to be a PURL, and we are searching via a purl. - depVulnPath, depVulnTableRows, err := guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) - if err != nil { - logger.Fatalf("error searching via hasSBOM: %v", err) + var depVulnPath []string + var depVulnTableRows []table.Row + //check if artifact + split := strings.Split(opts.searchString, ":") + if len(split) == 2 { + var err error + // The primaryCall parameter in searchForSBOMViaPkg is there for us to know whether + // the searchString is expected to be a PURL, and we are searching via a purl. + depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, opts.searchString, opts.depth) + if err != nil { + logger.Fatalf("error searching via hasSBOM: %v", err) + } + } else { + var err error + // The primaryCall parameter in searchForSBOMViaPkg is there for us to know whether + // the searchString is expected to be a PURL, and we are searching via a purl. + depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) + if err != nil { + logger.Fatalf("error searching via hasSBOM: %v", err) + } } path = append(path, depVulnPath...) tableRows = append(tableRows, depVulnTableRows...) - if len(depVulnPath) == 0 { - occur := searchArtToPkg(ctx, gqlclient, opts.searchString, logger) - - if occur != nil && len(occur.IsOccurrence) > 0 { - subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) - if ok { - // The primaryCall parameter in searchForSBOMViaPkg is there for us to know that - // the searchString is expected to be an artifact, but isn't, so we have to check via PURLs instead of artifacts. - depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, subjectPackage.Namespaces[0].Names[0].Versions[0].Id, opts.depth, false) - if err != nil { - logger.Fatalf("error searching via hasSBOM: %v", err) - } - - path = append(path, depVulnPath...) - tableRows = append(tableRows, depVulnTableRows...) - } - } - } - if len(path) > 0 { t.AppendRows(tableRows) @@ -212,18 +208,10 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl logger.Fatalf("error querying neighbor: %v", err) } } else { - foundHasSBOM, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Uri: &opts.searchString}) - if err != nil { - logger.Fatalf("failed getting hasSBOM via URI: %s with error: %w", opts.searchString, err) - } - if pkgResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); ok { - var vulnNeighborError error - path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) - if vulnNeighborError != nil { - logger.Fatalf("error querying neighbor: %v", err) - } - } else if artResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectArtifact); ok { - occur := searchArtToPkg(ctx, gqlclient, artResponse.Algorithm+":"+artResponse.Digest, logger) + + split := strings.Split(opts.searchString, ":") + if len(split) == 2 { + occur := searchArtToPkg(ctx, gqlclient, split[0]+":"+split[1], logger) subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) if ok { var vulnNeighborError error @@ -233,7 +221,29 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl } } } else { - logger.Fatalf("located hasSBOM does not have a subject that is a package or artifact") + foundHasSBOM, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Uri: &opts.searchString}) + if err != nil { + logger.Fatalf("failed getting hasSBOM via URI: %s with error: %w", opts.searchString, err) + } + if pkgResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); ok { + var vulnNeighborError error + path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) + if vulnNeighborError != nil { + logger.Fatalf("error querying neighbor: %v", err) + } + } else if artResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectArtifact); ok { + occur := searchArtToPkg(ctx, gqlclient, artResponse.Algorithm+":"+artResponse.Digest, logger) + subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) + if ok { + var vulnNeighborError error + path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, subjectPackage.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) + if vulnNeighborError != nil { + logger.Fatalf("error querying neighbor: %v", err) + } + } + } else { + logger.Fatalf("located hasSBOM does not have a subject that is a package or artifact") + } } } if len(path) > 0 { diff --git a/pkg/guacanalytics/searchForSBOM.go b/pkg/guacanalytics/searchForSBOM.go index 3391c03eda..a9ef1aabf5 100644 --- a/pkg/guacanalytics/searchForSBOM.go +++ b/pkg/guacanalytics/searchForSBOM.go @@ -3,6 +3,7 @@ package guacanalytics import ( "context" "fmt" + "strings" "github.com/Khan/genqlient/graphql" model "github.com/guacsec/guac/pkg/assembler/clients/generated" @@ -30,6 +31,180 @@ func getVulnAndVexNeighborsForPackage(ctx context.Context, gqlclient graphql.Cli return &pkgVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isDep: isDep}, nil } +func SearchForSBOMViaArtifact(ctx context.Context, gqlclient graphql.Client, searchString string, maxLength int) ([]string, []table.Row, error) { + var path []string + var tableRows []table.Row + checkedPkgIDs := make(map[string]bool) + var collectedPkgVersionResults []*pkgVersionNeighborQueryResults + + queue := make([]string, 0) // the queue of nodes in bfs + type dfsNode struct { + expanded bool // true once all node neighbors are added to queue + parent string + pkgID string + depth int + } + nodeMap := map[string]dfsNode{} + + nodeMap[searchString] = dfsNode{} + queue = append(queue, searchString) + + for len(queue) > 0 { + now := queue[0] + queue = queue[1:] + nowNode := nodeMap[now] + + if maxLength != 0 && nowNode.depth >= maxLength { + break + } + + var foundHasSBOM *model.HasSBOMsResponse + + // if the initial depth, check if it's a purl or an SBOM URI. Otherwise, always search by pkgID + // note that primaryCall will be static throughout the entire function. + if nowNode.depth == 0 { + artResponse, err := getArtifactResponseFromArtifact(ctx, gqlclient, now) + if err != nil { + return nil, nil, fmt.Errorf("getPkgResponseFromPurl - error: %v", err) + } + foundHasSBOM, err = model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Artifact: &model.ArtifactSpec{Id: &artResponse.Artifacts[0].Id}}}) + if err != nil { + return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + } + } else { + foundHasSBOMPkg, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Package: &model.PkgSpec{Id: &now}}}) + if err != nil { + return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + } + if foundHasSBOMPkg != nil { + foundHasSBOM = foundHasSBOMPkg + } else { + occur, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ + Subject: &model.PackageOrSourceSpec{Package: &model.PkgSpec{Id: &now}}, + }) + if err != nil { + return nil, nil, fmt.Errorf("error querying for occurrences: %v", err) + } + if occur != nil && len(occur.IsOccurrence) > 0 { + foundHasSBOMArt, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Artifact: &model.ArtifactSpec{Id: &occur.IsOccurrence[0].Artifact.Id}}}) + if err != nil { + return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + } + if foundHasSBOMArt != nil { + foundHasSBOM = foundHasSBOMArt + } + } + } + } + + for _, hasSBOM := range foundHasSBOM.HasSBOM { + + subjectArtifact, ok := hasSBOM.Subject.(*model.AllHasSBOMTreeSubjectArtifact) + if ok { + occur, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ + Artifact: &model.ArtifactSpec{Id: &subjectArtifact.Id}, + }) + if err != nil { + return nil, nil, fmt.Errorf("error querying for occurrences: %v", err) + } + pkgResponse, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) + if ok { + if pkgResponse.Type != guacType { + if !checkedPkgIDs[pkgResponse.Namespaces[0].Names[0].Versions[0].Id] { + vulnPath, pkgVulnTableRows, err := queryVulnsViaPackageNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id) + if err != nil { + return nil, nil, fmt.Errorf("error querying neighbor: %w", err) + } + path = append(path, vulnPath...) + tableRows = append(tableRows, pkgVulnTableRows...) + path = append([]string{pkgResponse.Namespaces[0].Names[0].Versions[0].Id, + pkgResponse.Namespaces[0].Names[0].Id, pkgResponse.Namespaces[0].Id, + pkgResponse.Id}, path...) + checkedPkgIDs[pkgResponse.Namespaces[0].Names[0].Versions[0].Id] = true + } + } + } + } + + for _, isDep := range hasSBOM.IncludedDependencies { + if isDep.DependencyPackage.Type == guacType { + continue + } + depPkgID := isDep.DependencyPackage.Namespaces[0].Names[0].Versions[0].Id + dfsN, seen := nodeMap[depPkgID] + if !seen { + dfsN = dfsNode{ + parent: now, + pkgID: depPkgID, + depth: nowNode.depth + 1, + } + nodeMap[depPkgID] = dfsN + } + if !dfsN.expanded { + queue = append(queue, depPkgID) + } + pkgVersionNeighbors, err := getVulnAndVexNeighborsForPackage(ctx, gqlclient, depPkgID, isDep) + if err != nil { + return nil, nil, fmt.Errorf("getVulnAndVexNeighbors failed with error: %w", err) + } + collectedPkgVersionResults = append(collectedPkgVersionResults, pkgVersionNeighbors) + checkedPkgIDs[depPkgID] = true + + } + + for _, isDep := range hasSBOM.IncludedDependencies { + if isDep.DependencyPackage.Type == guacType { + continue + } + + depPkgID := isDep.DependencyPackage.Namespaces[0].Names[0].Versions[0].Id + if _, seen := nodeMap[depPkgID]; !seen { + dfsN := dfsNode{ + parent: now, + pkgID: depPkgID, + depth: nowNode.depth + 1, + } + nodeMap[depPkgID] = dfsN + } + } + } + nowNode.expanded = true + nodeMap[now] = nowNode + } + + checkedCertifyVulnIDs := make(map[string]bool) + + // Collect results from the channel + for _, result := range collectedPkgVersionResults { + for _, neighbor := range result.pkgVersionNeighborResponse.Neighbors { + if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { + if !checkedCertifyVulnIDs[certifyVuln.Id] && certifyVuln.Vulnerability.Type != noVulnType { + checkedCertifyVulnIDs[certifyVuln.Id] = true + for _, vuln := range certifyVuln.Vulnerability.VulnerabilityIDs { + tableRows = append(tableRows, table.Row{certifyVulnStr, certifyVuln.Id, "vulnerability ID: " + vuln.VulnerabilityID}) + path = append(path, []string{vuln.Id, certifyVuln.Id, + certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, + certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, + certifyVuln.Package.Id}...) + } + path = append(path, result.isDep.Id, result.isDep.Package.Namespaces[0].Names[0].Versions[0].Id, + result.isDep.Package.Namespaces[0].Names[0].Id, result.isDep.Package.Namespaces[0].Id, + result.isDep.Package.Id) + } + } + + if certifyVex, ok := neighbor.(*model.NeighborsNeighborsCertifyVEXStatement); ok { + for _, vuln := range certifyVex.Vulnerability.VulnerabilityIDs { + tableRows = append(tableRows, table.Row{vexLinkStr, certifyVex.Id, "vulnerability ID: " + vuln.VulnerabilityID + ", Vex Status: " + string(certifyVex.Status) + ", Subject: " + VexSubjectString(certifyVex.Subject)}) + path = append(path, certifyVex.Id, vuln.Id) + } + path = append(path, vexSubjectIds(certifyVex.Subject)...) + } + } + } + return path, tableRows, nil +} + // SearchForSBOMViaPkg takes in either a purl or URI for the initial value to find the hasSBOM node. // From there is recursively searches through all the dependencies to determine if it contains hasSBOM nodes. // It concurrent checks the package version node if it contains vulnerabilities and VEX data. @@ -67,7 +242,6 @@ func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchSt // if the initial depth, check if it's a purl or an SBOM URI. Otherwise, always search by pkgID // note that primaryCall will be static throughout the entire function. if nowNode.depth == 0 { - if isPurl { pkgResponse, err := getPkgResponseFromPurl(ctx, gqlclient, now) if err != nil { @@ -186,6 +360,26 @@ func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchSt return path, tableRows, nil } +func getArtifactResponseFromArtifact(ctx context.Context, gqlclient graphql.Client, artifactString string) (*model.ArtifactsResponse, error) { + + split := strings.Split(artifactString, ":") + if len(split) != 2 { + return nil, fmt.Errorf("failed to split artifact into algo and digest") + } + artFilter := &model.ArtifactSpec{ + Algorithm: &split[0], + Digest: &split[1], + } + artResponse, err := model.Artifacts(ctx, gqlclient, *artFilter) + if err != nil { + return nil, fmt.Errorf("error querying for package: %v", err) + } + if len(artResponse.Artifacts) != 1 { + return nil, fmt.Errorf("failed to located package based on purl") + } + return artResponse, nil +} + func getPkgResponseFromPurl(ctx context.Context, gqlclient graphql.Client, purl string) (*model.PackagesResponse, error) { pkgInput, err := helpers.PurlToPkg(purl) if err != nil { From d192b083f5fedbad898a798df8b2413041f8be51 Mon Sep 17 00:00:00 2001 From: pxp928 Date: Mon, 23 Sep 2024 15:24:26 -0400 Subject: [PATCH 2/5] spdx add occur for top level artifact/package Signed-off-by: pxp928 --- pkg/ingestor/parser/spdx/parse_spdx.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index 4bb5249b04..709a04acab 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -267,10 +267,12 @@ func (s *spdxParser) GetPredicates(ctx context.Context) *assembler.IngestPredica } if len(s.topLevelArtifacts) > 0 { - for _, arts := range s.topLevelArtifacts { + for id, arts := range s.topLevelArtifacts { for _, art := range arts { preds.HasSBOM = append(preds.HasSBOM, common.CreateTopLevelHasSBOMFromArtifact(art, s.doc, s.spdxDoc.DocumentNamespace, timestamp)) } + // append to packageArtifacts so that isOccurrence is created + s.packageArtifacts[id] = append(s.packageArtifacts[id], arts...) } if len(s.topLevelArtifacts) != len(s.topLevelPackages) { From 29fdcd7bdf65caeda38cd19126cb8909b0108450 Mon Sep 17 00:00:00 2001 From: pxp928 Date: Mon, 23 Sep 2024 15:31:16 -0400 Subject: [PATCH 3/5] remove SPDX fix as it was reduandant Signed-off-by: pxp928 --- pkg/ingestor/parser/spdx/parse_spdx.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index 709a04acab..4bb5249b04 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -267,12 +267,10 @@ func (s *spdxParser) GetPredicates(ctx context.Context) *assembler.IngestPredica } if len(s.topLevelArtifacts) > 0 { - for id, arts := range s.topLevelArtifacts { + for _, arts := range s.topLevelArtifacts { for _, art := range arts { preds.HasSBOM = append(preds.HasSBOM, common.CreateTopLevelHasSBOMFromArtifact(art, s.doc, s.spdxDoc.DocumentNamespace, timestamp)) } - // append to packageArtifacts so that isOccurrence is created - s.packageArtifacts[id] = append(s.packageArtifacts[id], arts...) } if len(s.topLevelArtifacts) != len(s.topLevelPackages) { From f292dbcc182b00f6e891c4b6513d081a8bf86a02 Mon Sep 17 00:00:00 2001 From: nathannaveen <42319948+nathannaveen@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:13:06 -0500 Subject: [PATCH 4/5] Cleaned up code and fix some functionality Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --- cmd/guacone/cmd/vulnerability.go | 253 ++++++++++++++++++++++------- pkg/guacanalytics/searchForSBOM.go | 203 ++++++++++++----------- 2 files changed, 301 insertions(+), 155 deletions(-) diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 681ad73be9..9338489d53 100644 --- a/cmd/guacone/cmd/vulnerability.go +++ b/cmd/guacone/cmd/vulnerability.go @@ -22,24 +22,25 @@ import ( "os" "strings" - "github.com/guacsec/guac/pkg/guacanalytics" - "go.uber.org/zap" - "github.com/guacsec/guac/internal/testing/ptrfrom" - - "github.com/Khan/genqlient/graphql" model "github.com/guacsec/guac/pkg/assembler/clients/generated" "github.com/guacsec/guac/pkg/assembler/helpers" "github.com/guacsec/guac/pkg/cli" + "github.com/guacsec/guac/pkg/guacanalytics" "github.com/guacsec/guac/pkg/logging" + + "github.com/Khan/genqlient/graphql" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/viper" ) const ( - guacType string = "guac" - noVulnType string = "novuln" + guacType string = "guac" + noVulnType string = "novuln" + purlType string = "purl" + uriType string = "uri" + artifactType string = "artifact" ) type queryOptions struct { @@ -50,14 +51,28 @@ type queryOptions struct { vulnerabilityID string depth int pathsToReturn int + inputType string } var queryVulnCmd = &cobra.Command{ - Use: "vuln [flags] ", + Use: "vuln [flags] ", Short: "query if a package is affected by the specified vulnerability", + Long: `The vuln command allows you to query whether a specific package, SBOM URI, or artifact is affected by a given vulnerability. + +Positional Arguments: + Specify the input type: 'artifact', 'uri', or 'purl' + The corresponding input based on the specified type`, Run: func(cmd *cobra.Command, args []string) { ctx := logging.WithLogger(context.Background()) + // Ensure exactly two arguments are provided: and + if len(args) != 2 { + fmt.Println("error: Exactly two arguments must be provided: and .") + _ = cmd.Help() + os.Exit(1) + } + + // Extract and validate other flags opts, err := validateQueryVulnFlags( viper.GetString("gql-addr"), viper.GetString("header-file"), @@ -67,7 +82,7 @@ var queryVulnCmd = &cobra.Command{ args, ) if err != nil { - fmt.Printf("unable to validate flags: %v\n", err) + fmt.Printf("Unable to validate flags: %v\n", err) _ = cmd.Help() os.Exit(1) } @@ -80,8 +95,9 @@ var queryVulnCmd = &cobra.Command{ tTemp.Render() t.AppendHeader(rowHeader) + // Process based on the specified input type if opts.vulnerabilityID != "" { - printVulnInfoByVulnId(ctx, gqlclient, t, opts) + printVulnInfoByVulnID(ctx, gqlclient, t, opts) } else { printVulnInfo(ctx, gqlclient, t, opts) } @@ -124,48 +140,114 @@ func getPkgResponseFromPurl(ctx context.Context, gqlclient graphql.Client, purl func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer, opts queryOptions) { logger := logging.FromContext(ctx) - var path []string + var paths []string var tableRows []table.Row - var depVulnPath []string + var depVulnPaths []string var depVulnTableRows []table.Row - //check if artifact - split := strings.Split(opts.searchString, ":") - if len(split) == 2 { + + switch opts.inputType { + case artifactType: + // If it's an artifact, search for SBOMs via artifact var err error - // The primaryCall parameter in searchForSBOMViaPkg is there for us to know whether - // the searchString is expected to be a PURL, and we are searching via a purl. - depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, opts.searchString, opts.depth) + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, opts.searchString, opts.depth) if err != nil { - logger.Fatalf("error searching via hasSBOM: %v", err) + logger.Fatalf("error searching via hasSBOM for artifact: %v", err) } - } else { + + if len(depVulnPaths) == 0 { + depVulnPaths, depVulnTableRows, err = findConnectedPkgAndSearchViaPkg(ctx, gqlclient, opts) + if err != nil { + logger.Fatalf("error finding purl connected to artifact and searching via package: %v", err) + } + } + default: + // Otherwise, search for SBOMs via package var err error - // The primaryCall parameter in searchForSBOMViaPkg is there for us to know whether - // the searchString is expected to be a PURL, and we are searching via a purl. - depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) if err != nil { - logger.Fatalf("error searching via hasSBOM: %v", err) + logger.Fatalf("error searching via hasSBOM for package: %v", err) + } + + if len(depVulnPaths) == 0 { + depVulnPaths, depVulnTableRows, err = findConnectedArtAndSearchViaArt(ctx, gqlclient, opts) + if err != nil { + logger.Fatalf("error finding artifact connected to package and searching via artifact: %v", err) + } } } - path = append(path, depVulnPath...) + paths = append(paths, depVulnPaths...) tableRows = append(tableRows, depVulnTableRows...) - if len(path) > 0 { + if len(paths) > 0 { t.AppendRows(tableRows) - fmt.Println(t.Render()) - fmt.Printf("Visualizer url: http://localhost:3000/?path=%v\n", strings.Join(removeDuplicateValuesFromPath(path), `,`)) + fmt.Printf("Visualizer URL: http://localhost:3000/?path=%v\n", strings.Join(removeDuplicateValuesFromPath(paths), `,`)) } else { fmt.Printf("No path to vulnerabilities found!\n") } } -func searchArtToPkg(ctx context.Context, gqlclient graphql.Client, searchString string, logger *zap.SugaredLogger) *model.OccurrencesResponse { +// findConnectedArtAndSearchViaArt finds the artifact attached to the packages with the given purl. +// After finding the artifact, the graph is searched via that artifact. +func findConnectedArtAndSearchViaArt(ctx context.Context, gqlclient graphql.Client, opts queryOptions) ([]string, []table.Row, error) { + var depVulnPaths []string + var depVulnTableRows []table.Row + + // convert the package to an artifact via an isOccurrence. + pkgSpec, err := helpers.PurlToPkgFilter(opts.searchString) + if err != nil { + return nil, nil, fmt.Errorf("error converting purl to pkg %v", err) + } + + occ, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ + Subject: &model.PackageOrSourceSpec{ + Package: &pkgSpec, + }, + }) + if err != nil { + return nil, nil, fmt.Errorf("error getting occurrences for package: %v", err) + } + + art := occ.IsOccurrence[0].Artifact + + newSearchString := art.Algorithm + ":" + art.Digest + + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, newSearchString, opts.depth) + if err != nil { + return nil, nil, fmt.Errorf("error searching via hasSBOM for artifact: %v", err) + } + return depVulnPaths, depVulnTableRows, nil +} + +// findConnectedPkgAndSearchViaPkg finds the pkg attached to the artifact. +// After finding the pkg, the graph is searched via that package. +func findConnectedPkgAndSearchViaPkg(ctx context.Context, gqlclient graphql.Client, opts queryOptions) ([]string, []table.Row, error) { + var depVulnPaths []string + var depVulnTableRows []table.Row + + occurrence, err := searchArtToPkg(ctx, gqlclient, opts.searchString) + if err != nil { + return nil, nil, fmt.Errorf("error searching for package via artifact: %v", err) + } + + pkg, ok := occurrence.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) + if !ok { + return nil, nil, fmt.Errorf("error converting isOccurrence to package subject") + } + + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, pkg.Namespaces[0].Names[0].Versions[0].Purl, opts.depth, true) + if err != nil { + return nil, nil, fmt.Errorf("error searching via hasSBOM for artifact: %v", err) + } + return depVulnPaths, depVulnTableRows, nil +} + +func searchArtToPkg(ctx context.Context, gqlclient graphql.Client, searchString string) (*model.OccurrencesResponse, error) { split := strings.Split(searchString, ":") if len(split) != 2 { - logger.Fatalf("failed to parse artifact. Needs to be in algorithm:digest form") + return nil, fmt.Errorf("failed to parse artifact. Needs to be in algorithm:digest form") } artifactFilter := model.ArtifactSpec{ Algorithm: ptrfrom.String(strings.ToLower(split[0])), @@ -176,13 +258,13 @@ func searchArtToPkg(ctx context.Context, gqlclient graphql.Client, searchString Artifact: &artifactFilter, }) if err != nil { - logger.Fatalf("error querying for occurrences: %v", err) + return nil, fmt.Errorf("error querying for occurrences: %v", err) } - return o + return o, nil } -func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t table.Writer, opts queryOptions) { +func printVulnInfoByVulnID(ctx context.Context, gqlclient graphql.Client, t table.Writer, opts queryOptions) { logger := logging.FromContext(ctx) var tableRows []table.Row @@ -197,7 +279,8 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl } var path []string - if opts.isPurl { + switch opts.inputType { + case purlType: pkgResponse, err := getPkgResponseFromPurl(ctx, gqlclient, opts.searchString) if err != nil { logger.Fatalf("getPkgResponseFromPurl - error: %v", err) @@ -207,11 +290,37 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl if vulnNeighborError != nil { logger.Fatalf("error querying neighbor: %v", err) } - } else { - + case artifactType: split := strings.Split(opts.searchString, ":") - if len(split) == 2 { - occur := searchArtToPkg(ctx, gqlclient, split[0]+":"+split[1], logger) + + occur, err := searchArtToPkg(ctx, gqlclient, split[0]+":"+split[1]) + if err != nil { + logger.Fatalf("error searching for package via artifact: %v", err) + } + subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) + if ok { + var vulnNeighborError error + path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, subjectPackage.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) + if vulnNeighborError != nil { + logger.Fatalf("error querying neighbor: %v", err) + } + } + case uriType: + foundHasSBOM, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Uri: &opts.searchString}) + if err != nil { + logger.Fatalf("failed getting hasSBOM via URI: %s with error: %w", opts.searchString, err) + } + if pkgResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); ok { + var vulnNeighborError error + path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) + if vulnNeighborError != nil { + logger.Fatalf("error querying neighbor: %v", err) + } + } else if artResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectArtifact); ok { + occur, err := searchArtToPkg(ctx, gqlclient, artResponse.Algorithm+":"+artResponse.Digest) + if err != nil { + logger.Fatalf("error searching for package via artifact: %v", err) + } subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) if ok { var vulnNeighborError error @@ -221,31 +330,10 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl } } } else { - foundHasSBOM, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Uri: &opts.searchString}) - if err != nil { - logger.Fatalf("failed getting hasSBOM via URI: %s with error: %w", opts.searchString, err) - } - if pkgResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); ok { - var vulnNeighborError error - path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) - if vulnNeighborError != nil { - logger.Fatalf("error querying neighbor: %v", err) - } - } else if artResponse, ok := foundHasSBOM.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectArtifact); ok { - occur := searchArtToPkg(ctx, gqlclient, artResponse.Algorithm+":"+artResponse.Digest, logger) - subjectPackage, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) - if ok { - var vulnNeighborError error - path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, subjectPackage.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) - if vulnNeighborError != nil { - logger.Fatalf("error querying neighbor: %v", err) - } - } - } else { - logger.Fatalf("located hasSBOM does not have a subject that is a package or artifact") - } + logger.Fatalf("located hasSBOM does not have a subject that is a package or artifact") } } + if len(path) > 0 { t.AppendRows(tableRows) fmt.Println(t.Render()) @@ -460,19 +548,58 @@ func validateQueryVulnFlags(graphqlEndpoint, headerFile, vulnID string, depth, p opts.pathsToReturn = path if len(args) > 0 { - _, err := helpers.PurlToPkg(args[0]) + validTypes := []string{artifactType, uriType, purlType} + + // Initialize variables to hold type and input + var typeArg, inputArg string + + // Iterate through arguments to identify type and input, because they might not be in order + for _, arg := range args { + lowered := strings.ToLower(arg) + if contains(validTypes, lowered) { + if typeArg != "" { + fmt.Println("error: Multiple types provided. Please specify only one type.") + os.Exit(1) + } + typeArg = lowered + } else { + if inputArg != "" { + fmt.Println("error: Multiple inputs provided. Please specify only one input.") + os.Exit(1) + } + inputArg = arg + } + } + + // Validate that typeArg has been set + if typeArg == "" { + fmt.Printf("error: Input type not specified or invalid. Valid types are: %v\n", validTypes) + os.Exit(1) + } + + _, err := helpers.PurlToPkg(inputArg) if err != nil { opts.isPurl = false } else { opts.isPurl = true } - opts.searchString = args[0] + opts.searchString = inputArg + opts.inputType = typeArg } else { return opts, fmt.Errorf("expected subject input to be purl or SBOM URI") } return opts, nil } +func contains(slice []string, item string) bool { + for _, a := range slice { + if a == item { + return true + } + } + return false +} + func init() { set, err := cli.BuildFlags([]string{"vuln-id", "search-depth", "num-path"}) if err != nil { diff --git a/pkg/guacanalytics/searchForSBOM.go b/pkg/guacanalytics/searchForSBOM.go index a9ef1aabf5..84cf3632fd 100644 --- a/pkg/guacanalytics/searchForSBOM.go +++ b/pkg/guacanalytics/searchForSBOM.go @@ -5,9 +5,10 @@ import ( "fmt" "strings" - "github.com/Khan/genqlient/graphql" model "github.com/guacsec/guac/pkg/assembler/clients/generated" "github.com/guacsec/guac/pkg/assembler/helpers" + + "github.com/Khan/genqlient/graphql" "github.com/jedib0t/go-pretty/v6/table" ) @@ -36,59 +37,85 @@ func SearchForSBOMViaArtifact(ctx context.Context, gqlclient graphql.Client, sea var tableRows []table.Row checkedPkgIDs := make(map[string]bool) var collectedPkgVersionResults []*pkgVersionNeighborQueryResults + AlreadyIncludedTableRows := make(map[string]bool) - queue := make([]string, 0) // the queue of nodes in bfs + // Define the node structure for traversal type dfsNode struct { - expanded bool // true once all node neighbors are added to queue - parent string - pkgID string - depth int + expanded bool // Indicates if all node neighbors are added to the queue + parent string // Parent node in the traversal + pkgID string // Package ID + depth int // Depth in the traversal } - nodeMap := map[string]dfsNode{} - nodeMap[searchString] = dfsNode{} - queue = append(queue, searchString) + // Initialize the node map and queue with the search string + nodeMap := map[string]dfsNode{ + searchString: {}, + } + queue := []string{searchString} for len(queue) > 0 { now := queue[0] queue = queue[1:] nowNode := nodeMap[now] + // Stop traversal if the maximum depth is reached if maxLength != 0 && nowNode.depth >= maxLength { break } var foundHasSBOM *model.HasSBOMsResponse - // if the initial depth, check if it's a purl or an SBOM URI. Otherwise, always search by pkgID - // note that primaryCall will be static throughout the entire function. if nowNode.depth == 0 { + // Initial depth: treat searchString as an artifact artResponse, err := getArtifactResponseFromArtifact(ctx, gqlclient, now) if err != nil { - return nil, nil, fmt.Errorf("getPkgResponseFromPurl - error: %v", err) + return nil, nil, fmt.Errorf("getArtifactResponseFromArtifact - error: %v", err) + } + + artifactID := artResponse.Artifacts[0].Id + hasSBOMSpec := model.HasSBOMSpec{ + Subject: &model.PackageOrArtifactSpec{ + Artifact: &model.ArtifactSpec{Id: &artifactID}, + }, } - foundHasSBOM, err = model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Artifact: &model.ArtifactSpec{Id: &artResponse.Artifacts[0].Id}}}) + foundHasSBOM, err = model.HasSBOMs(ctx, gqlclient, hasSBOMSpec) if err != nil { - return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + return nil, nil, fmt.Errorf("failed getting hasSBOM via artifact: %s with error: %w", now, err) } } else { - foundHasSBOMPkg, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Package: &model.PkgSpec{Id: &now}}}) + // Subsequent depths: treat 'now' as a package ID + hasSBOMSpec := model.HasSBOMSpec{ + Subject: &model.PackageOrArtifactSpec{ + Package: &model.PkgSpec{Id: &now}, + }, + } + foundHasSBOMPkg, err := model.HasSBOMs(ctx, gqlclient, hasSBOMSpec) if err != nil { - return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + return nil, nil, fmt.Errorf("failed getting hasSBOM via package: %s with error: %w", now, err) } if foundHasSBOMPkg != nil { foundHasSBOM = foundHasSBOMPkg } else { - occur, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ - Subject: &model.PackageOrSourceSpec{Package: &model.PkgSpec{Id: &now}}, - }) + // If no HasSBOM found via package, try via occurrences + occurSpec := model.IsOccurrenceSpec{ + Subject: &model.PackageOrSourceSpec{ + Package: &model.PkgSpec{Id: &now}, + }, + } + occurrences, err := model.Occurrences(ctx, gqlclient, occurSpec) if err != nil { return nil, nil, fmt.Errorf("error querying for occurrences: %v", err) } - if occur != nil && len(occur.IsOccurrence) > 0 { - foundHasSBOMArt, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Artifact: &model.ArtifactSpec{Id: &occur.IsOccurrence[0].Artifact.Id}}}) + if occurrences != nil && len(occurrences.IsOccurrence) > 0 { + artifactID := occurrences.IsOccurrence[0].Artifact.Id + hasSBOMSpec := model.HasSBOMSpec{ + Subject: &model.PackageOrArtifactSpec{ + Artifact: &model.ArtifactSpec{Id: &artifactID}, + }, + } + foundHasSBOMArt, err := model.HasSBOMs(ctx, gqlclient, hasSBOMSpec) if err != nil { - return nil, nil, fmt.Errorf("failed getting hasSBOM via purl: %s with error :%w", now, err) + return nil, nil, fmt.Errorf("failed getting hasSBOM via occurrence: %s with error: %w", now, err) } if foundHasSBOMArt != nil { foundHasSBOM = foundHasSBOMArt @@ -97,40 +124,21 @@ func SearchForSBOMViaArtifact(ctx context.Context, gqlclient graphql.Client, sea } } - for _, hasSBOM := range foundHasSBOM.HasSBOM { - - subjectArtifact, ok := hasSBOM.Subject.(*model.AllHasSBOMTreeSubjectArtifact) - if ok { - occur, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ - Artifact: &model.ArtifactSpec{Id: &subjectArtifact.Id}, - }) - if err != nil { - return nil, nil, fmt.Errorf("error querying for occurrences: %v", err) - } - pkgResponse, ok := occur.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage) - if ok { - if pkgResponse.Type != guacType { - if !checkedPkgIDs[pkgResponse.Namespaces[0].Names[0].Versions[0].Id] { - vulnPath, pkgVulnTableRows, err := queryVulnsViaPackageNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id) - if err != nil { - return nil, nil, fmt.Errorf("error querying neighbor: %w", err) - } - path = append(path, vulnPath...) - tableRows = append(tableRows, pkgVulnTableRows...) - path = append([]string{pkgResponse.Namespaces[0].Names[0].Versions[0].Id, - pkgResponse.Namespaces[0].Names[0].Id, pkgResponse.Namespaces[0].Id, - pkgResponse.Id}, path...) - checkedPkgIDs[pkgResponse.Namespaces[0].Names[0].Versions[0].Id] = true - } - } - } - } + if foundHasSBOM == nil { + // If no HasSBOM is found, continue to the next node + continue + } + // Process the HasSBOM results + for _, hasSBOM := range foundHasSBOM.HasSBOM { + // Handle included dependencies for _, isDep := range hasSBOM.IncludedDependencies { if isDep.DependencyPackage.Type == guacType { continue } depPkgID := isDep.DependencyPackage.Namespaces[0].Names[0].Versions[0].Id + + // Check if the dependency package ID is already in the node map dfsN, seen := nodeMap[depPkgID] if !seen { dfsN = dfsNode{ @@ -140,68 +148,79 @@ func SearchForSBOMViaArtifact(ctx context.Context, gqlclient graphql.Client, sea } nodeMap[depPkgID] = dfsN } + + // **Include the missing `if !dfsN.expanded` check** if !dfsN.expanded { queue = append(queue, depPkgID) } - pkgVersionNeighbors, err := getVulnAndVexNeighborsForPackage(ctx, gqlclient, depPkgID, isDep) - if err != nil { - return nil, nil, fmt.Errorf("getVulnAndVexNeighbors failed with error: %w", err) - } - collectedPkgVersionResults = append(collectedPkgVersionResults, pkgVersionNeighbors) - checkedPkgIDs[depPkgID] = true - - } - - for _, isDep := range hasSBOM.IncludedDependencies { - if isDep.DependencyPackage.Type == guacType { - continue - } - depPkgID := isDep.DependencyPackage.Namespaces[0].Names[0].Versions[0].Id - if _, seen := nodeMap[depPkgID]; !seen { - dfsN := dfsNode{ - parent: now, - pkgID: depPkgID, - depth: nowNode.depth + 1, + // Process vulnerabilities and VEX statements for the dependency + if !checkedPkgIDs[depPkgID] { + pkgVersionNeighbors, err := getVulnAndVexNeighborsForPackage(ctx, gqlclient, depPkgID, isDep) + if err != nil { + return nil, nil, fmt.Errorf("getVulnAndVexNeighbors failed with error: %w", err) } - nodeMap[depPkgID] = dfsN + collectedPkgVersionResults = append(collectedPkgVersionResults, pkgVersionNeighbors) + checkedPkgIDs[depPkgID] = true } } } + nowNode.expanded = true nodeMap[now] = nowNode } + // Process collected package version results checkedCertifyVulnIDs := make(map[string]bool) - // Collect results from the channel for _, result := range collectedPkgVersionResults { for _, neighbor := range result.pkgVersionNeighborResponse.Neighbors { - if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { - if !checkedCertifyVulnIDs[certifyVuln.Id] && certifyVuln.Vulnerability.Type != noVulnType { - checkedCertifyVulnIDs[certifyVuln.Id] = true - for _, vuln := range certifyVuln.Vulnerability.VulnerabilityIDs { - tableRows = append(tableRows, table.Row{certifyVulnStr, certifyVuln.Id, "vulnerability ID: " + vuln.VulnerabilityID}) - path = append(path, []string{vuln.Id, certifyVuln.Id, - certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, - certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, - certifyVuln.Package.Id}...) + switch n := neighbor.(type) { + case *model.NeighborsNeighborsCertifyVuln: + if !checkedCertifyVulnIDs[n.Id] && n.Vulnerability.Type != noVulnType { + checkedCertifyVulnIDs[n.Id] = true + for _, vuln := range n.Vulnerability.VulnerabilityIDs { + if !AlreadyIncludedTableRows[vuln.VulnerabilityID] { + tableRows = append(tableRows, table.Row{ + certifyVulnStr, + n.Id, + "vulnerability ID: " + vuln.VulnerabilityID, + }) + path = append(path, []string{ + vuln.Id, + n.Id, + n.Package.Namespaces[0].Names[0].Versions[0].Id, + n.Package.Namespaces[0].Names[0].Id, + n.Package.Namespaces[0].Id, + n.Package.Id, + }...) + AlreadyIncludedTableRows[vuln.VulnerabilityID] = true + } } - path = append(path, result.isDep.Id, result.isDep.Package.Namespaces[0].Names[0].Versions[0].Id, - result.isDep.Package.Namespaces[0].Names[0].Id, result.isDep.Package.Namespaces[0].Id, - result.isDep.Package.Id) + // Include dependency path + isDep := result.isDep + path = append(path, []string{ + isDep.Id, + isDep.Package.Namespaces[0].Names[0].Versions[0].Id, + isDep.Package.Namespaces[0].Names[0].Id, + isDep.Package.Namespaces[0].Id, + isDep.Package.Id, + }...) } - } - - if certifyVex, ok := neighbor.(*model.NeighborsNeighborsCertifyVEXStatement); ok { - for _, vuln := range certifyVex.Vulnerability.VulnerabilityIDs { - tableRows = append(tableRows, table.Row{vexLinkStr, certifyVex.Id, "vulnerability ID: " + vuln.VulnerabilityID + ", Vex Status: " + string(certifyVex.Status) + ", Subject: " + VexSubjectString(certifyVex.Subject)}) - path = append(path, certifyVex.Id, vuln.Id) + case *model.NeighborsNeighborsCertifyVEXStatement: + for _, vuln := range n.Vulnerability.VulnerabilityIDs { + tableRows = append(tableRows, table.Row{ + vexLinkStr, + n.Id, + "vulnerability ID: " + vuln.VulnerabilityID + ", Vex Status: " + string(n.Status) + ", Subject: " + VexSubjectString(n.Subject), + }) + path = append(path, n.Id, vuln.Id) } - path = append(path, vexSubjectIds(certifyVex.Subject)...) + path = append(path, vexSubjectIds(n.Subject)...) } } } + return path, tableRows, nil } @@ -372,10 +391,10 @@ func getArtifactResponseFromArtifact(ctx context.Context, gqlclient graphql.Clie } artResponse, err := model.Artifacts(ctx, gqlclient, *artFilter) if err != nil { - return nil, fmt.Errorf("error querying for package: %v", err) + return nil, fmt.Errorf("error querying for artifact: %v", err) } if len(artResponse.Artifacts) != 1 { - return nil, fmt.Errorf("failed to located package based on purl") + return nil, fmt.Errorf("failed to locate artifact based on algorithm and digest") } return artResponse, nil } From 52c423f8dc59b3f3782ac05a3cb25dd335e83009 Mon Sep 17 00:00:00 2001 From: nathannaveen <42319948+nathannaveen@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:05:19 -0500 Subject: [PATCH 5/5] Fixed errors and removed duplicates Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --- cmd/guacone/cmd/vulnerability.go | 15 +++++++++------ pkg/guacanalytics/searchForSBOM.go | 23 ++++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 9338489d53..1e5c40d71b 100644 --- a/cmd/guacone/cmd/vulnerability.go +++ b/cmd/guacone/cmd/vulnerability.go @@ -169,7 +169,7 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer logger.Fatalf("error searching via hasSBOM for package: %v", err) } - if len(depVulnPaths) == 0 { + if len(depVulnPaths) == 0 && opts.inputType == purlType { depVulnPaths, depVulnTableRows, err = findConnectedArtAndSearchViaArt(ctx, gqlclient, opts) if err != nil { logger.Fatalf("error finding artifact connected to package and searching via artifact: %v", err) @@ -210,14 +210,17 @@ func findConnectedArtAndSearchViaArt(ctx context.Context, gqlclient graphql.Clie return nil, nil, fmt.Errorf("error getting occurrences for package: %v", err) } - art := occ.IsOccurrence[0].Artifact + if len(occ.IsOccurrence) > 0 { + art := occ.IsOccurrence[0].Artifact - newSearchString := art.Algorithm + ":" + art.Digest + newSearchString := art.Algorithm + ":" + art.Digest - depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, newSearchString, opts.depth) - if err != nil { - return nil, nil, fmt.Errorf("error searching via hasSBOM for artifact: %v", err) + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, newSearchString, opts.depth) + if err != nil { + return nil, nil, fmt.Errorf("error searching via hasSBOM for artifact: %v", err) + } } + return depVulnPaths, depVulnTableRows, nil } diff --git a/pkg/guacanalytics/searchForSBOM.go b/pkg/guacanalytics/searchForSBOM.go index 84cf3632fd..7d9a46bc58 100644 --- a/pkg/guacanalytics/searchForSBOM.go +++ b/pkg/guacanalytics/searchForSBOM.go @@ -233,6 +233,7 @@ func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchSt var tableRows []table.Row checkedPkgIDs := make(map[string]bool) var collectedPkgVersionResults []*pkgVersionNeighborQueryResults + AlreadyIncludedTableRows := make(map[string]bool) queue := make([]string, 0) // the queue of nodes in bfs type dfsNode struct { @@ -354,16 +355,20 @@ func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchSt if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { if !checkedCertifyVulnIDs[certifyVuln.Id] && certifyVuln.Vulnerability.Type != noVulnType { checkedCertifyVulnIDs[certifyVuln.Id] = true - for _, vuln := range certifyVuln.Vulnerability.VulnerabilityIDs { - tableRows = append(tableRows, table.Row{certifyVulnStr, certifyVuln.Id, "vulnerability ID: " + vuln.VulnerabilityID}) - path = append(path, []string{vuln.Id, certifyVuln.Id, - certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, - certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, - certifyVuln.Package.Id}...) + if !AlreadyIncludedTableRows[certifyVuln.Vulnerability.VulnerabilityIDs[0].VulnerabilityID] { + for _, vuln := range certifyVuln.Vulnerability.VulnerabilityIDs { + tableRows = append(tableRows, table.Row{certifyVulnStr, certifyVuln.Id, "vulnerability ID: " + vuln.VulnerabilityID}) + path = append(path, []string{vuln.Id, certifyVuln.Id, + certifyVuln.Package.Namespaces[0].Names[0].Versions[0].Id, + certifyVuln.Package.Namespaces[0].Names[0].Id, certifyVuln.Package.Namespaces[0].Id, + certifyVuln.Package.Id}...) + } + path = append(path, result.isDep.Id, result.isDep.Package.Namespaces[0].Names[0].Versions[0].Id, + result.isDep.Package.Namespaces[0].Names[0].Id, result.isDep.Package.Namespaces[0].Id, + result.isDep.Package.Id) + + AlreadyIncludedTableRows[certifyVuln.Vulnerability.VulnerabilityIDs[0].VulnerabilityID] = true } - path = append(path, result.isDep.Id, result.isDep.Package.Namespaces[0].Names[0].Versions[0].Id, - result.isDep.Package.Namespaces[0].Names[0].Id, result.isDep.Package.Namespaces[0].Id, - result.isDep.Package.Id) } }