Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Searching for hasSBOMs via Artifacts in Vuln cli #1965

Merged
merged 7 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading