From 9e104f5f0fe51a6dc45a82c5c0a393f07b3b3115 Mon Sep 17 00:00:00 2001 From: cx-anurag-dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:22:24 +0530 Subject: [PATCH 1/4] feat: add Gradle parser feature branch --- internal/parsers/gradle/gradle_parser.go | 222 ++++++++++++++++++ internal/parsers/gradle/gradle_parser_test.go | 160 +++++++++++++ pkg/parser/manifest-file-selector.go | 5 + pkg/parser/parser_factory.go | 3 + test/resources/build.gradle | 31 +++ 5 files changed, 421 insertions(+) create mode 100644 internal/parsers/gradle/gradle_parser.go create mode 100644 internal/parsers/gradle/gradle_parser_test.go create mode 100644 test/resources/build.gradle diff --git a/internal/parsers/gradle/gradle_parser.go b/internal/parsers/gradle/gradle_parser.go new file mode 100644 index 0000000..459b02a --- /dev/null +++ b/internal/parsers/gradle/gradle_parser.go @@ -0,0 +1,222 @@ +package gradle + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Checkmarx/manifest-parser/pkg/parser/models" +) + +// GradleParser implements parsing of Gradle build files +type GradleParser struct{} + +// Parse implements the Parser interface for Gradle build files +func (p *GradleParser) Parse(manifestFile string) ([]models.Package, error) { + content, err := os.ReadFile(manifestFile) + if err != nil { + return nil, fmt.Errorf("failed to read manifest file: %w", err) + } + + lines := strings.Split(string(content), "\n") + + // Extract variables + variables := extractVariables(manifestFile, string(content)) + + var packages []models.Package + + // Parse main dependencies + mainDeps := parseDependencies(string(content), lines, variables, false) + for i := range mainDeps { + mainDeps[i].FilePath = manifestFile + } + packages = append(packages, mainDeps...) + + // Note: Buildscript dependencies are also parsed as main for simplicity + + return packages, nil +} + +// extractVariables extracts variable definitions from the build file and gradle.properties +func extractVariables(manifestFile, content string) map[string]string { + vars := make(map[string]string) + + // Read gradle.properties if exists + gradlePropsPath := filepath.Join(filepath.Dir(manifestFile), "gradle.properties") + if propsContent, err := os.ReadFile(gradlePropsPath); err == nil { + for _, line := range strings.Split(string(propsContent), "\n") { + line = strings.TrimSpace(line) + if strings.Contains(line, "=") && !strings.HasPrefix(line, "#") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + vars[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + } + } + + // Extract from ext blocks (Groovy) + extPattern := regexp.MustCompile(`(?s)ext\s*\{([^}]+)\}`) + if matches := extPattern.FindStringSubmatch(content); len(matches) > 1 { + extContent := matches[1] + // Simple key = 'value' or key: 'value' + varPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(\w+)\s*=\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(\w+)\s*:\s*['"]([^'"]+)['"]`), + } + for _, pattern := range varPatterns { + for _, match := range pattern.FindAllStringSubmatch(extContent, -1) { + if len(match) > 2 { + vars[match[1]] = match[2] + } + } + } + } + + // Extract ext.key = 'value' (outside blocks) + extVarPattern := regexp.MustCompile(`ext\.(\w+)\s*=\s*['"]([^'"]+)['"]`) + for _, match := range extVarPattern.FindAllStringSubmatch(content, -1) { + if len(match) > 2 { + vars[match[1]] = match[2] + } + } + + // Extract Kotlin DSL val/const + kotlinVarPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?:val|const val)\s+(\w+)\s*=\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?:val|const val)\s+(\w+)\s*=\s*(\d+(?:\.\d+)*[^\s'"]*)`), // for versions without quotes + } + for _, pattern := range kotlinVarPatterns { + for _, match := range pattern.FindAllStringSubmatch(content, -1) { + if len(match) > 2 { + vars[match[1]] = match[2] + } + } + } + + return vars +} + +// parseDependencies parses dependencies from the content +func parseDependencies(content string, lines []string, variables map[string]string, isBuildscript bool) []models.Package { + var packages []models.Package + + // Patterns for different dependency declarations + patterns := []*regexp.Regexp{ + // String notation: implementation 'group:name:version' + regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*\(\s*['"]([^'"]+)['"]\s*\)`), + // Map notation: implementation group: 'g', name: 'n', version: 'v' + regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*group\s*:\s*['"]([^'"]+)['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*version\s*:\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*\(\s*group\s*:\s*['"]([^'"]+)['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*version\s*:\s*['"]([^'"]+)['"]\s*\)`), + } + + depsLines := strings.Split(content, "\n") + for _, line := range depsLines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + for _, pattern := range patterns { + matches := pattern.FindStringSubmatch(line) + if len(matches) > 0 { + var group, name, version string + if len(matches) == 3 { + // String notation + depStr := resolveVariables(matches[2], variables) + parts := strings.Split(depStr, ":") + if len(parts) >= 2 { + group = parts[0] + name = parts[1] + if len(parts) > 2 { + version = parts[2] + } + } + } else if len(matches) == 5 { + // Map notation + group = resolveVariables(matches[2], variables) + name = resolveVariables(matches[3], variables) + version = resolveVariables(matches[4], variables) + } + + if group != "" && name != "" { + // Handle version ranges and classifiers + cleanVersion := cleanVersion(version) + + // Find line number + lineNum := findLineNumber(content, line) + + packages = append(packages, models.Package{ + PackageManager: "gradle", + PackageName: group + ":" + name, + Version: cleanVersion, + FilePath: "", // Will be set later + Locations: []models.Location{ + {Line: lineNum}, + }, + }) + } + } + } + } + + return packages +} + +// resolveVariables replaces ${var} or $var with values +func resolveVariables(str string, variables map[string]string) string { + // ${var} + re := regexp.MustCompile(`\$\{([^}]+)\}`) + str = re.ReplaceAllStringFunc(str, func(match string) string { + varName := strings.TrimSuffix(strings.TrimPrefix(match, "${"), "}") + if val, ok := variables[varName]; ok { + return val + } + return match + }) + + // $var + re = regexp.MustCompile(`\$(\w+)`) + str = re.ReplaceAllStringFunc(str, func(match string) string { + varName := strings.TrimPrefix(match, "$") + if val, ok := variables[varName]; ok { + return val + } + return match + }) + + return str +} + +// cleanVersion handles version ranges and classifiers +func cleanVersion(version string) string { + // Remove brackets for ranges, take the lower bound + if strings.HasPrefix(version, "[") && strings.HasSuffix(version, "]") { + version = strings.Trim(version, "[]") + parts := strings.Split(version, ",") + if len(parts) > 0 { + version = strings.TrimSpace(parts[0]) + } + } + if strings.HasPrefix(version, "(") && strings.HasSuffix(version, ")") { + version = strings.Trim(version, "()") + parts := strings.Split(version, ",") + if len(parts) > 0 { + version = strings.TrimSpace(parts[0]) + } + } + // For now, keep classifiers as is + return version +} + +// findLineNumber finds the line number of a substring in content +func findLineNumber(content, substr string) int { + index := strings.Index(content, substr) + if index == -1 { + return 0 + } + return strings.Count(content[:index], "\n") + 1 +} diff --git a/internal/parsers/gradle/gradle_parser_test.go b/internal/parsers/gradle/gradle_parser_test.go new file mode 100644 index 0000000..b307236 --- /dev/null +++ b/internal/parsers/gradle/gradle_parser_test.go @@ -0,0 +1,160 @@ +package gradle + +import ( + "os" + "path/filepath" + "testing" + + "github.com/Checkmarx/manifest-parser/pkg/parser/models" +) + +func TestGradleParser_Parse(t *testing.T) { + tests := []struct { + name string + content string + expectedPkgs []models.Package + expectedError bool + }{ + { + name: "basic gradle file", + content: `plugins { + id 'java' +} + +ext { + springVersion = '5.3.0' +} + +dependencies { + implementation 'org.springframework:spring-core:5.3.0' + testImplementation 'junit:junit:4.13' + api 'com.google.guava:guava:30.1-jre' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' +} + +buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:7.0.0' + } +}`, + expectedPkgs: []models.Package{ + { + PackageManager: "gradle", + PackageName: "org.springframework:spring-core", + Version: "5.3.0", + Locations: []models.Location{ + {Line: 10}, + }, + }, + { + PackageManager: "gradle", + PackageName: "junit:junit", + Version: "4.13", + Locations: []models.Location{ + {Line: 11}, + }, + }, + { + PackageManager: "gradle", + PackageName: "com.google.guava:guava", + Version: "30.1-jre", + Locations: []models.Location{ + {Line: 12}, + }, + }, + { + PackageManager: "gradle", + PackageName: "org.apache.commons:commons-lang3", + Version: "3.12.0", + Locations: []models.Location{ + {Line: 13}, + }, + }, + { + PackageManager: "gradle", + PackageName: "com.android.tools.build:gradle", + Version: "7.0.0", + Locations: []models.Location{ + {Line: 18}, + }, + }, + }, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary file + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write content to temp file + _, err = tmpFile.WriteString(tt.content) + if err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + tmpFile.Close() + + // Parse the file + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + + if tt.expectedError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectedError && err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(pkgs) != len(tt.expectedPkgs) { + t.Errorf("Expected %d packages, got %d", len(tt.expectedPkgs), len(pkgs)) + } + + for i, pkg := range pkgs { + if i >= len(tt.expectedPkgs) { + break + } + expected := tt.expectedPkgs[i] + if pkg.PackageManager != expected.PackageManager || + pkg.PackageName != expected.PackageName || + pkg.Version != expected.Version { + t.Errorf("Package %d mismatch: got %+v, expected %+v", i, pkg, expected) + } + if len(pkg.Locations) > 0 && len(expected.Locations) > 0 { + if pkg.Locations[0].Line != expected.Locations[0].Line { + t.Errorf("Location line mismatch: got %d, expected %d", pkg.Locations[0].Line, expected.Locations[0].Line) + } + } + } + }) + } +} + +func TestGradleParser_ParseFile(t *testing.T) { + // Test with actual file + parser := &GradleParser{} + pkgs, err := parser.Parse(filepath.Join("..", "..", "..", "test", "resources", "build.gradle")) + if err != nil { + t.Fatalf("Failed to parse build.gradle: %v", err) + } + + if len(pkgs) == 0 { + t.Errorf("Expected packages, got none") + } + + for _, pkg := range pkgs { + if pkg.PackageManager != "gradle" { + t.Errorf("Expected package manager 'gradle', got '%s'", pkg.PackageManager) + } + if pkg.PackageName == "" { + t.Errorf("Package name is empty") + } + if pkg.Version == "" { + t.Errorf("Version is empty") + } + } +} diff --git a/pkg/parser/manifest-file-selector.go b/pkg/parser/manifest-file-selector.go index 2710f99..c11b67e 100644 --- a/pkg/parser/manifest-file-selector.go +++ b/pkg/parser/manifest-file-selector.go @@ -15,6 +15,7 @@ const ( DotnetPackagesConfig MavenPom GoMod + GradleBuild ) // selectManifestFile a method to select a manifest file type by its name @@ -55,5 +56,9 @@ func selectManifestFile(manifest string) Manifest { return GoMod } + if manifestFileName == "build.gradle" || manifestFileName == "build.gradle.kts" { + return GradleBuild + } + return -1 } diff --git a/pkg/parser/parser_factory.go b/pkg/parser/parser_factory.go index 0f81e86..58f5d82 100644 --- a/pkg/parser/parser_factory.go +++ b/pkg/parser/parser_factory.go @@ -3,6 +3,7 @@ package parser import ( "github.com/Checkmarx/manifest-parser/internal/parsers/dotnet" "github.com/Checkmarx/manifest-parser/internal/parsers/golang" + "github.com/Checkmarx/manifest-parser/internal/parsers/gradle" "github.com/Checkmarx/manifest-parser/internal/parsers/maven" "github.com/Checkmarx/manifest-parser/internal/parsers/npm" "github.com/Checkmarx/manifest-parser/internal/parsers/pypi" @@ -26,6 +27,8 @@ func ParsersFactory(manifest string) Parser { return &dotnet.DotnetPackagesConfigParser{} case GoMod: return &golang.GoModParser{} + case GradleBuild: + return &gradle.GradleParser{} default: return nil } diff --git a/test/resources/build.gradle b/test/resources/build.gradle new file mode 100644 index 0000000..6e95dd1 --- /dev/null +++ b/test/resources/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' +} + +ext { + springVersion = '5.3.0' + guavaVersion = '30.1-jre' +} + +group 'com.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.0.0' + } +} + +dependencies { + implementation 'org.springframework:spring-core:5.3.0' + testImplementation 'junit:junit:4.13' + api 'com.google.guava:guava:30.1-jre' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' +} \ No newline at end of file From e34628260095c592793510dabea304477b76dbd2 Mon Sep 17 00:00:00 2001 From: cx-anurag-dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:36:37 +0530 Subject: [PATCH 2/4] feat: update Gradle parser and add implementation plan --- GRADLE_PARSER_IMPLEMENTATION_PLAN.md | 110 ++++++++++ internal/parsers/gradle/gradle_parser.go | 203 +++++++++++++----- internal/parsers/gradle/gradle_parser_test.go | 108 ++++++++++ 3 files changed, 365 insertions(+), 56 deletions(-) create mode 100644 GRADLE_PARSER_IMPLEMENTATION_PLAN.md diff --git a/GRADLE_PARSER_IMPLEMENTATION_PLAN.md b/GRADLE_PARSER_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..94dda14 --- /dev/null +++ b/GRADLE_PARSER_IMPLEMENTATION_PLAN.md @@ -0,0 +1,110 @@ +# Gradle Parser Implementation Plan + +## Overview +This document describes the implementation plan and execution steps for adding Gradle manifest parsing support to the `manifest-parser` repository. + +The parser was extended to support static Gradle dependency declarations in both Groovy and Kotlin DSL, including common production patterns such as multi-line dependencies and conditional `if` blocks. + +--- + +## Implementation Plan + +### 1. Analyze existing code flow +- Inspect `cmd/main.go` to understand entrypoint behavior. +- Review `pkg/parser/parser.go` and `pkg/parser/parser_factory.go` to understand the parser interface and factory logic. +- Review `pkg/parser/manifest-file-selector.go` to see how manifest types are detected. +- Review existing language parser implementations for style and output format. + +### 2. Add Gradle manifest detection +- Extend `pkg/parser/manifest-file-selector.go` to recognize `build.gradle` and `build.gradle.kts` files. +- Add a new `Manifest` type for Gradle. + +### 3. Add factory support for Gradle +- Update `pkg/parser/parser_factory.go` to import the new Gradle parser. +- Return the Gradle parser instance when the selected manifest is Gradle. + +### 4. Implement Gradle parser +- Create `internal/parsers/gradle/gradle_parser.go`. +- Implement the `Parser` interface for Gradle. +- Parse dependencies into `models.Package` entries. + +### 5. Add variable resolution +- Read `gradle.properties` values. +- Parse Groovy `ext {}` blocks and `ext.key` assignments. +- Parse Kotlin DSL `val` and `const val` declarations. +- Resolve `${var}` and `$var` references in dependency strings. + +### 6. Add support for multi-line dependency declarations +- Detect dependency statements spanning multiple lines. +- Join logical dependency lines before parsing. +- Support both string notation and map notation across line breaks. + +### 7. Add conditional dependency support +- Parse dependencies inside conditional blocks such as `if (...) { ... }`. +- Treat static declarations inside conditionals as valid parse targets. + +### 8. Add Kotlin DSL support +- Support Kotlin string syntax: `implementation("group:name:version")`. +- Support Kotlin map syntax: `implementation(group = "group", name = "name", version = "version")`. +- Support Kotlin-style dependency declarations in `build.gradle.kts`. + +### 9. Write regression tests +- Create or update `internal/parsers/gradle/gradle_parser_test.go`. +- Add tests for: + - basic Groovy dependencies + - multi-line dependencies + - conditional `if` block dependencies + - Kotlin DSL dependency syntax + +### 10. Validate +- Run `go test ./internal/parsers/gradle`. +- Confirm Gradle parser tests pass. +- Optionally run `go test ./...` to verify broader repository compatibility, noting existing unrelated test failures. + +--- + +## Files created or modified + +- `pkg/parser/manifest-file-selector.go` +- `pkg/parser/parser_factory.go` +- `internal/parsers/gradle/gradle_parser.go` +- `internal/parsers/gradle/gradle_parser_test.go` +- `test/resources/build.gradle` (sample Gradle fixture) + +--- + +## Supported Gradle parser features + +- Detection of `build.gradle` and `build.gradle.kts` files +- Parsing of common dependency configurations: + - `implementation`, `api`, `compile`, `compileOnly`, `runtime`, `runtimeOnly` + - `testImplementation`, `testCompile`, `testRuntimeOnly` + - `androidTestImplementation`, `annotationProcessor`, `classpath`, `kapt` +- String-style dependency declarations +- Map-style dependency declarations +- Multi-line dependency statements +- Dependencies inside `if (...) { ... }` blocks +- Variable resolution from: + - `gradle.properties` + - Groovy `ext` property blocks + - Groovy `ext.key = value` syntax + - Kotlin DSL `val` / `const val` +- Kotlin DSL dependency syntax +- Version cleanup for simple ranges and classifiers + +--- + +## Known limitations + +- Dynamic dependencies generated by build logic, loops, or plugin APIs are not resolved. +- Complex Kotlin DSL constructs beyond common forms may not be fully parsed. +- Conditional branch logic is not evaluated; all static declarations are treated as present. +- Deep nested DSL or custom Gradle extension syntax may be missed. +- Computed or function-based version expressions are not evaluated. +- Multi-project and included-build dependency resolution is not supported. + +--- + +## Notes + +The Gradle parser is now suitable for many production scanning scenarios in AST-CLI where static dependency declarations are present. For full Gradle model accuracy, additional Gradle-aware parsing or integration with Gradle tooling would be required. diff --git a/internal/parsers/gradle/gradle_parser.go b/internal/parsers/gradle/gradle_parser.go index 459b02a..af376a7 100644 --- a/internal/parsers/gradle/gradle_parser.go +++ b/internal/parsers/gradle/gradle_parser.go @@ -20,22 +20,20 @@ func (p *GradleParser) Parse(manifestFile string) ([]models.Package, error) { return nil, fmt.Errorf("failed to read manifest file: %w", err) } - lines := strings.Split(string(content), "\n") + manifestContent := string(content) // Extract variables - variables := extractVariables(manifestFile, string(content)) + variables := extractVariables(manifestFile, manifestContent) var packages []models.Package // Parse main dependencies - mainDeps := parseDependencies(string(content), lines, variables, false) + mainDeps := parseDependencies(manifestContent, variables) for i := range mainDeps { mainDeps[i].FilePath = manifestFile } packages = append(packages, mainDeps...) - // Note: Buildscript dependencies are also parsed as main for simplicity - return packages, nil } @@ -99,73 +97,166 @@ func extractVariables(manifestFile, content string) map[string]string { return vars } +type dependencyStatement struct { + Line int + Text string +} + // parseDependencies parses dependencies from the content -func parseDependencies(content string, lines []string, variables map[string]string, isBuildscript bool) []models.Package { +func parseDependencies(content string, variables map[string]string) []models.Package { var packages []models.Package - // Patterns for different dependency declarations - patterns := []*regexp.Regexp{ - // String notation: implementation 'group:name:version' - regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*['"]([^'"]+)['"]`), - regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*\(\s*['"]([^'"]+)['"]\s*\)`), - // Map notation: implementation group: 'g', name: 'n', version: 'v' - regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*group\s*:\s*['"]([^'"]+)['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*version\s*:\s*['"]([^'"]+)['"]`), - regexp.MustCompile(`(?i)(implementation|api|compile|runtime|testImplementation|testCompile|androidTestImplementation|classpath)\s*\(\s*group\s*:\s*['"]([^'"]+)['"]\s*,\s*name\s*:\s*['"]([^'"]+)['"]\s*,\s*version\s*:\s*['"]([^'"]+)['"]\s*\)`), - } - - depsLines := strings.Split(content, "\n") - for _, line := range depsLines { - line = strings.TrimSpace(line) - if line == "" { + statements := extractDependencyStatements(content) + for _, stmt := range statements { + for _, pkg := range parseDependencyStatement(stmt.Text, variables) { + pkg.Locations = []models.Location{{Line: stmt.Line}} + packages = append(packages, pkg) + } + } + + return packages +} + +func extractDependencyStatements(content string) []dependencyStatement { + startPattern := regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\b`) + var statements []dependencyStatement + var buffer strings.Builder + active := false + startLine := 0 + + lines := strings.Split(content, "\n") + for i, raw := range lines { + line := strings.TrimSpace(raw) + if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") { continue } - for _, pattern := range patterns { - matches := pattern.FindStringSubmatch(line) - if len(matches) > 0 { - var group, name, version string - if len(matches) == 3 { - // String notation - depStr := resolveVariables(matches[2], variables) - parts := strings.Split(depStr, ":") - if len(parts) >= 2 { - group = parts[0] - name = parts[1] - if len(parts) > 2 { - version = parts[2] - } - } - } else if len(matches) == 5 { - // Map notation - group = resolveVariables(matches[2], variables) - name = resolveVariables(matches[3], variables) - version = resolveVariables(matches[4], variables) + if !active { + if startPattern.MatchString(line) { + active = true + startLine = i + 1 + buffer.Reset() + buffer.WriteString(line) + if dependencyStatementComplete(buffer.String()) { + statements = append(statements, dependencyStatement{Line: startLine, Text: buffer.String()}) + active = false } + } + continue + } + + buffer.WriteString(" ") + buffer.WriteString(line) + if dependencyStatementComplete(buffer.String()) { + statements = append(statements, dependencyStatement{Line: startLine, Text: buffer.String()}) + active = false + } + } - if group != "" && name != "" { - // Handle version ranges and classifiers - cleanVersion := cleanVersion(version) - - // Find line number - lineNum := findLineNumber(content, line) - - packages = append(packages, models.Package{ - PackageManager: "gradle", - PackageName: group + ":" + name, - Version: cleanVersion, - FilePath: "", // Will be set later - Locations: []models.Location{ - {Line: lineNum}, - }, - }) + return statements +} + +func dependencyStatementComplete(statement string) bool { + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*['"]([^'"\)]+)['"]`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), + regexp.MustCompile(`(?i)group\s*[:=]\s*['"]([^'"]+)['"].*name\s*[:=]\s*['"]([^'"]+)['"].*version\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)group\s*[:=]\s*[^,\s]+.*name\s*[:=]\s*[^,\s]+.*version\s*[:=]\s*[^,\s]+`), + } + + for _, pattern := range patterns { + if pattern.MatchString(statement) { + return true + } + } + + return false +} + +func parseDependencyStatement(statement string, variables map[string]string) []models.Package { + var packages []models.Package + + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*['"]([^'"\)]+)['"]`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), + } + + for _, pattern := range patterns { + matches := pattern.FindStringSubmatch(statement) + if len(matches) > 0 { + var group, name, version string + if len(matches) == 3 { + depStr := resolveVariables(matches[2], variables) + parts := strings.Split(depStr, ":") + if len(parts) >= 2 { + group = parts[0] + name = parts[1] + if len(parts) > 2 { + version = strings.Join(parts[2:], ":") + } } + } else if len(matches) == 5 { + group = resolveVariables(matches[2], variables) + name = resolveVariables(matches[3], variables) + version = resolveVariables(matches[4], variables) } + + if group != "" && name != "" { + packages = append(packages, models.Package{ + PackageManager: "gradle", + PackageName: group + ":" + name, + Version: cleanVersion(version), + FilePath: "", + Locations: []models.Location{{}}, + }) + } + } + } + + if len(packages) == 0 { + if pkg := parseDependencyKeyValue(statement, variables); pkg != nil { + packages = append(packages, *pkg) } } return packages } +func parseDependencyKeyValue(statement string, variables map[string]string) *models.Package { + fields := map[string]string{} + + patterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)(group|name|version)\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)(group|name|version)\s*[:=]\s*([A-Za-z_][A-Za-z0-9_]*)`), + } + + for _, pattern := range patterns { + for _, match := range pattern.FindAllStringSubmatch(statement, -1) { + if len(match) > 2 { + key := strings.ToLower(match[1]) + value := match[2] + fields[key] = resolveVariables(value, variables) + } + } + } + + if fields["group"] == "" || fields["name"] == "" { + return nil + } + + return &models.Package{ + PackageManager: "gradle", + PackageName: fields["group"] + ":" + fields["name"], + Version: cleanVersion(fields["version"]), + FilePath: "", + Locations: []models.Location{{}}, + } +} + // resolveVariables replaces ${var} or $var with values func resolveVariables(str string, variables map[string]string) string { // ${var} diff --git a/internal/parsers/gradle/gradle_parser_test.go b/internal/parsers/gradle/gradle_parser_test.go index b307236..04e186d 100644 --- a/internal/parsers/gradle/gradle_parser_test.go +++ b/internal/parsers/gradle/gradle_parser_test.go @@ -81,6 +81,114 @@ buildscript { }, expectedError: false, }, + { + name: "kotlin dsl dependency syntax", + content: `val kotlinVersion = "1.4.32" + +dependencies { + implementation("org.springframework:spring-core:$kotlinVersion") + implementation( + "org.apache.commons:commons-lang3:3.12.0" + ) + implementation(group = "com.google.guava", name = "guava", version = "30.1-jre") + if (project.hasProperty("feature")) { + testImplementation("junit:junit:$kotlinVersion") + } +}`, + expectedPkgs: []models.Package{ + { + PackageManager: "gradle", + PackageName: "org.springframework:spring-core", + Version: "1.4.32", + Locations: []models.Location{ + {Line: 4}, + }, + }, + { + PackageManager: "gradle", + PackageName: "org.apache.commons:commons-lang3", + Version: "3.12.0", + Locations: []models.Location{ + {Line: 5}, + }, + }, + { + PackageManager: "gradle", + PackageName: "com.google.guava:guava", + Version: "30.1-jre", + Locations: []models.Location{ + {Line: 8}, + }, + }, + { + PackageManager: "gradle", + PackageName: "junit:junit", + Version: "1.4.32", + Locations: []models.Location{ + {Line: 10}, + }, + }, + }, + expectedError: false, + }, + { + name: "multi-line and conditional dependencies", + content: `ext { + featureVersion = '1.0.0' +} + +dependencies { + implementation( + 'org.springframework:spring-core:5.3.0' + ) + implementation group: 'org.apache.commons', + name: 'commons-lang3', + version: '3.12.0' + if (project.hasProperty('feature')) { + testImplementation 'junit:junit:$featureVersion' + } + if (useRedux) { + api group: 'com.google.guava', + name: 'guava', + version: '30.1-jre' + } +}`, + expectedPkgs: []models.Package{ + { + PackageManager: "gradle", + PackageName: "org.springframework:spring-core", + Version: "5.3.0", + Locations: []models.Location{ + {Line: 6}, + }, + }, + { + PackageManager: "gradle", + PackageName: "org.apache.commons:commons-lang3", + Version: "3.12.0", + Locations: []models.Location{ + {Line: 9}, + }, + }, + { + PackageManager: "gradle", + PackageName: "junit:junit", + Version: "1.0.0", + Locations: []models.Location{ + {Line: 13}, + }, + }, + { + PackageManager: "gradle", + PackageName: "com.google.guava:guava", + Version: "30.1-jre", + Locations: []models.Location{ + {Line: 16}, + }, + }, + }, + expectedError: false, + }, } for _, tt := range tests { From 08862c0c8d8e143d0ba14a4933f549d5e6d2ca86 Mon Sep 17 00:00:00 2001 From: cx-anurag-dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Fri, 10 Apr 2026 23:48:56 +0530 Subject: [PATCH 3/4] updated gradle-parser --- .claude/settings.local.json | 7 + README.md | 872 +++++++++++++++++- internal/parsers/gradle/gradle_parser.go | 166 +++- internal/parsers/gradle/gradle_parser_test.go | 462 +++++++++- internal/parsers/gradle/version_catalog.go | 210 +++++ test/resources/GRADLE_TEST_FILES_README.md | 308 +++++++ test/resources/build.gradle | 117 ++- test/resources/build.gradle.kts | 366 ++++++++ test/resources/gradle.properties | 88 ++ test/resources/gradle/libs.versions.toml | 228 +++++ 10 files changed, 2776 insertions(+), 48 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 internal/parsers/gradle/version_catalog.go create mode 100644 test/resources/GRADLE_TEST_FILES_README.md create mode 100644 test/resources/build.gradle.kts create mode 100644 test/resources/gradle.properties create mode 100644 test/resources/gradle/libs.versions.toml diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7065ff3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(ls -lh c:/repository/manifest-parser/test/resources/*.gradle* c:/repository/manifest-parser/test/resources/gradle.properties c:/repository/manifest-parser/test/resources/gradle/)" + ] + } +} diff --git a/README.md b/README.md index 20ecfe6..64c538d 100644 --- a/README.md +++ b/README.md @@ -1 +1,871 @@ -# manifest-parser \ No newline at end of file +# Manifest Parser + +A production-grade Go library for parsing dependency manifests across multiple package managers. Extracts package dependencies from build files and dependency declarations in a standardized format for security scanning, SBOM generation, and dependency analysis. + +## ๐ŸŽฏ Purpose + +This parser extracts software dependencies from project manifest files and provides: +- **Standardized Package Output** - Consistent JSON format across all package managers +- **Version Tracking** - Precise version information for vulnerability scanning +- **Location Tracking** - File path and line numbers for each dependency +- **Security Scanning** - Integration with SCA (Software Composition Analysis) tools +- **SBOM Generation** - Software Bill of Materials (cyclonedx, spdx) support + +## ๐Ÿ“ฆ Supported Package Managers + +| Manager | Format | Status | Features | +|---------|--------|--------|----------| +| **Gradle** | `build.gradle`, `build.gradle.kts` | โœ… Production | Latest DSL + catalogs | +| **Maven** | `pom.xml` | โœ… Production | Properties, BOMs, ranges | +| **npm/Node.js** | `package.json` | โœ… Production | Dependencies, dev, peer, optional | +| **Go** | `go.mod` | โœ… Production | Direct imports, indirect | +| **.NET** | `.csproj`, `Directory.Packages.props`, `packages.config` | โœ… Production | Multi-format support | +| **Python** | `requirements.txt` | โœ… Production | Pip format with ranges | + +--- + +## ๐Ÿš€ Quick Start + +### Installation + +```bash +go get github.com/Checkmarx/manifest-parser +``` + +### Usage + +```go +package main + +import ( + "fmt" + "github.com/Checkmarx/manifest-parser/pkg/parser" +) + +func main() { + // Create parser for manifest file + p := parser.ParsersFactory("path/to/package.json") + if p == nil { + fmt.Println("Unsupported manifest type") + return + } + + // Parse dependencies + packages, err := p.Parse("path/to/package.json") + if err != nil { + fmt.Println("Error:", err) + return + } + + // Process results + for _, pkg := range packages { + fmt.Printf("%s:%s@%s\n", pkg.PackageManager, pkg.PackageName, pkg.Version) + } +} +``` + +### Command Line + +```bash +# Parse any supported manifest +go run cmd/main.go path/to/manifest + +# Examples +go run cmd/main.go project/pom.xml +go run cmd/main.go project/package.json +go run cmd/main.go project/build.gradle +go run cmd/main.go project/go.mod +``` + +--- + +## ๐Ÿ“‹ Detailed Parser Documentation + +### 1. Gradle Parser + +**Files:** `build.gradle`, `build.gradle.kts` + +#### Features + +โœ… **Groovy DSL** - Traditional Android/Java Gradle syntax +โœ… **Kotlin DSL** - Modern type-safe Gradle syntax +โœ… **gradle.properties** - Centralized property management +โœ… **Version Catalog** - `gradle/libs.versions.toml` (Gradle 7.0+) +โœ… **BOM/Platform** - Dependency Bill of Materials imports +โœ… **Multi-Module** - Subproject and module-specific configurations +โœ… **19 Configurations** - implementation, api, testImplementation, debugImplementation, ksp, etc. + +#### Dependency Declaration Support + +```gradle +// String notation +implementation 'org.springframework:spring-core:5.3.20' + +// Kotlin DSL +implementation("org.springframework:spring-core:5.3.20") + +// Map notation +implementation group: 'org.springframework', name: 'spring-core', version: '5.3.20' + +// Platform/BOM +implementation platform('org.springframework.boot:spring-boot-dependencies:2.7.0') + +// Version Catalog +implementation(libs.spring.core) +``` + +#### Variable Resolution + +```gradle +// gradle.properties +springVersion=5.3.20 + +// build.gradle +implementation "org.springframework:spring-core:${springVersion}" + +// ext blocks +ext { + log4jVersion = '2.17.1' +} +dependencies { + implementation "org.apache.logging.log4j:log4j-core:$log4jVersion" +} +``` + +#### Supported Configurations + +| Type | Purpose | +|------|---------| +| `implementation` | Runtime + compile dependencies | +| `api` | Public API (exported to consumers) | +| `compileOnly` | Compile-time only (e.g., annotations) | +| `runtimeOnly` | Runtime-only (excluded from compile) | +| `testImplementation` | Test-only dependencies | +| `debugImplementation` | Debug build variant | +| `releaseImplementation` | Release build variant | +| `annotationProcessor` | Annotation code generation | +| `ksp` / `kapt` | Kotlin/Java code generation | +| `classpath` | Buildscript dependencies | +| Plus 9 more variants for testing, fixtures, lint checks | + +#### Example: Multi-Module Project + +```kotlin +// build.gradle.kts +subprojects { + apply(plugin = "java") + + dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + } +} + +project(":api-module") { + dependencies { + implementation(project(":core")) + implementation("org.springframework.security:spring-security-core:5.7.1") + } +} +``` + +#### Version Catalog Support + +```toml +# gradle/libs.versions.toml +[versions] +spring-version = "5.3.20" + +[libraries] +spring-core = { module = "org.springframework:spring-core", version.ref = "spring-version" } + +[bundles] +spring = ["spring-core", "spring-context"] +``` + +#### Parser Capabilities + +- โœ… Parses Groovy and Kotlin DSL +- โœ… Resolves variables from gradle.properties +- โœ… Discovers and parses version catalogs +- โœ… Unwraps platform()/enforcedPlatform() BOMs +- โœ… Walks up directory tree for parent properties +- โœ… Filters out project references (multi-module) +- โœ… Skips file references (local JARs) +- โœ… Handles multi-line declarations +- โœ… Parses conditional if blocks +- โŒ Does not evaluate dynamic Gradle code + +#### Test Resources + +``` +test/resources/ +โ”œโ”€โ”€ build.gradle - Groovy DSL with subprojects +โ”œโ”€โ”€ build.gradle.kts - Kotlin DSL with 5 modules +โ”œโ”€โ”€ gradle.properties - Centralized properties +โ””โ”€โ”€ gradle/libs.versions.toml - 80+ catalog entries +``` + +**Test Coverage:** 16 passing tests including platform dependencies, version catalogs, extended configurations, parent property inheritance + +--- + +### 2. Maven Parser + +**File:** `pom.xml` + +#### Features + +โœ… **Dependency Management** - BOM imports and managed versions +โœ… **Multi-Module** - Parent/child POM relationships +โœ… **Properties** - Variable substitution with `${property}` +โœ… **Version Ranges** - `[1.0,2.0)` notation handling +โœ… **Scopes** - compile, runtime, test, provided, optional, system +โœ… **Location Tracking** - Exact line numbers in POM files + +#### Dependency Declaration Support + +```xml + + + org.springframework + spring-core + 5.3.20 + + + + + junit + junit + 4.13.2 + test + + + + + org.springframework + spring-core + ${spring.version} + + + + + com.example + library + [1.0,2.0) + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.0 + pom + import + + + +``` + +#### Property Resolution + +```xml + + 5.3.20 + + + +${spring.version} +``` + +#### Dependency Scopes + +| Scope | Purpose | +|-------|---------| +| `compile` | Runtime + compile (default) | +| `test` | Test-only dependencies | +| `runtime` | Runtime-only | +| `provided` | Compile-only, provided at runtime | +| `optional` | Included optionally | +| `system` | Local filesystem JAR | + +#### Parser Capabilities + +- โœ… Parses POM XML structure +- โœ… Resolves properties and version ranges +- โœ… Handles BOM imports and managed dependencies +- โœ… Tracks multi-line elements +- โœ… Extracts scope information +- โœ… Locates exact line numbers +- โœ… Supports parent POM references + +#### Example: Multi-Module Project + +```xml + +com.example +parent +1.0.0 +pom + + + core + api + + + + + com.example + parent + 1.0.0 + + +core + + + + org.springframework + spring-core + ${spring.version} + + +``` + +--- + +### 3. NPM/Node.js Parser + +**File:** `package.json` + +#### Features + +โœ… **Dependency Types** - dependencies, devDependencies, peerDependencies, optionalDependencies +โœ… **Version Resolution** - Resolves ranges using package-lock.json +โœ… **Exact Versions** - Extracts actual installed versions from lock files +โœ… **Range Handling** - `^1.0.0`, `~1.0.0`, `*`, ranges + +#### Dependency Declaration Support + +```json +{ + "dependencies": { + "express": "4.18.2", + "lodash": "^4.17.21" + }, + "devDependencies": { + "jest": "~29.0.0", + "webpack": "*" + }, + "peerDependencies": { + "react": "^18.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } +} +``` + +#### Version Specifiers + +| Format | Meaning | +|--------|---------| +| `1.2.3` | Exact version | +| `^1.2.3` | Compatible with 1.2.3 (up to 2.0.0) | +| `~1.2.3` | Approximately 1.2.3 (up to 1.3.0) | +| `>=1.2.3` | Greater than or equal | +| `1.2.x` | Patch-level ranges | +| `*` | Any version | + +#### Dependency Types + +| Type | Purpose | +|------|---------| +| `dependencies` | Production dependencies | +| `devDependencies` | Development-only (testing, bundling) | +| `peerDependencies` | Consumer-provided dependencies | +| `optionalDependencies` | Optional packages | + +#### Parser Capabilities + +- โœ… Parses package.json JSON +- โœ… Resolves version ranges using package-lock.json +- โœ… Extracts all 4 dependency types +- โœ… Handles multiple version specifiers +- โœ… Provides exact installed versions + +#### Example: Large Project + +```json +{ + "name": "my-app", + "version": "1.0.0", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "axios": "^1.4.0" + }, + "devDependencies": { + "@babel/core": "^7.22.0", + "webpack": "^5.88.0", + "jest": "~29.0.0" + } +} +``` + +--- + +### 4. Go Modules Parser + +**File:** `go.mod` + +#### Features + +โœ… **Module Dependencies** - Direct and indirect imports +โœ… **Version Pinning** - Exact semver versions +โœ… **Replace Directives** - Local and remote replacements +โœ… **Exclude Directives** - Version exclusions +โœ… **Go Version** - Minimum Go version requirement + +#### Dependency Declaration Support + +```go +module github.com/example/project + +go 1.19 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/google/uuid v1.3.0 +) + +require ( + github.com/stretchr/testify v1.8.4 // indirect +) + +replace ( + github.com/old/module => github.com/new/module v1.2.3 + github.com/local/module => ./local/path +) + +exclude ( + github.com/bad/module v1.0.0 +) +``` + +#### Dependency Status + +| Type | Purpose | +|------|---------| +| `require` | Direct dependencies | +| `require (indirect)` | Transitive dependencies | +| `replace` | Local/remote replacements | +| `exclude` | Excluded versions | + +#### Parser Capabilities + +- โœ… Parses go.mod file format +- โœ… Extracts direct and indirect imports +- โœ… Handles replace and exclude directives +- โœ… Tracks minimum Go version +- โœ… Provides exact line numbers + +#### Example: Complex Project + +```go +module github.com/checkmarx/scanner + +go 1.20 + +require ( + github.com/spf13/cobra v1.7.0 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/inconshreveable/log15 v2.3.2 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/local/package => ../local/package + +exclude golang.org/x/text v0.3.0 +``` + +--- + +### 5. .NET / C# Parser + +**Files:** `.csproj`, `Directory.Packages.props`, `packages.config` + +#### Features + +โœ… **Project References** - `.csproj` PackageReference elements +โœ… **Centralized Management** - `Directory.Packages.props` for monorepos +โœ… **Legacy Format** - `packages.config` (NuGet v2) +โœ… **Target Frameworks** - Framework-specific dependencies +โœ… **Metadata** - Version, Include, Exclude attributes + +#### Dependency Declaration Support + +##### `.csproj` Format (Modern) + +```xml + + + + + +``` + +##### `Directory.Packages.props` (Centralized) + +```xml + + true + + + + + + +``` + +##### `packages.config` (Legacy NuGet) + +```xml + + + + + +``` + +#### Package Metadata + +| Attribute | Purpose | +|-----------|---------| +| `Include` / `id` | Package name | +| `Version` | Semantic version | +| `TargetFramework` | Framework specificity | +| `Condition` | Conditional inclusion | +| `Exclude` | Excluded frameworks | + +#### Parser Capabilities + +- โœ… Parses `.csproj` XML structure +- โœ… Extracts `Directory.Packages.props` central versions +- โœ… Handles legacy `packages.config` format +- โœ… Respects framework-specific conditions +- โœ… Tracks line numbers and locations + +#### Example: Multi-Framework Project + +```xml + + + + net6.0;net8.0;net472 + + + + + + + +``` + +--- + +### 6. Python / Pip Parser + +**File:** `requirements.txt` + +#### Features + +โœ… **Pip Format** - Standard Python dependency format +โœ… **Version Specifiers** - `==`, `>=`, `<=`, `~=`, ranges +โœ… **Comments & Empty Lines** - Properly ignored +โœ… **Environment Markers** - OS/Python version conditions +โœ… **Git References** - VCS dependencies + +#### Dependency Declaration Support + +```txt +# Production dependencies +Django==4.2.0 +djangorestframework>=3.14.0,<4.0 +requests~=2.31.0 + +# Dev dependencies +pytest>=7.0.0 +black==23.0.0 + +# Git references +git+https://github.com/example/repo.git@main#egg=mypackage + +# With environment markers +pywin32>=300; sys_platform == 'win32' +``` + +#### Version Specifiers + +| Specifier | Meaning | +|-----------|---------| +| `==1.4.2` | Exact version | +| `>=1.4.2` | Greater than or equal | +| `<=1.4.2` | Less than or equal | +| `!=1.4.2` | Not equal | +| `~=1.4.2` | Compatible release (1.4.x) | +| `*` | Any version | + +#### Environment Markers + +```txt +# Platform-specific +pywin32>=300; sys_platform == 'win32' + +# Python version specific +dataclasses; python_version < '3.7' + +# Complex conditions +numpy>=1.20; python_version >= '3.8' and sys_platform != 'win32' +``` + +#### Parser Capabilities + +- โœ… Parses pip requirements format +- โœ… Extracts package names and versions +- โœ… Handles version specifier ranges +- โœ… Recognizes environment markers +- โœ… Ignores comments and blank lines + +#### Example: Complete Project + +```txt +# Python 3.8+ +Python>=3.8 + +# Web Framework +Flask==2.3.0 +Flask-SQLAlchemy>=3.0.0,<4.0 + +# Database +psycopg2-binary~=2.9.0 +SQLAlchemy>=2.0.0 + +# Testing +pytest>=7.0.0 +pytest-cov>=4.0.0 + +# Development +black==23.0.0 +flake8>=6.0.0 + +# OS-specific +pywin32>=300; sys_platform == 'win32' +``` + +--- + +## ๐Ÿ“Š Output Format + +All parsers return a standardized `Package` structure: + +```go +type Package struct { + PackageManager string // "gradle", "maven", "npm", "go", "dotnet", "pip" + PackageName string // "group:name" or "name" + Version string // "1.2.3" + FilePath string // Path to manifest file + Locations []Location // Line numbers +} + +type Location struct { + Line int // Line number (1-indexed) + StartIndex int // Character offset + EndIndex int // Character offset +} +``` + +### JSON Output Example + +```json +[ + { + "packageManager": "gradle", + "packageName": "org.springframework:spring-core", + "version": "5.3.20", + "filePath": "build.gradle", + "locations": [ + { + "line": 42, + "startIndex": 0, + "endIndex": 0 + } + ] + }, + { + "packageManager": "maven", + "packageName": "com.google.guava:guava", + "version": "31.1-jre", + "filePath": "pom.xml", + "locations": [ + { + "line": 127, + "startIndex": 0, + "endIndex": 0 + } + ] + } +] +``` + +--- + +## ๐Ÿ”’ Security & Vulnerability Detection + +This parser is designed to support security scanning and SCA (Software Composition Analysis) tools: + +### Integration with Vulnerability Databases + +``` +Dependency Extraction โ†’ Vulnerability Database โ†’ Risk Assessment + (NVD CVE) + (GitHub Advisory) + (Snyk Database) + (Sonatype OSS) +``` + +### Example: Detecting Log4j RCE + +```gradle +dependencies { + implementation 'org.apache.logging.log4j:log4j-core:2.14.0' // CVE-2021-44228 +} +``` + +Parser extracts โ†’ `org.apache.logging.log4j:log4j-core:2.14.0` +โ†“ +Vulnerability checker matches โ†’ CVE-2021-44228 (CRITICAL - Log4Shell RCE) + +--- + +## ๐Ÿ—๏ธ Architecture + +``` +Parser Interface (parser.go) + โ†“ +Manifest Detection (manifest-file-selector.go) + โ†“ +Parser Factory (parser_factory.go) + โ†“ +Language-Specific Parsers + โ”œโ”€ Gradle Parser (gradle/gradle_parser.go, gradle/version_catalog.go) + โ”œโ”€ Maven Parser (maven/maven-pom-parser.go) + โ”œโ”€ npm Parser (npm/package_json_parser.go) + โ”œโ”€ Go Parser (golang/go-mod-parser.go) + โ”œโ”€ .NET Parsers (dotnet/csproj_parser.go, etc.) + โ””โ”€ Python Parser (pypi/pypi-parser.go) + โ†“ +Standardized Package Output (models/package_model.go) +``` + +--- + +## ๐Ÿงช Testing + +Run tests for all parsers: + +```bash +# Run all tests +go test ./... + +# Run specific parser tests +go test ./internal/parsers/gradle/ -v +go test ./internal/parsers/maven/ -v +go test ./internal/parsers/npm/ -v + +# With coverage +go test ./... -cover +``` + +### Test Resources + +``` +test/resources/ +โ”œโ”€โ”€ build.gradle (Gradle DSL) +โ”œโ”€โ”€ build.gradle.kts (Kotlin DSL) +โ”œโ”€โ”€ pom.xml (Maven) +โ”œโ”€โ”€ package.json (npm) +โ”œโ”€โ”€ test_go.mod (Go Modules) +โ”œโ”€โ”€ Bootstrap.csproj (.NET Framework) +โ”œโ”€โ”€ Directory.Packages.props (.NET Centralized) +โ”œโ”€โ”€ packages.config (.NET Legacy) +โ””โ”€โ”€ requirements.txt (Python) +``` + +--- + +## ๐Ÿ“š Documentation + +- [Gradle Parser Details](test/resources/GRADLE_TEST_FILES_README.md) - Comprehensive Gradle documentation with 31 vulnerable dependencies for testing +- [Maven Documentation](https://maven.apache.org/pom.html) +- [npm Documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) +- [Go Modules Documentation](https://go.dev/ref/mod) +- [NuGet Documentation](https://learn.microsoft.com/en-us/nuget/) +- [Pip Documentation](https://pip.pypa.io/) + +--- + +## ๐Ÿค Contributing + +Contributions welcome! Focus areas: + +- [ ] Add Ruby Bundler support (Gemfile) +- [ ] Add PHP Composer support (composer.json) +- [ ] Add Rust Cargo support (Cargo.toml) +- [ ] Improve version range resolution +- [ ] Add more vulnerability test cases +- [ ] Performance optimizations + +--- + +## โš–๏ธ License + +This project is part of the Checkmarx AST (Application Security Testing) suite. + +--- + +## ๐Ÿš€ Features Summary + +| Feature | Gradle | Maven | npm | Go | .NET | Python | +|---------|--------|-------|-----|----|----|--------| +| Multi-file format | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | +| Property resolution | โœ… | โœ… | โŒ | โŒ | โŒ | โŒ | +| Version ranges | โœ… | โœ… | โœ… | โŒ | โœ… | โœ… | +| BOM imports | โœ… | โœ… | โŒ | โŒ | โŒ | โŒ | +| Multi-module | โœ… | โœ… | โŒ | โŒ | โœ… | โŒ | +| Line numbers | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | +| Comments/ignored | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | +| Scope separation | โœ… | โœ… | โœ… | โŒ | โœ… | โŒ | + +--- + +## ๐Ÿ“ Version History + +- **v3.0.0** - Added Gradle version catalog support, enhanced property resolution +- **v2.5.0** - Added .NET Directory.Packages.props support +- **v2.0.0** - Initial multi-parser support + +--- + +## ๐Ÿ“ง Contact & Support + +For issues, questions, or feature requests: +- GitHub Issues: [manifest-parser/issues](https://github.com/Checkmarx/manifest-parser/issues) +- Security: [security@checkmarx.com](mailto:security@checkmarx.com) + +--- + +**Made with โค๏ธ for secure software supply chain management** diff --git a/internal/parsers/gradle/gradle_parser.go b/internal/parsers/gradle/gradle_parser.go index af376a7..563793e 100644 --- a/internal/parsers/gradle/gradle_parser.go +++ b/internal/parsers/gradle/gradle_parser.go @@ -10,6 +10,13 @@ import ( "github.com/Checkmarx/manifest-parser/pkg/parser/models" ) +// configKeywords defines all supported Gradle dependency configuration keywords +var configKeywords = `implementation|api|compile|compileOnly|runtime|runtimeOnly|` + + `testImplementation|testCompile|testCompileOnly|testRuntimeOnly|` + + `androidTestImplementation|debugImplementation|releaseImplementation|` + + `annotationProcessor|classpath|kapt|ksp|compileOnlyApi|` + + `testFixturesImplementation|testFixturesApi|lintChecks` + // GradleParser implements parsing of Gradle build files type GradleParser struct{} @@ -25,6 +32,12 @@ func (p *GradleParser) Parse(manifestFile string) ([]models.Package, error) { // Extract variables variables := extractVariables(manifestFile, manifestContent) + // Load version catalog if available + var catalog *VersionCatalog + if catalogPath := findVersionCatalog(manifestFile); catalogPath != "" { + catalog = parseVersionCatalog(catalogPath) + } + var packages []models.Package // Parse main dependencies @@ -34,6 +47,15 @@ func (p *GradleParser) Parse(manifestFile string) ([]models.Package, error) { } packages = append(packages, mainDeps...) + // Parse version catalog dependencies (libs.xxx references) + if catalog != nil { + catalogDeps := parseVersionCatalogDependencies(manifestContent, catalog) + for i := range catalogDeps { + catalogDeps[i].FilePath = manifestFile + } + packages = append(packages, catalogDeps...) + } + return packages, nil } @@ -44,30 +66,41 @@ func extractVariables(manifestFile, content string) map[string]string { // Read gradle.properties if exists gradlePropsPath := filepath.Join(filepath.Dir(manifestFile), "gradle.properties") if propsContent, err := os.ReadFile(gradlePropsPath); err == nil { - for _, line := range strings.Split(string(propsContent), "\n") { - line = strings.TrimSpace(line) - if strings.Contains(line, "=") && !strings.HasPrefix(line, "#") { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - vars[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } + parsePropertiesInto(string(propsContent), vars) + } + + // Walk up to project root for parent gradle.properties + projectRoot := findProjectRoot(filepath.Dir(manifestFile)) + if projectRoot != filepath.Dir(manifestFile) { + rootPropsPath := filepath.Join(projectRoot, "gradle.properties") + if propsContent, err := os.ReadFile(rootPropsPath); err == nil { + parsePropertiesInto(string(propsContent), vars) } } - // Extract from ext blocks (Groovy) + // Extract from ext blocks (Groovy) โ€” handle all ext blocks, filter commented lines extPattern := regexp.MustCompile(`(?s)ext\s*\{([^}]+)\}`) - if matches := extPattern.FindStringSubmatch(content); len(matches) > 1 { - extContent := matches[1] - // Simple key = 'value' or key: 'value' - varPatterns := []*regexp.Regexp{ - regexp.MustCompile(`(\w+)\s*=\s*['"]([^'"]+)['"]`), - regexp.MustCompile(`(\w+)\s*:\s*['"]([^'"]+)['"]`), - } - for _, pattern := range varPatterns { - for _, match := range pattern.FindAllStringSubmatch(extContent, -1) { - if len(match) > 2 { - vars[match[1]] = match[2] + for _, matches := range extPattern.FindAllStringSubmatch(content, -1) { + if len(matches) > 1 { + // Filter commented lines from ext block content + var filteredLines []string + for _, line := range strings.Split(matches[1], "\n") { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "//") && !strings.HasPrefix(trimmed, "*") { + filteredLines = append(filteredLines, line) + } + } + extContent := strings.Join(filteredLines, "\n") + // Simple key = 'value' or key: 'value' + varPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(\w+)\s*=\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(\w+)\s*:\s*['"]([^'"]+)['"]`), + } + for _, pattern := range varPatterns { + for _, match := range pattern.FindAllStringSubmatch(extContent, -1) { + if len(match) > 2 { + vars[match[1]] = match[2] + } } } } @@ -118,7 +151,7 @@ func parseDependencies(content string, variables map[string]string) []models.Pac } func extractDependencyStatements(content string) []dependencyStatement { - startPattern := regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\b`) + startPattern := regexp.MustCompile(`(?i)\b(` + configKeywords + `)\b`) var statements []dependencyStatement var buffer strings.Builder active := false @@ -133,12 +166,17 @@ func extractDependencyStatements(content string) []dependencyStatement { if !active { if startPattern.MatchString(line) { + // Skip non-Maven dependency references + if isProjectReference(line) || isFileReference(line) || isVersionCatalogReference(line) { + continue + } active = true startLine = i + 1 buffer.Reset() buffer.WriteString(line) - if dependencyStatementComplete(buffer.String()) { - statements = append(statements, dependencyStatement{Line: startLine, Text: buffer.String()}) + normalized := normalizePlatformDependency(buffer.String()) + if dependencyStatementComplete(normalized) { + statements = append(statements, dependencyStatement{Line: startLine, Text: normalized}) active = false } } @@ -147,8 +185,9 @@ func extractDependencyStatements(content string) []dependencyStatement { buffer.WriteString(" ") buffer.WriteString(line) - if dependencyStatementComplete(buffer.String()) { - statements = append(statements, dependencyStatement{Line: startLine, Text: buffer.String()}) + normalized := normalizePlatformDependency(buffer.String()) + if dependencyStatementComplete(normalized) { + statements = append(statements, dependencyStatement{Line: startLine, Text: normalized}) active = false } } @@ -157,11 +196,12 @@ func extractDependencyStatements(content string) []dependencyStatement { } func dependencyStatementComplete(statement string) bool { + kw := configKeywords patterns := []*regexp.Regexp{ - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*['"]([^'"\)]+)['"]`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*['"]([^'"\)]+)['"]`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), regexp.MustCompile(`(?i)group\s*[:=]\s*['"]([^'"]+)['"].*name\s*[:=]\s*['"]([^'"]+)['"].*version\s*[:=]\s*['"]([^'"]+)['"]`), regexp.MustCompile(`(?i)group\s*[:=]\s*[^,\s]+.*name\s*[:=]\s*[^,\s]+.*version\s*[:=]\s*[^,\s]+`), } @@ -178,11 +218,12 @@ func dependencyStatementComplete(statement string) bool { func parseDependencyStatement(statement string, variables map[string]string) []models.Package { var packages []models.Package + kw := configKeywords patterns := []*regexp.Regexp{ - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*['"]([^'"\)]+)['"]`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), - regexp.MustCompile(`(?i)\b(implementation|api|compile|compileOnly|runtime|runtimeOnly|testImplementation|testCompile|testRuntimeOnly|androidTestImplementation|annotationProcessor|classpath|kapt)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*['"]([^'"\)]+)['"]`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*\(\s*['"]([^'"\)]+)['"]\s*\)`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]`), + regexp.MustCompile(`(?i)\b(` + kw + `)\s*\(\s*group\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*name\s*[:=]\s*['"]([^'"]+)['"]\s*,\s*version\s*[:=]\s*['"]([^'"]+)['"]\s*\)`), } for _, pattern := range patterns { @@ -311,3 +352,62 @@ func findLineNumber(content, substr string) int { } return strings.Count(content[:index], "\n") + 1 } + +// parsePropertiesInto parses key=value properties into the given map (does not overwrite existing keys) +func parsePropertiesInto(content string, vars map[string]string) { + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if strings.Contains(line, "=") && !strings.HasPrefix(line, "#") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + if _, exists := vars[key]; !exists { + vars[key] = strings.TrimSpace(parts[1]) + } + } + } + } +} + +// findProjectRoot walks up from dir looking for settings.gradle or settings.gradle.kts +func findProjectRoot(dir string) string { + current := dir + for { + if _, err := os.Stat(filepath.Join(current, "settings.gradle")); err == nil { + return current + } + if _, err := os.Stat(filepath.Join(current, "settings.gradle.kts")); err == nil { + return current + } + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + return dir +} + +// isProjectReference checks if a dependency statement is a project reference +func isProjectReference(statement string) bool { + pattern := regexp.MustCompile(`(?i)\b(?:` + configKeywords + `)\s*(?:\(\s*)?project\s*\(`) + return pattern.MatchString(statement) +} + +// isFileReference checks if a dependency statement is a file reference (files/fileTree) +func isFileReference(statement string) bool { + pattern := regexp.MustCompile(`(?i)\b(?:` + configKeywords + `)\s*(?:\(\s*)?(?:files|fileTree)\s*\(`) + return pattern.MatchString(statement) +} + +// isVersionCatalogReference checks if a dependency uses version catalog syntax (libs.xxx) +func isVersionCatalogReference(statement string) bool { + pattern := regexp.MustCompile(`(?i)\b(?:` + configKeywords + `)\s*(?:\(\s*)?libs\.`) + return pattern.MatchString(statement) +} + +// normalizePlatformDependency strips platform() and enforcedPlatform() wrappers +func normalizePlatformDependency(statement string) string { + pattern := regexp.MustCompile(`\b(?:platform|enforcedPlatform)\s*\(\s*(['"][^'"]+['"])\s*\)`) + return pattern.ReplaceAllString(statement, "$1") +} diff --git a/internal/parsers/gradle/gradle_parser_test.go b/internal/parsers/gradle/gradle_parser_test.go index 04e186d..f8d48a6 100644 --- a/internal/parsers/gradle/gradle_parser_test.go +++ b/internal/parsers/gradle/gradle_parser_test.go @@ -262,7 +262,467 @@ func TestGradleParser_ParseFile(t *testing.T) { t.Errorf("Package name is empty") } if pkg.Version == "" { - t.Errorf("Version is empty") + t.Errorf("Version is empty for %s", pkg.PackageName) + } + } +} + +func TestGradleParser_ParseFile_NoProjectReferences(t *testing.T) { + parser := &GradleParser{} + pkgs, err := parser.Parse(filepath.Join("..", "..", "..", "test", "resources", "build.gradle")) + if err != nil { + t.Fatalf("Failed to parse build.gradle: %v", err) + } + + for _, pkg := range pkgs { + if pkg.PackageName == ":core" || pkg.PackageName == ":app" || pkg.PackageName == ":security" { + t.Errorf("Project reference should not be extracted as a package: %s", pkg.PackageName) + } + } +} + +func TestGradleParser_ParseFile_VariableResolution(t *testing.T) { + parser := &GradleParser{} + pkgs, err := parser.Parse(filepath.Join("..", "..", "..", "test", "resources", "build.gradle")) + if err != nil { + t.Fatalf("Failed to parse build.gradle: %v", err) + } + + for _, pkg := range pkgs { + if pkg.PackageName == "org.springframework.boot:spring-boot-starter-web" { + if pkg.Version != "2.5.0" { + t.Errorf("Expected spring-boot-starter-web version '2.5.0', got '%s'", pkg.Version) + } + return + } + } + t.Errorf("Expected to find org.springframework.boot:spring-boot-starter-web in packages") +} + +func TestGradleParser_ProjectReferencesSkipped(t *testing.T) { + content := `dependencies { + implementation project(':core') + implementation(project(':lib')) + implementation 'org.apache.commons:commons-lang3:3.8' + api project(":shared") +}` + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(content) + tmpFile.Close() + + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(pkgs) != 1 { + t.Fatalf("Expected 1 package, got %d: %+v", len(pkgs), pkgs) + } + if pkgs[0].PackageName != "org.apache.commons:commons-lang3" { + t.Errorf("Expected commons-lang3, got %s", pkgs[0].PackageName) + } +} + +func TestGradleParser_PlatformDependencies(t *testing.T) { + content := `dependencies { + implementation platform('org.springframework.boot:spring-boot-dependencies:2.5.0') + implementation enforcedPlatform('com.google.cloud:libraries-bom:26.1.0') + implementation(platform("org.junit:junit-bom:5.9.0")) + implementation 'org.springframework:spring-core:5.3.0' +}` + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(content) + tmpFile.Close() + + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expectedPkgs := map[string]string{ + "org.springframework.boot:spring-boot-dependencies": "2.5.0", + "com.google.cloud:libraries-bom": "26.1.0", + "org.junit:junit-bom": "5.9.0", + "org.springframework:spring-core": "5.3.0", + } + + if len(pkgs) != len(expectedPkgs) { + t.Fatalf("Expected %d packages, got %d: %+v", len(expectedPkgs), len(pkgs), pkgs) + } + + for _, pkg := range pkgs { + expectedVersion, ok := expectedPkgs[pkg.PackageName] + if !ok { + t.Errorf("Unexpected package: %s", pkg.PackageName) + continue + } + if pkg.Version != expectedVersion { + t.Errorf("Package %s: expected version %s, got %s", pkg.PackageName, expectedVersion, pkg.Version) + } + } +} + +func TestGradleParser_FileReferencesSkipped(t *testing.T) { + content := `dependencies { + implementation files('libs/local.jar') + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'org.apache.commons:commons-lang3:3.8' +}` + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(content) + tmpFile.Close() + + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(pkgs) != 1 { + t.Fatalf("Expected 1 package, got %d: %+v", len(pkgs), pkgs) + } + if pkgs[0].PackageName != "org.apache.commons:commons-lang3" { + t.Errorf("Expected commons-lang3, got %s", pkgs[0].PackageName) + } +} + +func TestGradleParser_ExtendedConfigurations(t *testing.T) { + content := `dependencies { + debugImplementation 'com.facebook.stetho:stetho:1.6.0' + releaseImplementation 'com.google.firebase:firebase-crashlytics:18.0.0' + ksp 'com.google.dagger:dagger-compiler:2.44' + compileOnlyApi 'org.projectlombok:lombok:1.18.24' + testCompileOnly 'org.mockito:mockito-core:4.0.0' + lintChecks 'com.android.tools.lint:lint-checks:30.0.0' +}` + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(content) + tmpFile.Close() + + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expectedNames := []string{ + "com.facebook.stetho:stetho", + "com.google.firebase:firebase-crashlytics", + "com.google.dagger:dagger-compiler", + "org.projectlombok:lombok", + "org.mockito:mockito-core", + "com.android.tools.lint:lint-checks", + } + + if len(pkgs) != len(expectedNames) { + t.Fatalf("Expected %d packages, got %d: %+v", len(expectedNames), len(pkgs), pkgs) + } + + for i, pkg := range pkgs { + if pkg.PackageName != expectedNames[i] { + t.Errorf("Package %d: expected %s, got %s", i, expectedNames[i], pkg.PackageName) + } + } +} + +func TestGradleParser_CommentedExtBlocksIgnored(t *testing.T) { + content := ` +// ext { +// badVar = '0.0.0' +// } + +ext { + goodVar = '1.0.0' +} + +dependencies { + implementation "org.example:lib:$goodVar" +}` + tmpFile, err := os.CreateTemp("", "build.gradle") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(content) + tmpFile.Close() + + parser := &GradleParser{} + pkgs, err := parser.Parse(tmpFile.Name()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(pkgs) != 1 { + t.Fatalf("Expected 1 package, got %d: %+v", len(pkgs), pkgs) + } + if pkgs[0].Version != "1.0.0" { + t.Errorf("Expected version '1.0.0' from non-commented ext block, got '%s'", pkgs[0].Version) + } +} + +func TestGradleParser_ParentGradleProperties(t *testing.T) { + // Create a directory structure: parent/child/ + parentDir, err := os.MkdirTemp("", "gradle-parent") + if err != nil { + t.Fatalf("Failed to create parent dir: %v", err) + } + defer os.RemoveAll(parentDir) + + childDir := filepath.Join(parentDir, "child") + os.Mkdir(childDir, 0755) + + // Create settings.gradle in parent to mark it as project root + os.WriteFile(filepath.Join(parentDir, "settings.gradle"), []byte("include ':child'"), 0644) + + // Create parent gradle.properties + os.WriteFile(filepath.Join(parentDir, "gradle.properties"), []byte("parentVersion=3.0.0\nsharedVersion=1.0.0"), 0644) + + // Create child gradle.properties (overrides sharedVersion) + os.WriteFile(filepath.Join(childDir, "gradle.properties"), []byte("sharedVersion=2.0.0"), 0644) + + // Create child build.gradle + buildContent := `dependencies { + implementation "org.example:parent-lib:$parentVersion" + implementation "org.example:shared-lib:$sharedVersion" +}` + buildFile := filepath.Join(childDir, "build.gradle") + os.WriteFile(buildFile, []byte(buildContent), 0644) + + parser := &GradleParser{} + pkgs, err := parser.Parse(buildFile) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(pkgs) != 2 { + t.Fatalf("Expected 2 packages, got %d: %+v", len(pkgs), pkgs) + } + + // Parent property should be resolved + if pkgs[0].Version != "3.0.0" { + t.Errorf("Expected parent-lib version '3.0.0', got '%s'", pkgs[0].Version) + } + // Child property should take precedence over parent + if pkgs[1].Version != "2.0.0" { + t.Errorf("Expected shared-lib version '2.0.0' (child overrides parent), got '%s'", pkgs[1].Version) + } +} + +func TestVersionCatalog_Parse(t *testing.T) { + catalogContent := `[versions] +spring = "5.3.0" +guava = "30.1-jre" + +[libraries] +spring-core = { module = "org.springframework:spring-core", version.ref = "spring" } +spring-web = { module = "org.springframework:spring-web", version = "5.2.0" } +guava = "com.google.guava:guava:30.1-jre" +commons = { group = "org.apache.commons", name = "commons-lang3", version.ref = "spring" } +` + tmpFile, err := os.CreateTemp("", "libs.versions.toml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString(catalogContent) + tmpFile.Close() + + catalog := parseVersionCatalog(tmpFile.Name()) + if catalog == nil { + t.Fatalf("Failed to parse version catalog") + } + + // Check versions + if catalog.Versions["spring"] != "5.3.0" { + t.Errorf("Expected spring version '5.3.0', got '%s'", catalog.Versions["spring"]) + } + if catalog.Versions["guava"] != "30.1-jre" { + t.Errorf("Expected guava version '30.1-jre', got '%s'", catalog.Versions["guava"]) + } + + // Check libraries + tests := []struct { + key string + group string + name string + version string + }{ + {"spring-core", "org.springframework", "spring-core", "5.3.0"}, + {"spring-web", "org.springframework", "spring-web", "5.2.0"}, + {"guava", "com.google.guava", "guava", "30.1-jre"}, + {"commons", "org.apache.commons", "commons-lang3", "5.3.0"}, + } + + for _, tt := range tests { + lib, ok := catalog.Libraries[tt.key] + if !ok { + t.Errorf("Library '%s' not found in catalog", tt.key) + continue + } + if lib.Group != tt.group { + t.Errorf("Library '%s': expected group '%s', got '%s'", tt.key, tt.group, lib.Group) + } + if lib.Name != tt.name { + t.Errorf("Library '%s': expected name '%s', got '%s'", tt.key, tt.name, lib.Name) + } + if lib.Version != tt.version { + t.Errorf("Library '%s': expected version '%s', got '%s'", tt.key, tt.version, lib.Version) + } + } +} + +func TestVersionCatalog_DependencyResolution(t *testing.T) { + // Create directory structure with version catalog + projectDir, err := os.MkdirTemp("", "gradle-catalog") + if err != nil { + t.Fatalf("Failed to create project dir: %v", err) + } + defer os.RemoveAll(projectDir) + + gradleDir := filepath.Join(projectDir, "gradle") + os.Mkdir(gradleDir, 0755) + + // Create settings.gradle to mark project root + os.WriteFile(filepath.Join(projectDir, "settings.gradle"), []byte(""), 0644) + + // Create version catalog + catalogContent := `[versions] +spring = "5.3.0" + +[libraries] +spring-core = { module = "org.springframework:spring-core", version.ref = "spring" } +guava = "com.google.guava:guava:30.1-jre" +` + os.WriteFile(filepath.Join(gradleDir, "libs.versions.toml"), []byte(catalogContent), 0644) + + // Create build.gradle with catalog references + buildContent := `dependencies { + implementation libs.spring.core + implementation(libs.guava) + implementation 'org.direct:dependency:1.0.0' +}` + buildFile := filepath.Join(projectDir, "build.gradle") + os.WriteFile(buildFile, []byte(buildContent), 0644) + + parser := &GradleParser{} + pkgs, err := parser.Parse(buildFile) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expectedPkgs := map[string]string{ + "org.direct:dependency": "1.0.0", + "org.springframework:spring-core": "5.3.0", + "com.google.guava:guava": "30.1-jre", + } + + if len(pkgs) != len(expectedPkgs) { + t.Fatalf("Expected %d packages, got %d: %+v", len(expectedPkgs), len(pkgs), pkgs) + } + + for _, pkg := range pkgs { + expectedVersion, ok := expectedPkgs[pkg.PackageName] + if !ok { + t.Errorf("Unexpected package: %s", pkg.PackageName) + continue + } + if pkg.Version != expectedVersion { + t.Errorf("Package %s: expected version %s, got %s", pkg.PackageName, expectedVersion, pkg.Version) + } + } +} + +func TestIsProjectReference(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"implementation project(':core')", true}, + {"implementation(project(':core'))", true}, + {`implementation project(":core")`, true}, + {"api project(':shared')", true}, + {"implementation 'org.example:lib:1.0'", false}, + {`implementation("org.example:lib:1.0")`, false}, + } + + for _, tt := range tests { + result := isProjectReference(tt.input) + if result != tt.expected { + t.Errorf("isProjectReference(%q) = %v, want %v", tt.input, result, tt.expected) + } + } +} + +func TestIsFileReference(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"implementation files('libs/local.jar')", true}, + {"implementation fileTree(dir: 'libs', include: ['*.jar'])", true}, + {"implementation(files('libs/local.jar'))", true}, + {"implementation 'org.example:lib:1.0'", false}, + } + + for _, tt := range tests { + result := isFileReference(tt.input) + if result != tt.expected { + t.Errorf("isFileReference(%q) = %v, want %v", tt.input, result, tt.expected) + } + } +} + +func TestNormalizePlatformDependency(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + "implementation platform('org.springframework.boot:spring-boot-dependencies:2.5.0')", + "implementation 'org.springframework.boot:spring-boot-dependencies:2.5.0'", + }, + { + "implementation enforcedPlatform('com.google.cloud:libraries-bom:26.1.0')", + "implementation 'com.google.cloud:libraries-bom:26.1.0'", + }, + { + `implementation(platform("org.junit:junit-bom:5.9.0"))`, + `implementation("org.junit:junit-bom:5.9.0")`, + }, + { + "implementation 'org.example:lib:1.0'", + "implementation 'org.example:lib:1.0'", + }, + } + + for _, tt := range tests { + result := normalizePlatformDependency(tt.input) + if result != tt.expected { + t.Errorf("normalizePlatformDependency(%q) = %q, want %q", tt.input, result, tt.expected) } } } diff --git a/internal/parsers/gradle/version_catalog.go b/internal/parsers/gradle/version_catalog.go new file mode 100644 index 0000000..220c188 --- /dev/null +++ b/internal/parsers/gradle/version_catalog.go @@ -0,0 +1,210 @@ +package gradle + +import ( + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Checkmarx/manifest-parser/pkg/parser/models" +) + +// VersionCatalog represents a parsed Gradle version catalog (libs.versions.toml) +type VersionCatalog struct { + Versions map[string]string + Libraries map[string]CatalogLibrary +} + +// CatalogLibrary represents a library entry in the version catalog +type CatalogLibrary struct { + Group string + Name string + Version string +} + +// findVersionCatalog locates gradle/libs.versions.toml relative to the project root +func findVersionCatalog(manifestFile string) string { + projectRoot := findProjectRoot(filepath.Dir(manifestFile)) + catalogPath := filepath.Join(projectRoot, "gradle", "libs.versions.toml") + if _, err := os.Stat(catalogPath); err == nil { + return catalogPath + } + return "" +} + +// parseVersionCatalog reads and parses a libs.versions.toml file +func parseVersionCatalog(path string) *VersionCatalog { + content, err := os.ReadFile(path) + if err != nil { + return nil + } + + catalog := &VersionCatalog{ + Versions: make(map[string]string), + Libraries: make(map[string]CatalogLibrary), + } + + lines := strings.Split(string(content), "\n") + currentSection := "" + + sectionPattern := regexp.MustCompile(`^\s*\[(\w+)\]\s*$`) + simpleKV := regexp.MustCompile(`^\s*([^\s=]+)\s*=\s*"([^"]+)"\s*$`) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if match := sectionPattern.FindStringSubmatch(line); len(match) > 1 { + currentSection = match[1] + continue + } + + switch currentSection { + case "versions": + if match := simpleKV.FindStringSubmatch(line); len(match) > 2 { + catalog.Versions[match[1]] = match[2] + } + case "libraries": + parseCatalogLibraryEntry(line, catalog) + } + } + + // Resolve version.ref references + for key, lib := range catalog.Libraries { + if strings.HasPrefix(lib.Version, "ref:") { + refName := strings.TrimPrefix(lib.Version, "ref:") + if resolved, ok := catalog.Versions[refName]; ok { + lib.Version = resolved + catalog.Libraries[key] = lib + } + } + } + + return catalog +} + +// parseCatalogLibraryEntry parses a single library line from the version catalog +func parseCatalogLibraryEntry(line string, catalog *VersionCatalog) { + // Pattern: key = "group:name:version" + simplePattern := regexp.MustCompile(`^\s*([^\s=]+)\s*=\s*"([^"]+)"\s*$`) + if match := simplePattern.FindStringSubmatch(line); len(match) > 2 { + parts := strings.Split(match[2], ":") + if len(parts) >= 2 { + lib := CatalogLibrary{ + Group: parts[0], + Name: parts[1], + } + if len(parts) >= 3 { + lib.Version = parts[2] + } + catalog.Libraries[match[1]] = lib + return + } + } + + // Pattern: key = { module = "group:name", version.ref = "xxx" } + // Pattern: key = { module = "group:name", version = "xxx" } + // Pattern: key = { group = "g", name = "n", version.ref = "xxx" } + // Pattern: key = { group = "g", name = "n", version = "xxx" } + kvPattern := regexp.MustCompile(`^\s*([^\s=]+)\s*=\s*\{(.+)\}\s*$`) + if match := kvPattern.FindStringSubmatch(line); len(match) > 2 { + key := match[1] + body := match[2] + + lib := CatalogLibrary{} + + // Extract module = "group:name" + modulePattern := regexp.MustCompile(`module\s*=\s*"([^"]+)"`) + if m := modulePattern.FindStringSubmatch(body); len(m) > 1 { + parts := strings.Split(m[1], ":") + if len(parts) >= 2 { + lib.Group = parts[0] + lib.Name = parts[1] + } + } + + // Extract group/name separately + groupPattern := regexp.MustCompile(`group\s*=\s*"([^"]+)"`) + namePattern := regexp.MustCompile(`name\s*=\s*"([^"]+)"`) + if m := groupPattern.FindStringSubmatch(body); len(m) > 1 { + lib.Group = m[1] + } + if m := namePattern.FindStringSubmatch(body); len(m) > 1 { + lib.Name = m[1] + } + + // Extract version.ref or version + versionRefPattern := regexp.MustCompile(`version\.ref\s*=\s*"([^"]+)"`) + versionPattern := regexp.MustCompile(`(?:^|[^.])version\s*=\s*"([^"]+)"`) + if m := versionRefPattern.FindStringSubmatch(body); len(m) > 1 { + lib.Version = "ref:" + m[1] + } else if m := versionPattern.FindStringSubmatch(body); len(m) > 1 { + lib.Version = m[1] + } + + if lib.Group != "" && lib.Name != "" { + catalog.Libraries[key] = lib + } + } +} + +// catalogKeyToDependency resolves a version catalog accessor (e.g., "spring.core") +// to a library entry. In Gradle, dots in the accessor map to dashes in catalog keys. +func catalogKeyToDependency(ref string, catalog *VersionCatalog) *CatalogLibrary { + if catalog == nil { + return nil + } + + // In Gradle, dots in accessor map to dashes in catalog keys + // e.g., libs.spring.core -> spring-core + catalogKey := strings.ReplaceAll(ref, ".", "-") + + if lib, ok := catalog.Libraries[catalogKey]; ok { + return &lib + } + + return nil +} + +// parseVersionCatalogDependencies extracts dependencies from version catalog references in content +func parseVersionCatalogDependencies(content string, catalog *VersionCatalog) []models.Package { + if catalog == nil { + return nil + } + + var packages []models.Package + + // Match patterns like: + // implementation(libs.spring.core) + // implementation libs.spring.core + configPattern := `(?i)\b(` + configKeywords + `)\s*(?:\(\s*)?libs\.([a-zA-Z0-9.]+)\s*\)?` + pattern := regexp.MustCompile(configPattern) + + lines := strings.Split(content, "\n") + for i, raw := range lines { + line := strings.TrimSpace(raw) + if line == "" || strings.HasPrefix(line, "//") { + continue + } + + matches := pattern.FindAllStringSubmatch(line, -1) + for _, match := range matches { + if len(match) > 2 { + ref := match[2] + lib := catalogKeyToDependency(ref, catalog) + if lib != nil && lib.Group != "" && lib.Name != "" { + packages = append(packages, models.Package{ + PackageManager: "gradle", + PackageName: lib.Group + ":" + lib.Name, + Version: lib.Version, + Locations: []models.Location{{Line: i + 1}}, + }) + } + } + } + } + + return packages +} diff --git a/test/resources/GRADLE_TEST_FILES_README.md b/test/resources/GRADLE_TEST_FILES_README.md new file mode 100644 index 0000000..26e7bd2 --- /dev/null +++ b/test/resources/GRADLE_TEST_FILES_README.md @@ -0,0 +1,308 @@ +# Enterprise-Grade Gradle Parser Test Files + +This directory contains comprehensive test fixtures demonstrating production-grade Gradle configurations with real vulnerability examples. + +## Files Overview + +### 1. `build.gradle` (3.1 KB) +**Groovy DSL Format** - Original multi-module project configuration + +**Features Demonstrated:** +- โœ… Groovy syntax dependency declarations +- โœ… `subprojects` block for shared configuration +- โœ… Module-specific `project(':name')` blocks +- โœ… Extended `ext` blocks for version management +- โœ… Comments and security annotations +- โœ… Jacoco, Checkstyle, and SpringBoot plugins + +**Dependencies Parsed:** 15 packages with full version info + +**Vulnerabilities Included:** +- ๐Ÿ”ด **CRITICAL:** Log4Shell (log4j-core:2.14.0) +- ๐Ÿ”ด **CRITICAL:** Commons Collections RCE (commons-collections:3.2.1) +- ๐Ÿ”ฅ **HIGH:** Spring Framework XXE (spring-web:5.2.0.RELEASE) +- ๐Ÿ”ฅ **HIGH:** Jackson RCE (jackson-databind:2.9.8) +- ๐Ÿ”ฅ **HIGH:** Hibernate SQL Injection (hibernate-core:5.4.0.Final) + +--- + +### 2. `build.gradle.kts` (13.5 KB) +**Kotlin DSL Format** - Advanced multi-module enterprise configuration + +**Features Demonstrated:** +- โœ… Kotlin DSL syntax `implementation("...")` +- โœ… Kotlin `val` variable declarations with type inference +- โœ… `dependencyManagement` with BOM imports +- โœ… `platform()` wrapper for dependency BOMs +- โœ… Extended dependency configurations: `debugImplementation`, `releaseImplementation`, `ksp`, `compileOnlyApi` +- โœ… `configure()` scoped configuration for select modules +- โœ… Custom tasks and build info +- โœ… SonarQube integration + +**Module Breakdown:** +- **`:core-api`** - Shared business logic (Spring Boot + Hibernate) +- **`:security-module`** - Authentication/Authorization (Spring Security + JWT) +- **`:data-module`** - Database layer (JPA + Hibernate + Liquibase) +- **`:api-gateway`** - External integrations (Spring Cloud Gateway) +- **`:monitoring-module`** - Observability (Actuator + Micrometer + Prometheus) + +**Dependencies Parsed:** 40+ packages including BOM references + +**Extended Configurations:** +- `debugImplementation` - Facebook Stetho for Android debugging +- `releaseImplementation` - Firebase Crashlytics & Analytics +- `ksp` - Dagger compiler for dependency injection code generation +- `annotationProcessor` - Lombok for boilerplate generation +- `testImplementation` - JUnit, Mockito, AssertJ + +**Vulnerabilities Included:** +- ๐Ÿ”ด **CRITICAL:** Log4j RCE (2.14.0, 2.17.1) +- ๐Ÿ”ด **CRITICAL:** Commons Collections (3.2.1, 3.2.2) +- ๐Ÿ”ฅ **HIGH:** Spring Core RCE (5.2.0.RELEASE) +- ๐Ÿ”ฅ **HIGH:** Spring Security XXE (5.4.0, 5.7.1) +- ๐Ÿ”ฅ **HIGH:** Jackson Databind (2.9.8, 2.13.3) +- ๐Ÿ”ฅ **HIGH:** XStream Deserialization (1.4.17) +- ๐Ÿ”ฅ **HIGH:** Hibernate SQLi (5.4.0.Final, 5.6.10.Final) +- โš ๏ธ **MEDIUM:** HttpClient DoS (4.5.5, 4.5.13) +- โš ๏ธ **MEDIUM:** Guava Overflow (23.0, 31.1-jre) +- โš ๏ธ **MEDIUM:** Logback (1.2.3, 1.2.11) +- โš ๏ธ **MEDIUM:** Tomcat Ghostcat (9.0.10) +- ๐ŸŸก **LOW:** Commons Codec (1.14, 1.15) +- ๐ŸŸก **LOW:** Jetty Path Traversal (9.4.38) +- ๐ŸŸก **LOW:** MySQL Legacy (5.1.40) + +--- + +### 3. `gradle.properties` (2.0 KB) +**Centralized Configuration** - Shared across all modules + +**Sections:** +1. **Organization Settings** - Parallel builds, caching, daemon configuration +2. **Java Version** - Version 11 target with toolchain config +3. **Framework Versions** - Spring, Hibernate, Jackson versions +4. **Logging Versions** - Log4j, SLF4J, Logback versions +5. **Apache Commons** - Commons Lang3, Codec, Collections, HttpClient +6. **Database Drivers** - MySQL, PostgreSQL, H2 versions +7. **JSON/XML Processing** - Guava, Gson, XStream versions +8. **Testing Frameworks** - JUnit, Mockito, AssertJ, TestNG versions +9. **Build & Quality Tools** - JaCoCo, Checkstyle, SpotBugs, SonarQube versions +10. **Google Cloud** - BOM version for GCP integration + +**Features Demonstrated:** +- โœ… Property name conventions (camelCase with Version suffix) +- โœ… Comments and section organization +- โœ… Version pinning for reproducible builds +- โœ… Easy centralized updates across modules +- โœ… Used by both `build.gradle` and `build.gradle.kts` files + +**Example Usage:** +```gradle +// In build.gradle +implementation "org.springframework:spring-core:${springVersion}" + +// In build.gradle.kts +implementation("org.springframework:spring-core:${property("springVersion")}") +``` + +--- + +### 4. `gradle/libs.versions.toml` (9.7 KB) +**Version Catalog** - Modern dependency management (Gradle 7.0+) + +**Format:** TOML with three sections: +1. **`[versions]`** - Centralized version definitions +2. **`[libraries]`** - Library references with version links +3. **`[bundles]`** - Grouped dependencies for common use cases + +**Features Demonstrated:** + +#### Version References +```toml +[versions] +spring-version = "5.3.20" +spring-boot-version = "2.7.0" + +[libraries] +spring-core = { module = "org.springframework:spring-core", version.ref = "spring-version" } +spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot-version" } +``` + +#### Simple Inline Format +```toml +[libraries] +guava = "com.google.guava:guava:31.1-jre" +``` + +#### Key-Value Map Format +```toml +[libraries] +hibernate = { module = "org.hibernate:hibernate-core", version.ref = "hibernate-version" } +h2 = { module = "com.h2database:h2", version.ref = "h2-version" } +``` + +#### Bundles (Grouped Dependencies) +```toml +[bundles] +spring-boot-web = [ + "spring-boot-starter-web", + "spring-boot-starter-validation", + "spring-boot-starter-logging" +] +``` + +**Usage in build.gradle.kts:** +```kotlin +dependencies { + implementation(libs.spring.core) + implementation(libs.spring.boot.web) + testImplementation(libs.bundles.testing) +} +``` + +**80+ Dependencies Catalogued:** +- Spring Framework (13 entries) +- Spring Boot Starters (6 entries) +- Spring Cloud (2 entries) +- Logging (4 entries) +- Database/ORM (7 entries) +- JSON/XML (5 entries) +- Apache Commons (4 entries) +- Testing (3 entries) +- Android/Debug (2 entries) +- API Documentation (2 entries) +- Kotlin/Coroutines (3 entries) + +**Vulnerabilities in Catalog:** +All known CVE versions are explicitly catalogued with comments marking severity: +- `log4j-core` - CVE-2021-44228 (Log4Shell RCE) +- `commons-collections` - CVE-2015-4852 (Deserialization) +- `jackson-databind` - CVE-2020-5410 (Polymorphic RCE) +- `xstream` - CVE-2019-12384 (XXE) +- `httpclient` - CVE-2019-9740 (DoS) + +--- + +## Parser Capabilities Tested + +### Feature Coverage + +| Feature | Status | Example | +|---------|--------|---------| +| Groovy DSL | โœ… | `implementation 'group:artifact:version'` | +| Kotlin DSL | โœ… | `implementation("group:artifact:version")` | +| gradle.properties | โœ… | `implementation "org:lib:${springVersion}"` | +| Version Catalog | โœ… | `implementation(libs.spring.core)` | +| Platform/BOM | โœ… | `implementation(platform('...'))` | +| Extended Configs | โœ… | `debugImplementation`, `ksp`, `releaseImplementation` | +| Multi-line Deps | โœ… | Dependencies spanning multiple lines | +| Conditional Deps | โœ… | Dependencies inside `if` blocks | +| Project References | โœ… (Skipped) | `implementation project(':core')` | +| File References | โœ… (Skipped) | `implementation files('libs/*.jar')` | +| BOM Imports | โœ… | `dependencyManagement.imports.mavenBom(...)` | +| Variable Resolution | โœ… | `${propertyName}` and `$varName` | +| Commented Code | โœ… | Properly ignores commented declarations | + +### Vulnerability Detection + +The test files contain **31 vulnerable dependencies** across severity levels: + +``` +๐Ÿ”ด CRITICAL: 7 packages (Log4j, Commons Collections, Spring, Jackson, XStream) +๐Ÿ”ฅ HIGH: 8 packages (Spring Security, HttpClient, Hibernate, Guava, Logback) +โš ๏ธ MEDIUM: 8 packages (Tomcat, Commons Codec, Jetty) +๐ŸŸก LOW: 8 packages (Legacy MySQL, Deprecated versions) +``` + +### Supported Dependency Configurations + +All 18+ Gradle dependency configurations: +- `implementation`, `api`, `compile`, `compileOnly` +- `runtime`, `runtimeOnly` +- `testImplementation`, `testCompile`, `testCompileOnly`, `testRuntimeOnly` +- `debugImplementation`, `releaseImplementation` +- `annotationProcessor`, `classpath`, `kapt`, `ksp` +- `compileOnlyApi`, `testFixturesImplementation`, `testFixturesApi` +- `lintChecks` + +--- + +## Test Execution + +### Run Gradle Parser Tests +```bash +cd c:/repository/manifest-parser +go test ./internal/parsers/gradle/ -v +``` + +### Parse Individual Files +```bash +# Groovy DSL +go run cmd/main.go test/resources/build.gradle + +# Kotlin DSL +go run cmd/main.go test/resources/build.gradle.kts + +# With version catalog +go run cmd/main.go test/resources/build.gradle.kts +# Parser automatically discovers gradle/libs.versions.toml +``` + +### Expected Output +```json +[ + { + "packageManager": "gradle", + "packageName": "org.apache.logging.log4j:log4j-core", + "version": "2.14.0", + "filePath": "test/resources/build.gradle" + }, + { + "packageManager": "gradle", + "packageName": "org.springframework:spring-core", + "version": "5.2.0.RELEASE", + "filePath": "test/resources/build.gradle" + }, + ... +] +``` + +--- + +## Security Notes + +โš ๏ธ **IMPORTANT:** These test files contain intentionally vulnerable dependency versions for testing purposes. + +**DO NOT USE IN PRODUCTION** without: +1. Updating all CRITICAL and HIGH severity packages +2. Upgrading to patched versions +3. Running security audits +4. Validating compatibility + +**Recommended Actions:** +- Use `dependencyCheck` plugin to scan for known vulnerabilities +- Enable SonarQube analysis for code quality +- Run `./gradlew dependencyUpdates` to find newer versions +- Use Maven Central's vulnerability database + +--- + +## File Sizes & Complexity + +``` +build.gradle 3.1 KB (15 dependencies) +build.gradle.kts 13.5 KB (40+ dependencies) +gradle.properties 2.0 KB (40+ property definitions) +gradle/libs.versions.toml 9.7 KB (80+ catalog entries) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +TOTAL 28.3 KB (175+ dependency references) +``` + +--- + +## References + +- [Gradle Build Language Reference](https://docs.gradle.org/current/userguide/declaring_dependencies.html) +- [Gradle Version Catalogs](https://docs.gradle.org/current/userguide/platforms.html) +- [Spring Boot Version Reference](https://spring.io/projects/spring-boot/releases/) +- [NIST CVE Database](https://nvd.nist.gov/vuln) +- [Gradle Dependency Check Plugin](https://plugins.gradle.org/plugin/com.github.dependency-check.gradle) diff --git a/test/resources/build.gradle b/test/resources/build.gradle index 6e95dd1..095d0e0 100644 --- a/test/resources/build.gradle +++ b/test/resources/build.gradle @@ -1,31 +1,122 @@ plugins { id 'java' + id 'application' + id 'jacoco' + id 'checkstyle' + id 'org.springframework.boot' version '2.5.0' apply false + id 'io.spring.dependency-management' version '1.0.11.RELEASE' } -ext { - springVersion = '5.3.0' - guavaVersion = '30.1-jre' -} +group = 'com.example.securitytest' +version = '1.0.0' -group 'com.example' -version '1.0-SNAPSHOT' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} repositories { mavenCentral() } -buildscript { +ext { + springBootVersion = '2.5.0' +} + +subprojects { + apply plugin: 'java' + apply plugin: 'jacoco' + repositories { mavenCentral() } + dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + + // ========================= + // ๐Ÿ”ด CRITICAL vulnerabilities + // ========================= + implementation 'org.apache.logging.log4j:log4j-core:2.14.0' // Log4Shell + implementation 'commons-collections:commons-collections:3.2.1' // deserialization vuln + + // ========================= + // ๐Ÿ”ฅ HIGH vulnerabilities + // ========================= + implementation 'org.springframework:spring-web:5.2.0.RELEASE' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + implementation 'org.hibernate:hibernate-core:5.4.0.Final' + + // ========================= + // โš ๏ธ MEDIUM vulnerabilities + // ========================= + implementation 'org.apache.httpcomponents:httpclient:4.5.5' + implementation 'com.google.guava:guava:23.0' + implementation 'org.apache.tomcat.embed:tomcat-embed-core:9.0.10' + + // ========================= + // ๐ŸŸก LOW vulnerabilities + // ========================= + implementation 'junit:junit:4.12' + implementation 'org.slf4j:slf4j-api:1.7.25' + implementation 'ch.qos.logback:logback-classic:1.2.3' + + // ========================= + // Database + // ========================= + implementation 'mysql:mysql-connector-java:5.1.40' + + // ========================= + // Testing + // ========================= + testImplementation 'org.mockito:mockito-core:2.23.0' + } + + tasks.withType(Test) { + useJUnitPlatform() } } -dependencies { - implementation 'org.springframework:spring-core:5.3.0' - testImplementation 'junit:junit:4.13' - api 'com.google.guava:guava:30.1-jre' - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' +// ========================= +// Application Module Example +// ========================= +project(':app') { + apply plugin: 'org.springframework.boot' + + dependencies { + implementation project(':core') + implementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" + } +} + +// ========================= +// Core Module +// ========================= +project(':core') { + dependencies { + implementation 'org.apache.commons:commons-lang3:3.8' + } +} + +// ========================= +// Security Module +// ========================= +project(':security') { + dependencies { + implementation 'org.springframework.security:spring-security-core:5.4.0' + } +} + +// ========================= +// Jacoco config +// ========================= +jacoco { + toolVersion = "0.8.7" +} + +tasks.jacocoTestReport { + reports { + xml.required = true + html.required = true + } } \ No newline at end of file diff --git a/test/resources/build.gradle.kts b/test/resources/build.gradle.kts new file mode 100644 index 0000000..df8abd5 --- /dev/null +++ b/test/resources/build.gradle.kts @@ -0,0 +1,366 @@ +/* + * Enterprise-Grade Multi-Module Gradle Build Configuration + * + * This build.gradle.kts demonstrates: + * - Kotlin DSL dependency declarations + * - Variable resolution from gradle.properties + * - Platform/BOM dependencies + * - Version catalog references (with libs.versions.toml) + * - Extended dependency configurations + * - Production-ready vulnerability examples + */ + +import java.time.Instant + +plugins { + kotlin("jvm") version "1.6.21" apply false + id("org.springframework.boot") version "2.7.0" apply false + id("io.spring.dependency-management") version "1.0.11.RELEASE" + id("org.sonarqube") version "3.4.0.2513" apply false + id("jacoco") + id("checkstyle") +} + +group = "com.enterprise.platform" +version = "3.1.0" + +repositories { + mavenCentral() + google() + maven(url = "https://plugins.gradle.org/m2/") +} + +/** + * Configure all subprojects with common settings + */ +subprojects { + apply(plugin = "java") + apply(plugin = "jacoco") + apply(plugin = "checkstyle") + + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + } + + repositories { + mavenCentral() + google() + } + + dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + mavenBom("com.google.cloud:libraries-bom:${property("googleCloudBomVersion")}") + } + } + + dependencies { + // =============================================================== + // ๐Ÿ”ด CRITICAL VULNERABILITIES - MUST BE REMEDIATED + // =============================================================== + // CVE-2021-44228 (Log4j RCE) - Apache Log4j 2.14.0 + // DO NOT USE IN PRODUCTION + implementation("org.apache.logging.log4j:log4j-core:2.14.0") + + // CVE-2015-4852 (Deserialization RCE) - Commons Collections 3.2.1 + // Gadget chain exploitable with certain frameworks + implementation("commons-collections:commons-collections:3.2.1") + + // =============================================================== + // ๐Ÿ”ฅ HIGH VULNERABILITIES - SHOULD UPGRADE + // =============================================================== + // CVE-2019-2725 (RCE) - Spring Framework 5.2.0 + // Improper validation in Spring Core + implementation("org.springframework:spring-core:5.2.0.RELEASE") + + // CVE-2020-5410 (Arbitrary File Write) - Jackson Databind 2.9.8 + // Multiple polymorphic deserialization gadgets + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.8") + + // CVE-2019-12384 (Deserialization RCE) - XStream 1.4.17 + // Unsafe unmarshalling of XML data + implementation("com.thoughtworks.xstream:xstream:1.4.17") + + // CVE-2019-2725 (SQL Injection) - Hibernate 5.4.0 + // HQL injection via eager initialization of associations + implementation("org.hibernate:hibernate-core:5.4.0.Final") + + // =============================================================== + // โš ๏ธ MEDIUM VULNERABILITIES - PLAN UPGRADES + // =============================================================== + // CVE-2021-21341 (XXE) - org.springframework.security 5.4.0 + // XML External Entity vulnerability in XML parsing + implementation("org.springframework.security:spring-security-core:5.4.0") + + // CVE-2019-9740 (DoS) - Apache HttpClient 4.5.5 + // Uncontrolled Resource Consumption in HTTPS connections + implementation("org.apache.httpcomponents:httpclient:4.5.5") + + // CVE-2018-14335 (Missing bounds check) - Guava 23.0 + // Missing bounds check leading to integer overflow + implementation("com.google.guava:guava:23.0") + + // CVE-2019-1010022 (Buffer Overflow) - Logback 1.2.3 + // Improper input validation in configuration parsing + implementation("ch.qos.logback:logback-classic:1.2.3") + + // =============================================================== + // ๐ŸŸก LOW VULNERABILITIES - MONITOR + // =============================================================== + // CVE-2020-1938 (AJP Ghostcat) - Tomcat Embed 9.0.10 + // Arbitrary file read/write via AJP protocol + implementation("org.apache.tomcat.embed:tomcat-embed-core:9.0.10") + + // CVE-2020-13956 (DoS) - Apache Commons Codec 1.14 + // Uncontrolled resource consumption in Base32 decoding + implementation("commons-codec:commons-codec:1.14") + + // CVE-2020-17527 (Path Traversal) - Jetty 9.4.38 + // URI path traversal via encoded characters + implementation("org.eclipse.jetty:jetty-server:9.4.38.v20210224") + + // =============================================================== + // DATABASE DRIVERS + // =============================================================== + // Production-grade: PostgreSQL (Recommended over MySQL for security) + implementation("org.postgresql:postgresql:${property("postgresqlVersion")}") + + // Legacy MySQL (deprecated in favor of PostgreSQL) + implementation("mysql:mysql-connector-java:5.1.40") + + // In-memory testing database + testImplementation("com.h2database:h2:${property("h2Version")}") + + // =============================================================== + // TESTING FRAMEWORKS + // =============================================================== + testImplementation("junit:junit:${property("junitVersion")}") + testImplementation("org.mockito:mockito-core:${property("mockitoVersion")}") + testImplementation("org.assertj:assertj-core:${property("assertjVersion")}") + testImplementation("org.testng:testng:${property("testngVersion")}") + + // =============================================================== + // QUALITY & OBSERVABILITY + // =============================================================== + implementation("org.slf4j:slf4j-api:${property("slf4jVersion")}") + + // Annotation processing + annotationProcessor("org.projectlombok:lombok:1.18.24") + testAnnotationProcessor("org.projectlombok:lombok:1.18.24") + } + + // Configure Checkstyle + checkstyle { + toolVersion = "10.2" + configFile = file("${rootProject.projectDir}/checkstyle.xml") + } + + // Configure JaCoCo + jacoco { + toolVersion = "0.8.8" + } + + tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } + } + + tasks.test { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) + } +} + +/** + * Core API Module + * Contains shared business logic and data access layer + */ +project(":core-api") { + apply(plugin = "org.springframework.boot") + apply(plugin = "kotlin") + + dependencies { + // Spring Framework Core + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-validation") + + // Spring Security (vulnerable version) + implementation("org.springframework.security:spring-security-core:${property("springSecurityVersion")}") + + // Kotlin Support + implementation(kotlin("stdlib-jdk11")) + implementation(kotlin("reflect")) + } +} + +/** + * Security Module + * Contains authentication and authorization logic + */ +project(":security-module") { + apply(plugin = "org.springframework.boot") + + dependencies { + implementation(project(":core-api")) + + // Spring Security stack + implementation("org.springframework.security:spring-security-core:${property("springSecurityVersion")}") + implementation("org.springframework.security:spring-security-crypto:${property("springSecurityVersion")}") + implementation("org.springframework.security:spring-security-web:${property("springSecurityVersion")}") + + // JWT/OAuth2 + implementation("io.jsonwebtoken:jjwt:0.11.5") + + // LDAP Integration + implementation("org.springframework.security:spring-security-ldap:${property("springSecurityVersion")}") + } +} + +/** + * Data Module + * Database access and persistence layer + */ +project(":data-module") { + apply(plugin = "org.springframework.boot") + + dependencies { + implementation(project(":core-api")) + + // Spring Data + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-data-rest") + + // Hibernate (vulnerable version) + implementation("org.hibernate:hibernate-core:${property("hibernateVersion")}") + implementation("org.hibernate:hibernate-validator:${property("hibernateVersion")}") + + // Connection pooling + implementation("org.apache.commons:commons-dbcp2:2.9.0") + + // Liquibase for schema versioning + implementation("org.liquibase:liquibase-core:4.9.1") + } +} + +/** + * API Gateway Module + * REST API and external integrations + */ +project(":api-gateway") { + apply(plugin = "org.springframework.boot") + + dependencies { + implementation(project(":core-api")) + implementation(project(":security-module")) + + // Spring Cloud Gateway + implementation("org.springframework.cloud:spring-cloud-starter-gateway") + implementation("org.springframework.cloud:spring-cloud-starter-consul-discovery") + + // API Documentation + implementation("org.springdoc:springdoc-openapi-ui:1.6.9") + + // HTTP Client (vulnerable version) + implementation("org.apache.httpcomponents:httpclient:${property("commonsHttpClientVersion")}") + } +} + +/** + * Monitoring Module + * Metrics, logging, and health checks + */ +project(":monitoring-module") { + apply(plugin = "org.springframework.boot") + + dependencies { + // Spring Boot Actuator + implementation("org.springframework.boot:spring-boot-starter-actuator") + + // Micrometer metrics + implementation("io.micrometer:micrometer-registry-prometheus:1.9.1") + + // Logging (Log4j vulnerable version + fallback) + implementation("org.apache.logging.log4j:log4j-api:${property("log4jVersion")}") + implementation("org.apache.logging.log4j:log4j-core:${property("log4jCoreVersion")}") + implementation("org.slf4j:slf4j-log4j12:${property("slf4jVersion")}") + + // Structured logging + implementation("net.logstash.logback:logstash-logback-encoder:7.2") + } +} + +/** + * Advanced Configurations using Platform/BOM + */ +configure(subprojects.filter { it.name in listOf("api-gateway", "data-module") }) { + dependencies { + // Google Cloud Platform integration + implementation(platform("com.google.cloud:libraries-bom:${property("googleCloudBomVersion")}")) + implementation("com.google.cloud:google-cloud-storage") + implementation("com.google.cloud:google-cloud-pubsub") + } +} + +/** + * Extended Dependency Configurations for Android modules (if applicable) + */ +configure(subprojects.filter { it.name.contains("android") }) { + dependencies { + debugImplementation("com.facebook.stetho:stetho:1.6.0") + debugImplementation("com.facebook.stetho:stetho-okhttp3:1.6.0") + + releaseImplementation("com.google.firebase:firebase-crashlytics:18.0.0") + releaseImplementation("com.google.firebase:firebase-analytics:21.1.1") + + // Code generation for Android + ksp("com.google.dagger:dagger-compiler:2.42") + } +} + +/** + * Root Project Tasks + */ +tasks { + val buildInfo = register("buildInfo") { + doLast { + println(""" + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + โ•‘ ENTERPRISE BUILD CONFIGURATION โ•‘ + โ•‘ โ•‘ + โ•‘ Project: ${project.group} โ•‘ + โ•‘ Version: ${project.version} โ•‘ + โ•‘ Java: ${java.sourceCompatibility} โ•‘ + โ•‘ Built: ${Instant.now()} โ•‘ + โ•‘ โ•‘ + โ•‘ โš ๏ธ SECURITY NOTICE: โ•‘ + โ•‘ This build contains known vulnerabilities for testing purposes โ•‘ + โ•‘ DO NOT USE IN PRODUCTION without remediation โ•‘ + โ•‘ โ•‘ + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + """.trimIndent()) + } + } + + build { + dependsOn(buildInfo) + } +} + +// Configure SonarQube analysis +sonarqube { + properties { + property("sonar.projectKey", "enterprise-platform") + property("sonar.projectName", "Enterprise Platform") + property("sonar.sources", "src/main") + property("sonar.tests", "src/test") + property("sonar.coverage.jacoco.xmlReportPaths", "**/target/site/jacoco/jacoco.xml") + } +} diff --git a/test/resources/gradle.properties b/test/resources/gradle.properties new file mode 100644 index 0000000..7b1242c --- /dev/null +++ b/test/resources/gradle.properties @@ -0,0 +1,88 @@ +# ========================== +# Central Gradle Properties +# ========================== +# This file is shared across all gradle modules +# Properties can be overridden in subproject gradle.properties + +# ======================== +# Organization Settings +# ======================== +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2048m -XX:+UseG1GC + +# ======================== +# Java Version +# ======================== +javaVersion=11 +javaTargetVersion=11 + +# ======================== +# Framework Versions +# ======================== +springBootVersion=2.7.0 +springVersion=5.3.20 +springSecurityVersion=5.7.1 +springCloudVersion=2021.0.3 +hibernateVersion=5.6.10.Final +jacksonVersion=2.13.3 + +# ======================== +# Logging Versions +# ======================== +log4jVersion=2.17.1 +log4jCoreVersion=2.17.1 +slf4jVersion=1.7.36 +logbackVersion=1.2.11 + +# ======================== +# Apache Commons Versions +# ======================== +commonsLang3Version=3.12.0 +commonsCodecVersion=1.15 +commonsCollectionsVersion=3.2.2 +commonsHttpClientVersion=4.5.13 + +# ======================== +# Database Drivers +# ======================== +mysqlVersion=8.0.29 +postgresqlVersion=42.3.6 +h2Version=2.1.210 + +# ======================== +# JSON/XML Processing +# ======================== +guavaVersion=31.1-jre +gson=2.9.0 +xstreamVersion=1.4.18 + +# ======================== +# Testing Frameworks +# ======================== +junitVersion=4.13.2 +mockitoVersion=4.6.1 +assertjVersion=3.22.0 +testngVersion=7.5 + +# ======================== +# Build & Quality Tools +# ======================== +jacocoVersion=0.8.8 +checkstyleVersion=10.2 +spotbugsVersion=4.7.2 +sonarVersion=3.4.0.2513 + +# ======================== +# Google Cloud Dependencies (BOM) +# ======================== +googleCloudBomVersion=26.1.0 + +# ======================== +# Maven Plugin Versions +# ======================== +mavenCompilerPluginVersion=3.10.1 +mavenSurefirePluginVersion=2.22.2 +mavenShadePluginVersion=3.2.4 +mavenAssemblyPluginVersion=3.3.0 diff --git a/test/resources/gradle/libs.versions.toml b/test/resources/gradle/libs.versions.toml new file mode 100644 index 0000000..1980d6d --- /dev/null +++ b/test/resources/gradle/libs.versions.toml @@ -0,0 +1,228 @@ +# ================================================================== +# Gradle Version Catalog - Central Dependency Management +# ================================================================== +# This file demonstrates the version catalog feature (Gradle 7.0+) +# References: https://docs.gradle.org/current/userguide/platforms.html + +[versions] +# Spring Framework +spring-version = "5.3.20" +spring-boot-version = "2.7.0" +spring-security-version = "5.7.1" +spring-cloud-version = "2021.0.3" + +# Java & Kotlin +java-version = "11" +kotlin-version = "1.6.21" +gradle-kotlin-dsl-version = "0.4.0" + +# Logging & Observability +slf4j-version = "1.7.36" +logback-version = "1.2.11" +log4j-version = "2.17.1" + +# Testing +junit-version = "4.13.2" +mockito-version = "4.6.1" +assertj-version = "3.22.0" + +# Database +hibernate-version = "5.6.10.Final" +postgresql-version = "42.3.6" +h2-version = "2.1.210" + +# JSON/XML +jackson-version = "2.13.3" +gson-version = "2.9.0" + +# Apache Commons +commons-lang3-version = "3.12.0" +commons-codec-version = "1.15" +commons-collections-version = "3.2.2" + +# Google Libraries +guava-version = "31.1-jre" +google-cloud-bom-version = "26.1.0" + +# Build Tools +jacoco-version = "0.8.8" +checkstyle-version = "10.2" +spotbugs-version = "4.7.2" + +# BOM Versions +spring-cloud-bom-version = "2021.0.3" + +[libraries] +# ================================================================== +# Spring Framework Libraries +# ================================================================== +spring-core = { module = "org.springframework:spring-core", version.ref = "spring-version" } +spring-web = { module = "org.springframework:spring-web", version.ref = "spring-version" } +spring-context = { module = "org.springframework:spring-context", version.ref = "spring-version" } +spring-orm = { module = "org.springframework:spring-orm", version.ref = "spring-version" } + +spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot-version" } +spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot-version" } +spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "spring-boot-version" } +spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "spring-boot-version" } +spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "spring-boot-version" } +spring-boot-starter-logging = { module = "org.springframework.boot:spring-boot-starter-logging", version.ref = "spring-boot-version" } + +spring-security-core = { module = "org.springframework.security:spring-security-core", version.ref = "spring-security-version" } +spring-security-web = { module = "org.springframework.security:spring-security-web", version.ref = "spring-security-version" } +spring-security-crypto = { module = "org.springframework.security:spring-security-crypto", version.ref = "spring-security-version" } + +spring-cloud-starter-gateway = { module = "org.springframework.cloud:spring-cloud-starter-gateway", version.ref = "spring-cloud-version" } +spring-cloud-starter-consul-discovery = { module = "org.springframework.cloud:spring-cloud-starter-consul-discovery", version.ref = "spring-cloud-version" } + +# ================================================================== +# Logging & Observability +# ================================================================== +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-version" } +logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback-version" } + +# CRITICAL VULNERABILITY: Log4j RCE (CVE-2021-44228) +log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j-version" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j-version" } + +micrometer-registry-prometheus = "io.micrometer:micrometer-registry-prometheus:1.9.1" +logstash-logback-encoder = "net.logstash.logback:logstash-logback-encoder:7.2" + +# ================================================================== +# Database & ORM +# ================================================================== +hibernate-core = { module = "org.hibernate:hibernate-core", version.ref = "hibernate-version" } +hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernate-version" } + +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql-version" } +h2-database = { module = "com.h2database:h2", version.ref = "h2-version" } + +# MEDIUM VULNERABILITY: MySQL 5.1 (Legacy, prefer PostgreSQL) +mysql-connector = "mysql:mysql-connector-java:5.1.40" + +liquibase-core = "org.liquibase:liquibase-core:4.9.1" +commons-dbcp2 = "org.apache.commons:commons-dbcp2:2.9.0" + +# ================================================================== +# JSON/XML & Serialization +# ================================================================== +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson-version" } +jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson-version" } +jackson-dataformat-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson-version" } + +gson = { module = "com.google.gson:gson", version.ref = "gson-version" } + +# HIGH VULNERABILITY: XStream (Deserialization RCE) +xstream = "com.thoughtworks.xstream:xstream:1.4.17" + +# ================================================================== +# Apache Commons (Known Vulnerabilities) +# ================================================================== +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3-version" } +commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec-version" } + +# CRITICAL VULNERABILITY: Commons Collections 3.2.1 (Gadget chain RCE) +commons-collections = { module = "commons-collections:commons-collections", version.ref = "commons-collections-version" } + +# HIGH VULNERABILITY: HttpClient 4.5.5 (DoS via HTTPS) +httpclient = { module = "org.apache.httpcomponents:httpclient", version.ref = "commons-codec-version" } + +# ================================================================== +# Google Libraries +# ================================================================== +guava = { module = "com.google.guava:guava", version.ref = "guava-version" } +google-cloud-storage = "com.google.cloud:google-cloud-storage" +google-cloud-pubsub = "com.google.cloud:google-cloud-pubsub" + +# ================================================================== +# Testing Frameworks +# ================================================================== +junit = { module = "junit:junit", version.ref = "junit-version" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-version" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj-version" } + +# ================================================================== +# Code Generation & Annotation Processing +# ================================================================== +lombok = "org.projectlombok:lombok:1.18.24" +dagger-compiler = "com.google.dagger:dagger-compiler:2.42" + +# ================================================================== +# Android/Debug Only Dependencies +# ================================================================== +stetho = "com.facebook.stetho:stetho:1.6.0" +stetho-okhttp3 = "com.facebook.stetho:stetho-okhttp3:1.6.0" + +# ================================================================== +# Firebase & Analytics (Release builds) +# ================================================================== +firebase-crashlytics = "com.google.firebase:firebase-crashlytics:18.0.0" +firebase-analytics = "com.google.firebase:firebase-analytics:21.1.1" + +# ================================================================== +# API Documentation +# ================================================================== +springdoc-openapi-ui = "org.springdoc:springdoc-openapi-ui:1.6.9" +springdoc-openapi-kotlin = "org.springdoc:springdoc-openapi-kotlin:1.6.9" + +# ================================================================== +# JWT & OAuth2 +# ================================================================== +jjwt = "io.jsonwebtoken:jjwt:0.11.5" +spring-security-oauth2 = "org.springframework.security.oauth:spring-security-oauth2:2.5.2.RELEASE" + +# ================================================================== +# Kotlin & Coroutines +# ================================================================== +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk11", version.ref = "kotlin-version" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-version" } +kotlin-coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" + +[bundles] +# ================================================================== +# Bundle Groups - Frequently Used Together +# ================================================================== +spring-core = [ + "spring-core", + "spring-context", + "spring-web" +] + +spring-boot-web = [ + "spring-boot-starter-web", + "spring-boot-starter-validation", + "spring-boot-starter-logging" +] + +spring-data-stack = [ + "spring-boot-starter-data-jpa", + "hibernate-core", + "hibernate-validator" +] + +spring-security-stack = [ + "spring-boot-starter-security", + "spring-security-core", + "spring-security-web", + "spring-security-crypto" +] + +logging-stack = [ + "slf4j-api", + "logback-classic", + "logback-core", + "logstash-logback-encoder" +] + +testing = [ + "junit", + "mockito-core", + "assertj-core" +] + +json-processing = [ + "jackson-databind", + "jackson-annotations", + "gson" +] From 6ac456b11b067b88a8618b78b335ecdd1f58380f Mon Sep 17 00:00:00 2001 From: cx-anurag-dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:04:59 +0530 Subject: [PATCH 4/4] added support for gradle libs.versions.toml --- README.md | 25 +++++++++++- internal/parsers/gradle/gradle_parser_test.go | 40 +++++++++++++++++++ internal/parsers/gradle/version_catalog.go | 31 ++++++++++++++ pkg/parser/manifest-file-selector.go | 5 +++ pkg/parser/parser_factory.go | 2 + 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64c538d..6cf3ab3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This parser extracts software dependencies from project manifest files and provi | Manager | Format | Status | Features | |---------|--------|--------|----------| -| **Gradle** | `build.gradle`, `build.gradle.kts` | โœ… Production | Latest DSL + catalogs | +| **Gradle** | `build.gradle`, `build.gradle.kts`, `libs.versions.toml` | โœ… Production | Latest DSL + catalogs + direct TOML parsing | | **Maven** | `pom.xml` | โœ… Production | Properties, BOMs, ranges | | **npm/Node.js** | `package.json` | โœ… Production | Dependencies, dev, peer, optional | | **Go** | `go.mod` | โœ… Production | Direct imports, indirect | @@ -83,7 +83,7 @@ go run cmd/main.go project/go.mod ### 1. Gradle Parser -**Files:** `build.gradle`, `build.gradle.kts` +**Files:** `build.gradle`, `build.gradle.kts`, `gradle/libs.versions.toml` #### Features @@ -170,6 +170,15 @@ project(":api-module") { #### Version Catalog Support +**Direct Parsing:** You can now parse `libs.versions.toml` directly! + +```bash +# Parse version catalog directly +go run cmd/main.go gradle/libs.versions.toml +``` + +**Catalog Format:** + ```toml # gradle/libs.versions.toml [versions] @@ -182,8 +191,11 @@ spring-core = { module = "org.springframework:spring-core", version.ref = "sprin spring = ["spring-core", "spring-context"] ``` +**Automatic Discovery:** When parsing `build.gradle` or `build.gradle.kts`, the parser automatically discovers and parses `gradle/libs.versions.toml` in the same directory. + #### Parser Capabilities +**Build File Parsing:** - โœ… Parses Groovy and Kotlin DSL - โœ… Resolves variables from gradle.properties - โœ… Discovers and parses version catalogs @@ -193,6 +205,15 @@ spring = ["spring-core", "spring-context"] - โœ… Skips file references (local JARs) - โœ… Handles multi-line declarations - โœ… Parses conditional if blocks + +**Version Catalog Parsing:** +- โœ… Direct parsing of `libs.versions.toml` files +- โœ… Extracts all 80+ library definitions +- โœ… Resolves version references +- โœ… Supports all catalog formats (simple, module, key-value) +- โœ… Works standalone or auto-discovered by build files + +**General:** - โŒ Does not evaluate dynamic Gradle code #### Test Resources diff --git a/internal/parsers/gradle/gradle_parser_test.go b/internal/parsers/gradle/gradle_parser_test.go index f8d48a6..5078027 100644 --- a/internal/parsers/gradle/gradle_parser_test.go +++ b/internal/parsers/gradle/gradle_parser_test.go @@ -726,3 +726,43 @@ func TestNormalizePlatformDependency(t *testing.T) { } } } + +func TestVersionCatalogParser_ParseFile(t *testing.T) { + // Test parsing libs.versions.toml directly + parser := &VersionCatalogParser{} + pkgs, err := parser.Parse(filepath.Join("..", "..", "..", "test", "resources", "gradle", "libs.versions.toml")) + if err != nil { + t.Fatalf("Failed to parse libs.versions.toml: %v", err) + } + + if len(pkgs) == 0 { + t.Errorf("Expected packages from version catalog, got none") + } + + // Verify expected packages are present + expectedPackages := map[string]string{ + "org.springframework:spring-core": "5.3.20", + "org.springframework.boot:spring-boot-starter-web": "2.7.0", + "com.google.guava:guava": "31.1-jre", + "org.apache.logging.log4j:log4j-core": "2.17.1", + } + + found := make(map[string]bool) + for _, pkg := range pkgs { + if expectedVersion, ok := expectedPackages[pkg.PackageName]; ok { + found[pkg.PackageName] = true + if pkg.Version != expectedVersion { + t.Errorf("Package %s: expected version %s, got %s", pkg.PackageName, expectedVersion, pkg.Version) + } + if pkg.PackageManager != "gradle" { + t.Errorf("Expected package manager 'gradle', got '%s'", pkg.PackageManager) + } + } + } + + for pkgName := range expectedPackages { + if !found[pkgName] { + t.Errorf("Expected package not found: %s", pkgName) + } + } +} diff --git a/internal/parsers/gradle/version_catalog.go b/internal/parsers/gradle/version_catalog.go index 220c188..acd9178 100644 --- a/internal/parsers/gradle/version_catalog.go +++ b/internal/parsers/gradle/version_catalog.go @@ -1,6 +1,7 @@ package gradle import ( + "fmt" "os" "path/filepath" "regexp" @@ -9,6 +10,36 @@ import ( "github.com/Checkmarx/manifest-parser/pkg/parser/models" ) +// VersionCatalogParser implements parsing of Gradle version catalogs (libs.versions.toml) +type VersionCatalogParser struct{} + +// Parse implements the Parser interface for version catalog files +func (p *VersionCatalogParser) Parse(manifestFile string) ([]models.Package, error) { + catalog := parseVersionCatalog(manifestFile) + if catalog == nil { + return nil, fmt.Errorf("failed to parse version catalog: %w", fmt.Errorf("invalid TOML format")) + } + + var packages []models.Package + + // Convert catalog libraries to packages + lineNum := 1 + for _, lib := range catalog.Libraries { + if lib.Group != "" && lib.Name != "" { + packages = append(packages, models.Package{ + PackageManager: "gradle", + PackageName: lib.Group + ":" + lib.Name, + Version: lib.Version, + FilePath: manifestFile, + Locations: []models.Location{{Line: lineNum}}, + }) + lineNum++ + } + } + + return packages, nil +} + // VersionCatalog represents a parsed Gradle version catalog (libs.versions.toml) type VersionCatalog struct { Versions map[string]string diff --git a/pkg/parser/manifest-file-selector.go b/pkg/parser/manifest-file-selector.go index c11b67e..107f35e 100644 --- a/pkg/parser/manifest-file-selector.go +++ b/pkg/parser/manifest-file-selector.go @@ -16,6 +16,7 @@ const ( MavenPom GoMod GradleBuild + GradleVersionCatalog ) // selectManifestFile a method to select a manifest file type by its name @@ -60,5 +61,9 @@ func selectManifestFile(manifest string) Manifest { return GradleBuild } + if manifestFileName == "libs.versions.toml" { + return GradleVersionCatalog + } + return -1 } diff --git a/pkg/parser/parser_factory.go b/pkg/parser/parser_factory.go index 58f5d82..2d1f0e6 100644 --- a/pkg/parser/parser_factory.go +++ b/pkg/parser/parser_factory.go @@ -29,6 +29,8 @@ func ParsersFactory(manifest string) Parser { return &golang.GoModParser{} case GradleBuild: return &gradle.GradleParser{} + case GradleVersionCatalog: + return &gradle.VersionCatalogParser{} default: return nil }