Skip to content

Commit 3035542

Browse files
authored
feat: support parsing Gradle lockfiles (#164)
1 parent adfea86 commit 3035542

File tree

12 files changed

+284
-35
lines changed

12 files changed

+284
-35
lines changed

README.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,22 @@ osv-detector path/to/my/
5050

5151
The detector supports parsing the following lockfiles:
5252

53-
| Lockfile | Ecosystem | Tool |
54-
| -------------------- | ----------- | ---------- |
55-
| `Cargo.lock` | `crates.io` | `cargo` |
56-
| `package-lock.json` | `npm` | `npm` |
57-
| `yarn.lock` | `npm` | `yarn` |
58-
| `pnpm-lock.yaml` | `npm` | `pnpm` |
59-
| `composer.lock` | `Packagist` | `composer` |
60-
| `Gemfile.lock` | `RubyGems` | `bundler` |
61-
| `go.mod` | `Go` | `go mod` |
62-
| `mix.lock` | `Hex` | `mix` |
63-
| `poetry.lock` | `PyPI` | `poetry` |
64-
| `pubspec.lock` | `Pub` | `pub` |
65-
| `pom.xml`\* | `Maven` | `maven` |
66-
| `requirements.txt`\* | `PyPI` | `pip` |
53+
| Lockfile | Ecosystem | Tool |
54+
| ----------------------------- | ----------- | ---------- |
55+
| `buildscript-gradle.lockfile` | `Maven` | `gradle` |
56+
| `Cargo.lock` | `crates.io` | `cargo` |
57+
| `package-lock.json` | `npm` | `npm` |
58+
| `yarn.lock` | `npm` | `yarn` |
59+
| `pnpm-lock.yaml` | `npm` | `pnpm` |
60+
| `composer.lock` | `Packagist` | `composer` |
61+
| `Gemfile.lock` | `RubyGems` | `bundler` |
62+
| `go.mod` | `Go` | `go mod` |
63+
| `gradle.lockfile` | `Maven` | `gradle` |
64+
| `mix.lock` | `Hex` | `mix` |
65+
| `poetry.lock` | `PyPI` | `poetry` |
66+
| `pubspec.lock` | `Pub` | `pub` |
67+
| `pom.xml`\* | `Maven` | `maven` |
68+
| `requirements.txt`\* | `PyPI` | `pip` |
6769

6870
\*: `pom.xml` and `requirements.txt` are technically not lockfiles, as they
6971
don't have to specify the complete dependency tree and can have version

main_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,12 @@ func TestRun(t *testing.T) {
131131
wantStdout: "",
132132
wantStderr: `
133133
Don't know how to parse files as "my-file" - supported values are:
134+
buildscript-gradle.lockfile
134135
Cargo.lock
135136
composer.lock
136137
Gemfile.lock
137138
go.mod
139+
gradle.lockfile
138140
mix.lock
139141
package-lock.json
140142
pnpm-lock.yaml

pkg/lockfile/ecosystems_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ func TestKnownEcosystems(t *testing.T) {
3333

3434
expectedCount := numberOfLockfileParsers(t)
3535

36-
// npm, yarn, and pnpm, and pip and poetry, all use the same ecosystem
37-
// so "ignore" those parsers in the count
38-
expectedCount -= 3
36+
// npm, yarn, and pnpm, and pip and poetry, and maven and gradle, all
37+
// use the same ecosystem so "ignore" those parsers in the count
38+
expectedCount -= 4
3939

4040
ecosystems := lockfile.KnownEcosystems()
4141

pkg/lockfile/fixtures/gradle/5-pkg

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
##
2+
## Comments
3+
##
4+
5+
org.springframework.boot:spring-boot-autoconfigure:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath
6+
org.springframework.boot:spring-boot-configuration-processor:2.7.5=annotationProcessor,compileClasspath
7+
org.springframework.boot:spring-boot-devtools:2.7.6=developmentOnly,runtimeClasspath
8+
org.springframework.boot:spring-boot-starter-aop:2.7.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath
9+
org.springframework.boot:spring-boot-starter-data-jpa:2.7.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath
10+
empty=
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Comment example
2+
#
3+
4+
org.springframework.security:spring-security-crypto:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath
5+
empty=
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This
2+
# is
3+
# a
4+
# comment
5+
6+
7+
8+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
empty=
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This sample gradle lockfile has bad lines
2+
#
3+
>>>
4+
////
5+
empty=======
6+
7+
org.springframework.boot:spring-boot-autoconfigure:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath
8+
9+
a
10+
b
11+
12+
13+
14+
org.springframework.boot:spring-boot-configuration-processor:2.7.5=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath
15+
16+
17+
18+
19+

pkg/lockfile/parse-gradle-lock.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package lockfile
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
)
9+
10+
func isNotGradleDependencyLine(line string) bool {
11+
return strings.HasPrefix(line, "#") || strings.HasPrefix(line, "empty=")
12+
}
13+
14+
func parseGradleLine(line string) (PackageDetails, error) {
15+
parts := strings.SplitN(line, ":", 3)
16+
if len(parts) < 3 {
17+
return PackageDetails{}, fmt.Errorf("invalid line in gradle lockfile: %s", line) //nolint:goerr113
18+
}
19+
20+
group, artifact, version := parts[0], parts[1], parts[2]
21+
version = strings.SplitN(version, "=", 2)[0]
22+
23+
return PackageDetails{
24+
Name: fmt.Sprintf("%s:%s", group, artifact),
25+
Version: version,
26+
Ecosystem: MavenEcosystem,
27+
CompareAs: MavenEcosystem,
28+
}, nil
29+
}
30+
31+
func ParseGradleLock(pathToLockfile string) ([]PackageDetails, error) {
32+
var packages []PackageDetails
33+
34+
lockFile, err := os.Open(pathToLockfile)
35+
if err != nil {
36+
return []PackageDetails{}, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
37+
}
38+
defer lockFile.Close()
39+
40+
scanner := bufio.NewScanner(lockFile)
41+
42+
for scanner.Scan() {
43+
lockLine := strings.TrimSpace(scanner.Text())
44+
45+
if isNotGradleDependencyLine(lockLine) {
46+
continue
47+
}
48+
49+
pkg, err := parseGradleLine(lockLine)
50+
if err != nil {
51+
fmt.Fprintf(os.Stderr, "skipping %s\n", err.Error())
52+
53+
continue
54+
}
55+
56+
packages = append(packages, pkg)
57+
}
58+
59+
if err := scanner.Err(); err != nil {
60+
return []PackageDetails{}, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
61+
}
62+
63+
return packages, nil
64+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package lockfile_test
2+
3+
import (
4+
"github.com/g-rath/osv-detector/pkg/lockfile"
5+
"testing"
6+
)
7+
8+
func TestParseGradleLock_FileDoesNotExist(t *testing.T) {
9+
t.Parallel()
10+
11+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/does-not-exist")
12+
13+
expectErrContaining(t, err, "could not open")
14+
expectPackages(t, packages, []lockfile.PackageDetails{})
15+
}
16+
17+
func TestParseGradleLock_OnlyComments(t *testing.T) {
18+
t.Parallel()
19+
20+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/only-comments")
21+
22+
if err != nil {
23+
t.Errorf("Got unexpected error: %v", err)
24+
}
25+
26+
expectPackages(t, packages, []lockfile.PackageDetails{})
27+
}
28+
29+
func TestParseGradleLock_EmptyStatement(t *testing.T) {
30+
t.Parallel()
31+
32+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/only-empty")
33+
34+
if err != nil {
35+
t.Errorf("Got unexpected error: %v", err)
36+
}
37+
38+
expectPackages(t, packages, []lockfile.PackageDetails{})
39+
}
40+
41+
func TestParseGradleLock_OnePackage(t *testing.T) {
42+
t.Parallel()
43+
44+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/one-pkg")
45+
46+
if err != nil {
47+
t.Errorf("Got unexpected error: %v", err)
48+
}
49+
50+
expectPackages(t, packages, []lockfile.PackageDetails{
51+
{
52+
Name: "org.springframework.security:spring-security-crypto",
53+
Version: "5.7.3",
54+
Ecosystem: lockfile.MavenEcosystem,
55+
CompareAs: lockfile.MavenEcosystem,
56+
},
57+
})
58+
}
59+
60+
func TestParseGradleLock_MultiplePackage(t *testing.T) {
61+
t.Parallel()
62+
63+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/5-pkg")
64+
65+
if err != nil {
66+
t.Errorf("Got unexpected error: %v", err)
67+
}
68+
69+
expectPackages(t, packages, []lockfile.PackageDetails{
70+
{
71+
Name: "org.springframework.boot:spring-boot-autoconfigure",
72+
Version: "2.7.4",
73+
Ecosystem: lockfile.MavenEcosystem,
74+
CompareAs: lockfile.MavenEcosystem,
75+
},
76+
{
77+
Name: "org.springframework.boot:spring-boot-configuration-processor",
78+
Version: "2.7.5",
79+
Ecosystem: lockfile.MavenEcosystem,
80+
CompareAs: lockfile.MavenEcosystem,
81+
},
82+
{
83+
Name: "org.springframework.boot:spring-boot-devtools",
84+
Version: "2.7.6",
85+
Ecosystem: lockfile.MavenEcosystem,
86+
CompareAs: lockfile.MavenEcosystem,
87+
},
88+
89+
{
90+
Name: "org.springframework.boot:spring-boot-starter-aop",
91+
Version: "2.7.7",
92+
Ecosystem: lockfile.MavenEcosystem,
93+
CompareAs: lockfile.MavenEcosystem,
94+
},
95+
{
96+
Name: "org.springframework.boot:spring-boot-starter-data-jpa",
97+
Version: "2.7.8",
98+
Ecosystem: lockfile.MavenEcosystem,
99+
CompareAs: lockfile.MavenEcosystem,
100+
},
101+
})
102+
}
103+
104+
func TestParseGradleLock_WithInvalidLines(t *testing.T) {
105+
t.Parallel()
106+
107+
packages, err := lockfile.ParseGradleLock("fixtures/gradle/with-bad-pkg")
108+
109+
if err != nil {
110+
t.Errorf("Got unexpected error: %v", err)
111+
}
112+
113+
expectPackages(t, packages, []lockfile.PackageDetails{
114+
{
115+
Name: "org.springframework.boot:spring-boot-autoconfigure",
116+
Version: "2.7.4",
117+
Ecosystem: lockfile.MavenEcosystem,
118+
CompareAs: lockfile.MavenEcosystem,
119+
},
120+
{
121+
Name: "org.springframework.boot:spring-boot-configuration-processor",
122+
Version: "2.7.5",
123+
Ecosystem: lockfile.MavenEcosystem,
124+
CompareAs: lockfile.MavenEcosystem,
125+
},
126+
})
127+
}

0 commit comments

Comments
 (0)