Skip to content

Commit

Permalink
update vuln CLI to handle artifact being the subject
Browse files Browse the repository at this point in the history
Signed-off-by: pxp928 <[email protected]>
  • Loading branch information
pxp928 committed Sep 17, 2024
1 parent 27d63bd commit b9f0318
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 252 deletions.
125 changes: 35 additions & 90 deletions cmd/guacone/cmd/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ package cmd
import (
"context"
"fmt"
"github.com/guacsec/guac/pkg/guacanalytics"
"go.uber.org/zap"
"net/http"
"os"
"strings"

"github.com/guacsec/guac/pkg/guacanalytics"
"go.uber.org/zap"

"github.com/guacsec/guac/internal/testing/ptrfrom"

"github.com/Khan/genqlient/graphql"
Expand Down Expand Up @@ -126,50 +127,21 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer
var path []string
var tableRows []table.Row

if 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, true)
if err != nil {
logger.Fatalf("error searching via hasSBOM: %v", err)
}

path = append(path, depVulnPath...)
tableRows = append(tableRows, depVulnTableRows...)

if len(depVulnPath) == 0 {
err, o := searchStringToOccurrence(ctx, gqlclient, opts, logger)
if err != nil {
logger.Fatalf("error searching for occurrence: %v", err)
}

// The primaryCall parameter in searchForSBOMViaArtifact is there for us to know that
// the searchString is expected to be a PURL, but isn't, so we have to check via artifacts instead of PURLs.
depVulnPath, depVulnTableRows, err = guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, o.IsOccurrence[0].Artifact.Id, opts.depth, false)
if err != nil {
logger.Fatalf("error searching for SBOMs via artifact: %v", err)
}

path = append(path, depVulnPath...)
tableRows = append(tableRows, depVulnTableRows...)
}
} else {
// The primaryCall parameter in searchForSBOMViaArtifact is there for us to know that
// the searchString isn't expected to be a PURL, so we have to check artifacts.
depVulnPath, depVulnTableRows, err := guacanalytics.SearchForSBOMViaArtifact(ctx, gqlclient, opts.searchString, opts.depth, true)
if err != nil {
logger.Fatalf("error searching for SBOMs via artifact: %v", err)
}
// 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...)
path = append(path, depVulnPath...)
tableRows = append(tableRows, depVulnTableRows...)

if len(depVulnPath) == 0 {
err, subjectPackage := searchStringToPkg(ctx, gqlclient, opts, logger)
if err != nil {
logger.Fatalf("error searching for packages: %v", err)
}
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)
Expand All @@ -192,8 +164,8 @@ func printVulnInfo(ctx context.Context, gqlclient graphql.Client, t table.Writer
}
}

func searchStringToPkg(ctx context.Context, gqlclient graphql.Client, opts queryOptions, logger *zap.SugaredLogger) (error, *model.AllIsOccurrencesTreeSubjectPackage) {
split := strings.Split(opts.searchString, ":")
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")
}
Expand All @@ -209,47 +181,7 @@ func searchStringToPkg(ctx context.Context, gqlclient graphql.Client, opts query
logger.Fatalf("error querying for occurrences: %v", err)
}

subjectPackage, ok := o.IsOccurrence[0].Subject.(*model.AllIsOccurrencesTreeSubjectPackage)
if !ok {
logger.Fatalf("error asserting type for Subject as *model.AllIsOccurrencesTreeSubjectPackage")
}
return err, subjectPackage
}

