package config import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" "github.com/google/osv-scanner/v2/internal/cachedregexp" "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() for _, override := range config.PackageOverrides { if override.NameIsRegex && override.Name != "" { if _, err := cachedregexp.Compile("^" + override.Name + "$"); err != nil { return Config{}, fmt.Errorf("invalid regex %q in package override: %w", override.Name, err) } } } } return config, err }