diff --git a/cmd/guacone/cmd/vulnerability.go b/cmd/guacone/cmd/vulnerability.go index 00bc56529c..032f0b9782 100644 --- a/cmd/guacone/cmd/vulnerability.go +++ b/cmd/guacone/cmd/vulnerability.go @@ -22,6 +22,11 @@ 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" @@ -122,13 +127,33 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer var path []string var tableRows []table.Row - depVulnPath, depVulnTableRows, err := searchPkgViaHasSBOM(ctx, gqlclient, opts.searchString, opts.depth, opts.isPurl) + // 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) + + 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) @@ -139,6 +164,26 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer } } +func searchArtToPkg(ctx context.Context, gqlclient graphql.Client, searchString string, logger *zap.SugaredLogger) *model.OccurrencesResponse { + split := strings.Split(searchString, ":") + if len(split) != 2 { + logger.Fatalf("failed to parse artifact. Needs to be in algorithm:digest form") + } + artifactFilter := model.ArtifactSpec{ + Algorithm: ptrfrom.String(strings.ToLower(split[0])), + Digest: ptrfrom.String(strings.ToLower(split[1])), + } + + o, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{ + Artifact: &artifactFilter, + }) + if err != nil { + logger.Fatalf("error querying for occurrences: %v", err) + } + + return o +} + func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t table.Writer, opts queryOptions) { logger := logging.FromContext(ctx) var tableRows []table.Row @@ -159,25 +204,37 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl if err != nil { logger.Fatalf("getPkgResponseFromPurl - error: %v", err) } - path, tableRows, err = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) - if err != nil { + var vulnNeighborError error + path, tableRows, vulnNeighborError = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) + if vulnNeighborError != nil { logger.Fatalf("error querying neighbor: %v", err) } } else { - foundHasSBOMPkg, err := model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Uri: &opts.searchString}) + 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 len(foundHasSBOMPkg.HasSBOM) != 1 { + if len(foundHasSBOM.HasSBOM) != 1 { logger.Fatalf("failed to located singular hasSBOM based on URI") } - if pkgResponse, ok := foundHasSBOMPkg.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); ok { - path, tableRows, err = queryVulnsViaVulnNodeNeighbors(ctx, gqlclient, pkgResponse.Namespaces[0].Names[0].Versions[0].Id, vulnResponse.Vulnerabilities, opts.depth, opts.pathsToReturn) - if err != nil { + 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") + logger.Fatalf("located hasSBOM does not have a subject that is a package or artifact") } } if len(path) > 0 { @@ -189,60 +246,6 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl } } -func queryVulnsViaPackageNeighbors(ctx context.Context, gqlclient graphql.Client, pkgVersionID string) ([]string, []table.Row, error) { - var path []string - var tableRows []table.Row - var edgeTypes = []model.Edge{model.EdgePackageCertifyVuln, model.EdgePackageCertifyVexStatement} - - pkgVersionNeighborResponse, err := model.Neighbors(ctx, gqlclient, pkgVersionID, edgeTypes) - if err != nil { - return nil, nil, fmt.Errorf("error querying neighbor for vulnerability: %w", err) - } - certifyVulnFound := false - for _, neighbor := range pkgVersionNeighborResponse.Neighbors { - if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { - certifyVulnFound = true - if certifyVuln.Vulnerability.Type != noVulnType { - 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 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)...) - } - - } - if !certifyVulnFound { - return nil, nil, fmt.Errorf("error certify vulnerability node not found, incomplete data. Please ensure certifier has run by running guacone certifier osv") - } - return path, tableRows, nil -} - -func vexSubjectString(s model.AllCertifyVEXStatementSubjectPackageOrArtifact) string { - switch v := s.(type) { - case *model.AllCertifyVEXStatementSubjectArtifact: - return fmt.Sprintf("artifact (id:%v) %v:%v", v.Id, v.Algorithm, v.Digest) - case *model.AllCertifyVEXStatementSubjectPackage: - return fmt.Sprintf("package (id:%v) %v:%v/%v@%v", - v.Id, - v.Type, - v.Namespaces[0].Namespace, - v.Namespaces[0].Names[0].Name, - v.Namespaces[0].Names[0].Versions[0].Version) - default: - return "unknown subject" - } -} func vexSubjectIds(s model.AllCertifyVEXStatementSubjectPackageOrArtifact) []string { switch v := s.(type) { case *model.AllCertifyVEXStatementSubjectArtifact: @@ -306,7 +309,7 @@ func queryVulnsViaVulnNodeNeighbors(ctx context.Context, gqlclient graphql.Clien if certifyVex, ok := neighbor.node.(*model.NeighborsNeighborsCertifyVEXStatement); ok { certifyVulnFound = true 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)}) + tableRows = append(tableRows, table.Row{vexLinkStr, certifyVex.Id, "vulnerability ID: " + vuln.VulnerabilityID + ", Vex Status: " + string(certifyVex.Status) + ", Subject: " + guacanalytics.VexSubjectString(certifyVex.Subject)}) path = append(path, certifyVex.Id, vuln.Id) } path = append(path, vexSubjectIds(certifyVex.Subject)...) @@ -376,8 +379,9 @@ func searchDependencyPackagesReverse(ctx context.Context, gqlclient graphql.Clie nodeMap[now] = nowNode } + // not found so return nil if topPkgID != "" && !found { - return nil, fmt.Errorf("no path found up to specified length") + return nil, nil } var now string @@ -425,158 +429,6 @@ func searchDependencyPackagesReverse(ctx context.Context, gqlclient graphql.Clie } } -type pkgVersionNeighborQueryResults struct { - pkgVersionNeighborResponse *model.NeighborsResponse - isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency -} - -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 { - return nil, fmt.Errorf("failed to get neighbors for pkgID: %s with error %w", pkgID, err) - } - return &pkgVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isDep: isDep}, nil -} - -// searchPkgViaHasSBOM 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. -func searchPkgViaHasSBOM(ctx context.Context, gqlclient graphql.Client, searchString string, maxLength int, isPurl bool) ([]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 foundHasSBOMPkg *model.HasSBOMsResponse - var err error - - // if the initial depth, check if its a purl or an SBOM URI. Otherwise always search by pkgID - if nowNode.depth == 0 { - if isPurl { - pkgResponse, err := getPkgResponseFromPurl(ctx, gqlclient, now) - if err != nil { - return nil, nil, fmt.Errorf("getPkgResponseFromPurl - error: %v", err) - } - foundHasSBOMPkg, err = model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Package: &model.PkgSpec{Id: &pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[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{Uri: &now}) - if err != nil { - return nil, nil, fmt.Errorf("failed getting hasSBOM via URI: %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) - } - } - - for _, hasSBOM := range foundHasSBOMPkg.HasSBOM { - if pkgResponse, ok := foundHasSBOMPkg.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); 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: %v", 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 := getVulnAndVexNeighbors(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 - } - - 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.Vulnerability.VulnerabilityIDs[0].Id] { - if certifyVuln.Vulnerability.Type != noVulnType { - checkedCertifyVulnIDs[certifyVuln.Vulnerability.VulnerabilityIDs[0].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 -} - func removeDuplicateValuesFromPath(path []string) []string { keys := make(map[string]bool) var list []string diff --git a/pkg/guacanalytics/searchForSBOM.go b/pkg/guacanalytics/searchForSBOM.go new file mode 100644 index 0000000000..3391c03eda --- /dev/null +++ b/pkg/guacanalytics/searchForSBOM.go @@ -0,0 +1,291 @@ +package guacanalytics + +import ( + "context" + "fmt" + + "github.com/Khan/genqlient/graphql" + model "github.com/guacsec/guac/pkg/assembler/clients/generated" + "github.com/guacsec/guac/pkg/assembler/helpers" + "github.com/jedib0t/go-pretty/v6/table" +) + +const ( + guacType string = "guac" + noVulnType string = "novuln" + vexLinkStr string = "vexLink" + certifyVulnStr string = "certifyVuln" +) + +type pkgVersionNeighborQueryResults struct { + pkgVersionNeighborResponse *model.NeighborsResponse + isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency +} + +func getVulnAndVexNeighborsForPackage(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 { + return nil, fmt.Errorf("failed to get neighbors for pkgID: %s with error %w", pkgID, err) + } + return &pkgVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isDep: isDep}, 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. +// The isPurl parameter is used to know whether the searchString is expected to be a PURL. +func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchString string, maxLength int, isPurl bool) ([]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 foundHasSBOMPkg *model.HasSBOMsResponse + var err error + + // 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 { + return nil, nil, fmt.Errorf("getPkgResponseFromPurl - error: %v", err) + } + foundHasSBOMPkg, err = model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{Package: &model.PkgSpec{Id: &pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[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{Uri: &now}) + if err != nil { + return nil, nil, fmt.Errorf("failed getting hasSBOM via URI: %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) + } + } + + for _, hasSBOM := range foundHasSBOMPkg.HasSBOM { + if pkgResponse, ok := foundHasSBOMPkg.HasSBOM[0].Subject.(*model.AllHasSBOMTreeSubjectPackage); 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 +} + +func getPkgResponseFromPurl(ctx context.Context, gqlclient graphql.Client, purl string) (*model.PackagesResponse, error) { + pkgInput, err := helpers.PurlToPkg(purl) + if err != nil { + return nil, fmt.Errorf("failed to parse PURL: %v", err) + } + + pkgQualifierFilter := []model.PackageQualifierSpec{} + for _, qualifier := range pkgInput.Qualifiers { + // to prevent https://github.com/golang/go/discussions/56010 + qualifier := qualifier + pkgQualifierFilter = append(pkgQualifierFilter, model.PackageQualifierSpec{ + Key: qualifier.Key, + Value: &qualifier.Value, + }) + } + + pkgFilter := &model.PkgSpec{ + Type: &pkgInput.Type, + Namespace: pkgInput.Namespace, + Name: &pkgInput.Name, + Version: pkgInput.Version, + Subpath: pkgInput.Subpath, + Qualifiers: pkgQualifierFilter, + } + pkgResponse, err := model.Packages(ctx, gqlclient, *pkgFilter) + if err != nil { + return nil, fmt.Errorf("error querying for package: %v", err) + } + if len(pkgResponse.Packages) != 1 { + return nil, fmt.Errorf("failed to located package based on purl") + } + return pkgResponse, nil +} + +func vexSubjectIds(s model.AllCertifyVEXStatementSubjectPackageOrArtifact) []string { + switch v := s.(type) { + case *model.AllCertifyVEXStatementSubjectArtifact: + return []string{v.Id} + case *model.AllCertifyVEXStatementSubjectPackage: + return []string{ + v.Id, + v.Namespaces[0].Id, + v.Namespaces[0].Names[0].Id, + v.Namespaces[0].Names[0].Versions[0].Id} + default: + return []string{} + } +} + +func queryVulnsViaPackageNeighbors(ctx context.Context, gqlclient graphql.Client, pkgVersionID string) ([]string, []table.Row, error) { + var path []string + var tableRows []table.Row + var edgeTypes = []model.Edge{model.EdgePackageCertifyVuln, model.EdgePackageCertifyVexStatement} + + pkgVersionNeighborResponse, err := model.Neighbors(ctx, gqlclient, pkgVersionID, edgeTypes) + if err != nil { + return nil, nil, fmt.Errorf("error querying neighbor for vulnerability: %w", err) + } + certifyVulnFound := false + for _, neighbor := range pkgVersionNeighborResponse.Neighbors { + if certifyVuln, ok := neighbor.(*model.NeighborsNeighborsCertifyVuln); ok { + certifyVulnFound = true + if certifyVuln.Vulnerability.Type != noVulnType { + 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 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)...) + } + + } + if !certifyVulnFound { + return nil, nil, fmt.Errorf("error certify vulnerability node not found, incomplete data. Please ensure certifier has run by running guacone certifier osv") + } + return path, tableRows, nil +} + +func VexSubjectString(s model.AllCertifyVEXStatementSubjectPackageOrArtifact) string { + switch v := s.(type) { + case *model.AllCertifyVEXStatementSubjectArtifact: + return fmt.Sprintf("artifact (id:%v) %v:%v", v.Id, v.Algorithm, v.Digest) + case *model.AllCertifyVEXStatementSubjectPackage: + return fmt.Sprintf("package (id:%v) %v:%v/%v@%v", + v.Id, + v.Type, + v.Namespaces[0].Namespace, + v.Namespaces[0].Names[0].Name, + v.Namespaces[0].Names[0].Versions[0].Version) + default: + return "unknown subject" + } +}