Skip to content

Commit d593a9c

Browse files
committed
feat!: set golang mvs true as default
Signed-off-by: Ruben Romero Montes <rromerom@redhat.com>
1 parent 7a4181d commit d593a9c

11 files changed

Lines changed: 104 additions & 67 deletions

File tree

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/v1@v1.1
512-
- namespace/c/v1@v1.2
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/v1@v1.1`
515+
- `namespace/c/v1@v1.2`
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ private Sbom buildSbomFromGraph(
277277
startingIndex += deps.size();
278278
}
279279
}
280-
boolean goMvsLogicEnabled = Environment.getBoolean(PROP_EXHORT_GO_MVS_LOGIC_ENABLED, false);
280+
boolean goMvsLogicEnabled = Environment.getBoolean(PROP_EXHORT_GO_MVS_LOGIC_ENABLED, true);
281281
if (goMvsLogicEnabled) {
282282
edges = getFinalPackagesVersionsForModule(edges, manifestPath);
283283
}

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

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,6 @@
1515
*/
1616
package com.redhat.exhort.providers;
1717

18-
import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded;
19-
20-
import com.github.packageurl.MalformedPackageURLException;
21-
import com.github.packageurl.PackageURL;
22-
import com.redhat.exhort.Api;
23-
import com.redhat.exhort.Provider;
24-
import com.redhat.exhort.logging.LoggersFactory;
25-
import com.redhat.exhort.sbom.Sbom;
26-
import com.redhat.exhort.sbom.SbomFactory;
27-
import com.redhat.exhort.tools.Ecosystem.Type;
28-
import com.redhat.exhort.tools.Operations;
29-
import com.redhat.exhort.utils.Environment;
3018
import java.io.IOException;
3119
import java.nio.file.Files;
3220
import java.nio.file.Path;
@@ -37,13 +25,27 @@
3725
import java.util.Map;
3826
import java.util.Objects;
3927
import java.util.TreeMap;
28+
import java.util.logging.Level;
4029
import java.util.logging.Logger;
4130
import java.util.stream.Collectors;
31+
4232
import javax.xml.stream.XMLInputFactory;
4333
import javax.xml.stream.XMLStreamConstants;
4434
import javax.xml.stream.XMLStreamException;
4535
import javax.xml.stream.XMLStreamReader;
4636

37+
import com.github.packageurl.MalformedPackageURLException;
38+
import com.github.packageurl.PackageURL;
39+
import com.redhat.exhort.Api;
40+
import com.redhat.exhort.Provider;
41+
import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded;
42+
import com.redhat.exhort.logging.LoggersFactory;
43+
import com.redhat.exhort.sbom.Sbom;
44+
import com.redhat.exhort.sbom.SbomFactory;
45+
import com.redhat.exhort.tools.Ecosystem.Type;
46+
import com.redhat.exhort.tools.Operations;
47+
import com.redhat.exhort.utils.Environment;
48+
4749
/**
4850
* Concrete implementation of the {@link Provider} used for converting dependency trees for Java
4951
* Maven projects (pom.xml) into a content Dot Graphs for Stack analysis or Json for Component
@@ -426,7 +428,7 @@ public int hashCode() {
426428

427429
private String selectMvnRuntime(final Path manifestPath) {
428430
boolean preferWrapper = Operations.getWrapperPreference(MVN);
429-
if (preferWrapper) {
431+
if (preferWrapper && manifestPath != null) {
430432
String wrapperName = Operations.isWindows() ? "mvnw.cmd" : "mvnw";
431433
String mvnw = traverseForMvnw(wrapperName, manifestPath.toString());
432434
if (mvnw != null) {
@@ -438,8 +440,7 @@ private String selectMvnRuntime(final Path manifestPath) {
438440
}
439441
return mvnw;
440442
} catch (Exception e) {
441-
log.warning(
442-
"Failed to check for mvnw due to: " + e.getMessage() + " Fall back to use mvn");
443+
log.log(Level.WARNING, "Failed to check for mvnw due to: {0} Fall back to use mvn", e.getMessage());
443444
}
444445
}
445446
}

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)