Skip to content

Commit f25f276

Browse files
authored
feat!: set golang mvs true as default (#188)
## Description Set the use of the MVS algorithm default to `true` as it is the default behaviour in Go * Fix how environment variables are parsed in Windows * Fix NPE when processing dependencies * The last dependency was being ignored **Related issue (if any):** fixes #174 fixes #192 ## Checklist - [x] I have followed this repository's contributing guidelines. - [x] I will adhere to the project's code of conduct. --------- Signed-off-by: Ruben Romero Montes <[email protected]>
1 parent 91c0a95 commit f25f276

File tree

19 files changed

+15151
-24925
lines changed

19 files changed

+15151
-24925
lines changed

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -506,22 +506,23 @@ Two possible values for this setting:
506506

507507
#### Golang Support
508508

509-
By default, all go.mod' packages' transitive modules will be taken to analysis with their original package version, that is,
510-
if go.mod has 2 modules, `a` and `b`, and each one of them has the same package c with same major version v1, but different minor versions:
511-
- namespace/c/[email protected]
512-
- namespace/c/[email protected]
509+
By default, Golang dependency resolution follows the [Minimal Version Selection (MVS) Algorithm](https://go.dev/ref/mod#minimal-version-selection).
510+
This means that when analyzing a project, only the module versions that would actually be included in the final executable are considered.
513511

512+
For example, if your `go.mod` file declares two modules, `a` and `b`, and both depend on the same package `c` (same major version `v1`) but with different minor versions:
514513

515-
Then both of these packages will be entered to the generated sbom and will be included in analysis returned to client.
516-
In golang, in an actual build of an application into an actual application executable binary, only one of the minor versions will be included in the executable, as only packages with same name but different major versions considered different packages ,
517-
hence can co-exist together in the application executable.
514+
- `namespace/c/[email protected]`
515+
- `namespace/c/[email protected]`
518516

519-
Go ecosystem knows how to select one minor version among all the minor versions of the same major version of a given package, using the [MVS Algorithm](https://go.dev/ref/mod#minimal-version-selection).
520-
521-
In order to enable this behavior, that only shows in analysis modules versions that are actually built into the application executable, please set
522-
system property/environment variable - `EXHORT_GO_MVS_LOGIC_ENABLED=true`(Default is false)
517+
Only one of these versions — the minimal version selected by MVS — will be included in the generated SBOM and analysis results.
518+
This mirrors the behavior of a real Go build, where only one minor version of a given major version can be present in the executable (since Go treats packages with the same name and major version as identical).
523519

520+
The MVS-based resolution is **enabled by default**.
521+
If you want to disable this behavior and instead include **all transitive module versions** (as listed in `go.mod` dependencies), set the system property or environment variable:
524522

523+
```bash
524+
EXHORT_GO_MVS_LOGIC_ENABLED=false
525+
```
525526

526527
#### Python Support
527528

pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ limitations under the License.]]>
492492
<additionalOptions>
493493
<!--suppress UnresolvedMavenProperty -->
494494
<jacoco>${jacoco.java.option}</jacoco>
495+
<option>-XX:+EnableDynamicAgentLoading</option>
495496
</additionalOptions>
496497
</javaOptions>
497498
<tags>
@@ -531,7 +532,7 @@ limitations under the License.]]>
531532
**/*ImageUtilsTest.java
532533
</exclude>
533534
</excludes>
534-
<argLine>@{surefire.argLine}</argLine>
535+
<argLine>@{surefire.argLine} -XX:+EnableDynamicAgentLoading</argLine>
535536
</configuration>
536537
<dependencies>
537538
<!-- https://mvnrepository.com/artifact/me.fabriciorby/maven-surefire-junit5-tree-reporter -->

src/main/java/com/redhat/exhort/providers/GoModulesProvider.java

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,12 @@ private Sbom buildSbomFromGraph(
271271
if (!edges.containsKey(getParentVertex(line))) {
272272
// Collect all direct dependencies of the current module into a list.
273273
List<String> deps =
274-
collectAllDirectDependencies(
275-
linesList.subList(startingIndex, linesList.size() - 1), line);
274+
collectAllDirectDependencies(linesList.subList(startingIndex, linesList.size()), line);
276275
edges.put(getParentVertex(line), deps);
277276
startingIndex += deps.size();
278277
}
279278
}
280-
boolean goMvsLogicEnabled = Environment.getBoolean(PROP_EXHORT_GO_MVS_LOGIC_ENABLED, false);
279+
boolean goMvsLogicEnabled = Environment.getBoolean(PROP_EXHORT_GO_MVS_LOGIC_ENABLED, true);
281280
if (goMvsLogicEnabled) {
282281
edges = getFinalPackagesVersionsForModule(edges, manifestPath);
283282
}
@@ -326,24 +325,35 @@ private Map<String, List<String>> getFinalPackagesVersionsForModule(
326325
Collectors.toMap(
327326
t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second));
328327
Map<String, List<String>> listWithModifiedVersions = new HashMap<>();
329-
edges.entrySet().stream()
330-
.filter(string -> string.getKey().trim().split("@").length == 2)
331-
.collect(Collectors.toList())
332-
.forEach(
333-
(entry) -> {
334-
String packageWithSelectedVersion =
335-
getPackageWithFinalVersion(finalModulesVersions, entry.getKey());
336-
List<String> packagesWithFinalVersions =
337-
getListOfPackagesWithFinalVersions(finalModulesVersions, entry);
338-
listWithModifiedVersions.put(packageWithSelectedVersion, packagesWithFinalVersions);
339-
});
328+
// Process all entries, including those without versions (like the root module)
329+
edges.forEach(
330+
(key, value) -> {
331+
// Handle both cases: module with version (module@version) and without (just module name)
332+
String packageWithSelectedVersion;
333+
if (key.contains("@")) {
334+
packageWithSelectedVersion = getPackageWithFinalVersion(finalModulesVersions, key);
335+
} else {
336+
// For root module or modules without version, get version from finalModulesVersions
337+
// or use default version
338+
String version = finalModulesVersions.get(key);
339+
if (version != null) {
340+
packageWithSelectedVersion = String.format("%s@%s", key, version);
341+
} else {
342+
// If not found, keep original key and append default version
343+
packageWithSelectedVersion = String.format("%s@%s", key, this.mainModuleVersion);
344+
}
345+
}
346+
List<String> packagesWithFinalVersions =
347+
getListOfPackagesWithFinalVersions(finalModulesVersions, value);
348+
listWithModifiedVersions.put(packageWithSelectedVersion, packagesWithFinalVersions);
349+
});
340350

341351
return listWithModifiedVersions;
342352
}
343353

344354
private List<String> getListOfPackagesWithFinalVersions(
345-
Map<String, String> finalModulesVersions, Map.Entry<String, List<String>> entry) {
346-
return entry.getValue().stream()
355+
Map<String, String> finalModulesVersions, List<String> packages) {
356+
return packages.stream()
347357
.map(
348358
(packageWithVersion) ->
349359
getPackageWithFinalVersion(finalModulesVersions, packageWithVersion))

src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.Map;
3838
import java.util.Objects;
3939
import java.util.TreeMap;
40+
import java.util.logging.Level;
4041
import java.util.logging.Logger;
4142
import java.util.stream.Collectors;
4243
import javax.xml.stream.XMLInputFactory;
@@ -426,7 +427,7 @@ public int hashCode() {
426427

427428
private String selectMvnRuntime(final Path manifestPath) {
428429
boolean preferWrapper = Operations.getWrapperPreference(MVN);
429-
if (preferWrapper) {
430+
if (preferWrapper && manifestPath != null) {
430431
String wrapperName = Operations.isWindows() ? "mvnw.cmd" : "mvnw";
431432
String mvnw = traverseForMvnw(wrapperName, manifestPath.toString());
432433
if (mvnw != null) {
@@ -438,8 +439,10 @@ private String selectMvnRuntime(final Path manifestPath) {
438439
}
439440
return mvnw;
440441
} catch (Exception e) {
441-
log.warning(
442-
"Failed to check for mvnw due to: " + e.getMessage() + " Fall back to use mvn");
442+
log.log(
443+
Level.WARNING,
444+
"Failed to check for mvnw due to: {0} Fall back to use mvn",
445+
e.getMessage());
443446
}
444447
}
445448
}

src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.redhat.exhort.utils.Environment;
2424
import java.util.ArrayList;
2525
import java.util.Collection;
26+
import java.util.Collections;
2627
import java.util.Date;
2728
import java.util.List;
2829
import java.util.Objects;
@@ -288,18 +289,19 @@ public boolean checkIfPackageInsideDependsOnList(PackageURL component, String na
288289
if (comp.isPresent()) {
289290
Dependency targetComponent = comp.get();
290291
List<Dependency> deps = targetComponent.getDependencies();
291-
List<PackageURL> allDirectDeps =
292-
deps.stream()
293-
.map(
294-
dep -> {
295-
try {
296-
return new PackageURL(dep.getRef());
297-
} catch (MalformedPackageURLException e) {
298-
throw new RuntimeException(e);
299-
}
300-
})
301-
.collect(Collectors.toList());
302-
292+
List<PackageURL> allDirectDeps = Collections.emptyList();
293+
if (deps != null) {
294+
deps.stream()
295+
.map(
296+
dep -> {
297+
try {
298+
return new PackageURL(dep.getRef());
299+
} catch (MalformedPackageURLException e) {
300+
throw new RuntimeException(e);
301+
}
302+
})
303+
.collect(Collectors.toList());
304+
}
303305
result = allDirectDeps.stream().anyMatch(dep -> dep.getName().equals(name));
304306
}
305307
return result;

src/main/java/com/redhat/exhort/tools/Operations.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,19 @@ private Operations() {
5656
* @return the custom path from the relevant environment variable or the original argument.
5757
*/
5858
public static String getCustomPathOrElse(String defaultExecutable) {
59-
var target = defaultExecutable.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_");
60-
var executableKey = String.format("EXHORT_%s_PATH", target);
61-
return Environment.get(executableKey, defaultExecutable);
59+
String normalized = defaultExecutable;
60+
int dotIndex = normalized.indexOf('.');
61+
if (dotIndex > 0) {
62+
normalized = normalized.substring(0, dotIndex);
63+
}
64+
65+
var target = normalized.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_");
66+
var primaryKey = String.format("EXHORT_%s_PATH", target);
67+
String primary = Environment.get(primaryKey);
68+
if (primary != null) {
69+
return primary;
70+
}
71+
return defaultExecutable;
6272
}
6373

