Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gradle lockfile support #46

Merged
merged 1 commit into from
Dec 15, 2022
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ A wide range of lockfiles are supported by utilizing this [lockfile package](htt
- `pubspec.lock`
- `pom.xml`[\*](https://github.com/google/osv-scanner/issues/35)
- `requirements.txt`[\*](https://github.com/google/osv-scanner/issues/34)
- `gradle.lockfile`
- `buildscript-gradle.lockfile`

#### Example

Expand Down
9 changes: 5 additions & 4 deletions pkg/lockfile/ecosystems_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package lockfile_test

import (
"github.com/google/osv-scanner/pkg/lockfile"
"os"
"strings"
"testing"

"github.com/google/osv-scanner/pkg/lockfile"
)

func numberOfLockfileParsers(t *testing.T) int {
Expand Down Expand Up @@ -33,9 +34,9 @@ func TestKnownEcosystems(t *testing.T) {

expectedCount := numberOfLockfileParsers(t)

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

ecosystems := lockfile.KnownEcosystems()

Expand Down
10 changes: 10 additions & 0 deletions pkg/lockfile/fixtures/gradle/5-pkg
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
##
## Comments
##

org.springframework.boot:spring-boot-autoconfigure:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath
org.springframework.boot:spring-boot-configuration-processor:2.7.5=annotationProcessor,compileClasspath
org.springframework.boot:spring-boot-devtools:2.7.6=developmentOnly,runtimeClasspath
org.springframework.boot:spring-boot-starter-aop:2.7.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath
org.springframework.boot:spring-boot-starter-data-jpa:2.7.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath
empty=
5 changes: 5 additions & 0 deletions pkg/lockfile/fixtures/gradle/one-pkg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Comment example
#

org.springframework.security:spring-security-crypto:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath
empty=
8 changes: 8 additions & 0 deletions pkg/lockfile/fixtures/gradle/only-comments
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This
# is
# a
# comment




1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/gradle/only-empty
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
empty=
19 changes: 19 additions & 0 deletions pkg/lockfile/fixtures/gradle/with-bad-pkg
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This sample gradle lockfile has bad lines
#
>>>
////
empty=======

org.springframework.boot:spring-boot-autoconfigure:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath

a
b



org.springframework.boot:spring-boot-configuration-processor:2.7.5=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath





70 changes: 70 additions & 0 deletions pkg/lockfile/parse-gradle-lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package lockfile

import (
"bufio"
"fmt"
"os"
"strings"
)

const (
gradleLockFileCommentPrefix = "#"
gradleLockFileEmptyPrefix = "empty="
)

func isGradleLockFileDepLine(line string) bool {
ret := strings.HasPrefix(line, gradleLockFileCommentPrefix) ||
strings.HasPrefix(line, gradleLockFileEmptyPrefix)

return !ret
}

func parseToGradlePackageDetail(line string) (PackageDetails, error) {
parts := strings.SplitN(line, ":", 3)
if len(parts) < 3 {
return PackageDetails{}, fmt.Errorf("invalid line in gradle lockfile: %s", line)
}

group, artifact, version := parts[0], parts[1], parts[2]
version = strings.SplitN(version, "=", 2)[0]

return PackageDetails{
Name: fmt.Sprintf("%s:%s", group, artifact),
Version: version,
Ecosystem: MavenEcosystem,
CompareAs: MavenEcosystem,
}, nil
}

func ParseGradleLock(pathToLockfile string) ([]PackageDetails, error) {
lockFile, err := os.Open(pathToLockfile)
if err != nil {
return []PackageDetails{}, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
}

defer lockFile.Close()

pkgs := make([]PackageDetails, 0, 0)
scanner := bufio.NewScanner(lockFile)

for scanner.Scan() {
lockLine := strings.TrimSpace(scanner.Text())
if !isGradleLockFileDepLine(lockLine) {
continue
}

pkg, err := parseToGradlePackageDetail(lockLine)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse lockline: %s\n", err.Error())
continue
}

pkgs = append(pkgs, pkg)
}

if err := scanner.Err(); err != nil {
return []PackageDetails{}, fmt.Errorf("failed to read %s: %w", pathToLockfile, err)
}

return pkgs, nil
}
128 changes: 128 additions & 0 deletions pkg/lockfile/parse-gradle-lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package lockfile_test

import (
"testing"

"github.com/google/osv-scanner/pkg/lockfile"
)

func TestParseGradleLock_FileDoesNotExist(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/does-not-exist")

expectErrContaining(t, err, "could not open")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseGradleLock_OnlyComments(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/only-comments")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseGradleLock_EmptyStatement(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/only-empty")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseGradleLock_OnePackage(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/one-pkg")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "org.springframework.security:spring-security-crypto",
Version: "5.7.3",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
})
}

func TestParseGradleLock_MultiplePackage(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/5-pkg")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "org.springframework.boot:spring-boot-autoconfigure",
Version: "2.7.4",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
{
Name: "org.springframework.boot:spring-boot-configuration-processor",
Version: "2.7.5",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
{
Name: "org.springframework.boot:spring-boot-devtools",
Version: "2.7.6",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},

{
Name: "org.springframework.boot:spring-boot-starter-aop",
Version: "2.7.7",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
{
Name: "org.springframework.boot:spring-boot-starter-data-jpa",
Version: "2.7.8",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
})
}

func TestParseGradleLock_WithInvalidLines(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseGradleLock("fixtures/gradle/with-bad-pkg")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "org.springframework.boot:spring-boot-autoconfigure",
Version: "2.7.4",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
{
Name: "org.springframework.boot:spring-boot-configuration-processor",
Version: "2.7.5",
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
},
})
}
26 changes: 14 additions & 12 deletions pkg/lockfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ func FindParser(pathToLockfile string, parseAs string) (PackageDetailsParser, st

//nolint:gochecknoglobals // this is an optimisation and read-only
var parsers = map[string]PackageDetailsParser{
"Cargo.lock": ParseCargoLock,
"composer.lock": ParseComposerLock,
"Gemfile.lock": ParseGemfileLock,
"go.mod": ParseGoLock,
"mix.lock": ParseMixLock,
"package-lock.json": ParseNpmLock,
"pnpm-lock.yaml": ParsePnpmLock,
"poetry.lock": ParsePoetryLock,
"pom.xml": ParseMavenLock,
"pubspec.lock": ParsePubspecLock,
"requirements.txt": ParseRequirementsTxt,
"yarn.lock": ParseYarnLock,
"Cargo.lock": ParseCargoLock,
"composer.lock": ParseComposerLock,
"Gemfile.lock": ParseGemfileLock,
"go.mod": ParseGoLock,
"mix.lock": ParseMixLock,
"package-lock.json": ParseNpmLock,
"pnpm-lock.yaml": ParsePnpmLock,
"poetry.lock": ParsePoetryLock,
"pom.xml": ParseMavenLock,
"pubspec.lock": ParsePubspecLock,
"requirements.txt": ParseRequirementsTxt,
"yarn.lock": ParseYarnLock,
"gradle.lockfile": ParseGradleLock,
"buildscript-gradle.lockfile": ParseGradleLock,
}

func ListParsers() []string {
Expand Down
19 changes: 14 additions & 5 deletions pkg/lockfile/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package lockfile_test

import (
"errors"
"github.com/google/osv-scanner/pkg/lockfile"
"os"
"reflect"
"strings"
"testing"

"github.com/google/osv-scanner/pkg/lockfile"
)

func expectNumberOfParsersCalled(t *testing.T, numberOfParsersCalled int) {
Expand Down Expand Up @@ -95,6 +96,8 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
"poetry.lock",
"pubspec.lock",
"requirements.txt",
"gradle.lockfile",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildscript-gradle.lockfile should be included on this list (effectively this should have all lockfiles that are listed in the readme as being supported)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to include buildscript-gradle.lockfile. But had to decrement the asserted count because both gradle.lockfile and buildscript-gradle.lockfile use the same parser.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet as - that test is more meant to help catch people forgetting to update this section of tests if a new parser is added, which is always a bit fuzzy (arguably I should have used a map against the function name and then check that each was called at least once, which I think could be possible, but not worth the effort imo)

"buildscript-gradle.lockfile",
}

count := 0
Expand All @@ -109,6 +112,9 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
count++
}

// gradle.lockfile and buildscript-gradle.lockfile use the same parser
count = count - 1

expectNumberOfParsersCalled(t, count)
}

Expand All @@ -131,12 +137,15 @@ func TestListParsers(t *testing.T) {

parsers := lockfile.ListParsers()

if first := parsers[0]; first != "Cargo.lock" {
t.Errorf("Expected first element to be Cargo.lock, but got %s", first)
firstExpected := "buildscript-gradle.lockfile"
lastExpected := "yarn.lock"

if first := parsers[0]; first != firstExpected {
t.Errorf("Expected first element to be %s, but got %s", firstExpected, first)
}

if last := parsers[len(parsers)-1]; last != "yarn.lock" {
t.Errorf("Expected last element to be requirements.txt, but got %s", last)
if last := parsers[len(parsers)-1]; last != lastExpected {
t.Errorf("Expected last element to be %s, but got %s", lastExpected, last)
}
}

Expand Down