Skip to content

Commit 113aae9

Browse files
G-Rathjulieqiu
authored andcommitted
feat: support specifying what parser to use in --lockfile (google#94)
Resolves google#67 Resolves google#124
1 parent e47cbb0 commit 113aae9

File tree

11 files changed

+262
-8
lines changed

11 files changed

+262
-8
lines changed

README.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ If you want to check for known vulnerabilities in specific lockfiles, you can us
119119
osv-scanner --lockfile=/path/to/your/package-lock.json --lockfile=/path/to/another/Cargo.lock
120120
```
121121

122-
It is possible to specify more than one lockfile at a time.
122+
It is possible to specify more than one lockfile at a time; you can also specify how to parse an arbitrary file:
123+
124+
```console
125+
osv-scanner --lockfile 'requirements.txt:/path/to/your/extra-requirements.txt'
126+
```
123127

124128
A wide range of lockfiles are supported by utilizing this [lockfile package](https://github.com/google/osv-scanner/tree/main/pkg/lockfile). This is the current list of supported lockfiles:
125129

@@ -140,7 +144,21 @@ A wide range of lockfiles are supported by utilizing this [lockfile package](htt
140144
- `pubspec.lock`
141145
- `requirements.txt`[\*](https://github.com/google/osv-scanner/issues/34)
142146
- `yarn.lock`
143-
- `/lib/apk/db/installed` (Alpine)
147+
148+
The scanner also supports `installed` files used by the Alpine Package Keeper (apk) that typically live at `/lib/apk/db/installed`,
149+
however you must specify this explicitly using the `--lockfile` flag:
150+
151+
```console
152+
osv-scanner --lockfile 'apk-installed:/lib/apk/db/installed'
153+
```
154+
155+
If the file you are scanning is located in a directory that has a colon in its name,
156+
you can prefix the path to just a colon to explicitly signal to the scanner that
157+
it should infer the parser based on the filename:
158+
159+
```bash
160+
$ osv-scanner --lockfile ':/path/to/my:projects/package-lock.json'
161+
```
144162

145163
### Scanning a Debian based docker image packages (preview)
146164

cmd/osv-scanner/fixtures/locks-empty/Gemfile.lock

Whitespace-only changes.

cmd/osv-scanner/fixtures/locks-empty/composer.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+

cmd/osv-scanner/fixtures/locks-insecure/composer.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/osv-scanner/fixtures/locks-insecure/my-package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
C:Q1Ef3iwt+cMdGngEgaFr2URIJhKzQ=
2+
P:apk-tools
3+
V:2.12.10-r1
4+
A:x86_64
5+
S:120973
6+
I:307200
7+
T:Alpine Package Keeper - package manager for alpine
8+
U:https://gitlab.alpinelinux.org/alpine/apk-tools
9+
L:GPL-2.0-only
10+
o:apk-tools
11+
m:Natanael Copa <[email protected]>
12+
t:1666552494
13+
c:0188f510baadbae393472103427b9c1875117136
14+
D:musl>=1.2 ca-certificates-bundle so:libc.musl-x86_64.so.1 so:libcrypto.so.3 so:libssl.so.3 so:libz.so.1
15+
p:so:libapk.so.3.12.0=3.12.0 cmd:apk=2.12.10-r1
16+
F:etc
17+
F:etc/apk
18+
F:etc/apk/keys
19+
F:etc/apk/protected_paths.d
20+
F:lib
21+
R:libapk.so.3.12.0
22+
a:0:0:755
23+
Z:Q1opjpYqXgzmOVo7EbNe8l5Xol08g=
24+
F:lib/apk
25+
F:lib/apk/exec
26+
F:sbin
27+
R:apk
28+
a:0:0:755
29+
Z:Q1/4bmOPe/H1YhHRzlrj27oufThMw=
30+
F:var
31+
F:var/lib
32+
F:var/lib/apk

cmd/osv-scanner/main_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package main
44
import (
55
"bytes"
66
"fmt"
7+
"path/filepath"
78
"regexp"
89
"strings"
910
"testing"
@@ -243,3 +244,135 @@ func TestRun(t *testing.T) {
243244
})
244245
}
245246
}
247+
248+
func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
249+
t.Parallel()
250+
251+
tests := []cliTestCase{
252+
// unsupported parse-as
253+
{
254+
name: "",
255+
args: []string{"", "-L", "my-file:my-file"},
256+
wantExitCode: 127,
257+
wantStdout: "",
258+
wantStderr: `
259+
could not determine parser, requested my-file
260+
`,
261+
},
262+
// empty is default
263+
{
264+
name: "",
265+
args: []string{
266+
"",
267+
"-L",
268+
":" + filepath.FromSlash("./fixtures/locks-many/composer.lock"),
269+
},
270+
wantExitCode: 0,
271+
wantStdout: `
272+
Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
273+
`,
274+
wantStderr: "",
275+
},
276+
// empty works as an escape (no fixture because it's not valid on Windows)
277+
{
278+
name: "",
279+
args: []string{
280+
"",
281+
"-L",
282+
":" + filepath.FromSlash("./path/to/my:file"),
283+
},
284+
wantExitCode: 127,
285+
wantStdout: "",
286+
wantStderr: `
287+
could not determine parser for %%/path/to/my:file
288+
`,
289+
},
290+
{
291+
name: "",
292+
args: []string{
293+
"",
294+
"-L",
295+
":" + filepath.FromSlash("./path/to/my:project/package-lock.json"),
296+
},
297+
wantExitCode: 127,
298+
wantStdout: "",
299+
wantStderr: `
300+
could not read %%/path/to/my:project/package-lock.json: open %%/path/to/my:project/package-lock.json: no such file or directory
301+
`,
302+
},
303+
// when an explicit parse-as is given, it's applied to that file
304+
{
305+
name: "",
306+
args: []string{
307+
"",
308+
"-L",
309+
"package-lock.json:" + filepath.FromSlash("./fixtures/locks-insecure/my-package-lock.json"),
310+
filepath.FromSlash("./fixtures/locks-insecure"),
311+
},
312+
wantExitCode: 1,
313+
wantStdout: `
314+
Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 packages
315+
Scanning dir ./fixtures/locks-insecure
316+
Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
317+
+-------------------------------------+-----------+-----------+---------+----------------------------------------------+
318+
| OSV URL (ID IN BOLD) | ECOSYSTEM | PACKAGE | VERSION | SOURCE |
319+
+-------------------------------------+-----------+-----------+---------+----------------------------------------------+
320+
| https://osv.dev/GHSA-whgm-jr23-g3j9 | npm | ansi-html | 0.0.1 | fixtures/locks-insecure/my-package-lock.json |
321+
+-------------------------------------+-----------+-----------+---------+----------------------------------------------+
322+
`,
323+
wantStderr: "",
324+
},
325+
// files that error on parsing stop parsable files from being checked
326+
{
327+
name: "",
328+
args: []string{
329+
"",
330+
"-L",
331+
"Cargo.lock:" + filepath.FromSlash("./fixtures/locks-insecure/my-package-lock.json"),
332+
filepath.FromSlash("./fixtures/locks-insecure"),
333+
filepath.FromSlash("./fixtures/locks-many"),
334+
},
335+
wantExitCode: 127,
336+
wantStdout: "",
337+
wantStderr: `
338+
(parsing as Cargo.lock) could not parse %%/fixtures/locks-insecure/my-package-lock.json: toml: line 1: expected '.' or '=', but got '{' instead
339+
`,
340+
},
341+
// parse-as takes priority, even if it's wrong
342+
{
343+
name: "",
344+
args: []string{
345+
"",
346+
"-L",
347+
"package-lock.json:" + filepath.FromSlash("./fixtures/locks-many/yarn.lock"),
348+
},
349+
wantExitCode: 127,
350+
wantStdout: "",
351+
wantStderr: `
352+
(parsing as package-lock.json) could not parse %%/fixtures/locks-many/yarn.lock: invalid character '#' looking for beginning of value
353+
`,
354+
},
355+
// "apk-installed" is supported
356+
{
357+
name: "",
358+
args: []string{
359+
"",
360+
"-L",
361+
"apk-installed:" + filepath.FromSlash("./fixtures/locks-many/installed"),
362+
},
363+
wantExitCode: 0,
364+
wantStdout: `
365+
Scanned %%/fixtures/locks-many/installed file as a apk-installed and found 1 packages
366+
`,
367+
wantStderr: "",
368+
},
369+
}
370+
for _, tt := range tests {
371+
tt := tt
372+
t.Run(tt.name, func(t *testing.T) {
373+
t.Parallel()
374+
375+
testCli(t, tt)
376+
})
377+
}
378+
}

pkg/lockfile/apk-installed.go

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"fmt"
66
"os"
7+
"sort"
78
"strings"
89
)
910

@@ -104,3 +105,23 @@ func ParseApkInstalled(pathToLockfile string) ([]PackageDetails, error) {
104105

105106
return packages, nil
106107
}
108+
109+
// FromApkInstalled attempts to parse the given file as an "apk-installed" lockfile
110+
// used by the Alpine Package Keeper (apk) to record installed packages.
111+
func FromApkInstalled(pathToInstalled string) (Lockfile, error) {
112+
packages, err := ParseApkInstalled(pathToInstalled)
113+
114+
sort.Slice(packages, func(i, j int) bool {
115+
if packages[i].Name == packages[j].Name {
116+
return packages[i].Version < packages[j].Version
117+
}
118+
119+
return packages[i].Name < packages[j].Name
120+
})
121+
122+
return Lockfile{
123+
FilePath: pathToInstalled,
124+
ParsedAs: "apk-installed",
125+
Packages: packages,
126+
}, err
127+
}

pkg/lockfile/parse.go

+8
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ func Parse(pathToLockfile string, parseAs string) (Lockfile, error) {
126126
parser, parsedAs := FindParser(pathToLockfile, parseAs)
127127

128128
if parser == nil {
129+
if parseAs != "" {
130+
return Lockfile{}, fmt.Errorf("%w, requested %s", ErrParserNotFound, parseAs)
131+
}
132+
129133
return Lockfile{}, fmt.Errorf("%w for %s", ErrParserNotFound, pathToLockfile)
130134
}
131135

132136
packages, err := parser(pathToLockfile)
133137

138+
if err != nil && parseAs != "" {
139+
err = fmt.Errorf("(parsing as %s) %w", parsedAs, err)
140+
}
141+
134142
sort.Slice(packages, func(i, j int) bool {
135143
if packages[i].Name == packages[j].Name {
136144
return packages[i].Version < packages[j].Version

pkg/osvscanner/osvscanner.go

+34-6
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func scanDir(r *output.Reporter, query *osv.BatchedQuery, dir string, skipGit bo
6767

6868
if !info.IsDir() {
6969
if parser, _ := lockfile.FindParser(path, ""); parser != nil {
70-
err := scanLockfile(r, query, path)
70+
err := scanLockfile(r, query, path, "")
7171
if err != nil {
7272
r.PrintError(fmt.Sprintf("Attempted to scan lockfile but failed: %s\n", path))
7373
}
@@ -89,12 +89,29 @@ func scanDir(r *output.Reporter, query *osv.BatchedQuery, dir string, skipGit bo
8989

9090
// scanLockfile will load, identify, and parse the lockfile path passed in, and add the dependencies specified
9191
// within to `query`
92-
func scanLockfile(r *output.Reporter, query *osv.BatchedQuery, path string) error {
93-
parsedLockfile, err := lockfile.Parse(path, "")
92+
func scanLockfile(r *output.Reporter, query *osv.BatchedQuery, path string, parseAs string) error {
93+
var err error
94+
var parsedLockfile lockfile.Lockfile
95+
96+
// special case for the APK parser because it has a very generic name while
97+
// living at a specific location, so it's not included in the map of parsers
98+
// used by lockfile.Parse to avoid false-positives when scanning projects
99+
if parseAs == "apk-installed" {
100+
parsedLockfile, err = lockfile.FromApkInstalled(path)
101+
} else {
102+
parsedLockfile, err = lockfile.Parse(path, parseAs)
103+
}
104+
94105
if err != nil {
95106
return err
96107
}
97-
r.PrintText(fmt.Sprintf("Scanned %s file and found %d packages\n", path, len(parsedLockfile.Packages)))
108+
parsedAsComment := ""
109+
110+
if parseAs != "" {
111+
parsedAsComment = fmt.Sprintf("as a %s ", parseAs)
112+
}
113+
114+
r.PrintText(fmt.Sprintf("Scanned %s file %sand found %d packages\n", path, parsedAsComment, len(parsedLockfile.Packages)))
98115

99116
for _, pkgDetail := range parsedLockfile.Packages {
100117
pkgDetailQuery := osv.MakePkgRequest(pkgDetail)
@@ -266,6 +283,16 @@ func filterResponse(r *output.Reporter, query osv.BatchedQuery, resp *osv.Batche
266283
return len(hiddenVulns)
267284
}
268285

286+
func parseLockfilePath(lockfileElem string) (string, string) {
287+
if !strings.Contains(lockfileElem, ":") {
288+
lockfileElem = ":" + lockfileElem
289+
}
290+
291+
splits := strings.SplitN(lockfileElem, ":", 2)
292+
293+
return splits[0], splits[1]
294+
}
295+
269296
// Perform osv scanner action, with optional reporter to output information
270297
func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityResults, error) {
271298
if r == nil {
@@ -294,12 +321,13 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
294321
}
295322

296323
for _, lockfileElem := range actions.LockfilePaths {
297-
lockfileElem, err := filepath.Abs(lockfileElem)
324+
parseAs, lockfilePath := parseLockfilePath(lockfileElem)
325+
lockfilePath, err := filepath.Abs(lockfilePath)
298326
if err != nil {
299327
r.PrintError(fmt.Sprintf("Failed to resolved path with error %s\n", err))
300328
return models.VulnerabilityResults{}, err
301329
}
302-
err = scanLockfile(r, &query, lockfileElem)
330+
err = scanLockfile(r, &query, lockfilePath, parseAs)
303331
if err != nil {
304332
return models.VulnerabilityResults{}, err
305333
}

0 commit comments

Comments
 (0)