6474
/**

src/main/java/com/redhat/exhort/utils/Environment.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public final class Environment {
2222
private Environment() {}
2323

2424
public static String get(String name, String defaultValue) {
25-
return Optional.ofNullable(System.getProperty(name))
26-
.or(() -> Optional.ofNullable(System.getenv(name)))
25+
return Optional.ofNullable(System.getenv(name))
26+
.or(() -> Optional.ofNullable(System.getProperty(name)))
2727
.orElse(defaultValue);
2828
}
2929

src/test/java/com/redhat/exhort/image/ImageUtilsTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,12 @@ void test_get_syft_envs() {
496496
@Test
497497
@SetSystemProperty(key = "PATH", value = mockPath)
498498
void test_update_PATH_env() {
499-
var path = ImageUtils.updatePATHEnv("test-exec-path");
500-
assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path);
499+
try (MockedStatic<Environment> mockEnv =
500+
Mockito.mockStatic(Environment.class, Mockito.CALLS_REAL_METHODS)) {
501+
mockEnv.when(() -> Environment.get("PATH")).thenReturn("test-path");
502+
var path = ImageUtils.updatePATHEnv("test-exec-path");
503+
assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path);
504+
}
501505
}
502506

503507
@Test

src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import static org.junit.jupiter.api.Assertions.assertThrows;
2424
import static org.junit.jupiter.api.Assertions.assertTrue;
2525

26+
import com.fasterxml.jackson.core.JsonProcessingException;
27+
import com.fasterxml.jackson.databind.JsonNode;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import com.fasterxml.jackson.databind.SerializationFeature;
2630
import com.redhat.exhort.Api;
2731
import com.redhat.exhort.ExhortTest;
2832
import java.io.IOException;
@@ -38,6 +42,9 @@
3842

3943
@ExtendWith(HelperExtension.class)
4044
class Golang_Modules_Provider_Test extends ExhortTest {
45+
private static final ObjectMapper JSON_MAPPER =
46+
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
47+
4148
// test folder are located at src/test/resources/tst_manifests/npm
4249
// each folder should contain:
4350
// - package.json: the target manifest for testing
@@ -78,7 +85,8 @@ void test_the_provideStack(String testFolder) throws IOException {
7885
Files.deleteIfExists(tmpGolangFile);
7986
// verify expected SBOM is returned
8087
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
81-
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
88+
assertThat(prettyJson(dropIgnoredKeepFormat(new String(content.buffer))))
89+
.isEqualTo(prettyJson(dropIgnoredKeepFormat(expectedSbom)));
8290
}
8391

8492
@ParameterizedTest
@@ -107,7 +115,8 @@ void test_the_provideComponent(String testFolder) throws IOException {
107115
Files.deleteIfExists(tmpGolangFile);
108116
// verify expected SBOM is returned
109117
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
110-
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
118+
assertThat(prettyJson(dropIgnoredKeepFormat(new String(content.buffer))))
119+
.isEqualTo(prettyJson(dropIgnoredKeepFormat(expectedSbom)));
111120
}
112121

113122
@Test
@@ -149,14 +158,15 @@ void Test_Golang_Modules_with_Match_Manifest_Version(boolean MatchManifestVersio
149158
String actualSbomWithTSStripped = dropIgnoredKeepFormat(sbomString);
150159

151160
assertEquals(
152-
dropIgnored(getStringFromFile("msc/golang/expected_sbom_ca.json")).trim(),
153-
dropIgnored(actualSbomWithTSStripped));
161+
prettyJson(
162+
dropIgnoredKeepFormat(getStringFromFile("msc/golang/expected_sbom_ca.json").trim())),
163+
prettyJson(actualSbomWithTSStripped));
154164
}
155165
}
156166

157167
@Test
158-
void Test_Golang_MvS_Logic_Enabled() throws IOException {
159-
System.setProperty(GoModulesProvider.PROP_EXHORT_GO_MVS_LOGIC_ENABLED, "true");
168+
void Test_Golang_MvS_Logic_Disabled() throws IOException {
169+
System.setProperty(GoModulesProvider.PROP_EXHORT_GO_MVS_LOGIC_ENABLED, "false");
160170
String goModPath = getFileFromResource("go.mod", "msc/golang/mvs_logic/go.mod");
161171
Path manifest = Path.of(goModPath);
162172
GoModulesProvider goModulesProvider = new GoModulesProvider(manifest);
@@ -165,12 +175,10 @@ void Test_Golang_MvS_Logic_Enabled() throws IOException {
165175
goModulesProvider.getDependenciesSbom(manifest, true).getAsJsonString());
166176
String expectedSbom =
167177
getStringFromFile("msc/golang/mvs_logic/expected_sbom_stack_analysis.json").trim();
168-
assertEquals(dropIgnored(expectedSbom), dropIgnored(resultSbom));
178+
assertEquals(prettyJson(dropIgnoredKeepFormat(expectedSbom)), prettyJson(resultSbom));
169179

170-
// check that only one version of package golang/go.opencensus.io is in sbom for
171-
// EXHORT_GO_MVS_LOGIC_ENABLED=true
172180
assertEquals(
173-
1,
181+
5,
174182
Arrays.stream(resultSbom.split(System.lineSeparator()))
175183
.filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@"))
176184
.count());
@@ -186,17 +194,20 @@ void Test_Golang_MvS_Logic_Enabled() throws IOException {
186194
Arrays.stream(resultSbom.split(System.lineSeparator()))
187195
.filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@"))
188196
.count()
189-
> 1);
190-
}
191-
192-
private String dropIgnored(String s) {
193-
return s.replaceAll("goarch=\\w+&goos=\\w+&", "")
194-
.replaceAll("\\s+", "")
195-
.replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", "");
197+
== 1);
196198
}
197199

198200
private String dropIgnoredKeepFormat(String s) {
199201
return s.replaceAll("goarch=\\w+&goos=\\w+&", "")
200202
.replaceAll("\"timestamp\" : \"[a-zA-Z0-9\\-\\:]+\",\n ", "");
201203
}
204+
205+
private String prettyJson(String s) {
206+
try {
207+
JsonNode node = JSON_MAPPER.readTree(s);
208+
return JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(node);
209+
} catch (JsonProcessingException e) {
210+
return s; // Fallback if not valid JSON after sanitization
211+
}
212+
}
202213
}

src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ public class Java_Envs_Test {
3030
@Test
3131
@SetSystemProperty(key = "JAVA_HOME", value = "test-java-home")
3232
void test_java_get_envs() {
33-
var envs = new JavaMavenProvider(null).getMvnExecEnvs();
34-
assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs);
33+
try (MockedStatic<Environment> mockEnv =
34+
Mockito.mockStatic(Environment.class, Mockito.CALLS_REAL_METHODS)) {
35+
mockEnv.when(() -> Environment.get("JAVA_HOME")).thenReturn("test-java-home");
36+
var envs = new JavaMavenProvider(null).getMvnExecEnvs();
37+
assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs);
38+
}
3539
}
3640

3741
@Test

0 commit comments

Comments
 (0)