From 31d771715161e1ca4423910aca02383c6d2503fd Mon Sep 17 00:00:00 2001 From: Henry Sachs Date: Thu, 15 Dec 2022 21:03:06 +0100 Subject: [PATCH 1/5] feat: initial gradle implementation Co-authored-by: @bolshoytoster Signed-off-by: Henry Sachs --- syft/pkg/cataloger/cataloger.go | 2 + syft/pkg/cataloger/java/cataloger.go | 8 + syft/pkg/cataloger/java/parse_gradle.go | 243 ++++++++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 syft/pkg/cataloger/java/parse_gradle.go diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 631ae7bb5c9..c7ab929b6ab 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -67,6 +67,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { rpm.NewFileCataloger(), java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), + java.NewJavaGradleCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), @@ -97,6 +98,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { rpm.NewFileCataloger(), java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), + java.NewJavaGradleCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index c132b61c74d..aa77295a839 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, pomXMLDirGlob) } + +// NewJavaGradleCataloger returns a cataloger capable of parsing +// dependencies from a pom.xml file. +// Pom files list dependencies that maybe not be locally installed yet. +func NewJavaGradleCataloger() *generic.Cataloger { + return generic.NewCataloger("java-gradle-cataloger"). + WithParserByGlobs(parserBuildGradle, buildGradleDirGlob) +} diff --git a/syft/pkg/cataloger/java/parse_gradle.go b/syft/pkg/cataloger/java/parse_gradle.go new file mode 100644 index 00000000000..ac45827f540 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_gradle.go @@ -0,0 +1,243 @@ +package java + +import ( + "bufio" + "regexp" + "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 buildGradleGlob = "*build.gradle*" +const buildGradleDirGlob = "**/build.gradle*" + +var propertyMatcherGradle = regexp.MustCompile("[$][{][^}]+[}]") + +// Dependency represents a single dependency in the build.gradle file +type Dependency struct { + Group string + Name string + Version string +} + +// Plugin represents a single plugin in the build.gradle file +type Plugin struct { + Id string + Version string +} + +func parserBuildGradle(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + //Gradle, err := decodeBuildGradle(reader) + // if err != nil { + // return nil, nil, err + // } + + 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 := []Dependency{} + plugins := []Plugin{} + // Create a map to hold the variables + variables := map[string]string{} + + // Keep track of whether we are in the dependencies or plugins section + inDependenciesSection := false + inPluginsSection := false + + // 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) + + // Check if the line starts with "dependencies {" + if strings.HasPrefix(line, "dependencies {") { + inDependenciesSection = true + continue + } + + // Check if the line starts with "plugins {" + if strings.HasPrefix(line, "plugins {") { + inPluginsSection = true + continue + } + + // Check if the line is "}" + if line == "}" { + inDependenciesSection = false + inPluginsSection = false + continue + } + + // Check if we are in the plugins section + if inPluginsSection { + // Split the line on whitespace to extract the group, name, and version of the dependency + fields := strings.Fields(line) + // Check if the line contains at least 3 fields (group, version as a literal string, and version as the version number) + if len(fields) >= 3 { + start := strings.Index(fields[0], "(") + 1 + end := strings.Index(fields[0], ")") + groupName := fields[0][start:end] + groupName = strings.Trim(groupName, `"`) + version := strings.Trim(fields[2], `"`) + // Create a new Dependency struct and add it to the dependencies slice + plugin := Plugin{Id: groupName, Version: version} + plugins = append(plugins, plugin) + } + } + + // Check if we are in the dependencies section + if inDependenciesSection { + // Extract the group, name, and version from the function call + start := strings.IndexFunc(line, func(r rune) bool { + return r == '(' || r == ' ' + }) + 1 + // Extract the group, name, and version from the function call + end := strings.IndexFunc(line, func(r rune) bool { + return r == ')' || r == ' ' + }) + groupNameVersion := line[start:end] + groupNameVersion = strings.Trim(groupNameVersion, "\"") + parts := strings.Split(groupNameVersion, ":") + // if we only have 2 sections the version is probably missing + if len(parts) == 2 { + // search for the version in the plugin section + version := searchInPlugins(parts[0], plugins) + // Create a new Dependency struct and add it to the dependencies slice + dep := Dependency{Group: parts[0], Name: parts[1], Version: version} + dependencies = append(dependencies, dep) + } + // we have a version directly specified + if len(parts) == 3 { + // Create a new Dependency struct and add it to the dependencies slice + dep := Dependency{Group: parts[0], Name: parts[1], Version: parts[2]} + dependencies = append(dependencies, dep) + } + } + + // Check if the line contains an assignment + if strings.Contains(line, "=") { + // Split the line on the "=" character to separate the key and value + parts := strings.Split(line, "=") + + // Trim any leading and trailing whitespace from the key and value + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + // Add the key and value to the map + variables[key] = value + } + + } + // map the dependencies + for _, dep := range dependencies { + mappedPkg := pkg.Package{Name: dep.Name, Version: dep.Version} + pkgs = append(pkgs, mappedPkg) + } + + return pkgs, nil, nil +} + +func searchInPlugins(groupName string, plugins []Plugin) string { + for _, v := range plugins { + if v.Id == groupName { + return v.Version + } + } + return "" +} + +// func parseBuildGradleProject(path string, reader io.Reader) (*pkg.GradleProject, error) { +// project, err := decodeBuildGradle(reader) +// if err != nil { +// return nil, err +// } +// return newGradleProject(path, project), nil +// } + +// func newGradleProject(path string, p goGradle.Project) *pkg.GradleProject { +// return &pkg.GradleProject{ +// Path: path, +// Parent: GradleParent(p, p.Parent), +// GroupID: resolvePropertyGradle(p, p.GroupID), +// ArtifactID: p.ArtifactID, +// Version: resolvePropertyGradle(p, p.Version), +// Name: p.Name, +// Description: formatDescription(p.Description), +// URL: p.URL, +// } +// } + +// func newPackageFromGradle(Gradle goGradle.Project, dep goGradle.Dependency, locations ...source.Location) pkg.Package { +// m := pkg.JavaMetadata{ +// GradleProperties: &pkg.GradleProperties{ +// GroupID: resolvePropertyGradle(Gradle, dep.GroupID), +// }, +// } + +// name := dep.ArtifactID +// version := resolvePropertyGradle(Gradle, dep.Version) + +// p := pkg.Package{ +// Name: name, +// Version: version, +// Locations: source.NewLocationSet(locations...), +// PURL: packageURL(name, version, m), +// Language: pkg.Java, +// Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a Gradle.xml that were not installed yet? +// MetadataType: pkg.JavaMetadataType, +// Metadata: m, +// } + +// p.SetID() + +// return p +// } + +// func decodeBuildGradle(content io.Reader) (project goGradle.Project, err error) { +// decoder := xml.NewDecoder(content) +// // prevent against warnings for "xml: encoding "iso-8859-1" declared but Decoder.CharsetReader is nil" +// decoder.CharsetReader = charset.NewReaderLabel +// if err := decoder.Decode(&project); err != nil { +// return project, fmt.Errorf("unable to unmarshal Gradle.xml: %w", err) +// } + +// return project, nil +// } + +// func GradleParent(Gradle goGradle.Project, parent goGradle.Parent) (result *pkg.GradleParent) { +// if parent.ArtifactID != "" || parent.GroupID != "" || parent.Version != "" { +// result = &pkg.GradleParent{ +// GroupID: resolvePropertyGradle(Gradle, parent.GroupID), +// ArtifactID: parent.ArtifactID, +// Version: resolvePropertyGradle(Gradle, parent.Version), +// } +// } +// return result +// } + +// func formatDescription(original string) (cleaned string) { +// descriptionLines := strings.Split(original, "\n") +// for _, line := range descriptionLines { +// line = strings.TrimSpace(line) +// if len(line) == 0 { +// continue +// } +// cleaned += line + " " +// } +// return strings.TrimSpace(cleaned) +// } + +// // resolvePropertyGradle emulates some maven property resolution logic by looking in the project's variables +// // as well as supporting the project expressions like ${project.parent.groupId}. +// // If no match is found, the entire expression including ${} is returned +// func resolvePropertyGradle(Gradle goGradle.Project, property string) string { +// return "1.0.0" +// } From bf5a453d95927c1bcc3a61249baaf3a03ef95f5d Mon Sep 17 00:00:00 2001 From: Henry Sachs Date: Thu, 15 Dec 2022 21:40:23 +0100 Subject: [PATCH 2/5] feat: add support for gradle lockfile Signed-off-by: Henry Sachs --- syft/pkg/cataloger/cataloger.go | 2 + syft/pkg/cataloger/java/cataloger.go | 8 +++ .../cataloger/java/parse_gradle_lockfile.go | 63 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 syft/pkg/cataloger/java/parse_gradle_lockfile.go diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index c7ab929b6ab..6a0adf83dbc 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -68,6 +68,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger { java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), java.NewJavaGradleCataloger(), + java.NewJavaGradleLockfileCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), @@ -99,6 +100,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { java.NewJavaCataloger(cfg.Java()), java.NewJavaPomCataloger(), java.NewJavaGradleCataloger(), + java.NewJavaGradleLockfileCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index aa77295a839..91fa11892cc 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -39,3 +39,11 @@ func NewJavaGradleCataloger() *generic.Cataloger { return generic.NewCataloger("java-gradle-cataloger"). WithParserByGlobs(parserBuildGradle, buildGradleDirGlob) } + +// NewJavaGradleLockfileCataloger returns a cataloger capable of parsing +// dependencies from a pom.xml file. +// Pom files list dependencies that maybe not be locally installed yet. +func NewJavaGradleLockfileCataloger() *generic.Cataloger { + return generic.NewCataloger("java-gradle-lockfile-cataloger"). + WithParserByGlobs(parserGradleLockfile, gradleLockfileDirGlob) +} 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..e5e4b45db39 --- /dev/null +++ b/syft/pkg/cataloger/java/parse_gradle_lockfile.go @@ -0,0 +1,63 @@ +package java + +import ( + "bufio" + "regexp" + "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*" +const gradleLockfileDirGlob = "**/gradle.lockfile*" + +var propertyMatcherGradleLock = regexp.MustCompile("[$][{][^}]+[}]") + +// Dependency represents a single dependency in the gradle.lockfile file +type LockfileDependency struct { + Group string + Name string + Version string +} + +func parserGradleLockfile(_ 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.Trim(groupNameVersion, "\"") + parts := strings.Split(groupNameVersion, ":") + + // we have a version directly specified + if len(parts) == 3 { + version := strings.Split(parts[2], "=") + // Create a new Dependency struct and add it to the dependencies slice + dep := LockfileDependency{Group: parts[0], Name: parts[1], Version: version[0]} + dependencies = append(dependencies, dep) + } + + } + // map the dependencies + for _, dep := range dependencies { + mappedPkg := pkg.Package{Name: dep.Name, Version: dep.Version} + pkgs = append(pkgs, mappedPkg) + } + + return pkgs, nil, nil +} From 7a6ec5e19d28b45979f9a5602bb52312560e6636 Mon Sep 17 00:00:00 2001 From: Henry Sachs Date: Sat, 4 Mar 2023 18:21:14 +0100 Subject: [PATCH 3/5] feat: initial tests upgrade gradle sample to gradle v8 fix package task not creating a executable jar fix gradle dependency parser crashing when scanning groovy gradle files feat add java metadata Signed-off-by: Henry Sachs --- syft/pkg/cataloger/java/parse_gradle.go | 112 ++++++++++++------ .../cataloger/java/parse_gradle_lockfile.go | 9 +- .../java/parse_gradle_lockfile_test.go | 52 ++++++++ syft/pkg/cataloger/java/parse_gradle_test.go | 45 +++++++ .../java/test-fixtures/gradle/build.gradle | 58 +++++++++ .../java/test-fixtures/gradle/gradle.lockfile | 7 ++ .../build-example-java-app-gradle.sh | 2 +- .../java-builds/example-java-app/build.gradle | 53 +++++++-- .../example-java-app/gradle.lockfile | 7 ++ 9 files changed, 291 insertions(+), 54 deletions(-) create mode 100644 syft/pkg/cataloger/java/parse_gradle_lockfile_test.go create mode 100644 syft/pkg/cataloger/java/parse_gradle_test.go create mode 100644 syft/pkg/cataloger/java/test-fixtures/gradle/build.gradle create mode 100644 syft/pkg/cataloger/java/test-fixtures/gradle/gradle.lockfile create mode 100644 syft/pkg/cataloger/java/test-fixtures/java-builds/example-java-app/gradle.lockfile diff --git a/syft/pkg/cataloger/java/parse_gradle.go b/syft/pkg/cataloger/java/parse_gradle.go index ac45827f540..595ef76d6ca 100644 --- a/syft/pkg/cataloger/java/parse_gradle.go +++ b/syft/pkg/cataloger/java/parse_gradle.go @@ -78,48 +78,12 @@ func parserBuildGradle(_ source.FileResolver, _ *generic.Environment, reader sou // Check if we are in the plugins section if inPluginsSection { - // Split the line on whitespace to extract the group, name, and version of the dependency - fields := strings.Fields(line) - // Check if the line contains at least 3 fields (group, version as a literal string, and version as the version number) - if len(fields) >= 3 { - start := strings.Index(fields[0], "(") + 1 - end := strings.Index(fields[0], ")") - groupName := fields[0][start:end] - groupName = strings.Trim(groupName, `"`) - version := strings.Trim(fields[2], `"`) - // Create a new Dependency struct and add it to the dependencies slice - plugin := Plugin{Id: groupName, Version: version} - plugins = append(plugins, plugin) - } + plugins = extractPlugins(line, plugins) } // Check if we are in the dependencies section if inDependenciesSection { - // Extract the group, name, and version from the function call - start := strings.IndexFunc(line, func(r rune) bool { - return r == '(' || r == ' ' - }) + 1 - // Extract the group, name, and version from the function call - end := strings.IndexFunc(line, func(r rune) bool { - return r == ')' || r == ' ' - }) - groupNameVersion := line[start:end] - groupNameVersion = strings.Trim(groupNameVersion, "\"") - parts := strings.Split(groupNameVersion, ":") - // if we only have 2 sections the version is probably missing - if len(parts) == 2 { - // search for the version in the plugin section - version := searchInPlugins(parts[0], plugins) - // Create a new Dependency struct and add it to the dependencies slice - dep := Dependency{Group: parts[0], Name: parts[1], Version: version} - dependencies = append(dependencies, dep) - } - // we have a version directly specified - if len(parts) == 3 { - // Create a new Dependency struct and add it to the dependencies slice - dep := Dependency{Group: parts[0], Name: parts[1], Version: parts[2]} - dependencies = append(dependencies, dep) - } + dependencies = extractDependencies(line, plugins, dependencies) } // Check if the line contains an assignment @@ -138,13 +102,83 @@ func parserBuildGradle(_ source.FileResolver, _ *generic.Environment, reader sou } // map the dependencies for _, dep := range dependencies { - mappedPkg := pkg.Package{Name: dep.Name, Version: dep.Version} + mappedPkg := pkg.Package{ + Name: dep.Name, + Version: dep.Version, + Locations: source.NewLocationSet(reader.Location), + Language: pkg.Java, + Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a Gradle.xml that were not installed yet? + MetadataType: pkg.JavaMetadataType, + } pkgs = append(pkgs, mappedPkg) } return pkgs, nil, nil } +func extractPlugins(line string, plugins []Plugin) []Plugin { + // Split the line on whitespace to extract the group, name, and version of the dependency + fields := strings.Fields(line) + // Check if the line contains at least 3 fields (group, version as a literal string, and version as the version number) + if len(fields) >= 3 { + start := strings.Index(fields[0], "(") + 1 + end := strings.Index(fields[0], ")") + groupName := fields[0][start:end] + groupName = strings.Trim(groupName, `"`) + version := strings.Trim(fields[2], `"`) + // Create a new Dependency struct and add it to the dependencies slice + plugin := Plugin{Id: groupName, Version: version} + plugins = append(plugins, plugin) + } + return plugins +} + +func extractDependencies(line string, plugins []Plugin, dependencies []Dependency) []Dependency { + /* + * Extract the group, name, and version from the function call + * there are different strategies for groovy and kotlin in this case + * for kotlin dependencies are enclosed in round brackets + * for groovy they begin and end with quotation marks + * so we first check for the starting line for the groovy or kotlin starting rune + * after that we just check for the kotlin part and if it don't match we can expect to find the last index via the last quotation mark + * we probably could just take the end of the string but it was too dangerous for me as I don't know all features gradle allows when declaring dependencies + */ + start := strings.IndexFunc(line, func(r rune) bool { + return r == '(' || r == ' ' + }) + 1 + end := strings.IndexFunc(line, func(r rune) bool { + return r == ')' + }) + if end == -1 { + end = strings.LastIndexFunc(line, func(r rune) bool { + return r == '"' + }) + } + // split the dependency string + groupNameVersion := line[start:end] + groupNameVersion = strings.Trim(groupNameVersion, "\"") + parts := strings.Split(groupNameVersion, ":") + // if we only have 2 sections the version is probably missing + // search for the version in the plugin section + // Create a new Dependency struct and add it to the dependencies slice + // we have a version directly specified + // Create a new Dependency struct and add it to the dependencies slice + if len(parts) == 2 { + + version := searchInPlugins(parts[0], plugins) + + dep := Dependency{Group: parts[0], Name: parts[1], Version: version} + dependencies = append(dependencies, dep) + } + + if len(parts) == 3 { + + dep := Dependency{Group: parts[0], Name: parts[1], Version: parts[2]} + dependencies = append(dependencies, dep) + } + return dependencies +} + func searchInPlugins(groupName string, plugins []Plugin) string { for _, v := range plugins { if v.Id == groupName { diff --git a/syft/pkg/cataloger/java/parse_gradle_lockfile.go b/syft/pkg/cataloger/java/parse_gradle_lockfile.go index e5e4b45db39..268e3940877 100644 --- a/syft/pkg/cataloger/java/parse_gradle_lockfile.go +++ b/syft/pkg/cataloger/java/parse_gradle_lockfile.go @@ -55,7 +55,14 @@ func parserGradleLockfile(_ source.FileResolver, _ *generic.Environment, reader } // map the dependencies for _, dep := range dependencies { - mappedPkg := pkg.Package{Name: dep.Name, Version: dep.Version} + mappedPkg := pkg.Package{ + Name: dep.Name, + Version: dep.Version, + Locations: source.NewLocationSet(reader.Location), + Language: pkg.Java, + Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a Gradle.xml that were not installed yet? + MetadataType: pkg.JavaMetadataType, + } pkgs = append(pkgs, mappedPkg) } 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..69b13129f3e --- /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, parserGradleLockfile, test.expected, nil) + }) + } +} diff --git a/syft/pkg/cataloger/java/parse_gradle_test.go b/syft/pkg/cataloger/java/parse_gradle_test.go new file mode 100644 index 00000000000..8f613672f2f --- /dev/null +++ b/syft/pkg/cataloger/java/parse_gradle_test.go @@ -0,0 +1,45 @@ +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_parserGradle(t *testing.T) { + tests := []struct { + input string + expected []pkg.Package + }{ + { + input: "test-fixtures/gradle/build.gradle", + expected: []pkg.Package{ + { + 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, parserBuildGradle, test.expected, nil) + }) + } +} 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..35968b0a448 --- /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() } + } +} \ No newline at end of file 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..35968b0a448 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() } + } +} \ No newline at end of file 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 From 4c972d00f0f0075d2d346036987fec91e130b1b0 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 17 Apr 2023 12:23:33 -0400 Subject: [PATCH 4/5] chore: rename parseBuildGradle Signed-off-by: Keith Zantow --- syft/pkg/cataloger/java/cataloger.go | 2 +- syft/pkg/cataloger/java/parse_gradle.go | 3 +-- syft/pkg/cataloger/java/parse_gradle_test.go | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index 4b357346861..c66416e29b2 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -37,7 +37,7 @@ func NewJavaPomCataloger() *generic.Cataloger { // Pom files list dependencies that maybe not be locally installed yet. func NewJavaGradleCataloger() *generic.Cataloger { return generic.NewCataloger("java-gradle-cataloger"). - WithParserByGlobs(parserBuildGradle, buildGradleDirGlob) + WithParserByGlobs(parseBuildGradle, buildGradleDirGlob) } // NewJavaGradleLockfileCataloger returns a cataloger capable of parsing diff --git a/syft/pkg/cataloger/java/parse_gradle.go b/syft/pkg/cataloger/java/parse_gradle.go index f482583c8ad..f24e5fbca8c 100644 --- a/syft/pkg/cataloger/java/parse_gradle.go +++ b/syft/pkg/cataloger/java/parse_gradle.go @@ -11,7 +11,6 @@ import ( "github.com/anchore/syft/syft/source" ) -const buildGradleGlob = "*build.gradle*" const buildGradleDirGlob = "**/build.gradle*" var propertyMatcherGradle = regexp.MustCompile("[$][{][^}]+[}]") @@ -29,7 +28,7 @@ type Plugin struct { Version string } -func parserBuildGradle(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func parseBuildGradle(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // Gradle, err := decodeBuildGradle(reader) // if err != nil { // return nil, nil, err diff --git a/syft/pkg/cataloger/java/parse_gradle_test.go b/syft/pkg/cataloger/java/parse_gradle_test.go index 8f613672f2f..83f6ff3ee21 100644 --- a/syft/pkg/cataloger/java/parse_gradle_test.go +++ b/syft/pkg/cataloger/java/parse_gradle_test.go @@ -39,7 +39,7 @@ func Test_parserGradle(t *testing.T) { for i := range test.expected { test.expected[i].Locations.Add(source.NewLocation(test.input)) } - pkgtest.TestFileParser(t, test.input, parserBuildGradle, test.expected, nil) + pkgtest.TestFileParser(t, test.input, parseBuildGradle, test.expected, nil) }) } } From 4472f50b015eec93af2421b2055f180370837ecd Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 17 Apr 2023 12:55:47 -0400 Subject: [PATCH 5/5] chore: lint-fix Signed-off-by: Keith Zantow --- syft/pkg/cataloger/java/parse_gradle.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/syft/pkg/cataloger/java/parse_gradle.go b/syft/pkg/cataloger/java/parse_gradle.go index f24e5fbca8c..9765f9b6d2f 100644 --- a/syft/pkg/cataloger/java/parse_gradle.go +++ b/syft/pkg/cataloger/java/parse_gradle.go @@ -2,7 +2,6 @@ package java import ( "bufio" - "regexp" "strings" "github.com/anchore/syft/syft/artifact" @@ -13,7 +12,7 @@ import ( const buildGradleDirGlob = "**/build.gradle*" -var propertyMatcherGradle = regexp.MustCompile("[$][{][^}]+[}]") +// var propertyMatcherGradle = regexp.MustCompile("[$][{][^}]+[}]") // Dependency represents a single dependency in the build.gradle file type Dependency struct { @@ -24,10 +23,11 @@ type Dependency struct { // Plugin represents a single plugin in the build.gradle file type Plugin struct { - Id string + ID string Version string } +//nolint:funlen func parseBuildGradle(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // Gradle, err := decodeBuildGradle(reader) // if err != nil { @@ -125,7 +125,7 @@ func extractPlugins(line string, plugins []Plugin) []Plugin { groupName = strings.Trim(groupName, `"`) version := strings.Trim(fields[2], `"`) // Create a new Dependency struct and add it to the dependencies slice - plugin := Plugin{Id: groupName, Version: version} + plugin := Plugin{ID: groupName, Version: version} plugins = append(plugins, plugin) } return plugins @@ -177,7 +177,7 @@ func extractDependencies(line string, plugins []Plugin, dependencies []Dependenc func searchInPlugins(groupName string, plugins []Plugin) string { for _, v := range plugins { - if v.Id == groupName { + if v.ID == groupName { return v.Version } }