diff --git a/.gitignore b/.gitignore index 9fc77bf7..4f515078 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,11 @@ pids # Dependency directories (remove the comment below to include it) vendor/ + +# These files are generated during tests +fixtures/locks-insecure-nested/.osv-detector.yml +fixtures/locks-insecure-nested/nested/.osv-detector.yml +fixtures/locks-insecure-many/.osv-detector.yml +fixtures/existing-config-with-ignored-ignores.yml +fixtures/existing-config-with-ignores.yml +fixtures/existing-config.yml diff --git a/README.md b/README.md index 38371cd6..385c1ba8 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,12 @@ osv-detector --ignore GHSA-896r-f27r-55mw --ignore GHSA-74fj-2j2h-c42q package-l Ignores provided via the flag will be combined with any ignores specified in the loaded config file. -You can use `jq` to generate a list of OSV ids if you want to ignore all current -known vulnerabilities found by the detector: +You can use `--update-config-ignores` to have the detector update configs being +used for lockfiles to ignore any vulnerabilities that were found; it will also +remove ignores for vulnerabilities that are no longer present. + +Alternatively, you can use `jq` to generate a list of OSV ids if you want to +ignore all current known vulnerabilities found by the detector: ```shell osv-detector --json . | jq -r '[.results[].packages | map("- " + .vulnerabilities[].id)] | flatten | unique | sort | .[]' diff --git a/fixtures/locks-insecure-many/my-package-lock.json b/fixtures/locks-insecure-many/my-package-lock.json new file mode 100644 index 00000000..881fad09 --- /dev/null +++ b/fixtures/locks-insecure-many/my-package-lock.json @@ -0,0 +1,72 @@ +{ + "name": "1-package-lock.json", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz", + "integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/fixtures/locks-insecure-nested/my-package-lock.json b/fixtures/locks-insecure-nested/my-package-lock.json new file mode 100644 index 00000000..e3a2d449 --- /dev/null +++ b/fixtures/locks-insecure-nested/my-package-lock.json @@ -0,0 +1,9 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "ansi-html": { + "version": "0.0.1" + } + } +} diff --git a/fixtures/locks-insecure-nested/nested/my-composer-lock.json b/fixtures/locks-insecure-nested/nested/my-composer-lock.json new file mode 100644 index 00000000..bffac7e6 --- /dev/null +++ b/fixtures/locks-insecure-nested/nested/my-composer-lock.json @@ -0,0 +1,94 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "36f1605a5dac03350d3c7d40eafc8477", + "packages": [ + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2021-04-26T09:17:50+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "cwp/cwp-recipe-cms": 0, + "cwp/cwp-recipe-core": 0, + "innoweb/silverstripe-mailchimp-signup": 20, + "silverstripe/recipe-blog": 0, + "silverstripe/redirectedurls": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.4.0" + }, + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/internal/configer/load.go b/internal/configer/load.go index e50726b5..1a45e04e 100644 --- a/internal/configer/load.go +++ b/internal/configer/load.go @@ -14,16 +14,16 @@ import ( ) type rawDatabaseConfig struct { - Name string `yaml:"name"` - Type string `yaml:"type"` URL string `yaml:"url"` - WorkingDirectory string `yaml:"working-directory"` + Name string `yaml:"name,omitempty"` + Type string `yaml:"type,omitempty"` + WorkingDirectory string `yaml:"working-directory,omitempty"` } type rawConfig struct { FilePath string `yaml:"-"` Ignore []string `yaml:"ignore"` - Databases []rawDatabaseConfig `yaml:"extra-databases"` + Databases []rawDatabaseConfig `yaml:"extra-databases,omitempty"` } type Config struct { @@ -130,7 +130,7 @@ func Find(r *reporter.Reporter, pathToDirectory string) (Config, error) { return Config{}, nil } -func Load(r *reporter.Reporter, pathToConfig string) (Config, error) { +func load(pathToConfig string) (rawConfig, error) { var raw rawConfig pathToConfig = filepath.Clean(pathToConfig) @@ -140,13 +140,23 @@ func Load(r *reporter.Reporter, pathToConfig string) (Config, error) { configContents, err := os.ReadFile(pathToConfig) if err != nil { - return Config{FilePath: pathToConfig}, fmt.Errorf("could not read %s: %w", pathToConfig, err) + return raw, fmt.Errorf("could not read %s: %w", pathToConfig, err) } err = yaml.Unmarshal(configContents, &raw) if err != nil { - return Config{FilePath: pathToConfig}, fmt.Errorf("could not read %s: %w", pathToConfig, err) + return raw, fmt.Errorf("could not read %s: %w", pathToConfig, err) + } + + return raw, nil +} + +func Load(r *reporter.Reporter, pathToConfig string) (Config, error) { + raw, err := load(pathToConfig) + + if err != nil { + return Config{FilePath: raw.FilePath}, err } return newConfig(r, raw) diff --git a/internal/configer/update.go b/internal/configer/update.go new file mode 100644 index 00000000..c46a95ce --- /dev/null +++ b/internal/configer/update.go @@ -0,0 +1,34 @@ +package configer + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +func UpdateWithIgnores(pathToConfig string, ignores []string) error { + raw, err := load(pathToConfig) + + if err != nil { + return fmt.Errorf("%w", err) + } + + raw.Ignore = ignores + + f, err := os.OpenFile(pathToConfig, os.O_TRUNC|os.O_WRONLY, os.ModePerm) + + if err != nil { + return fmt.Errorf("%w", err) + } + + encoder := yaml.NewEncoder(f) + encoder.SetIndent(2) + err = encoder.Encode(raw) + + if err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/internal/configer/update_test.go b/internal/configer/update_test.go new file mode 100644 index 00000000..44014624 --- /dev/null +++ b/internal/configer/update_test.go @@ -0,0 +1,224 @@ +package configer_test + +import ( + "os" + "strconv" + "strings" + "testing" + + "github.com/g-rath/osv-detector/internal/cachedregexp" + "github.com/g-rath/osv-detector/internal/configer" + "github.com/google/go-cmp/cmp" + "gopkg.in/yaml.v3" +) + +func dedent(t *testing.T, str string) string { + t.Helper() + + // 0. replace all tabs with spaces + str = strings.ReplaceAll(str, "\t", " ") + + // 1. remove trailing whitespace + re := cachedregexp.MustCompile(`\r?\n([\t ]*)$`) + str = re.ReplaceAllString(str, "") + + // 2. if any of the lines are not indented, return as we're already dedent-ed + re = cachedregexp.MustCompile(`(^|\r?\n)[^\t \n]`) + if re.MatchString(str) { + return str + } + + // 3. find all line breaks to determine the highest common indentation level + re = cachedregexp.MustCompile(`\n[\t ]+`) + matches := re.FindAllString(str, -1) + + // 4. remove the common indentation from all strings + if matches != nil { + size := len(matches[0]) - 1 + + for _, match := range matches { + if len(match)-1 < size { + size = len(match) - 1 + } + } + + re := cachedregexp.MustCompile(`\n[\t ]{` + strconv.Itoa(size) + `}`) + str = re.ReplaceAllString(str, "\n") + } + + // 5. Remove leading whitespace. + re = cachedregexp.MustCompile(`^\r?\n`) + str = re.ReplaceAllString(str, "") + + return str +} + +func createTestConfigFile(t *testing.T, content string) (string, func()) { + t.Helper() + + f, err := os.CreateTemp(os.TempDir(), "osv-detector-test-*") + if err != nil { + t.Fatalf("could not create test file: %v", err) + } + + _, err = f.WriteString(dedent(t, content)) + + if err != nil { + t.Fatalf("could not write test config file file: %v", err) + } + + return f.Name(), func() { + _ = os.Remove(f.Name()) + } +} + +func readConfigFixture(t *testing.T, path string) string { + t.Helper() + + content, err := os.ReadFile(path) + + if err != nil { + t.Fatalf("could not read config fixture file: %v", err) + } + + return string(content) +} + +func TestUpdateWithIgnores_FileDoesNotExist(t *testing.T) { + t.Parallel() + + err := configer.UpdateWithIgnores("fixtures/does/not/exist", []string{}) + + if err == nil { + t.Errorf("Expected to get error, but did not") + } + + if !strings.Contains(err.Error(), "could not read") { + t.Errorf("Expected to get \"%s\" error, but got \"%v\"", "could not read", err) + } +} + +func expectAreEqual(t *testing.T, actual, expect string) { + t.Helper() + + actual = dedent(t, actual) + expect = dedent(t, expect) + + if !cmp.Equal(actual, expect) { + if os.Getenv("TEST_NO_DIFF") == "true" { + t.Errorf("\nactual does not match expected:\n got:\n%s\n\n want:\n%s", actual, expect) + } else { + t.Errorf("\nactual does not match expected:\n%s", cmp.Diff(expect, actual)) + } + } +} + +func TestUpdateWithIgnores(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ignores []string + initial string + updated string + }{ + { + name: "completely empty file", + ignores: []string{}, + initial: "", + updated: "ignore: []", + }, + { + name: "file with no ignores", + ignores: []string{}, + initial: "ignore:", + updated: "ignore: []", + }, + { + name: "file with no ignores (adding some ignores)", + ignores: []string{"OSV-1", "OSV-2", "OSV-3"}, + initial: "ignore:", + updated: ` + ignore: + - OSV-1 + - OSV-2 + - OSV-3 + `, + }, + { + name: "ignores are not sorted or deduplicated", + ignores: []string{"OSV-2", "OSV-5", "OSV-1", "OSV-2", "OSV-4", "OSV-3"}, + initial: "ignore:", + updated: ` + ignore: + - OSV-2 + - OSV-5 + - OSV-1 + - OSV-2 + - OSV-4 + - OSV-3 + `, + }, + { + name: "comments and existing ignores are not preserved", + ignores: []string{"OSV-1", "OSV-2"}, + initial: readConfigFixture(t, "fixtures/ext-yml/.osv-detector.yml"), + updated: ` + ignore: + - OSV-1 + - OSV-2 + `, + }, + { + name: "other config options are preserved", + ignores: []string{"OSV-4", "OSV-5"}, + initial: readConfigFixture(t, "fixtures/extra-databases/.osv-detector.yml"), + updated: ` + ignore: + - OSV-4 + - OSV-5 + extra-databases: + - url: https://github.com/github/advisory-database/archive/refs/heads/main.zip + - url: file:/relative/path/to/dir + - url: file:////root/path/to/dir + - url: https://api-staging.osv.dev/v1 + - url: https://github.com/github/advisory-database/archive/refs/heads/main.zip + name: GitHub Advisory Database + - url: https://my-site.com/osvs/all + type: zip + - url: https://github.com/github/advisory-database/archive/refs/heads/main.zip + working-directory: advisory-database-main/advisories/unreviewed + - url: https://my-site.com/osvs/all + type: file + - url: www.github.com/github/advisory-database/archive/refs/heads/main.zip + `, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + configFilePath, cleanupConfigFile := createTestConfigFile(t, tt.initial) + defer cleanupConfigFile() + + err := configer.UpdateWithIgnores(configFilePath, tt.ignores) + + if err != nil { + t.Fatalf("could not update config file: %v", err) + } + + content, err := os.ReadFile(configFilePath) + + if err != nil { + t.Fatalf("could not read test config file: %v", err) + } + + expectAreEqual(t, string(content), tt.updated) + + if err = yaml.Unmarshal(content, &struct{}{}); err != nil { + t.Fatalf("could not parse updated config as YAML: %v", err) + } + }) + } +} diff --git a/main.go b/main.go index f7c124a2..5e5d3f92 100644 --- a/main.go +++ b/main.go @@ -501,6 +501,7 @@ func run(args []string, stdout, stderr io.Writer) int { parseAs := cli.String("parse-as", "", "Name of a supported lockfile to parse the input files as") configPath := cli.String("config", "", "Path to a config file to use for all lockfiles") noConfig := cli.Bool("no-config", false, "Disable loading of any config files") + updateConfigIgnores := cli.Bool("update-config-ignores", false, "Update the list of ignored OSVs in config files") noConfigIgnores := cli.Bool("no-config-ignores", false, "Don't respect any OSVs listed as ignored in configs") noConfigDatabases := cli.Bool("no-config-databases", false, "Don't load any extra databases listed in configs") printVersion := cli.Bool("version", false, "Print version information") @@ -605,6 +606,8 @@ This flag can be passed multiple times to ignore different vulnerabilities`) exitCode = 127 } + vulnsPerConfig := make(map[string]map[string]struct{}) + for i, result := range files { if i >= 1 { r.PrintTextf("\n") @@ -687,14 +690,81 @@ This flag can be passed multiple times to ignore different vulnerabilities`) r.PrintResult(report) + // if we're meant to be updating the list of ignored osvs, + // then we need to capture the id of each osv from each file + // against the location of each config being used for that lockfile + if *updateConfigIgnores && config.FilePath != "" { + if _, ok := vulnsPerConfig[config.FilePath]; !ok { + vulnsPerConfig[config.FilePath] = make(map[string]struct{}) + } + + for _, pkg := range report.Packages { + for _, vul := range pkg.Vulnerabilities.Unique() { + vulnsPerConfig[config.FilePath][vul.ID] = struct{}{} + } + + for _, vul := range pkg.Ignored.Unique() { + vulnsPerConfig[config.FilePath][vul.ID] = struct{}{} + } + } + } + if report.HasKnownVulnerabilities() && exitCode == 0 { exitCode = 1 } } + if *updateConfigIgnores { + writeUpdatedConfigs(r, vulnsPerConfig) + } + return exitCode } +func writeUpdatedConfigs(r *reporter.Reporter, vulnsPerConfig map[string]map[string]struct{}) { + if len(vulnsPerConfig) != 0 { + r.PrintTextf("\n") + } + + lines := make([]string, 0, len(vulnsPerConfig)) + + for configPath, vulns := range vulnsPerConfig { + var ignores []string + + for id := range vulns { + ignores = append(ignores, id) + } + sort.Slice(ignores, func(i, j int) bool { + return ignores[i] < ignores[j] + }) + + err := configer.UpdateWithIgnores(configPath, ignores) + + if err == nil { + lines = append(lines, fmt.Sprintf( + "Updated %s with %d %s\n", + color.MagentaString(configPath), + len(vulns), + reporter.Form(len(vulns), "vulnerability", "vulnerabilities"), + )) + } else { + lines = append(lines, fmt.Sprintf("Error updating config: %v", err)) + } + } + + sort.Slice(lines, func(i, j int) bool { + return lines[i] < lines[j] + }) + + for _, line := range lines { + if strings.HasPrefix(line, "Error updating") { + r.PrintErrorf(line) + } else { + r.PrintTextf(line) + } + } +} + func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) } diff --git a/main_test.go b/main_test.go index d843f9d9..1b355750 100644 --- a/main_test.go +++ b/main_test.go @@ -73,6 +73,8 @@ type cliTestCase struct { wantExitCode int wantStdout string wantStderr string + + around func(t *testing.T) func() } // Attempts to normalize any file paths in the given `output` so that they can @@ -1377,6 +1379,374 @@ func TestRun_Ignores(t *testing.T) { } } +func setupConfigForUpdating(t *testing.T, path string, initial string, updated string) func() { + t.Helper() + + err := os.WriteFile(path, []byte(initial), os.ModePerm) + + if err != nil { + t.Fatalf("could not create test file: %v", err) + } + + return func() { + t.Helper() + + // ensure that we always try to remove the file + defer func() { + if err = os.Remove(path); err != nil { + // this will typically fail on Windows due to processes, + // so we just treat it as a warning instead of an error + t.Logf("could not remove test file: %v", err) + } + }() + + content, err := os.ReadFile(path) + + if err != nil { + t.Fatalf("could not read test config file: %v", err) + } + + expectAreEqual(t, "config", string(content), updated) + } +} + +func TestRun_UpdatingConfigIgnores(t *testing.T) { + t.Parallel() + + tests := []cliTestCase{ + // when there is no existing config, nothing should be updated + { + name: "", + args: []string{"--update-config-ignores", filepath.FromSlash("package-lock.json:./fixtures/locks-insecure/my-package-lock.json")}, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure/my-package-lock.json: found 1 package + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + ansi-html@0.0.1 is affected by the following vulnerabilities: + GHSA-whgm-jr23-g3j9: Uncontrolled Resource Consumption in ansi-html (https://github.com/advisories/GHSA-whgm-jr23-g3j9) + + 1 known vulnerability found in fixtures/locks-insecure/my-package-lock.json + `, + wantStderr: "", + }, + // when given an explicit config, that should be updated + { + name: "", + args: []string{ + "--update-config-ignores", + "--config", "fixtures/existing-config.yml", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure/my-package-lock.json"), + }, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure/my-package-lock.json: found 1 package + Using config at fixtures/existing-config.yml (0 ignores) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + ansi-html@0.0.1 is affected by the following vulnerabilities: + GHSA-whgm-jr23-g3j9: Uncontrolled Resource Consumption in ansi-html (https://github.com/advisories/GHSA-whgm-jr23-g3j9) + + 1 known vulnerability found in fixtures/locks-insecure/my-package-lock.json + + Updated fixtures/existing-config.yml with 1 vulnerability + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + return setupConfigForUpdating(t, + "fixtures/existing-config.yml", + "", + ` + ignore: + - GHSA-whgm-jr23-g3j9 + `, + ) + }, + }, + // when there are existing ignores + { + name: "", + args: []string{ + "--update-config-ignores", + "--config", "fixtures/existing-config-with-ignores.yml", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure/my-package-lock.json"), + }, + wantExitCode: 0, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure/my-package-lock.json: found 1 package + Using config at fixtures/existing-config-with-ignores.yml (1 ignore) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + no new vulnerabilities found (1 was ignored) + + Updated fixtures/existing-config-with-ignores.yml with 1 vulnerability + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + return setupConfigForUpdating(t, + "fixtures/existing-config-with-ignores.yml", + "ignore: [GHSA-whgm-jr23-g3j9]", + ` + ignore: + - GHSA-whgm-jr23-g3j9 + `, + ) + }, + }, + // when there are existing ignores but told to ignore those + { + name: "", + args: []string{ + "--update-config-ignores", + "--no-config-ignores", + "--config", "fixtures/existing-config-with-ignored-ignores.yml", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure/my-package-lock.json"), + }, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure/my-package-lock.json: found 1 package + Using config at fixtures/existing-config-with-ignored-ignores.yml (skipping any ignores) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + ansi-html@0.0.1 is affected by the following vulnerabilities: + GHSA-whgm-jr23-g3j9: Uncontrolled Resource Consumption in ansi-html (https://github.com/advisories/GHSA-whgm-jr23-g3j9) + + 1 known vulnerability found in fixtures/locks-insecure/my-package-lock.json + + Updated fixtures/existing-config-with-ignored-ignores.yml with 1 vulnerability + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + return setupConfigForUpdating(t, + "fixtures/existing-config-with-ignored-ignores.yml", + "ignore: [GHSA-whgm-jr23-g3j9]", + ` + ignore: + - GHSA-whgm-jr23-g3j9 + `, + ) + }, + }, + // when there are many lockfiles with one config + { + name: "", + args: []string{ + "--update-config-ignores", + "--config", "fixtures/existing-config-with-many-lockfiles.yml", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure/my-package-lock.json"), + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure-many/my-package-lock.json"), + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure-nested/my-package-lock.json"), + filepath.FromSlash("composer.lock:./fixtures/locks-insecure-nested/nested/my-composer-lock.json"), + }, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + Packagist (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure/my-package-lock.json: found 1 package + Using config at fixtures/existing-config-with-many-lockfiles.yml (1 ignore) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + no new vulnerabilities found (1 was ignored) + + fixtures/locks-insecure-many/my-package-lock.json: found 6 packages + Using config at fixtures/existing-config-with-many-lockfiles.yml (1 ignore) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + ansi-regex@4.1.0 is affected by the following vulnerabilities: + GHSA-93q8-gq69-wqmw: Inefficient Regular Expression Complexity in chalk/ansi-regex (https://github.com/advisories/GHSA-93q8-gq69-wqmw) + nth-check@1.0.2 is affected by the following vulnerabilities: + GHSA-rp65-9cf3-cjxr: Inefficient Regular Expression Complexity in nth-check (https://github.com/advisories/GHSA-rp65-9cf3-cjxr) + trim-newlines@3.0.0 is affected by the following vulnerabilities: + GHSA-7p7h-4mm5-852v: Uncontrolled Resource Consumption in trim-newlines (https://github.com/advisories/GHSA-7p7h-4mm5-852v) + ua-parser-js@1.0.2 is affected by the following vulnerabilities: + GHSA-fhg7-m89q-25r3: ReDoS Vulnerability in ua-parser-js version (https://github.com/advisories/GHSA-fhg7-m89q-25r3) + word-wrap@1.2.3 is affected by the following vulnerabilities: + GHSA-j8xg-fqg3-53r7: word-wrap vulnerable to Regular Expression Denial of Service (https://github.com/advisories/GHSA-j8xg-fqg3-53r7) + + 5 known vulnerabilities found in fixtures/locks-insecure-many/my-package-lock.json + + fixtures/locks-insecure-nested/my-package-lock.json: found 1 package + Using config at fixtures/existing-config-with-many-lockfiles.yml (1 ignore) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + no new vulnerabilities found (1 was ignored) + + fixtures/locks-insecure-nested/nested/my-composer-lock.json: found 1 package + Using config at fixtures/existing-config-with-many-lockfiles.yml (1 ignore) + Using db Packagist (%% vulnerabilities, including withdrawn - last updated %%) + + guzzlehttp/psr7@1.8.2 is affected by the following vulnerabilities: + GHSA-q7rv-6hp3-vh96: Improper Input Validation in guzzlehttp/psr7 (https://github.com/advisories/GHSA-q7rv-6hp3-vh96) + + 1 known vulnerability found in fixtures/locks-insecure-nested/nested/my-composer-lock.json + + Updated fixtures/existing-config-with-many-lockfiles.yml with 7 vulnerabilities + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + return setupConfigForUpdating(t, + "fixtures/existing-config-with-many-lockfiles.yml", + "ignore: [GHSA-whgm-jr23-g3j9]", + ` + ignore: + - GHSA-7p7h-4mm5-852v + - GHSA-93q8-gq69-wqmw + - GHSA-fhg7-m89q-25r3 + - GHSA-j8xg-fqg3-53r7 + - GHSA-q7rv-6hp3-vh96 + - GHSA-rp65-9cf3-cjxr + - GHSA-whgm-jr23-g3j9 + `, + ) + }, + }, + // when there are multiple implicit configs, it updates the right ones + { + name: "", + args: []string{ + "--update-config-ignores", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure-nested/my-package-lock.json"), + filepath.FromSlash("composer.lock:./fixtures/locks-insecure-nested/nested/my-composer-lock.json"), + }, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + Packagist (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure-nested/my-package-lock.json: found 1 package + Using config at fixtures/locks-insecure-nested/.osv-detector.yml (0 ignores) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + ansi-html@0.0.1 is affected by the following vulnerabilities: + GHSA-whgm-jr23-g3j9: Uncontrolled Resource Consumption in ansi-html (https://github.com/advisories/GHSA-whgm-jr23-g3j9) + + 1 known vulnerability found in fixtures/locks-insecure-nested/my-package-lock.json + + fixtures/locks-insecure-nested/nested/my-composer-lock.json: found 1 package + Using config at fixtures/locks-insecure-nested/nested/.osv-detector.yml (0 ignores) + Using db Packagist (%% vulnerabilities, including withdrawn - last updated %%) + + guzzlehttp/psr7@1.8.2 is affected by the following vulnerabilities: + GHSA-q7rv-6hp3-vh96: Improper Input Validation in guzzlehttp/psr7 (https://github.com/advisories/GHSA-q7rv-6hp3-vh96) + + 1 known vulnerability found in fixtures/locks-insecure-nested/nested/my-composer-lock.json + + Updated fixtures/locks-insecure-nested/.osv-detector.yml with 1 vulnerability + Updated fixtures/locks-insecure-nested/nested/.osv-detector.yml with 1 vulnerability + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + cleanupConfig1 := setupConfigForUpdating(t, + "fixtures/locks-insecure-nested/.osv-detector.yml", + "ignore: []", + ` + ignore: + - GHSA-whgm-jr23-g3j9 + `, + ) + + cleanupConfig2 := setupConfigForUpdating(t, + "fixtures/locks-insecure-nested/nested/.osv-detector.yml", + "ignore: []", + ` + ignore: + - GHSA-q7rv-6hp3-vh96 + `, + ) + + return func() { + cleanupConfig1() + cleanupConfig2() + } + }, + }, + // when there are existing ignores, it updates them and removes patched ones + { + name: "", + args: []string{ + "--update-config-ignores", + filepath.FromSlash("package-lock.json:./fixtures/locks-insecure-many/my-package-lock.json"), + }, + wantExitCode: 1, + wantStdout: ` + Loaded the following OSV databases: + npm (%% vulnerabilities, including withdrawn - last updated %%) + + fixtures/locks-insecure-many/my-package-lock.json: found 6 packages + Using config at fixtures/locks-insecure-many/.osv-detector.yml (3 ignores) + Using db npm (%% vulnerabilities, including withdrawn - last updated %%) + + nth-check@1.0.2 is affected by the following vulnerabilities: + GHSA-rp65-9cf3-cjxr: Inefficient Regular Expression Complexity in nth-check (https://github.com/advisories/GHSA-rp65-9cf3-cjxr) + ua-parser-js@1.0.2 is affected by the following vulnerabilities: + GHSA-fhg7-m89q-25r3: ReDoS Vulnerability in ua-parser-js version (https://github.com/advisories/GHSA-fhg7-m89q-25r3) + word-wrap@1.2.3 is affected by the following vulnerabilities: + GHSA-j8xg-fqg3-53r7: word-wrap vulnerable to Regular Expression Denial of Service (https://github.com/advisories/GHSA-j8xg-fqg3-53r7) + + 3 new vulnerabilities found in fixtures/locks-insecure-many/my-package-lock.json (2 were ignored) + + Updated fixtures/locks-insecure-many/.osv-detector.yml with 5 vulnerabilities + `, + wantStderr: "", + around: func(t *testing.T) func() { + t.Helper() + + return setupConfigForUpdating(t, + "fixtures/locks-insecure-many/.osv-detector.yml", + "ignore: [GHSA-7p7h-4mm5-852v, GHSA-93q8-gq69-wqmw, GHSA-67hx-6x53-jw92]", + ` + ignore: + - GHSA-7p7h-4mm5-852v + - GHSA-93q8-gq69-wqmw + - GHSA-fhg7-m89q-25r3 + - GHSA-j8xg-fqg3-53r7 + - GHSA-rp65-9cf3-cjxr + `, + ) + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if tt.around != nil { + teardown := tt.around(t) + + defer teardown() + } + + testCli(t, tt) + }) + } +} + func TestRun_EndToEnd(t *testing.T) { t.Parallel()