Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 12 additions & 139 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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{})

Expand Down
133 changes: 133 additions & 0 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading