Skip to content

Commit

Permalink
Searching for hasSBOMs via Artifacts in Vuln cli (#1965)
Browse files Browse the repository at this point in the history
* Using artifacts attached to hasSBOMs

Signed-off-by: nathannaveen <[email protected]>

* Fixed lint issues

Signed-off-by: nathannaveen <[email protected]>

* Updated based on code review

Signed-off-by: nathannaveen <[email protected]>

* Fix tests

Signed-off-by: nathannaveen <[email protected]>

* Updated based on code review

Signed-off-by: nathannaveen <[email protected]>

* update vuln CLI to handle artifact being the subject

Signed-off-by: pxp928 <[email protected]>

* fix bug on searchDependencyPackagesReverse

Signed-off-by: pxp928 <[email protected]>

---------

Signed-off-by: nathannaveen <[email protected]>
Signed-off-by: pxp928 <[email protected]>
Co-authored-by: pxp928 <[email protected]>
  • Loading branch information
nathannaveen and pxp928 authored Sep 17, 2024
1 parent c22cf02 commit c7501e8
Show file tree
Hide file tree
Showing 2 changed files with 360 additions and 217 deletions.
286 changes: 69 additions & 217 deletions cmd/guacone/cmd/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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)...)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit c7501e8

Please sign in to comment.