Skip to content

Commit 98cc63b

Browse files
authored
feat: support config file (#95)
* feat: support config * fix: support `--no-config` flag * docs: add section about config files * chore: remove unused fixture file * chore: ignore invalid yaml files from prettier
1 parent 3ac8bd0 commit 98cc63b

File tree

10 files changed

+276
-1
lines changed

10 files changed

+276
-1
lines changed

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
internal/*/fixtures/pnpm/*.yaml
2+
internal/configer/fixtures/ext-yaml-invalid/*.yaml
3+
internal/configer/fixtures/ext-yml-invalid/*.yml

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,31 @@ passed:
137137
Errors are always sent to `stderr` as plain text, even if the `--json` flag is
138138
passed.
139139

140+
### Config files
141+
142+
The detector supports loading ignores from a YAML file, which can be useful for
143+
tracking ignored vulnerabilities per-project:
144+
145+
```yaml
146+
ignore:
147+
- GHSA-4 # "Prototype pollution in xyz"
148+
- GHSA-5 # "RegExp DDoS in abc"
149+
- GHSA-6 # "Command injection in hjk"
150+
```
151+
152+
By default, the detector will look for a `.osv-detector.yaml` or
153+
`.osv-detector.yml` in the same folder as the current lockfile it's checking,
154+
and will _merge_ the config with any flags being passed.
155+
156+
You can also provide a path to a specific config file that will be used for all
157+
lockfiles being checked with the `--config` flag:
158+
159+
```shell
160+
osv-detector --config ruby-ignores.yml path/to/my/first-ruby-project path/to/my/second-ruby-project
161+
```
162+
163+
You can disable loading any configs with the `--no-config` flag.
164+
140165
### Auxiliary output commands
141166

142167
The detector supports a few auxiliary commands that have it output information
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ignore
2+
- GHSA-4 # "Prototype pollution in xyz"
3+
- GHSA-5 # "RegExp DDoS in abc"
4+
- GHSA-6 # "Command injection in hjk"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ignore:
2+
- GHSA-4 # "Prototype pollution in xyz"
3+
- GHSA-5 # "RegExp DDoS in abc"
4+
- GHSA-6 # "Command injection in hjk"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ignore
2+
- GHSA-4 # "Prototype pollution in xyz"
3+
- GHSA-5 # "RegExp DDoS in abc"
4+
- GHSA-6 # "Command injection in hjk"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ignore:
2+
- GHSA-1 # "Prototype pollution in xyz"
3+
- GHSA-2 # "RegExp DDoS in abc"
4+
- GHSA-3 # "Command injection in hjk"

internal/configer/fixtures/no-config/.gitkeep

Whitespace-only changes.

internal/configer/load.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package configer
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"gopkg.in/yaml.v2"
7+
"os"
8+
)
9+
10+
type Config struct {
11+
FilePath string
12+
Ignore []string `yaml:"ignore"`
13+
}
14+
15+
// Find attempts to locate & load a Config using the default name (".osv-detector")
16+
func Find(pathToDirectory string) (Config, error) {
17+
var config Config
18+
var err error
19+
20+
configName := ".osv-detector"
21+
22+
config, err = Load(pathToDirectory + "/" + configName + ".yml")
23+
24+
if err == nil {
25+
return config, nil
26+
}
27+
28+
if !errors.Is(err, os.ErrNotExist) {
29+
return config, err
30+
}
31+
32+
config, err = Load(pathToDirectory + "/" + configName + ".yaml")
33+
34+
if err == nil {
35+
return config, nil
36+
}
37+
38+
if !errors.Is(err, os.ErrNotExist) {
39+
return config, err
40+
}
41+
42+
// if we couldn't find a config at all,
43+
// we want to return an empty Config
44+
// that doesn't have FilePath set
45+
return Config{}, nil
46+
}
47+
48+
func Load(pathToConfig string) (Config, error) {
49+
var config Config
50+
51+
config.FilePath = pathToConfig
52+
53+
configContents, err := os.ReadFile(pathToConfig)
54+
55+
if err != nil {
56+
return config, fmt.Errorf("could not read %s: %w", pathToConfig, err)
57+
}
58+
59+
err = yaml.Unmarshal(configContents, &config)
60+
61+
if err != nil {
62+
return config, fmt.Errorf("could not read %s: %w", pathToConfig, err)
63+
}
64+
65+
return config, nil
66+
}

internal/configer/load_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package configer_test
2+
3+
import (
4+
"osv-detector/internal/configer"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func TestFind_NoConfig(t *testing.T) {
10+
t.Parallel()
11+
12+
config, err := configer.Find("fixtures/no-config")
13+
14+
if err != nil {
15+
t.Errorf("Find() error = %v, expected nothing", err)
16+
}
17+
18+
if config.FilePath != "" {
19+
t.Errorf("Find() config.FilePath = %s, expected empty", config.FilePath)
20+
}
21+
22+
if l := len(config.Ignore); l > 0 {
23+
t.Errorf("Find() config.Ignore = %d, expected empty", l)
24+
}
25+
}
26+
27+
func TestFind_ExtYml(t *testing.T) {
28+
t.Parallel()
29+
30+
expectedIgnores := []string{"GHSA-1", "GHSA-2", "GHSA-3"}
31+
expectedFilePath := "fixtures/ext-yml/.osv-detector.yml"
32+
33+
config, err := configer.Find("fixtures/ext-yml")
34+
35+
if err != nil {
36+
t.Errorf("Find() error = %v, expected nothing", err)
37+
}
38+
39+
if config.FilePath != expectedFilePath {
40+
t.Errorf("Find() config.FilePath = %s, expected %s", config.FilePath, expectedFilePath)
41+
}
42+
43+
if !reflect.DeepEqual(config.Ignore, expectedIgnores) {
44+
t.Errorf("Find() config.Ignore = %v, expected empty", expectedIgnores)
45+
}
46+
}
47+
48+
func TestFind_ExtYml_Invalid(t *testing.T) {
49+
t.Parallel()
50+
51+
expectedFilePath := "fixtures/ext-yml-invalid/.osv-detector.yml"
52+
53+
config, err := configer.Find("fixtures/ext-yml-invalid")
54+
55+
if err == nil {
56+
t.Errorf("Find() did not error, which was unexpected")
57+
}
58+
59+
if config.FilePath != expectedFilePath {
60+
t.Errorf("Find() config.FilePath = %s, expected %s", config.FilePath, expectedFilePath)
61+
}
62+
63+
if l := len(config.Ignore); l > 0 {
64+
t.Errorf("Find() config.Ignore = %d, expected empty", l)
65+
}
66+
}
67+
68+
func TestFind_ExtYaml(t *testing.T) {
69+
t.Parallel()
70+
71+
expectedIgnores := []string{"GHSA-4", "GHSA-5", "GHSA-6"}
72+
expectedFilePath := "fixtures/ext-yaml/.osv-detector.yaml"
73+
74+
config, err := configer.Find("fixtures/ext-yaml")
75+
76+
if err != nil {
77+
t.Errorf("Find() error = %v, expected nothing", err)
78+
}
79+
80+
if config.FilePath != expectedFilePath {
81+
t.Errorf("Find() config.FilePath = %s, expected %s", config.FilePath, expectedFilePath)
82+
}
83+
84+
if !reflect.DeepEqual(config.Ignore, expectedIgnores) {
85+
t.Errorf("Find() config.Ignore = %v, expected empty", expectedIgnores)
86+
}
87+
}
88+
89+
func TestFind_ExtYaml_Invalid(t *testing.T) {
90+
t.Parallel()
91+
92+
expectedFilePath := "fixtures/ext-yaml-invalid/.osv-detector.yaml"
93+
94+
config, err := configer.Find("fixtures/ext-yaml-invalid")
95+
96+
if err == nil {
97+
t.Errorf("Find() did not error, which was unexpected")
98+
}
99+
100+
if config.FilePath != expectedFilePath {
101+
t.Errorf("Find() config.FilePath = %s, expected %s", config.FilePath, expectedFilePath)
102+
}
103+
104+
if l := len(config.Ignore); l > 0 {
105+
t.Errorf("Find() config.Ignore = %d, expected empty", l)
106+
}
107+
}

main.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/fatih/color"
88
"os"
99
"osv-detector/internal"
10+
"osv-detector/internal/configer"
1011
"osv-detector/internal/database"
1112
"osv-detector/internal/lockfile"
1213
"osv-detector/internal/reporter"
@@ -185,11 +186,27 @@ func (s *stringsFlag) Set(value string) error {
185186
return nil
186187
}
187188

189+
func allIgnores(global, local []string) []string {
190+
ignores := make(
191+
[]string,
192+
0,
193+
// len cannot return negative numbers, but the types can't reflect that
194+
uint64(len(global))+uint64(len(local)),
195+
)
196+
197+
ignores = append(ignores, global...)
198+
ignores = append(ignores, local...)
199+
200+
return ignores
201+
}
202+
188203
func run() int {
189204
var ignores stringsFlag
190205

191206
offline := flag.Bool("offline", false, "Perform checks using only the cached databases on disk")
192207
parseAs := flag.String("parse-as", "", "Name of a supported lockfile to parse the input files as")
208+
configPath := flag.String("config", "", "Path to a config file to use for all lockfiles")
209+
noConfig := flag.Bool("no-config", false, "Disable loading of any config files")
193210
printVersion := flag.Bool("version", false, "Print version information")
194211
listEcosystems := flag.Bool("list-ecosystems", false, "List all of the known ecosystems that are supported by the detector")
195212
listPackages := flag.Bool("list-packages", false, "List the packages that are parsed from the input files")
@@ -254,11 +271,41 @@ This flag can be passed multiple times to ignore different vulnerabilities`)
254271

255272
exitCode := 0
256273

274+
var config configer.Config
275+
276+
if !*noConfig && *configPath != "" {
277+
con, err := configer.Load(*configPath)
278+
279+
if err != nil {
280+
r.PrintError(fmt.Sprintf("Error, %s\n", err))
281+
282+
return 127
283+
}
284+
285+
config = con
286+
}
287+
257288
for i, pathToLock := range pathsToLocks {
289+
config := config
290+
258291
if i >= 1 {
259292
r.PrintText("\n")
260293
}
261294

295+
if !*noConfig && *configPath == "" {
296+
base := path.Dir(pathToLock)
297+
con, err := configer.Find(base)
298+
299+
if err != nil {
300+
r.PrintError(fmt.Sprintf("Error, %s\n", err))
301+
exitCode = 127
302+
303+
continue
304+
}
305+
306+
config = con
307+
}
308+
262309
lockf, err := lockfile.Parse(pathToLock, *parseAs)
263310

264311
if err != nil {
@@ -281,6 +328,18 @@ This flag can be passed multiple times to ignore different vulnerabilities`)
281328
continue
282329
}
283330

331+
// an empty FilePath means we didn't load a config
332+
if config.FilePath != "" {
333+
r.PrintText(fmt.Sprintf(
334+
" Using config at %s (%s)\n",
335+
color.MagentaString(config.FilePath),
336+
color.YellowString("%d %s",
337+
len(config.Ignore),
338+
reporter.Form(len(config.Ignore), "ignore", "ignores"),
339+
),
340+
))
341+
}
342+
284343
dbs, err := loadEcosystemDatabases(r, lockf.Packages.Ecosystems(), *offline)
285344

286345
if err != nil {
@@ -290,7 +349,7 @@ This flag can be passed multiple times to ignore different vulnerabilities`)
290349
continue
291350
}
292351

293-
report := dbs.check(lockf, ignores)
352+
report := dbs.check(lockf, allIgnores(config.Ignore, ignores))
294353

295354
r.PrintResult(report)
296355

0 commit comments

Comments
 (0)