aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTravis Collins <travistx@gmail.com>2025-03-18 11:27:39 -0600
committersonartech <sonartech@sonarsource.com>2025-03-18 20:04:08 +0000
commit668a45f186181f82186154178c919541e8608b33 (patch)
tree0157b56197220252a79365666df7f68004eb6ce0
parentb9ce98f71c6bf7dbc337fae00f0a49d346d9c481 (diff)
downloadsonarqube-668a45f186181f82186154178c919541e8608b33.tar.gz
sonarqube-668a45f186181f82186154178c919541e8608b33.zip
SCA-140 Respect .gitignore rules
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java79
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java14
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java229
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java17
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java67
6 files changed, 358 insertions, 50 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
index 7ee00d8e08f..893a2a1649e 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
@@ -21,12 +21,18 @@ package org.sonar.scanner.sca;
import java.io.File;
import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
@@ -36,6 +42,8 @@ import org.sonar.api.utils.System2;
import org.sonar.core.util.ProcessWrapperFactory;
import org.sonar.scanner.config.DefaultConfiguration;
import org.sonar.scanner.repository.TelemetryCache;
+import org.sonar.scanner.scm.ScmConfiguration;
+import org.sonar.scm.git.JGitUtils;
/**
* The CliService class is meant to serve as the main entrypoint for any commands
@@ -45,16 +53,20 @@ import org.sonar.scanner.repository.TelemetryCache;
*/
public class CliService {
private static final Logger LOG = LoggerFactory.getLogger(CliService.class);
+ public static final String EXCLUDED_MANIFESTS_PROP_KEY = "sonar.sca.excludedManifests";
+
private final ProcessWrapperFactory processWrapperFactory;
private final TelemetryCache telemetryCache;
private final System2 system2;
private final Server server;
+ private final ScmConfiguration scmConfiguration;
- public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server) {
+ public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server, ScmConfiguration scmConfiguration) {
this.processWrapperFactory = processWrapperFactory;
this.telemetryCache = telemetryCache;
this.system2 = system2;
this.server = server;
+ this.scmConfiguration = scmConfiguration;
}
public File generateManifestsZip(DefaultInputModule module, File cliExecutable, DefaultConfiguration configuration) throws IOException, IllegalStateException {
@@ -75,6 +87,12 @@ public class CliService {
args.add("--directory");
args.add(module.getBaseDir().toString());
+ String excludeFlag = getExcludeFlag(module, configuration);
+ if (excludeFlag != null) {
+ args.add("--exclude");
+ args.add(excludeFlag);
+ }
+
boolean scaDebug = configuration.getBoolean("sonar.sca.debug").orElse(false);
if (LOG.isDebugEnabled() || scaDebug) {
LOG.info("Setting CLI to debug mode");
@@ -107,4 +125,63 @@ public class CliService {
telemetryCache.put("scanner.sca.execution.cli.success", String.valueOf(success));
}
}
+
+ private @Nullable String getExcludeFlag(DefaultInputModule module, DefaultConfiguration configuration) throws IOException {
+ List<String> configExcludedPaths = getConfigExcludedPaths(configuration);
+ List<String> scmIgnoredPaths = getScmIgnoredPaths(module);
+
+ ArrayList<String> mergedExclusionPaths = new ArrayList<>();
+ mergedExclusionPaths.addAll(configExcludedPaths);
+ mergedExclusionPaths.addAll(scmIgnoredPaths);
+
+ if (mergedExclusionPaths.isEmpty()) {
+ return null;
+ }
+
+ // wrap each exclusion path in quotes to handle commas in file paths
+ return toCsvString(mergedExclusionPaths);
+ }
+
+ private static List<String> getConfigExcludedPaths(DefaultConfiguration configuration) {
+ String[] excludedPaths = configuration.getStringArray(EXCLUDED_MANIFESTS_PROP_KEY);
+ if (excludedPaths == null) {
+ return List.of();
+ }
+ return Arrays.stream(excludedPaths).toList();
+ }
+
+ private List<String> getScmIgnoredPaths(DefaultInputModule module) {
+ var scmProvider = scmConfiguration.provider();
+ // Only Git is supported at this time
+ if (scmProvider == null || scmProvider.key() == null || !scmProvider.key().equals("git")) {
+ return List.of();
+ }
+
+ if (scmConfiguration.isExclusionDisabled()) {
+ // The user has opted out of using the SCM exclusion rules
+ return List.of();
+ }
+
+ Path baseDirPath = module.getBaseDir();
+ List<String> scmIgnoredPaths = JGitUtils.getAllIgnoredPaths(baseDirPath);
+ if (scmIgnoredPaths.isEmpty()) {
+ return List.of();
+ }
+ return scmIgnoredPaths.stream()
+ .map(ignoredPathRel -> {
+ boolean isDirectory = Files.isDirectory(baseDirPath.resolve(ignoredPathRel));
+ // Directories need to get turned into a glob for the Tidelift CLI
+ return isDirectory ? (ignoredPathRel + "/**") : ignoredPathRel;
+ })
+ .toList();
+ }
+
+ private static String toCsvString(List<String> values) throws IOException {
+ StringWriter sw = new StringWriter();
+ try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) {
+ printer.printRecord(values);
+ }
+ // trim to remove the trailing newline
+ return sw.toString().trim();
+ }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
index 26a98e65fdc..59b9293aa9d 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java
@@ -52,6 +52,8 @@ public class ScaProperties {
.entrySet()
.stream()
.filter(entry -> entry.getKey().startsWith(SONAR_SCA_PREFIX))
+ // EXCLUDED_MANIFESTS_PROP_KEY is a special case which we handle via --args, not environment variables
+ .filter(entry -> !entry.getKey().equals(CliService.EXCLUDED_MANIFESTS_PROP_KEY))
.collect(Collectors.toMap(entry -> convertPropToEnvVariable(entry.getKey()), Map.Entry::getValue));
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java
index 972a8ce8da3..5b3b3257142 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java
@@ -21,6 +21,9 @@ package org.sonar.scm.git;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.List;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
@@ -41,4 +44,15 @@ public class JGitUtils {
throw new IllegalStateException("Unable to open Git repository", e);
}
}
+
+ // Return a list of scm ignored paths relative to the baseDir.
+ public static List<String> getAllIgnoredPaths(Path baseDir) {
+ try (Repository repo = buildRepository(baseDir)) {
+ try (Git git = new Git(repo)) {
+ return git.status().call().getIgnoredNotInIndex().stream().sorted().toList();
+ } catch (GitAPIException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
index b8bb1a26961..27aa9bd16e2 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
@@ -23,25 +23,34 @@ import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.SystemUtils;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
import org.sonar.api.batch.bootstrap.ProjectDefinition;
import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.batch.scm.ScmProvider;
import org.sonar.api.platform.Server;
import org.sonar.api.testfixtures.log.LogTesterJUnit5;
import org.sonar.api.utils.System2;
import org.sonar.core.util.ProcessWrapperFactory;
import org.sonar.scanner.config.DefaultConfiguration;
import org.sonar.scanner.repository.TelemetryCache;
+import org.sonar.scanner.scm.ScmConfiguration;
+import org.sonar.scm.git.GitScmProvider;
+import org.sonar.scm.git.JGitUtils;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.slf4j.event.Level.DEBUG;
@@ -55,6 +64,11 @@ class CliServiceTest {
private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
@TempDir
Path rootModuleDir;
+ private final ScmConfiguration scmConfiguration = mock(ScmConfiguration.class);
+ private final ScmProvider scmProvider = mock(GitScmProvider.class);
+ ProcessWrapperFactory processWrapperFactory = mock(ProcessWrapperFactory.class, CALLS_REAL_METHODS);
+ private MockedStatic<JGitUtils> jGitUtilsMock;
+ DefaultConfiguration configuration = mock(DefaultConfiguration.class);
private CliService underTest;
@@ -63,17 +77,38 @@ class CliServiceTest {
telemetryCache = new TelemetryCache();
rootInputModule = new DefaultInputModule(
ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(rootModuleDir.toFile()));
- underTest = new CliService(new ProcessWrapperFactory(), telemetryCache, System2.INSTANCE, server);
+ when(scmConfiguration.provider()).thenReturn(scmProvider);
+ when(scmProvider.key()).thenReturn("git");
+ when(scmConfiguration.isExclusionDisabled()).thenReturn(false);
+ jGitUtilsMock = org.mockito.Mockito.mockStatic(JGitUtils.class);
+ jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of("ignored.txt"));
+ when(server.getVersion()).thenReturn("1.0.0");
+ logTester.setLevel(INFO);
+ when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true));
+
+ underTest = new CliService(processWrapperFactory, telemetryCache, System2.INSTANCE, server, scmConfiguration);
+ }
+
+ @AfterEach
+ void teardown() {
+ if (jGitUtilsMock != null) {
+ jGitUtilsMock.close();
+ }
}
@Test
void generateZip_shouldCallProcessCorrectly_andRegisterTelemetry() throws IOException, URISyntaxException {
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- // We need to set the logging level to debug in order to be able to view the shell script's output
- logTester.setLevel(DEBUG);
+ when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true", CliService.EXCLUDED_MANIFESTS_PROP_KEY, "foo,bar,baz/**"));
+ when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true"));
+ when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"foo", "bar", "baz/**"});
- List<String> args = List.of(
+ File producedZip = underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ assertThat(producedZip).exists();
+
+ var expectedArguments = List.of(
"projects",
"save-lockfiles",
"--zip",
@@ -81,35 +116,31 @@ class CliServiceTest {
rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--exclude",
+ "foo,bar,baz/**,ignored.txt",
"--debug");
- String argumentOutput = "Arguments Passed In: " + String.join(" ", args);
- DefaultConfiguration configuration = mock(DefaultConfiguration.class);
- when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true"));
- when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true"));
-
- File producedZip = underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
- assertThat(producedZip).exists();
-
- assertThat(logTester.logs(DEBUG))
- .contains(argumentOutput)
+ assertThat(logTester.logs(INFO))
+ .contains("Arguments Passed In: " + String.join(" ", expectedArguments))
.contains("TIDELIFT_SKIP_UPDATE_CHECK=1")
.contains("TIDELIFT_ALLOW_MANIFEST_FAILURES=1")
- .contains("TIDELIFT_RECURSIVE_MANIFEST_SEARCH=true");
- assertThat(logTester.logs(INFO)).contains("Generated manifests zip file: " + producedZip.getName());
+ .contains("TIDELIFT_RECURSIVE_MANIFEST_SEARCH=true")
+ .contains("Generated manifests zip file: " + producedZip.getName());
assertThat(telemetryCache.getAll()).containsKey("scanner.sca.execution.cli.duration").isNotNull();
assertThat(telemetryCache.getAll()).containsEntry("scanner.sca.execution.cli.success", "true");
}
@Test
- void generateZip_whenDebugLogLevel_shouldCallProcessCorrectly() throws IOException, URISyntaxException {
+ void generateZip_whenDebugLogLevelAndScaDebugNotEnabled_shouldWriteDebugLogsToDebugStream() throws IOException, URISyntaxException {
+ logTester.setLevel(DEBUG);
+ when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(false));
+
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- // We need to set the logging level to debug in order to be able to view the shell script's output
- logTester.setLevel(DEBUG);
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
- List<String> args = List.of(
+ var expectedArguments = List.of(
"projects",
"save-lockfiles",
"--zip",
@@ -117,27 +148,23 @@ class CliServiceTest {
rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--exclude",
+ "ignored.txt",
"--debug");
- String argumentOutput = "Arguments Passed In: " + String.join(" ", args);
- DefaultConfiguration configuration = mock(DefaultConfiguration.class);
- when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true"));
- when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true"));
-
- underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
-
assertThat(logTester.logs(DEBUG))
- .contains(argumentOutput);
+ .contains("Arguments Passed In: " + String.join(" ", expectedArguments));
}
@Test
- void generateZip_whenScaDebugEnabled_shouldCallProcessCorrectly() throws IOException, URISyntaxException {
+ void generateZip_whenScaDebugEnabled_shouldWriteDebugLogsToInfoStream() throws IOException, URISyntaxException {
+ when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true));
+
assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
- // Set the logging level to info so that we don't automatically set --debug flag
- logTester.setLevel(INFO);
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
- List<String> args = List.of(
+ var expectedArguments = List.of(
"projects",
"save-lockfiles",
"--zip",
@@ -145,34 +172,142 @@ class CliServiceTest {
rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
"--directory",
rootInputModule.getBaseDir().toString(),
+ "--exclude",
+ "ignored.txt",
"--debug");
- String argumentOutput = "Arguments Passed In: " + String.join(" ", args);
- DefaultConfiguration configuration = mock(DefaultConfiguration.class);
- when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true"));
- when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true"));
- when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true));
+ assertThat(logTester.logs(INFO))
+ .contains("Arguments Passed In: " + String.join(" ", expectedArguments));
+ }
+ @Test
+ void generateZip_shouldSendSQEnvVars() throws IOException, URISyntaxException {
underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
assertThat(logTester.logs(INFO))
- .contains(argumentOutput);
+ .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1")
+ .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=1.0.0");
}
@Test
- void generateZip_shouldSendSQEnvVars() throws IOException, URISyntaxException {
- // We need to set the logging level to debug in order to be able to view the shell script's output
- logTester.setLevel(DEBUG);
+ void generateZip_includesIgnoredPathsFromGitProvider() throws Exception {
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
- var version = "1.0.0";
- when(server.getVersion()).thenReturn(version);
+ var expectedArguments = List.of(
+ "projects",
+ "save-lockfiles",
+ "--zip",
+ "--zip-filename",
+ rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(),
+ "--directory",
+ rootInputModule.getBaseDir().toString(),
+ "--exclude",
+ "ignored.txt",
+ "--debug");
+
+ assertThat(logTester.logs(INFO))
+ .contains("Arguments Passed In: " + String.join(" ", expectedArguments))
+ .contains("TIDELIFT_SKIP_UPDATE_CHECK=1")
+ .contains("TIDELIFT_ALLOW_MANIFEST_FAILURES=1")
+ .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1")
+ .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=1.0.0");
+
+ }
+
+ @Test
+ void generateZip_withNoScm_doesNotIncludeScmIgnoredPaths() throws Exception {
+ when(scmConfiguration.provider()).thenReturn(null);
- DefaultConfiguration configuration = mock(DefaultConfiguration.class);
underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
- assertThat(logTester.logs(DEBUG))
- .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1")
- .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=" + version);
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).doesNotContain("--exclude");
+ }
+
+ @Test
+ void generateZip_withNonGit_doesNotIncludeScmIgnoredPaths() throws Exception {
+ when(scmProvider.key()).thenReturn("notgit");
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).doesNotContain("--exclude");
+ }
+
+ @Test
+ void generateZip_withExclusionDisabled_doesNotIncludeScmIgnoredPaths() throws Exception {
+ when(scmConfiguration.isExclusionDisabled()).thenReturn(true);
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).doesNotContain("--exclude");
+ }
+
+ @Test
+ void generateZip_withNoScmIgnores_doesNotIncludeScmIgnoredPaths() throws Exception {
+ jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of());
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).doesNotContain("--exclude");
+ }
+
+ @Test
+ void generateZip_withExistingExcludedManifests_appendsScmIgnoredPaths() throws Exception {
+ when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"**/test/**"});
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).contains("--exclude **/test/**,ignored.txt");
+ }
+
+ @Test
+ void generateZip_withExcludedManifestsSettingContainingBadCharacters_handlesTheBadCharacters() throws Exception {
+ when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {
+ "**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"});
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+
+ String expectedExcludeFlag = """
+ --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**",ignored.txt
+ """.strip();
+ assertThat(capturedArgs).contains(expectedExcludeFlag);
+ }
+
+ @Test
+ void generateZip_withScmIgnoresContainingBadCharacters_handlesTheBadCharacters() throws Exception {
+ jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class)))
+ .thenReturn(List.of("**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"));
+
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+
+ String expectedExcludeFlag = """
+ --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**"
+ """.strip();
+ assertThat(capturedArgs).contains(expectedExcludeFlag);
+ }
+
+ @Test
+ void generateZip_withIgnoredDirectories_GlobifiesDirectories() throws Exception {
+ String ignoredDirectory = "directory1";
+ Files.createDirectories(rootModuleDir.resolve(ignoredDirectory));
+ String ignoredFile = "directory2/file.txt";
+ Path ignoredFilePath = rootModuleDir.resolve(ignoredFile);
+ Files.createDirectories(ignoredFilePath.getParent());
+ Files.createFile(ignoredFilePath);
+
+ jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of(ignoredDirectory, ignoredFile));
+ underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration);
+
+ String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get();
+ assertThat(capturedArgs).contains("--exclude directory1/**,directory2/file.txt");
}
private URL scriptUrl() {
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
index c19600dcf3c..3ac8418e14a 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java
@@ -61,7 +61,6 @@ class ScaPropertiesTest {
@Test
void buildFromScannerProperties_shouldMapAllKnownProperties() {
var inputProperties = new HashMap<String, String>();
- inputProperties.put("sonar.sca.excludedManifests", "exclude/*");
inputProperties.put("sonar.sca.goNoResolve", "true");
inputProperties.put("sonar.sca.gradleConfigurationPattern", "pattern");
inputProperties.put("sonar.sca.gradleNoResolve", "false");
@@ -80,7 +79,6 @@ class ScaPropertiesTest {
when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
var expectedProperties = new HashMap<String, String>();
- expectedProperties.put("TIDELIFT_EXCLUDED_MANIFESTS", "exclude/*");
expectedProperties.put("TIDELIFT_GO_NO_RESOLVE", "true");
expectedProperties.put("TIDELIFT_GRADLE_CONFIGURATION_PATTERN", "pattern");
expectedProperties.put("TIDELIFT_GRADLE_NO_RESOLVE", "false");
@@ -100,4 +98,19 @@ class ScaPropertiesTest {
assertThat(result).containsExactlyInAnyOrderEntriesOf(expectedProperties);
}
+
+ @Test
+ void buildFromScannerProperties_shouldIgnoreExcludedManifests() {
+ var inputProperties = new HashMap<String, String>();
+ inputProperties.put("sonar.sca.unknownProperty", "value");
+ inputProperties.put("sonar.sca.excludedManifests", "ignore-me");
+ when(configuration.getProperties()).thenReturn(inputProperties);
+ when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class))));
+
+ var result = ScaProperties.buildFromScannerProperties(configuration);
+
+ assertThat(result).containsExactly(
+ Map.entry("TIDELIFT_UNKNOWN_PROPERTY", "value"));
+ }
+
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java
new file mode 100644
index 00000000000..3578a3c60f7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.scm.git;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.eclipse.jgit.api.Git;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.api.utils.MessageException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class JGitUtilsTest {
+
+ @TempDir
+ Path rootModuleDir;
+
+ @Test
+ void getAllIgnoredPaths_ReturnsIgnoredFiles() throws Exception {
+ Git.init().setDirectory(rootModuleDir.toFile()).call();
+ Files.createDirectories(rootModuleDir.resolve("directory1"));
+ Files.createDirectories(rootModuleDir.resolve("directory2"));
+ Files.createDirectories(rootModuleDir.resolve("directory3"));
+ Files.write(rootModuleDir.resolve("directory1/file_a.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve("directory1/file_b.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve("directory2/file_a.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve("directory2/file_b.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve("directory3/file_a.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve("directory3/file_b.txt"), "content".getBytes());
+ Files.write(rootModuleDir.resolve(".gitignore"), "ignored.txt\ndirectory1\ndirectory2/file_a.txt".getBytes());
+ Files.write(rootModuleDir.resolve("directory3/.gitignore"), "file_b.txt".getBytes());
+
+ List<String> result = JGitUtils.getAllIgnoredPaths(rootModuleDir);
+
+ // in directory1, the entire directory is ignored without listing each file
+ // in directory2, specific files are ignored, so those files are listed
+ // in directory3, specific files are ignored via a separate .gitignore file
+ assertThat(result).isEqualTo(List.of("directory1", "directory2/file_a.txt", "directory3/file_b.txt"));
+ }
+
+ @Test
+ void getIgnoredPaths_WithNonGitDirectory_ThrowsException() {
+ assertThatThrownBy(() -> JGitUtils.getAllIgnoredPaths(rootModuleDir))
+ .isInstanceOf(MessageException.class)
+ .hasMessageStartingWith("Not inside a Git work tree: ");
+ }
+}