func searchStringToOccurrence(ctx context.Context, gqlclient graphql.Client, opts queryOptions, logger *zap.SugaredLogger) (error, *model.OccurrencesResponse) {
pkgInput, err := helpers.PurlToPkg(opts.searchString)
if err != nil {
logger.Fatalf("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,
}

o, err := model.Occurrences(ctx, gqlclient, model.IsOccurrenceSpec{
Subject: &model.PackageOrSourceSpec{
Package: pkgFilter,
},
})
if err != nil {
logger.Fatalf("error querying for occurrences: %v", err)
}
return err, o
return o
}

func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t table.Writer, opts queryOptions) {
Expand All @@ -272,8 +204,9 @@ 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 {
Expand All @@ -285,10 +218,22 @@ func printVulnInfoByVulnId(ctx context.Context, gqlclient graphql.Client, t tabl
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 {
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 := foundHasSBOMPkg.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")
}
Expand Down
181 changes: 19 additions & 162 deletions pkg/guacanalytics/searchForSBOM.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ 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"
"strings"
)

const (
Expand All @@ -22,11 +22,6 @@ type pkgVersionNeighborQueryResults struct {
isDep model.AllHasSBOMTreeIncludedDependenciesIsDependency
}

type artifactVersionNeighborQueryResults struct {
pkgVersionNeighborResponse *model.NeighborsResponse
isArt model.AllHasSBOMTreeIncludedOccurrencesIsOccurrence
}

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 {
Expand All @@ -35,19 +30,11 @@ func getVulnAndVexNeighborsForPackage(ctx context.Context, gqlclient graphql.Cli
return &pkgVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isDep: isDep}, nil
}

func getVulnAndVexNeighborsForArtifact(ctx context.Context, gqlclient graphql.Client, pkgID string, isArt model.AllHasSBOMTreeIncludedOccurrencesIsOccurrence) (*artifactVersionNeighborQueryResults, error) {
pkgVersionNeighborResponse, err := model.Neighbors(ctx, gqlclient, pkgID, []model.Edge{model.EdgeArtifactCertifyVexStatement})
if err != nil {
return nil, fmt.Errorf("failed to get neighbors for pkgID: %s with error %w", pkgID, err)
}
return &artifactVersionNeighborQueryResults{pkgVersionNeighborResponse: pkgVersionNeighborResponse, isArt: isArt}, 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 primaryCall 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, primaryCall bool) ([]string, []table.Row, error) {
// 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)
Expand Down Expand Up @@ -79,14 +66,22 @@ 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 && primaryCall {
pkgResponse, err := getPkgResponseFromPurl(ctx, gqlclient, now)
if err != nil {
return nil, nil, fmt.Errorf("getPkgResponseFromPurl - error: %w", 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)
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}}})
Expand Down Expand Up @@ -191,144 +186,6 @@ func SearchForSBOMViaPkg(ctx context.Context, gqlclient graphql.Client, searchSt
return path, tableRows, nil
}

// SearchForSBOMViaArtifact takes in either a URI for the initial value to find the hasSBOM node.
// It concurrently checks the artifact node if it contains vulnerabilities and VEX data.
// The primaryCall parameter is used to know whether the searchString is expected to be an artifact or a package.
func SearchForSBOMViaArtifact(ctx context.Context, gqlclient graphql.Client, searchString string, maxLength int, primaryCall bool) ([]string, []table.Row, error) {
var path []string
var tableRows []table.Row
checkedArtifactIDs := make(map[string]bool)
var collectedArtifactResults []*artifactVersionNeighborQueryResults

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
artID 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 nowNode.depth == 0 && primaryCall {
split := strings.Split(now, ":")
if len(split) != 2 {
return nil, nil, fmt.Errorf("error splitting search string %s, search string should have two sections algorithm and digest: %v", now, split)
}
algorithm := strings.ToLower(split[0])
digest := strings.ToLower(split[1])

foundHasSBOMPkg, err = model.HasSBOMs(ctx, gqlclient, model.HasSBOMSpec{Subject: &model.PackageOrArtifactSpec{
Artifact: &model.ArtifactSpec{
Algorithm: &algorithm,
Digest: &digest,
},
}})
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{Artifact: &model.ArtifactSpec{Id: &now}}})
if err != nil {
return nil, nil, fmt.Errorf("failed getting hasSBOM via artifact: %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 !checkedArtifactIDs[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...)
checkedArtifactIDs[pkgResponse.Namespaces[0].Names[0].Versions[0].Id] = true
}
}
}
for _, isOcc := range hasSBOM.IncludedOccurrences {
if *isOcc.Subject.GetTypename() == guacType {
continue
}
var matchingArtifactIDs []string
matchingArtifactIDs = append(matchingArtifactIDs, isOcc.Artifact.Id)

for _, artID := range matchingArtifactIDs {
dfsN, seen := nodeMap[artID]
if !seen {
dfsN = dfsNode{
parent: now,
artID: artID,
depth: nowNode.depth + 1,
}
nodeMap[artID] = dfsN
}
if !dfsN.expanded {
queue = append(queue, artID)
}
artifactNeighbors, err := getVulnAndVexNeighborsForArtifact(ctx, gqlclient, artID, isOcc)
if err != nil {
return nil, nil, fmt.Errorf("getVulnAndVexNeighborsForArtifact failed with error: %w", err)
}
collectedArtifactResults = append(collectedArtifactResults, artifactNeighbors)
checkedArtifactIDs[artID] = true
}
}
}
nowNode.expanded = true
nodeMap[now] = nowNode
}

checkedCertifyVulnIDs := make(map[string]bool)

// Collect results from the channel
for _, result := range collectedArtifactResults {
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.isArt.Id, result.isArt.Artifact.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 {
Expand Down

0 comments on commit b9f0318

Please sign in to comment.