Skip to content

Commit

Permalink
[PatchPlanning] Add CLI cmd for patch planning (#1129)
Browse files Browse the repository at this point in the history
* fixing

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* adding other changed

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* lint error

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing todo

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing println

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* validate

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* review changes 2

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing comments

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing tooo

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing comments 3

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* removing test from code

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* removing test ingestion

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* pkgtopurl

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* minor

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing nits

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

* fixing flag

Signed-off-by: Rebecca Metzman <rmetzman@google.com>

---------

Signed-off-by: Rebecca Metzman <rmetzman@google.com>
Co-authored-by: Rebecca Metzman <rmetzman@google.com>
  • Loading branch information
rmetzman and Rebecca Metzman authored Aug 8, 2023
1 parent ec385a9 commit 7d1960b
Show file tree
Hide file tree
Showing 5 changed files with 409 additions and 72 deletions.
295 changes: 295 additions & 0 deletions cmd/guacone/cmd/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
//
// Copyright 2023 The GUAC Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"fmt"
"net/http"
"os"
"strings"

"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"
analysis "github.com/guacsec/guac/pkg/guacanalytics"
"github.com/guacsec/guac/pkg/logging"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type queryPatchOptions struct {
graphqlEndpoint string
startPurl string
stopPurl string
depth int
isPackageVersionStart bool
isPackageVersionStop bool
}

var queryPatchCmd = &cobra.Command{
Use: "patch plan [flags] purl",
Short: "query which packages are affected by the vulnerability associated with specified packageName or packageVersion",
Run: func(cmd *cobra.Command, args []string) {
ctx := logging.WithLogger(context.Background())
logger := logging.FromContext(ctx)

opts, err := validateQueryPatchFlags(
viper.GetString("gql-addr"),
viper.GetString("start-purl"),
viper.GetString("stop-purl"),
viper.GetInt("search-depth"),
viper.GetBool("is-pkg-version-start"),
viper.GetBool("is-pkg-version-stop"),
args,
)

if err != nil {
logger.Fatalf("unable to validate flags: %s\n", err)
}

httpClient := http.Client{}
gqlClient := graphql.NewClient(opts.graphqlEndpoint, &httpClient)

var startID string
var stopID *string
stopID = nil

startID, err = getPkgID(ctx, gqlClient, opts.startPurl, opts.isPackageVersionStart)

if err != nil {
logger.Fatalf("error getting start pkg from purl inputted %s \n", err)
}

if opts.stopPurl != "" {
stopPkg, err := getPkgID(ctx, gqlClient, opts.stopPurl, opts.isPackageVersionStop)

if err != nil {
logger.Fatalf("error getting stop pkg from purl inputted %s\n", err)
}

stopID = &stopPkg

}

bfsMap, path, err := analysis.SearchDependenciesFromStartNode(ctx, gqlClient, startID, stopID, opts.depth)

if err != nil {
logger.Fatalf("error searching dependencies-- %s\n", err)
}

frontiers, infoNodes, err := analysis.ToposortFromBfsNodeMap(ctx, gqlClient, bfsMap)

if err != nil {
fmt.Printf("WARNING: There was cycle detected in the toposort so the results are incomplete: %s\n", err)
}

var poc []string
for level := 0; level < len(frontiers); level++ {
frontierList := frontiers[level]
allNodes := []string{}
for _, id := range frontierList {
path = append(path, id)
allNodes = append(allNodes, id)
}
fmt.Printf("\n---FRONTIER LEVEL %d---\n", level)

nodesInfo, err := printNodesInfo(ctx, gqlClient, bfsMap, allNodes)

if err != nil {
logger.Fatalf("%s", err)
}

poc = append(poc, nodesInfo...)
}

fmt.Printf("\n---INFO NODES---\n")
if len(infoNodes) == 0 {
fmt.Printf("no info nodes found\n")
} else {
nodesInfo, err := printNodesInfo(ctx, gqlClient, bfsMap, infoNodes)

if err != nil {
logger.Fatalf("%s", err)
}

poc = append(poc, nodesInfo...)
}

fmt.Printf("\n---POINTS OF CONTACT---")
if len(poc) == 0 {
fmt.Printf("\nno POCs found\n")
} else {
for _, id := range poc {
fmt.Printf("\n%s: %s", id, makePOCPretty(bfsMap[id].PointOfContact))
}
}

fmt.Printf("\n\n---SUBGRAPH VISUALIZER URL--- \nhttp://localhost:3000/?path=%s\n", strings.Join(removeDuplicateValuesFromPath(path), `,`))
},
}

func printNodesInfo(ctx context.Context, gqlClient graphql.Client, bfsMap map[string]analysis.BfsNode, nodes []string) ([]string, error) {
poc := []string{}
for _, id := range nodes {
node, err := model.Node(ctx, gqlClient, id)

if err != nil {
fmt.Printf("error: %s \n", err)
}

var pretty string
switch node := node.Node.(type) {
case *model.NodeNodePackage:
if bfsMap[id].Type == analysis.PackageName {
pretty = makePkgPretty(*node, false)
} else {
pretty = makePkgPretty(*node, true)
}
case *model.NodeNodeSource:
pretty = makeSrcPretty(*node)
case *model.NodeNodeArtifact:
pretty = makeArtifactPretty(*node)
default:
return nil, fmt.Errorf("discovered unexpected node type in bfsMap (expect plg, source, or artifact)")
}

fmt.Printf("%s: %s\n", id, pretty)

if bfsMap[id].PointOfContact.Email != "" {
poc = append(poc, id)
}
}
return poc, nil
}

func makePOCPretty(poc model.AllPointOfContact) string {
return fmt.Sprintf("id- %s | email- %s | info- %s | collector- %s | justification- %s | origin- %s | since- %s", poc.Id, poc.Email, poc.Info, poc.Collector, poc.Justification, poc.Origin, poc.Since)
}

func makePkgPretty(pkg model.NodeNodePackage, isPackageVersion bool) string {
version := ""
subpath := ""
var qualifiers []string

if isPackageVersion {
version = pkg.Namespaces[0].Names[0].Versions[0].Version

subpath = pkg.Namespaces[0].Names[0].Versions[0].Subpath

for _, qualifier := range pkg.Namespaces[0].Names[0].Versions[0].Qualifiers {
qualifiers = append(qualifiers, qualifier.Key, qualifier.Value)
}

}

pkgString := helpers.PkgToPurl(pkg.Type, pkg.Namespaces[0].Namespace, pkg.Namespaces[0].Names[0].Name, version, subpath, qualifiers)

return pkgString
}

func makeSrcPretty(src model.NodeNodeSource) string {
return fmt.Sprintf("src:%s/%s/%s", src.Type, src.Namespaces[0].Namespace, src.Namespaces[0].Names[0].Name)
}

func makeArtifactPretty(artifact model.NodeNodeArtifact) string {
return fmt.Sprintf("artifact: algorithm-%s | digest:%s", artifact.Algorithm, artifact.Digest)
}

func getPkgID(ctx context.Context, gqlClient graphql.Client, purl string, isPackageVersion bool) (string, error) {
pkgInput, err := helpers.PurlToPkg(purl)

if err != nil {
return "", fmt.Errorf("error getting pkg ID: %s", err)
}

var pkgFilter model.PkgSpec
version := false

if isPackageVersion {
pkgQualifierFilter := []model.PackageQualifierSpec{}
for _, qualifier := range pkgInput.Qualifiers {
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,
}
version = true
} else {
pkgFilter = model.PkgSpec{
Type: &pkgInput.Type,
Namespace: pkgInput.Namespace,
Name: &pkgInput.Name,
}
}

pkgResponse, err := model.Packages(ctx, gqlClient, pkgFilter)

if err != nil || len(pkgResponse.Packages) == 0 {
return "", fmt.Errorf("error finding package with given purl (may have set is-pkg-version incorrectly): %s", purl)
}

if version {
return pkgResponse.Packages[0].Namespaces[0].Names[0].Versions[0].Id, nil
}
return pkgResponse.Packages[0].Namespaces[0].Names[0].Id, nil
}

func validateQueryPatchFlags(graphqlEndpoint, startPurl string, stopPurl string, depth int, isPackageVersionStart bool, isPackageVersionStop bool, args []string) (queryPatchOptions, error) {
var opts queryPatchOptions
opts.startPurl = startPurl

if _, err := helpers.PurlToPkg(startPurl); startPurl != "" && err != nil {
return opts, fmt.Errorf("expected start input to be purl")
}

opts.stopPurl = stopPurl

if _, err := helpers.PurlToPkg(stopPurl); stopPurl != "" && err != nil {
return opts, fmt.Errorf("expected stop input to be purl")
}

opts.graphqlEndpoint = graphqlEndpoint
opts.depth = depth
opts.isPackageVersionStart = isPackageVersionStart
opts.isPackageVersionStop = isPackageVersionStop

return opts, nil
}

func init() {
set, err := cli.BuildFlags([]string{"start-purl", "stop-purl", "search-depth", "is-pkg-version-start", "is-pkg-version-stop"})
if err != nil {
fmt.Fprintf(os.Stderr, "failed to setup flag: %s", err)
os.Exit(1)
}
queryPatchCmd.Flags().AddFlagSet(set)
if err := viper.BindPFlags(queryPatchCmd.Flags()); err != nil {
fmt.Fprintf(os.Stderr, "failed to bind flags: %s", err)
os.Exit(1)
}

queryCmd.AddCommand(queryPatchCmd)
}
6 changes: 5 additions & 1 deletion pkg/cli/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var NotFound = errors.New("Flag not found")
func init() {
set := &pflag.FlagSet{}

// Set of all flags used across GUAC clis and subcommands. Use consistant
// Set of all flags used across GUAC clis and subcommands. Use consistent
// names for config file.
set.String("nats-addr", "nats://127.0.0.1:4222", "address to connect to NATs Server")
set.String("csub-addr", "localhost:2782", "address to connect to collect-sub service")
Expand Down Expand Up @@ -74,6 +74,10 @@ func init() {

set.StringP("vuln-id", "v", "", "CVE, GHSA or OSV ID to check")
set.Int("num-path", 0, "number of paths to return, 0 means all paths")
set.String("start-purl", "", "string input of purl with package to start search from")
set.String("stop-purl", "", "string input of purl with package to stop search at")
set.Bool("is-pkg-version-start", false, "for query path are you inputting a packageVersion to start the search from (if false then packageName)")
set.Bool("is-pkg-version-stop", false, "for query path are you inputting a packageVersion to stop the search at (if false then packageName)")

// Google Cloud platform flags
set.String("gcp-credentials-path", "", "Path to the Google Cloud service account credentials json file.\nAlternatively you can set GOOGLE_APPLICATION_CREDENTIALS=<path> in your environment.")
Expand Down
Loading

0 comments on commit 7d1960b

Please sign in to comment.