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 }