public static final String ANALYSIS_ENDPOINT = "/analysis";
public static final String VERSION_ENDPOINT = ANALYSIS_ENDPOINT + "/version";
public static final String JRE_ENDPOINT = ANALYSIS_ENDPOINT + "/jres";
+ public static final String SCANNER_ENGINE_ENDPOINT = ANALYSIS_ENDPOINT + "/engine";
private WebApiEndpoints() {
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.controller;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.v2.api.analysis.response.EngineInfoRestResponse;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineMetadata;
+import org.springframework.core.io.InputStreamResource;
+
+import static java.lang.String.format;
+
+public class DefaultScannerEngineController implements ScannerEngineController {
+
+ private final ScannerEngineHandler scannerEngineHandler;
+
+ public DefaultScannerEngineController(ScannerEngineHandler scannerEngineHandler) {
+ this.scannerEngineHandler = scannerEngineHandler;
+ }
+
+ @Override
+ public EngineInfoRestResponse getScannerEngineMetadata() {
+ ScannerEngineMetadata metadata = scannerEngineHandler.getScannerEngineMetadata();
+ return new EngineInfoRestResponse(metadata.filename(), metadata.checksum());
+ }
+
+ @Override
+ public InputStreamResource downloadScannerEngine() {
+ File scannerEngine = scannerEngineHandler.getScannerEngine();
+ try {
+ return new InputStreamResource(new FileInputStream(scannerEngine));
+ } catch (FileNotFoundException e) {
+ throw new NotFoundException(format("Unable to find file: %s", scannerEngine.getName()));
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.sonar.server.v2.api.analysis.response.EngineInfoRestResponse;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.SCANNER_ENGINE_ENDPOINT;
+import static org.springframework.http.HttpStatus.OK;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+
+@RequestMapping(value = SCANNER_ENGINE_ENDPOINT, produces = APPLICATION_JSON_VALUE)
+@RestController
+public interface ScannerEngineController {
+
+ String GET_ENGINE_SUMMARY = "Scanner engine download/metadata";
+ String GET_ENGINE_DESCRIPTION =
+ "This endpoint return the Scanner Engine metadata by default. To download the Scanner Engine, set the Accept header of the request to 'application/octet-stream'.";
+
+ @GetMapping
+ @ResponseStatus(OK)
+ @Operation(summary = GET_ENGINE_SUMMARY, description = GET_ENGINE_DESCRIPTION)
+ EngineInfoRestResponse getScannerEngineMetadata();
+
+ @GetMapping(produces = APPLICATION_OCTET_STREAM_VALUE)
+ @ResponseStatus(OK)
+ @Operation(summary = GET_ENGINE_SUMMARY, description = GET_ENGINE_DESCRIPTION)
+ InputStreamResource downloadScannerEngine();
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.response;
+
+public record EngineInfoRestResponse(String filename, String checksum) {
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.service;
+
+import java.io.File;
+
+public interface ScannerEngineHandler {
+ ScannerEngineMetadata getScannerEngineMetadata();
+
+ File getScannerEngine();
+
+}
+
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static java.lang.String.format;
+import static org.apache.commons.codec.digest.DigestUtils.sha256Hex;
+import static org.apache.commons.io.FileUtils.listFiles;
+import static org.apache.commons.io.filefilter.FileFilterUtils.directoryFileFilter;
+import static org.apache.commons.io.filefilter.HiddenFileFilter.VISIBLE;
+
+public class ScannerEngineHandlerImpl implements ScannerEngineHandler {
+
+ private final ServerFileSystem fs;
+
+ private ScannerEngineMetadata scannerEngineMetadata;
+
+ public ScannerEngineHandlerImpl(ServerFileSystem fs) {
+ this.fs = fs;
+ }
+
+ @Override
+ public File getScannerEngine() {
+ File scannerDir = new File(fs.getHomeDir(), "lib/scanner");
+ if (!scannerDir.exists()) {
+ throw new NotFoundException(format("Scanner directory not found: %s", scannerDir.getAbsolutePath()));
+ }
+ return listFiles(scannerDir, VISIBLE, directoryFileFilter())
+ .stream()
+ .filter(file -> file.getName().endsWith(".jar"))
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException(format("Scanner JAR not found in directory: %s", scannerDir.getAbsolutePath())));
+ }
+
+ private static String getSha256(File file) {
+ try (FileInputStream fileInputStream = new FileInputStream(file)) {
+ return sha256Hex(fileInputStream);
+ } catch (IOException exception) {
+ throw new UncheckedIOException(new IOException("Unable to compute SHA-256 checksum of the Scanner Engine", exception));
+ }
+ }
+
+ @Override
+ public ScannerEngineMetadata getScannerEngineMetadata() {
+ if (scannerEngineMetadata == null) {
+ File scannerEngine = getScannerEngine();
+ scannerEngineMetadata = new ScannerEngineMetadata(scannerEngine.getName(), getSha256(scannerEngine));
+ }
+ return scannerEngineMetadata;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.service;
+
+public record ScannerEngineMetadata(String filename, String checksum) {
+}
import org.sonar.server.common.user.service.UserService;
import org.sonar.server.health.HealthChecker;
import org.sonar.server.platform.NodeInformation;
+import org.sonar.server.platform.ServerFileSystem;
import org.sonar.server.rule.RuleDescriptionFormatter;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.analysis.controller.DefaultJresController;
import org.sonar.server.v2.api.analysis.controller.DefaultVersionController;
import org.sonar.server.v2.api.analysis.controller.JresController;
+import org.sonar.server.v2.api.analysis.controller.ScannerEngineController;
+import org.sonar.server.v2.api.analysis.controller.DefaultScannerEngineController;
import org.sonar.server.v2.api.analysis.controller.VersionController;
import org.sonar.server.v2.api.analysis.service.JresHandler;
import org.sonar.server.v2.api.analysis.service.JresHandlerImpl;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandlerImpl;
import org.sonar.server.v2.api.dop.controller.DefaultDopSettingsController;
import org.sonar.server.v2.api.dop.controller.DopSettingsController;
import org.sonar.server.v2.api.gitlab.config.controller.DefaultGitlabConfigurationController;
return new DefaultJresController(jresHandler);
}
+ @Bean
+ public ScannerEngineHandler scannerEngineHandler(ServerFileSystem serverFileSystem) {
+ return new ScannerEngineHandlerImpl(serverFileSystem);
+ }
+
+ @Bean
+ public ScannerEngineController scannerEngineController(ScannerEngineHandler scannerEngineHandler) {
+ return new DefaultScannerEngineController(scannerEngineHandler);
+ }
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.controller;
+
+import java.io.File;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineMetadata;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+import static java.lang.String.format;
+import static java.nio.file.Files.createTempFile;
+import static java.nio.file.Files.write;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.v2.WebApiEndpoints.SCANNER_ENGINE_ENDPOINT;
+import static org.sonar.server.v2.api.ControllerTester.getMockMvc;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
+import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class DefaultScannerEngineControllerTest {
+
+ private final ScannerEngineHandler scannerEngineHandler = mock(ScannerEngineHandler.class);
+
+ private final MockMvc mockMvc = getMockMvc(new DefaultScannerEngineController(scannerEngineHandler));
+
+ @Test
+ void getEngine_shouldReturnScannerMetadataAsJson() throws Exception {
+ String anyName = "anyName";
+ String anyChecksum = "anyChecksum";
+ when(scannerEngineHandler.getScannerEngineMetadata()).thenReturn(new ScannerEngineMetadata(anyName, anyChecksum));
+ String expectedJson = format("{\"filename\":\"%s\",\"checksum\":\"%s\"}", anyName, anyChecksum);
+
+ mockMvc.perform(get(SCANNER_ENGINE_ENDPOINT))
+ .andExpectAll(
+ status().isOk(),
+ content().json(expectedJson));
+ }
+
+ @Test
+ void getEngine_shouldDownloadScanner_whenHeaderIsOctetStream(@TempDir Path tempDir) throws Exception {
+ File scanner = createTempFile(tempDir, "scanner", ".jar").toFile();
+ byte[] anyBinary = {1, 2, 3};
+ write(scanner.toPath(), anyBinary);
+ when(scannerEngineHandler.getScannerEngine()).thenReturn(new File(scanner.toString()));
+
+ mockMvc.perform(get(SCANNER_ENGINE_ENDPOINT)
+ .header("Accept", APPLICATION_OCTET_STREAM_VALUE))
+ .andExpectAll(
+ status().isOk(),
+ content().contentType(APPLICATION_OCTET_STREAM),
+ content().bytes(anyBinary));
+ }
+
+ @Test
+ void getEngine_shouldFail_whenScannerEngineNotFound() {
+ // Ideally we would like Spring to return a 404, but it's not the case at the moment. We suspect that it's because the Header Accept wants a binary file.
+ // So the Json corresponding to the NotFoundException is not sent and we have a 500 instead.
+ when(scannerEngineHandler.getScannerEngine()).thenReturn(new File("no-file"));
+ MockHttpServletRequestBuilder request = get(SCANNER_ENGINE_ENDPOINT).header("Accept", APPLICATION_OCTET_STREAM_VALUE);
+ assertThatThrownBy(() -> mockMvc.perform(request))
+ .hasMessageContaining("NotFoundException: Unable to find file: no-file");
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.server.v2.api.analysis.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.platform.ServerFileSystem;
+
+import static java.lang.String.format;
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.createFile;
+import static java.nio.file.Files.createTempFile;
+import static java.nio.file.Files.deleteIfExists;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+class ScannerEngineHandlerImplTest {
+
+ ServerFileSystem serverFileSystem = mock(ServerFileSystem.class);
+
+ ScannerEngineHandlerImpl scannerEngineHandler = new ScannerEngineHandlerImpl(serverFileSystem);
+
+ @TempDir
+ private File tempDir;
+
+ private Path scannerDir;
+
+ @BeforeEach
+ public void setup() throws IOException {
+ when(serverFileSystem.getHomeDir()).thenReturn(tempDir);
+ scannerDir = createDirectories(Path.of(tempDir.getAbsolutePath(), "lib/scanner"));
+ }
+
+ @Test
+ void getScannerEngineMetadata() throws IOException {
+ createFile(scannerDir.resolve("scanner.jar"));
+ ScannerEngineMetadata expected = new ScannerEngineMetadata("scanner.jar", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
+
+ ScannerEngineMetadata result = scannerEngineHandler.getScannerEngineMetadata();
+
+ assertThat(result).usingRecursiveComparison().isEqualTo(expected);
+ }
+
+ @Test
+ void getScannerEngineMetadata_shouldFail_whenHashComputingFailed() {
+ ScannerEngineHandlerImpl spy = spy(new ScannerEngineHandlerImpl(serverFileSystem));
+ doReturn(new File("no-file")).when(spy).getScannerEngine();
+ assertThatThrownBy(spy::getScannerEngineMetadata)
+ .isInstanceOf(UncheckedIOException.class)
+ .hasMessageContaining("Unable to compute SHA-256 checksum of the Scanner Engine");
+ }
+
+ @Test
+ void getScannerEngine_shouldFail_whenScannerDirNotFound() throws IOException {
+ deleteIfExists(scannerDir);
+ assertThatThrownBy(() -> scannerEngineHandler.getScannerEngine())
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage(format("Scanner directory not found: %s", scannerDir.toAbsolutePath()));
+ }
+
+ @Test
+ void getScannerEngine_shouldReturnScannerJar() throws IOException {
+ File scanner = createTempFile(scannerDir, "scanner", ".jar").toFile();
+
+ File result = scannerEngineHandler.getScannerEngine();
+
+ assertThat(result).isEqualTo(scanner);
+ }
+
+ @Test
+ void getScannerEngine_shouldFail_whenScannerNotFound() throws IOException {
+ Path tempDirectory = createDirectories(scannerDir);
+
+ assertThatThrownBy(() -> scannerEngineHandler.getScannerEngine())
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage(format("Scanner JAR not found in directory: %s", tempDirectory.toAbsolutePath()));
+ }
+
+
+}
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.sonar.api.platform.Server;
+import org.sonar.server.platform.ServerFileSystem;
import org.sonar.server.v2.api.analysis.controller.DefaultJresController;
import org.sonar.server.v2.api.analysis.controller.DefaultVersionController;
+import org.sonar.server.v2.api.analysis.controller.DefaultScannerEngineController;
import org.sonar.server.v2.api.analysis.service.JresHandler;
import org.sonar.server.v2.api.analysis.service.JresHandlerImpl;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandler;
+import org.sonar.server.v2.api.analysis.service.ScannerEngineHandlerImpl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
-public class PlatformLevel4WebConfigTest {
+class PlatformLevel4WebConfigTest {
private static final PlatformLevel4WebConfig platformLevel4WebConfig = new PlatformLevel4WebConfig();
return Stream.of(
arguments(platformLevel4WebConfig.versionController(mock(Server.class)), DefaultVersionController.class),
arguments(platformLevel4WebConfig.jresHandler(), JresHandlerImpl.class),
- arguments(platformLevel4WebConfig.jresController(mock(JresHandler.class)), DefaultJresController.class)
+ arguments(platformLevel4WebConfig.jresController(mock(JresHandler.class)), DefaultJresController.class),
+ arguments(platformLevel4WebConfig.scannerEngineHandler(mock(ServerFileSystem.class)), ScannerEngineHandlerImpl.class),
+ arguments(platformLevel4WebConfig.scannerEngineController(mock(ScannerEngineHandler.class)), DefaultScannerEngineController.class)
);
}