diff options
author | Travis Collins <travistx@gmail.com> | 2025-03-18 11:27:39 -0600 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2025-03-18 20:04:08 +0000 |
commit | 668a45f186181f82186154178c919541e8608b33 (patch) | |
tree | 0157b56197220252a79365666df7f68004eb6ce0 | |
parent | b9ce98f71c6bf7dbc337fae00f0a49d346d9c481 (diff) | |
download | sonarqube-668a45f186181f82186154178c919541e8608b33.tar.gz sonarqube-668a45f186181f82186154178c919541e8608b33.zip |
SCA-140 Respect .gitignore rules
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: "); + } +} |