diff options
author | Mike Young <mike.young@sonarsource.com> | 2025-02-06 14:16:37 -0500 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2025-02-19 20:03:12 +0000 |
commit | 49b800d8b7a9ade328bb5f3d01c0972290ee9f9d (patch) | |
tree | 0190669f1e605da1660b7d3915a3ccdafa4442c0 /sonar-scanner-engine | |
parent | 04b0453797b06c53fc9092c2961e5f24c10ebc28 (diff) | |
download | sonarqube-49b800d8b7a9ade328bb5f3d01c0972290ee9f9d.tar.gz sonarqube-49b800d8b7a9ade328bb5f3d01c0972290ee9f9d.zip |
SQRP-149 Generate Manifest Files
Diffstat (limited to 'sonar-scanner-engine')
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 |