github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/advisory_discover.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"os"
     9  
    10  	"chainguard.dev/melange/pkg/config"
    11  	tea "github.com/charmbracelet/bubbletea"
    12  	"github.com/spf13/cobra"
    13  	"github.com/wolfi-dev/wolfictl/pkg/advisory"
    14  	"github.com/wolfi-dev/wolfictl/pkg/cli/components/advisory/matchwatcher"
    15  	"github.com/wolfi-dev/wolfictl/pkg/configs"
    16  	v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2"
    17  	buildconfigs "github.com/wolfi-dev/wolfictl/pkg/configs/build"
    18  	rwfsOS "github.com/wolfi-dev/wolfictl/pkg/configs/rwfs/os"
    19  	"github.com/wolfi-dev/wolfictl/pkg/distro"
    20  	"github.com/wolfi-dev/wolfictl/pkg/vuln/nvdapi"
    21  	"golang.org/x/sync/errgroup"
    22  )
    23  
    24  //nolint:gosec // This is not a hard-coded credential value, it's the name of the env var to reference.
    25  const envVarNameForNVDAPIKey = "WOLFICTL_NVD_API_KEY"
    26  
    27  func cmdAdvisoryDiscover() *cobra.Command {
    28  	p := &discoverParams{}
    29  	cmd := &cobra.Command{
    30  		Use:           "discover",
    31  		Short:         "Automatically create advisories by matching distro packages to vulnerabilities in NVD",
    32  		SilenceErrors: true,
    33  		Args:          cobra.NoArgs,
    34  		RunE: func(cmd *cobra.Command, _ []string) error {
    35  			packageRepositoryURL := p.packageRepositoryURL
    36  
    37  			distroRepoDir := resolveDistroDir(p.distroRepoDir)
    38  			advisoriesRepoDir := resolveAdvisoriesDirInput(p.advisoriesRepoDir)
    39  			if distroRepoDir == "" || advisoriesRepoDir == "" {
    40  				if p.doNotDetectDistro {
    41  					return fmt.Errorf("distro repo dir and/or advisories repo dir was left unspecified")
    42  				}
    43  
    44  				d, err := distro.Detect()
    45  				if err != nil {
    46  					return fmt.Errorf("distro repo dir and/or advisories repo dir was left unspecified, and distro auto-detection failed: %w", err)
    47  				}
    48  
    49  				distroRepoDir = d.Local.PackagesRepo.Dir
    50  				advisoriesRepoDir = d.Local.AdvisoriesRepo.Dir
    51  
    52  				if packageRepositoryURL == "" {
    53  					packageRepositoryURL = d.Absolute.APKRepositoryURL
    54  				}
    55  
    56  				_, _ = fmt.Fprint(os.Stderr, renderDetectedDistro(d))
    57  			}
    58  
    59  			advisoriesFsys := rwfsOS.DirFS(advisoriesRepoDir)
    60  			advisoryCfgs, err := v2.NewIndex(cmd.Context(), advisoriesFsys)
    61  			if err != nil {
    62  				return err
    63  			}
    64  
    65  			fsys := rwfsOS.DirFS(distroRepoDir)
    66  			buildCfgs, err := buildconfigs.NewIndex(cmd.Context(), fsys)
    67  			if err != nil {
    68  				return fmt.Errorf("unable to select packages: %w", err)
    69  			}
    70  
    71  			selectedPackages := getSelectedOrDistroPackages(p.packageName, buildCfgs)
    72  			apiKey := p.resolveNVDAPIKey()
    73  
    74  			ctx := cmd.Context()
    75  			g, ctx := errgroup.WithContext(ctx)
    76  			events := make(chan interface{})
    77  
    78  			g.Go(func() error {
    79  				defer close(events)
    80  				model := matchwatcher.New(events, len(selectedPackages))
    81  				final, err := tea.NewProgram(model, tea.WithContext(ctx)).Run()
    82  
    83  				if err != nil {
    84  					return err
    85  				}
    86  
    87  				if m, ok := final.(matchwatcher.Model); ok && m.Err != nil {
    88  					return m.Err
    89  				}
    90  
    91  				// Return a sentinel error to cancel the other goroutines.
    92  				return errNormalExit
    93  			})
    94  
    95  			g.Go(func() error {
    96  				err = advisory.Discover(ctx, advisory.DiscoverOptions{
    97  					SelectedPackages:      selectedPackages,
    98  					BuildCfgs:             buildCfgs,
    99  					AdvisoryDocs:          advisoryCfgs,
   100  					PackageRepositoryURL:  packageRepositoryURL,
   101  					Arches:                []string{"x86_64", "aarch64"},
   102  					VulnerabilityDetector: nvdapi.NewDetector(http.DefaultClient, nvdapi.DefaultHost, apiKey),
   103  					VulnEvents:            events,
   104  				})
   105  				return err
   106  			})
   107  
   108  			if err := g.Wait(); err != nil {
   109  				if errors.Is(err, errNormalExit) {
   110  					return nil
   111  				}
   112  
   113  				return err
   114  			}
   115  
   116  			return nil
   117  		},
   118  	}
   119  
   120  	p.addFlagsTo(cmd)
   121  	return cmd
   122  }
   123  
   124  var errNormalExit = errors.New("normal exit")
   125  
   126  type discoverParams struct {
   127  	doNotDetectDistro bool
   128  
   129  	packageName string
   130  
   131  	distroRepoDir, advisoriesRepoDir string
   132  
   133  	packageRepositoryURL string
   134  
   135  	nvdAPIKey string
   136  }
   137  
   138  func (p *discoverParams) addFlagsTo(cmd *cobra.Command) {
   139  	addNoDistroDetectionFlag(&p.doNotDetectDistro, cmd)
   140  
   141  	addPackageFlag(&p.packageName, cmd)
   142  
   143  	addDistroDirFlag(&p.distroRepoDir, cmd)
   144  	addAdvisoriesDirFlag(&p.advisoriesRepoDir, cmd)
   145  
   146  	cmd.Flags().StringVarP(&p.packageRepositoryURL, "package-repo-url", "r", "", "URL of the APK package repository")
   147  
   148  	cmd.Flags().StringVar(&p.nvdAPIKey, "nvd-api-key", "", fmt.Sprintf("NVD API key (Can also be set via the environment variable '%s'. Using an API key significantly increases the rate limit for API requests. If you need an NVD API key, go to https://nvd.nist.gov/developers/request-an-api-key.)", envVarNameForNVDAPIKey))
   149  }
   150  
   151  func (p *discoverParams) resolveNVDAPIKey() string {
   152  	// TODO: use Viper for this!
   153  
   154  	if p.nvdAPIKey != "" {
   155  		return p.nvdAPIKey
   156  	}
   157  
   158  	keyFromEnv := os.Getenv(envVarNameForNVDAPIKey)
   159  	if keyFromEnv != "" {
   160  		return keyFromEnv
   161  	}
   162  
   163  	log.Print("⚠️  no NVD API key supplied. Searching NVD will be significantly faster if you use an API key. See command help for more information.")
   164  
   165  	return ""
   166  }
   167  
   168  func getSelectedOrDistroPackages(packageName string, buildCfgs *configs.Index[config.Configuration]) []string {
   169  	if packageName != "" {
   170  		return []string{packageName}
   171  	}
   172  
   173  	var pkgs []string
   174  	buildCfgs.Select().Each(func(e configs.Entry[config.Configuration]) {
   175  		pkgs = append(pkgs, e.Configuration().Package.Name)
   176  	})
   177  
   178  	return pkgs
   179  }