From e39fb227fde580646641b1da3cd3287c1f96874b Mon Sep 17 00:00:00 2001 From: Nathan Naveen <42319948+nathannaveen@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:51:02 -0500 Subject: [PATCH] Search for Vulns via Artifact (#2153) * update vuln cli to handle artifact input Signed-off-by: pxp928 * spdx add occur for top level artifact/package Signed-off-by: pxp928 * remove SPDX fix as it was reduandant Signed-off-by: pxp928 * Cleaned up code and fix some functionality Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> * Fixed errors and removed duplicates Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> --------- Signed-off-by: pxp928 Signed-off-by: nathannaveen <42319948+nathannaveen@users.noreply.github.com> Co-authored-by: pxp928 --- cmd/guacone/cmd/vulnerability.go | 232 ++++++++++++++++++++++------ pkg/guacanalytics/searchForSBOM.go | 240 +++++++++++++++++++++++++++-- 2 files changed, 415 insertions(+), 57 deletions(-) diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 0b220a3100..1e5c40d71b 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,52 +140,117 @@ 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 - // 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...) + var depVulnPaths []string + var depVulnTableRows []table.Row - if len(depVulnPath) == 0 { - occur := searchArtToPkg(ctx, gqlclient, opts.searchString, logger) + switch opts.inputType { + case artifactType: + // If it's an artifact, search for SBOMs via artifact + var err error + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, opts.searchString, opts.depth) + if err != nil { + logger.Fatalf("error searching via hasSBOM for artifact: %v", err) + } - 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) - } + 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 + depVulnPaths, depVulnTableRows, err = guacanalytics.SearchForSBOMViaPkg(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) + if err != nil { + logger.Fatalf("error searching via hasSBOM for package: %v", err) + } - path = append(path, depVulnPath...) - tableRows = append(tableRows, depVulnTableRows...) + 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) } } } - if len(path) > 0 { - t.AppendRows(tableRows) + paths = append(paths, depVulnPaths...) + tableRows = append(tableRows, depVulnTableRows...) + 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) + } + + if len(occ.IsOccurrence) > 0 { + 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])), @@ -180,13 +261,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 @@ -201,7 +282,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) @@ -211,7 +293,22 @@ 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, ":") + + 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) @@ -223,7 +320,10 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl 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) + 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 @@ -236,6 +336,7 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl 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()) @@ -450,19 +551,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 3391c03eda..7d9a46bc58 100644 --- a/pkg/guacanalytics/searchForSBOM.go +++ b/pkg/guacanalytics/searchForSBOM.go @@ -3,10 +3,12 @@ package guacanalytics import ( "context" "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" ) @@ -30,6 +32,198 @@ 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 + AlreadyIncludedTableRows := make(map[string]bool) + + // Define the node structure for traversal + type dfsNode struct { + 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 + } + + // 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 nowNode.depth == 0 { + // Initial depth: treat searchString as an artifact + artResponse, err := getArtifactResponseFromArtifact(ctx, gqlclient, now) + if err != nil { + 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, hasSBOMSpec) + if err != nil { + return nil, nil, fmt.Errorf("failed getting hasSBOM via artifact: %s with error: %w", now, err) + } + } else { + // 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 package: %s with error: %w", now, err) + } + if foundHasSBOMPkg != nil { + foundHasSBOM = foundHasSBOMPkg + } else { + // 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 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 occurrence: %s with error: %w", now, err) + } + if foundHasSBOMArt != nil { + foundHasSBOM = foundHasSBOMArt + } + } + } + } + + 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{ + parent: now, + pkgID: depPkgID, + depth: nowNode.depth + 1, + } + nodeMap[depPkgID] = dfsN + } + + // **Include the missing `if !dfsN.expanded` check** + if !dfsN.expanded { + queue = append(queue, depPkgID) + } + + // 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) + } + collectedPkgVersionResults = append(collectedPkgVersionResults, pkgVersionNeighbors) + checkedPkgIDs[depPkgID] = true + } + } + } + + nowNode.expanded = true + nodeMap[now] = nowNode + } + + // Process collected package version results + checkedCertifyVulnIDs := make(map[string]bool) + + for _, result := range collectedPkgVersionResults { + for _, neighbor := range result.pkgVersionNeighborResponse.Neighbors { + 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 + } + } + // 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, + }...) + } + 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(n.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. @@ -39,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 { @@ -67,7 +262,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 { @@ -161,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) } } @@ -186,6 +384,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 artifact: %v", err) + } + if len(artResponse.Artifacts) != 1 { + return nil, fmt.Errorf("failed to locate artifact based on algorithm and digest") + } + return artResponse, nil +} + func getPkgResponseFromPurl(ctx context.Context, gqlclient graphql.Client, purl string) (*model.PackagesResponse, error) { pkgInput, err := helpers.PurlToPkg(purl) if err != nil {