diff --git a/internal/config/config.go b/internal/config/config.go index 4ed0f4c5078..3ea7dcecbab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,30 +2,15 @@ package config import ( - "errors" - "fmt" - "os" - "path/filepath" "slices" - "strings" "time" - "github.com/BurntSushi/toml" "github.com/google/osv-scanner/v2/internal/cmdlogger" "github.com/google/osv-scanner/v2/internal/imodels" ) var OSVScannerConfigName = "osv-scanner.toml" -type Manager struct { - // Override to replace all other configs - OverrideConfig *Config - // Config to use if no config file is found alongside manifests - DefaultConfig Config - // Cache to store loaded configs - ConfigMap map[string]Config -} - type Config struct { IgnoredVulns []*IgnoreEntry `toml:"IgnoredVulns"` PackageOverrides []PackageOverrideEntry `toml:"PackageOverrides"` @@ -35,18 +20,6 @@ type Config struct { LoadPath string `toml:"-"` } -func (c *Config) UnusedIgnoredVulns() []*IgnoreEntry { - unused := make([]*IgnoreEntry, 0, len(c.IgnoredVulns)) - - for _, entry := range c.IgnoredVulns { - if !entry.Used { - unused = append(unused, entry) - } - } - - return unused -} - type IgnoreEntry struct { ID string `toml:"id"` IgnoreUntil time.Time `toml:"ignoreUntil"` @@ -101,6 +74,18 @@ type License struct { Ignore bool `toml:"ignore"` } +func (c *Config) UnusedIgnoredVulns() []*IgnoreEntry { + unused := make([]*IgnoreEntry, 0, len(c.IgnoredVulns)) + + for _, entry := range c.IgnoredVulns { + if !entry.Used { + unused = append(unused, entry) + } + } + + return unused +} + func (c *Config) ShouldIgnore(vulnID string) (bool, *IgnoreEntry) { index := slices.IndexFunc(c.IgnoredVulns, func(e *IgnoreEntry) bool { return e.ID == vulnID }) if index == -1 { @@ -156,118 +141,6 @@ func shouldIgnoreTimestamp(ignoreUntil time.Time) bool { return ignoreUntil.After(time.Now()) } -// UseOverride updates the Manager to use the config at the given path in place -// of any other config files that would be loaded when calling Get -func (c *Manager) UseOverride(configPath string) error { - config, configErr := tryLoadConfig(configPath) - if configErr != nil { - return configErr - } - c.OverrideConfig = &config - - return nil -} - -// Get returns the appropriate config to use based on the targetPath -func (c *Manager) Get(targetPath string) Config { - if c.OverrideConfig != nil { - return *c.OverrideConfig - } - - configPath, err := normalizeConfigLoadPath(targetPath) - if err != nil { - // TODO: This can happen when target is not a file (e.g. Docker container, git hash...etc.) - // Figure out a more robust way to load config from non files - // r.PrintErrorf("Can't find config path: %s\n", err) - return Config{} - } - - config, alreadyExists := c.ConfigMap[configPath] - if alreadyExists { - return config - } - - config, configErr := tryLoadConfig(configPath) - if configErr == nil { - cmdlogger.Infof("Loaded filter from: %s", config.LoadPath) - } else { - // anything other than the config file not existing is most likely due to an invalid config file - if !errors.Is(configErr, os.ErrNotExist) { - cmdlogger.Errorf("Ignored invalid config file at %s because: %v", configPath, configErr) - } - // If config doesn't exist, use the default config - config = c.DefaultConfig - } - c.ConfigMap[configPath] = config - - return config -} - -func (c *Manager) GetUnusedIgnoreEntries() map[string][]*IgnoreEntry { - m := make(map[string][]*IgnoreEntry) - - for _, config := range c.ConfigMap { - unusedEntries := config.UnusedIgnoredVulns() - - if len(unusedEntries) > 0 { - m[config.LoadPath] = unusedEntries - } - } - - if c.OverrideConfig != nil { - unusedEntries := c.OverrideConfig.UnusedIgnoredVulns() - - if len(unusedEntries) > 0 { - m[c.OverrideConfig.LoadPath] = unusedEntries - } - } - - return m -} - -// Finds the containing folder of `target`, then appends osvScannerConfigName -func normalizeConfigLoadPath(target string) (string, error) { - stat, err := os.Stat(target) - if err != nil { - return "", fmt.Errorf("failed to stat target: %w", err) - } - - var containingFolder string - if !stat.IsDir() { - containingFolder = filepath.Dir(target) - } else { - containingFolder = target - } - configPath := filepath.Join(containingFolder, OSVScannerConfigName) - - return configPath, nil -} - -// tryLoadConfig attempts to parse the config file at the given path as TOML, -// returning the Config object if successful or otherwise the error -func tryLoadConfig(configPath string) (Config, error) { - config := Config{} - m, err := toml.DecodeFile(configPath, &config) - if err == nil { - unknownKeys := m.Undecoded() - - if len(unknownKeys) > 0 { - keys := make([]string, 0, len(unknownKeys)) - - for _, key := range unknownKeys { - keys = append(keys, key.String()) - } - - return Config{}, fmt.Errorf("unknown keys in config file: %s", strings.Join(keys, ", ")) - } - - config.LoadPath = configPath - config.warnAboutDuplicates() - } - - return config, err -} - func (c *Config) warnAboutDuplicates() { seen := make(map[string]struct{}) diff --git a/internal/config/manager.go b/internal/config/manager.go new file mode 100644 index 00000000000..6b5e0acfc5d --- /dev/null +++ b/internal/config/manager.go @@ -0,0 +1,133 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" + "github.com/google/osv-scanner/v2/internal/cmdlogger" +) + +type Manager struct { + // Override to replace all other configs + OverrideConfig *Config + // Config to use if no config file is found alongside manifests + DefaultConfig Config + // Cache to store loaded configs + ConfigMap map[string]Config +} + +// UseOverride updates the Manager to use the config at the given path in place +// of any other config files that would be loaded when calling Get +func (m *Manager) UseOverride(configPath string) error { + config, configErr := tryLoadConfig(configPath) + if configErr != nil { + return configErr + } + m.OverrideConfig = &config + + return nil +} + +// Get returns the appropriate config to use based on the targetPath +func (m *Manager) Get(targetPath string) Config { + if m.OverrideConfig != nil { + return *m.OverrideConfig + } + + configPath, err := normalizeConfigLoadPath(targetPath) + if err != nil { + // TODO: This can happen when target is not a file (e.g. Docker container, git hash...etc.) + // Figure out a more robust way to load config from non files + // r.PrintErrorf("Can't find config path: %s\n", err) + return Config{} + } + + config, alreadyExists := m.ConfigMap[configPath] + if alreadyExists { + return config + } + + config, configErr := tryLoadConfig(configPath) + if configErr == nil { + cmdlogger.Infof("Loaded filter from: %s", config.LoadPath) + } else { + // anything other than the config file not existing is most likely due to an invalid config file + if !errors.Is(configErr, os.ErrNotExist) { + cmdlogger.Errorf("Ignored invalid config file at %s because: %v", configPath, configErr) + } + // If config doesn't exist, use the default config + config = m.DefaultConfig + } + m.ConfigMap[configPath] = config + + return config +} + +func (m *Manager) GetUnusedIgnoreEntries() map[string][]*IgnoreEntry { + entries := make(map[string][]*IgnoreEntry) + + for _, config := range m.ConfigMap { + unusedEntries := config.UnusedIgnoredVulns() + + if len(unusedEntries) > 0 { + entries[config.LoadPath] = unusedEntries + } + } + + if m.OverrideConfig != nil { + unusedEntries := m.OverrideConfig.UnusedIgnoredVulns() + + if len(unusedEntries) > 0 { + entries[m.OverrideConfig.LoadPath] = unusedEntries + } + } + + return entries +} + +// Finds the containing folder of `target`, then appends osvScannerConfigName +func normalizeConfigLoadPath(target string) (string, error) { + stat, err := os.Stat(target) + if err != nil { + return "", fmt.Errorf("failed to stat target: %w", err) + } + + var containingFolder string + if !stat.IsDir() { + containingFolder = filepath.Dir(target) + } else { + containingFolder = target + } + configPath := filepath.Join(containingFolder, OSVScannerConfigName) + + return configPath, nil +} + +// tryLoadConfig attempts to parse the config file at the given path as TOML, +// returning the Config object if successful or otherwise the error +func tryLoadConfig(configPath string) (Config, error) { + config := Config{} + c, err := toml.DecodeFile(configPath, &config) + if err == nil { + unknownKeys := c.Undecoded() + + if len(unknownKeys) > 0 { + keys := make([]string, 0, len(unknownKeys)) + + for _, key := range unknownKeys { + keys = append(keys, key.String()) + } + + return Config{}, fmt.Errorf("unknown keys in config file: %s", strings.Join(keys, ", ")) + } + + config.LoadPath = configPath + config.warnAboutDuplicates() + } + + return config, err +}