From 3ab75992280b423702dcdc74a2a09b4aa2b62337 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Tue, 12 May 2026 10:21:02 +0300 Subject: [PATCH 1/2] fix(go): handle replace directives in go list -m all parsing The stream filter in getFinalPackagesVersionsForModule() only accepted 2-part lines from `go list -m all`, silently dropping replace directive lines (5-part format: name v1 => replacement v2). This caused replaced modules to be absent from the MVS version map, resulting in incorrect SBOM versions. Extract parseModuleVersions() to handle both standard and replace directive lines, mirroring the JS client fix (PR #505). Implements TC-4359 Assisted-by: Claude Code --- .../providers/GoModulesProvider.java | 25 ++++++++++++++----- .../expected_sbom_stack_analysis.json | 8 +++--- .../golang/go_mod_light_no_ignore/go.mod | 2 ++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java index 59f1cd47..21162785 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java @@ -347,12 +347,7 @@ private Map> getFinalPackagesVersionsForModule( Operations.runProcessGetOutput(manifestPath.getParent(), goExecutable, "mod", "download"); String finalVersionsForAllModules = Operations.runProcessGetOutput(manifestPath.getParent(), goExecutable, "list", "-m", "all"); - Map finalModulesVersions = - Arrays.stream(finalVersionsForAllModules.split(Operations.GENERIC_LINE_SEPARATOR)) - .filter(string -> string.trim().split(" ").length == 2) - .collect( - Collectors.toMap( - t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second)); + Map finalModulesVersions = parseModuleVersions(finalVersionsForAllModules); Map> listWithModifiedVersions = new HashMap<>(); // Process all entries, including those without versions (like the root module) edges.forEach( @@ -387,6 +382,24 @@ private Map> getFinalPackagesVersionsForModule( return listWithModifiedVersions; } + /** + * Parses {@code go list -m all} output into a map of module names to their final resolved + * versions. Handles both standard lines ({@code name version}) and replace directive lines + * ({@code name v1 => replacement v2}). + */ + static Map parseModuleVersions(String goListOutput) { + return Arrays.stream(goListOutput.split(Operations.GENERIC_LINE_SEPARATOR)) + .map(String::trim) + .map(line -> line.split(" ")) + .filter(parts -> parts.length == 2 || (parts.length >= 4 && parts[2].equals("=>"))) + .collect( + Collectors.toMap( + parts -> parts[0], + parts -> + parts.length >= 4 && parts[2].equals("=>") ? parts[parts.length - 1] : parts[1], + (first, second) -> second)); + } + private List getListOfPackagesWithFinalVersions( Map finalModulesVersions, List packages) { return packages.stream() diff --git a/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/expected_sbom_stack_analysis.json b/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/expected_sbom_stack_analysis.json index 431520b8..11924470 100644 --- a/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/expected_sbom_stack_analysis.json +++ b/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/expected_sbom_stack_analysis.json @@ -263,11 +263,11 @@ }, { "type": "library", - "bom-ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.1", + "bom-ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.0", "group": "gopkg.in", "name": "yaml.v3", - "version": "v3.0.1", - "purl": "pkg:golang/gopkg.in/yaml.v3@v3.0.1" + "version": "v3.0.0", + "purl": "pkg:golang/gopkg.in/yaml.v3@v3.0.0" }, { "type": "library", @@ -497,7 +497,7 @@ "dependsOn": [] }, { - "ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.1", + "ref": "pkg:golang/gopkg.in/yaml.v3@v3.0.0", "dependsOn": [ "pkg:golang/gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405" ] diff --git a/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod b/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod index 7ba64b70..a3fc2531 100644 --- a/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod +++ b/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod @@ -6,3 +6,5 @@ require golang.org/x/tools v0.0.0-20210112183307-1e6ecd4bf1b0 require github.com/spf13/cobra v0.0.5 require gopkg.in/yaml.v3 v3.0.1 // indirect + +replace gopkg.in/yaml.v3 v3.0.1 => gopkg.in/yaml.v3 v3.0.0 From 8628bff362b953fe0bcbde05c9742ee7bbec91c3 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Tue, 12 May 2026 15:19:20 +0300 Subject: [PATCH 2/2] fix(go): split on \s+ in parseModuleVersions and add unit tests Use regex whitespace splitting instead of single space to handle tabs and multiple spaces in go list -m all output. Add unit tests covering standard lines, replace directives, varied whitespace, blank lines, malformed input, and duplicate handling. Co-Authored-By: Claude Opus 4.6 --- .../providers/GoModulesProvider.java | 3 +- .../GoModulesParseModuleVersionsTest.java | 86 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/github/guacsec/trustifyda/providers/GoModulesParseModuleVersionsTest.java diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java index 21162785..d9db5acd 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java @@ -390,7 +390,8 @@ private Map> getFinalPackagesVersionsForModule( static Map parseModuleVersions(String goListOutput) { return Arrays.stream(goListOutput.split(Operations.GENERIC_LINE_SEPARATOR)) .map(String::trim) - .map(line -> line.split(" ")) + .filter(line -> !line.isEmpty()) + .map(line -> line.split("\\s+")) .filter(parts -> parts.length == 2 || (parts.length >= 4 && parts[2].equals("=>"))) .collect( Collectors.toMap( diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/GoModulesParseModuleVersionsTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/GoModulesParseModuleVersionsTest.java new file mode 100644 index 00000000..714e98f5 --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/providers/GoModulesParseModuleVersionsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.providers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class GoModulesParseModuleVersionsTest { + + @Test + void parseStandardModuleLines() { + String input = "github.com/foo/bar v1.2.3\ngithub.com/baz/qux v0.1.0\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result) + .containsEntry("github.com/foo/bar", "v1.2.3") + .containsEntry("github.com/baz/qux", "v0.1.0") + .hasSize(2); + } + + @Test + void parseReplaceDirectiveLines() { + String input = "github.com/old/mod v1.0.0 => github.com/new/mod v2.0.0\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result).containsEntry("github.com/old/mod", "v2.0.0").hasSize(1); + } + + @Test + void parseWithMultipleSpacesAndTabs() { + String input = "github.com/foo/bar v1.2.3\ngithub.com/baz/qux\tv0.1.0\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result) + .containsEntry("github.com/foo/bar", "v1.2.3") + .containsEntry("github.com/baz/qux", "v0.1.0") + .hasSize(2); + } + + @Test + void parseWithBlankAndWhitespaceOnlyLines() { + String input = "\n \ngithub.com/foo/bar v1.0.0\n\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result).containsEntry("github.com/foo/bar", "v1.0.0").hasSize(1); + } + + @Test + void parseSkipsMalformedLines() { + String input = "github.com/foo/bar v1.0.0\nsingle-token\nthree tokens here\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result).containsEntry("github.com/foo/bar", "v1.0.0").hasSize(1); + } + + @Test + void parseEmptyInput() { + Map result = GoModulesProvider.parseModuleVersions(""); + assertThat(result).isEmpty(); + } + + @Test + void parseReplaceDirectiveWithTabSeparation() { + String input = "github.com/old/mod\tv1.0.0\t=>\tgithub.com/new/mod\tv2.0.0\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result).containsEntry("github.com/old/mod", "v2.0.0").hasSize(1); + } + + @Test + void parseDuplicateModuleKeepsLast() { + String input = "github.com/foo/bar v1.0.0\ngithub.com/foo/bar v2.0.0\n"; + Map result = GoModulesProvider.parseModuleVersions(input); + assertThat(result).containsEntry("github.com/foo/bar", "v2.0.0").hasSize(1); + } +}