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..d9db5acd 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,25 @@ 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) + .filter(line -> !line.isEmpty()) + .map(line -> line.split("\\s+")) + .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/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); + } +} 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