aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-scanner-engine
diff options
context:
space:
mode:
authorMike Young <mike.young@sonarsource.com>2025-02-06 14:16:37 -0500
committersonartech <sonartech@sonarsource.com>2025-02-19 20:03:12 +0000
commit49b800d8b7a9ade328bb5f3d01c0972290ee9f9d (patch)
tree0190669f1e605da1660b7d3915a3ccdafa4442c0 /sonar-scanner-engine
parent04b0453797b06c53fc9092c2961e5f24c10ebc28 (diff)
downloadsonarqube-49b800d8b7a9ade328bb5f3d01c0972290ee9f9d.tar.gz
sonarqube-49b800d8b7a9ade328bb5f3d01c0972290ee9f9d.zip
SQRP-149 Generate Manifest Files
Diffstat (limited to 'sonar-scanner-engine')
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java56
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java76
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java55
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringProjectScanContainer.java14
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java88
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java111
-rwxr-xr-xsonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh28
7 files changed, 427 insertions, 1 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java
new file mode 100644
index 00000000000..8a13f8aeded
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliCacheService.java
@@ -0,0 +1,56 @@
+/*
+ * 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.scanner.sca;
+
+import java.io.File;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
+import org.sonar.scanner.bootstrap.SonarUserHome;
+
+/**
+ * This class is responsible for checking the SQ server for the latest version of the CLI,
+ * caching the CLI for use across different projects, updating the cached CLI to the latest
+ * version, and holding on to the cached CLI's file location so that other service classes
+ * can make use of it.
+ */
+public class CliCacheService {
+ private static final Logger LOG = LoggerFactory.getLogger(CliCacheService.class);
+ private final SonarUserHome sonarUserHome;
+
+ public CliCacheService(SonarUserHome sonarUserHome) {
+ this.sonarUserHome = sonarUserHome;
+ }
+
+ // Right now you need to have the CLI installed in ~/.sonar/cache/tidelift locally to have
+ // the zip generation work.
+ public File cacheCli(String osName, String arch) {
+ LOG.debug("Requesting CLI for OS {} and arch {}", osName, arch);
+ return cliFile();
+ }
+
+ public File cliFile() {
+ return sonarUserHome.getPath().resolve("cache").resolve(fileName()).toFile();
+ }
+
+ private static String fileName() {
+ return System2.INSTANCE.isOsWindows() ? "tidelift.exe" : "tidelift";
+ }
+}
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
new file mode 100644
index 00000000000..8b16b303821
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.scanner.sca;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.core.util.ProcessWrapperFactory;
+
+/**
+ * The CliService class is meant to serve as the main entrypoint for any commands
+ * that should be executed by the CLI. It will handle manages the external process,
+ * raising any errors that happen while running a command, and passing back the
+ * data generated by the command to the caller.
+ */
+public class CliService {
+ private static final Logger LOG = LoggerFactory.getLogger(CliService.class);
+ private final ProcessWrapperFactory processWrapperFactory;
+ private final CliCacheService cliCacheService;
+
+ public CliService(ProcessWrapperFactory processWrapperFactory, CliCacheService cliCacheService) {
+ this.processWrapperFactory = processWrapperFactory;
+ this.cliCacheService = cliCacheService;
+ }
+
+ public File generateManifestsZip(DefaultInputModule module) throws IOException, IllegalStateException {
+ String zipName = "dependency-files.zip";
+ Path zipPath = module.getWorkDir().resolve(zipName);
+ List<String> args = new ArrayList<>();
+ args.add(cliCacheService.cliFile().getAbsolutePath());
+ args.add("projects");
+ args.add("save-lockfiles");
+ args.add("--zip");
+ args.add("--zip-filename");
+ args.add(zipPath.toAbsolutePath().toString());
+ args.add("--directory");
+ args.add(module.getBaseDir().toString());
+ args.add("--debug");
+
+ LOG.debug("Calling ProcessBuilder with args: {}", args);
+
+ Map<String, String> envProperties = new HashMap<>();
+ // sending this will tell the CLI to skip checking for the latest available version on startup
+ envProperties.put("TIDELIFT_SKIP_UPDATE_CHECK", "1");
+
+ processWrapperFactory.create(module.getWorkDir(), LOG::debug, envProperties, args.toArray(new String[0])).execute();
+ LOG.info("Generated manifests zip file: {}", zipName);
+
+ return zipPath.toFile();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java
new file mode 100644
index 00000000000..774b40f514e
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaExecutor.java
@@ -0,0 +1,55 @@
+/*
+ * 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.scanner.sca;
+
+import java.io.File;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.internal.apachecommons.lang3.SystemUtils;
+
+/**
+ * The ScaExecutor class is the main entrypoint for generating manifest dependency
+ * data during a Sonar scan and passing that data in the report so that it can
+ * be analyzed further by SQ server.
+ */
+public class ScaExecutor {
+ private static final Logger LOG = LoggerFactory.getLogger(ScaExecutor.class);
+
+ private final CliCacheService cliCacheService;
+ private final CliService cliService;
+
+ public ScaExecutor(CliCacheService cliCacheService, CliService cliService) {
+ this.cliCacheService = cliCacheService;
+ this.cliService = cliService;
+ }
+
+ public void execute(DefaultInputModule root) {
+ if (cliCacheService.cacheCli(SystemUtils.OS_NAME, SystemUtils.OS_ARCH).exists()) {
+ try {
+ File generatedZip = cliService.generateManifestsZip(root);
+ LOG.debug("Zip ready for report: {}", generatedZip);
+ } catch (IOException | IllegalStateException e) {
+ LOG.error("Error gathering manifests", e);
+ }
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringProjectScanContainer.java
index ef57ea5a076..ed1909c0b7c 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringProjectScanContainer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/SpringProjectScanContainer.java
@@ -52,6 +52,9 @@ import org.sonar.scanner.postjob.PostJobsExecutor;
import org.sonar.scanner.qualitygate.QualityGateCheck;
import org.sonar.scanner.report.ReportPublisher;
import org.sonar.scanner.rule.QProfileVerifier;
+import org.sonar.scanner.sca.CliCacheService;
+import org.sonar.scanner.sca.CliService;
+import org.sonar.scanner.sca.ScaExecutor;
import org.sonar.scanner.scan.filesystem.FileIndexer;
import org.sonar.scanner.scan.filesystem.InputFileFilterRepository;
import org.sonar.scanner.scan.filesystem.LanguageDetection;
@@ -131,7 +134,13 @@ public class SpringProjectScanContainer extends SpringComponentContainer {
// file system
InputFileFilterRepository.class,
FileIndexer.class,
- ProjectFileIndexer.class);
+ ProjectFileIndexer.class,
+
+ // SCA
+ CliService.class,
+ CliCacheService.class,
+ ScaExecutor.class
+ );
}
static ExtensionMatcher getScannerProjectExtensionsFilter() {
@@ -172,6 +181,9 @@ public class SpringProjectScanContainer extends SpringComponentContainer {
LOG.info("------------- Run sensors on project");
getComponentByType(ProjectSensorsExecutor.class).execute();
+ LOG.info("------------- Gather SCA dependencies on project");
+ getComponentByType(ScaExecutor.class).execute(tree.root());
+
getComponentByType(ScmPublisher.class).publish();
getComponentByType(CpdExecutor.class).execute();
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
new file mode 100644
index 00000000000..096c0ea7111
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.scanner.sca;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.event.Level;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.core.util.ProcessWrapperFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class CliServiceTest {
+ private final ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory();
+ private final CliCacheService cliCacheService = mock(CliCacheService.class);
+ private final CliService underTest = new CliService(processWrapperFactory, cliCacheService);
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
+
+ @Test
+ void generateZip_shouldCallProcessCorrectly(@TempDir Path rootModuleDir) throws IOException, URISyntaxException {
+ DefaultInputModule root = new DefaultInputModule(
+ ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(rootModuleDir.toFile()));
+
+ // There is a custom test Bash script available in src/test/resources/org/sonar/scanner/sca that
+ // will serve as our "CLI". This script will output some messages about what arguments were passed
+ // to it and will try to generate a zip file in the location the process specifies. This allows us
+ // to simulate a real CLI call without needing an OS specific CLI executable to run on a real project.
+ URL scriptUrl = CliServiceTest.class.getResource("echo_args.sh");
+ assertThat(scriptUrl).isNotNull();
+ File scriptDir = new File(scriptUrl.toURI());
+ assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue();
+ when(cliCacheService.cliFile()).thenReturn(scriptDir);
+
+ // We need to set the logging level to debug in order to be able to view the shell script's output
+ logTester.setLevel(Level.DEBUG);
+
+ List<String> args = new ArrayList<>();
+ args.add("projects");
+ args.add("save-lockfiles");
+ args.add("--zip");
+ args.add("--zip-filename");
+ args.add(root.getWorkDir().resolve("dependency-files.zip").toString());
+ args.add("--directory");
+ args.add(root.getBaseDir().toString());
+ args.add("--debug");
+
+ String argumentOutput = "Arguments Passed In: " + String.join(" ", args);
+
+
+ File producedZip = underTest.generateManifestsZip(root);
+ assertThat(producedZip).exists();
+ // The simulated CLI output will only be available at the debug level
+ assertThat(logTester.logs(Level.DEBUG)).contains(argumentOutput);
+ assertThat(logTester.logs(Level.DEBUG)).contains("TIDELIFT_SKIP_UPDATE_CHECK=1");
+ assertThat(logTester.logs(Level.INFO)).contains("Generated manifests zip file: " + producedZip.getName());
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java
new file mode 100644
index 00000000000..ea608e561ef
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaExecutorTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.scanner.sca;
+
+import java.io.File;
+import java.io.IOException;
+import org.assertj.core.util.Files;
+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.slf4j.event.Level;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.batch.fs.internal.DefaultInputModule;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ScaExecutorTest {
+ private final CliService cliService = mock(CliService.class);
+ private final CliCacheService cliCacheService = mock(CliCacheService.class);
+ private DefaultInputModule root;
+
+ @RegisterExtension
+ private final LogTesterJUnit5 logTester = new LogTesterJUnit5();
+
+ @TempDir File rootModuleDir;
+
+ private final ScaExecutor underTest = new ScaExecutor(cliCacheService, cliService);
+
+ @BeforeEach
+ public void before() {
+ root = new DefaultInputModule(
+ ProjectDefinition.create().setBaseDir(rootModuleDir).setWorkDir(rootModuleDir.toPath().getRoot().toFile()));
+ }
+
+ @Test
+ void execute_shouldCallCliService() throws IOException {
+ File mockCliFile = Files.newTemporaryFile();
+ File mockManifestZip = Files.newTemporaryFile();
+ when(cliCacheService.cacheCli(anyString(), anyString())).thenReturn(mockCliFile);
+ when(cliService.generateManifestsZip(root)).thenReturn(mockManifestZip);
+
+ logTester.setLevel(Level.DEBUG);
+
+ underTest.execute(root);
+
+ verify(cliService).generateManifestsZip(root);
+ assertThat(logTester.logs()).contains("Zip ready for report: " + mockManifestZip);
+ }
+
+ @Test
+ void execute_whenIOException_shouldHandleException() throws IOException {
+ File mockCliFile = Files.newTemporaryFile();
+ when(cliCacheService.cacheCli(anyString(), anyString())).thenReturn(mockCliFile);
+ doThrow(IOException.class).when(cliService).generateManifestsZip(root);
+
+ logTester.setLevel(Level.INFO);
+
+ underTest.execute(root);
+
+ verify(cliService).generateManifestsZip(root);
+ assertThat(logTester.logs(Level.ERROR)).contains("Error gathering manifests");
+ }
+
+ @Test
+ void execute_whenIllegalStateException_shouldHandleException() throws IOException {
+ File mockCliFile = Files.newTemporaryFile();
+ when(cliCacheService.cacheCli(anyString(), anyString())).thenReturn(mockCliFile);
+ doThrow(IllegalStateException.class).when(cliService).generateManifestsZip(root);
+
+ logTester.setLevel(Level.INFO);
+
+ underTest.execute(root);
+
+ verify(cliService).generateManifestsZip(root);
+ assertThat(logTester.logs(Level.ERROR)).contains("Error gathering manifests");
+ }
+
+ @Test
+ void execute_whenNoCliFound_shouldSkipAnalysis() throws IOException {
+ when(cliCacheService.cacheCli(anyString(), anyString())).thenReturn(new File(""));
+
+ underTest.execute(root);
+
+ verify(cliService, never()).generateManifestsZip(root);
+ }
+}
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh
new file mode 100755
index 00000000000..a0a0e5b9a6b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/sca/echo_args.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+echo "Arguments Passed In:" $@
+
+POSITIONAL_ARGS=()
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --zip-filename)
+ FILENAME="$2"
+ shift
+ shift
+ ;;
+ *)
+ POSITIONAL_ARGS+=("$1")
+ shift
+ ;;
+ esac
+done
+
+set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
+
+# print specific env variables that should be defined here
+echo "TIDELIFT_SKIP_UPDATE_CHECK=${TIDELIFT_SKIP_UPDATE_CHECK}"
+
+# print filename location for debug purposes
+echo "ZIP FILE LOCATION = ${FILENAME}"
+echo "" > $FILENAME