diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 9bab7043e3b..2d7cdf2353c 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -73,6 +73,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), java.NewNativeImageCataloger(), + java.NewJavaGradleLockfileCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(cfg.Go()), golang.NewGoModFileCataloger(cfg.Go()), @@ -107,6 +108,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), java.NewNativeImageCataloger(), + java.NewJavaGradleLockfileCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(cfg.Go()), golang.NewGoModFileCataloger(cfg.Go()), diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index cd2affb50ce..101c4e9476f 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -159,10 +159,39 @@ func TestParseJar(t *testing.T) { Manifest: &pkg.JavaManifest{ Main: map[string]string{ "Manifest-Version": "1.0", + "Main-Class": "hello.HelloWorld", }, }, }, }, + "joda-time": { + Name: "joda-time", + Version: "2.2", + PURL: "pkg:maven/joda-time/joda-time@2.2", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + Metadata: pkg.JavaMetadata{ + // ensure that nested packages with different names than that of the parent are appended as + // a suffix on the virtual path + VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar:joda-time", + PomProperties: &pkg.PomProperties{ + Path: "META-INF/maven/joda-time/joda-time/pom.properties", + GroupID: "joda-time", + ArtifactID: "joda-time", + Version: "2.2", + }, + PomProject: &pkg.PomProject{ + Path: "META-INF/maven/joda-time/joda-time/pom.xml", + GroupID: "joda-time", + ArtifactID: "joda-time", + Version: "2.2", + Name: "Joda time", + Description: "Date and time library to replace JDK date handling", + URL: "http://joda-time.sourceforge.net", + }, + }, + }, }, }, { diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index 09ed0d1ab01..be880a75723 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -31,3 +31,11 @@ func NewJavaPomCataloger() *generic.Cataloger { return generic.NewCataloger("java-pom-cataloger"). WithParserByGlobs(parserPomXML, "**/pom.xml") } + +// NewJavaGradleLockfileCataloger returns a cataloger capable of parsing +// dependencies from a gradle.lockfile file. +// older versions of lockfiles aren't supported yet +func NewJavaGradleLockfileCataloger() *generic.Cataloger { + return generic.NewCataloger("java-gradle-lockfile-cataloger"). + WithParserByGlobs(parseGradleLockfile, gradleLockfileGlob) +} diff --git a/syft/pkg/cataloger/java/parse_gradle_lockfile.go b/syft/pkg/cataloger/java/parse_gradle_lockfile.go new file mode 100644 index 00000000000..65ea51466f0 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_gradle_lockfile.go @@ -0,0 +1,63 @@ +package java + +import ( + "bufio" + "strings" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" +) + +const gradleLockfileGlob = "**/gradle.lockfile*" + +// Dependency represents a single dependency in the gradle.lockfile file +type LockfileDependency struct { + Group string + Name string + Version string +} + +func parseGradleLockfile(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + var pkgs []pkg.Package + + // Create a new scanner to read the file + scanner := bufio.NewScanner(reader) + + // Create slices to hold the dependencies and plugins + dependencies := []LockfileDependency{} + + // Loop over all lines in the file + for scanner.Scan() { + line := scanner.Text() + + // Trim leading and trailing whitespace from the line + line = strings.TrimSpace(line) + + groupNameVersion := line + groupNameVersion = strings.Split(groupNameVersion, "=")[0] + parts := strings.Split(groupNameVersion, ":") + + // we have a version directly specified + if len(parts) == 3 { + // Create a new Dependency struct and add it to the dependencies slice + dep := LockfileDependency{Group: parts[0], Name: parts[1], Version: parts[2]} + dependencies = append(dependencies, dep) + } + } + // map the dependencies + for _, dep := range dependencies { + mappedPkg := pkg.Package{ + Name: dep.Name, + Version: dep.Version, + Locations: source.NewLocationSet(reader.Location), + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + } + pkgs = append(pkgs, mappedPkg) + } + + return pkgs, nil, nil +} diff --git a/syft/pkg/cataloger/java/parse_gradle_lockfile_test.go b/syft/pkg/cataloger/java/parse_gradle_lockfile_test.go new file mode 100644 index 00000000000..65129efcff2 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_gradle_lockfile_test.go @@ -0,0 +1,52 @@ +package java + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" +) + +func Test_parserGradleLockfile(t *testing.T) { + tests := []struct { + input string + expected []pkg.Package + }{ + { + input: "test-fixtures/gradle/gradle.lockfile", + expected: []pkg.Package{ + { + Name: "hamcrest-core", + Version: "1.3", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + }, + { + Name: "joda-time", + Version: "2.2", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + }, + { + Name: "junit", + Version: "4.12", + Language: pkg.Java, + Type: pkg.JavaPkg, + MetadataType: pkg.JavaMetadataType, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + for i := range test.expected { + test.expected[i].Locations.Add(source.NewLocation(test.input)) + } + pkgtest.TestFileParser(t, test.input, parseGradleLockfile, test.expected, nil) + }) + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/gradle/.gitignore b/syft/pkg/cataloger/java/test-fixtures/gradle/.gitignore new file mode 100644 index 00000000000..08a55c09bdf --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/gradle/.gitignore @@ -0,0 +1 @@ +.gradle diff --git a/syft/pkg/cataloger/java/test-fixtures/gradle/build.gradle b/syft/pkg/cataloger/java/test-fixtures/gradle/build.gradle new file mode 100644 index 00000000000..1d700e273fe --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/gradle/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'eclipse' + id 'application' +} + +mainClassName = 'hello.HelloWorld' + +dependencyLocking { + lockAllConfigurations() +} +// tag::repositories[] +repositories { + mavenCentral() +} +// end::repositories[] + +// tag::dependencies[] +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + implementation "joda-time:joda-time:2.2" + testImplementation "junit:junit:4.12" +} +// end::dependencies[] + +// tag::jar[] +jar { + archivesBaseName = 'example-java-app-gradle' + version = '0.1.0' + manifest { + attributes( + 'Main-Class': 'hello.HelloWorld' + ) + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} +// end::jar[] + +// tag::wrapper[] +// end::wrapper[] + +// to invoke: gradle resolveAndLockAll --write-locks +tasks.register('resolveAndLockAll') { + notCompatibleWithConfigurationCache("Filters configurations at execution time") + doFirst { + assert gradle.startParameter.writeDependencyLocks + } + doLast { + configurations.findAll { + // Add any custom filtering on the configurations to be resolved + it.canBeResolved + }.each { it.resolve() } + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/gradle/gradle.lockfile b/syft/pkg/cataloger/java/test-fixtures/gradle/gradle.lockfile new file mode 100644 index 00000000000..b6edb43b980 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/gradle/gradle.lockfile @@ -0,0 +1,7 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +joda-time:joda-time:2.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +junit:junit:4.12=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,testAnnotationProcessor diff --git a/syft/pkg/cataloger/java/test-fixtures/java-builds/build-example-java-app-gradle.sh b/syft/pkg/cataloger/java/test-fixtures/java-builds/build-example-java-app-gradle.sh index 345542c93ee..075733ddce1 100755 --- a/syft/pkg/cataloger/java/test-fixtures/java-builds/build-example-java-app-gradle.sh +++ b/syft/pkg/cataloger/java/test-fixtures/java-builds/build-example-java-app-gradle.sh @@ -4,7 +4,7 @@ set -uxe # note: this can be easily done in a 1-liner, however circle CI does NOT allow volume mounts from the host in docker executors (since they are on remote hosts, where the host files are inaccessible) PKGSDIR=$1 -CTRID=$(docker create -u "$(id -u):$(id -g)" -v /example-java-app -w /example-java-app gradle:6.8.3-jdk gradle build) +CTRID=$(docker create -u "$(id -u):$(id -g)" -v /example-java-app -w /example-java-app gradle:8.0.2-jdk gradle build) function cleanup() { docker rm "${CTRID}" diff --git a/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/build.gradle b/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/build.gradle index 0dac0e13b0a..1d700e273fe 100644 --- a/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/build.gradle +++ b/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/build.gradle @@ -1,31 +1,58 @@ -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'application' +plugins { + id 'java' + id 'eclipse' + id 'application' +} mainClassName = 'hello.HelloWorld' +dependencyLocking { + lockAllConfigurations() +} // tag::repositories[] repositories { mavenCentral() } // end::repositories[] -// tag::jar[] -jar { - baseName = 'example-java-app-gradle' - version = '0.1.0' -} -// end::jar[] - // tag::dependencies[] sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { - compile "joda-time:joda-time:2.2" - testCompile "junit:junit:4.12" + implementation "joda-time:joda-time:2.2" + testImplementation "junit:junit:4.12" } // end::dependencies[] +// tag::jar[] +jar { + archivesBaseName = 'example-java-app-gradle' + version = '0.1.0' + manifest { + attributes( + 'Main-Class': 'hello.HelloWorld' + ) + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} +// end::jar[] + // tag::wrapper[] -// end::wrapper[] \ No newline at end of file +// end::wrapper[] + +// to invoke: gradle resolveAndLockAll --write-locks +tasks.register('resolveAndLockAll') { + notCompatibleWithConfigurationCache("Filters configurations at execution time") + doFirst { + assert gradle.startParameter.writeDependencyLocks + } + doLast { + configurations.findAll { + // Add any custom filtering on the configurations to be resolved + it.canBeResolved + }.each { it.resolve() } + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/gradle.lockfile b/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/gradle.lockfile new file mode 100644 index 00000000000..b6edb43b980 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/gradle.lockfile @@ -0,0 +1,7 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +joda-time:joda-time:2.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +junit:junit:4.12=testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,testAnnotationProcessor