diff options
author | Aurelien Poscia <aurelien.poscia@sonarsource.com> | 2022-11-09 14:35:07 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-11-15 20:02:59 +0000 |
commit | 53f94935f393750ba08a7e1fa00742acadbadafb (patch) | |
tree | 647e21c0a08c735234f87979c73f961493926037 /sonar-scanner-engine/src | |
parent | 59df4a4ad498fa1ce6df396c0b7a6afb70b7ec83 (diff) | |
download | sonarqube-53f94935f393750ba08a7e1fa00742acadbadafb.tar.gz sonarqube-53f94935f393750ba08a7e1fa00742acadbadafb.zip |
SONAR-17564 Import vulnerabilities from a SARIF report
Diffstat (limited to 'sonar-scanner-engine/src')
14 files changed, 1348 insertions, 0 deletions
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java index 41144e68466..9a0200f71ea 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java @@ -28,6 +28,12 @@ import org.sonar.core.sarif.SarifSerializerImpl; import org.sonar.scanner.cpd.JavaCpdBlockIndexerSensor; import org.sonar.scanner.deprecated.test.TestPlanBuilder; import org.sonar.scanner.externalissue.ExternalIssuesImportSensor; +import org.sonar.scanner.externalissue.sarif.DefaultSarif210Importer; +import org.sonar.scanner.externalissue.sarif.LocationMapper; +import org.sonar.scanner.externalissue.sarif.RegionMapper; +import org.sonar.scanner.externalissue.sarif.ResultMapper; +import org.sonar.scanner.externalissue.sarif.RunMapper; +import org.sonar.scanner.externalissue.sarif.SarifIssuesImportSensor; import org.sonar.scanner.genericcoverage.GenericCoverageSensor; import org.sonar.scanner.genericcoverage.GenericTestExecutionSensor; import org.sonar.scanner.source.ZeroCoverageSensor; @@ -56,6 +62,15 @@ public class BatchComponents { components.add(ExternalIssuesImportSensor.properties()); components.add(SarifSerializerImpl.class); + // Sarif issues + components.add(SarifIssuesImportSensor.class); + components.add(SarifIssuesImportSensor.properties()); + components.add(DefaultSarif210Importer.class); + components.add(RunMapper.class); + components.add(ResultMapper.class); + components.add(LocationMapper.class); + components.add(RegionMapper.class); + return components; } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210Importer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210Importer.java new file mode 100644 index 00000000000..840843160f9 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210Importer.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import javax.annotation.CheckForNull; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.sarif.Run; +import org.sonar.core.sarif.Sarif210; + +import static java.util.Objects.requireNonNull; + +@ScannerSide +public class DefaultSarif210Importer implements Sarif210Importer { + private static final Logger LOG = Loggers.get(DefaultSarif210Importer.class); + + private final RunMapper runMapper; + + DefaultSarif210Importer(RunMapper runMapper) { + this.runMapper = runMapper; + } + + @Override + public void importSarif(Sarif210 sarif210) { + Set<Run> runs = requireNonNull(sarif210.getRuns(), "The runs section of the Sarif report is null"); + runs.stream() + .map(this::toNewExternalIssues) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .forEach(NewExternalIssue::save); + } + + @CheckForNull + private List<NewExternalIssue> toNewExternalIssues(Run run) { + try { + return runMapper.mapRun(run); + } catch (Exception exception) { + LOG.warn("Failed to import a sarif run, error: {}", exception.getMessage()); + return null; + } + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/LocationMapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/LocationMapper.java new file mode 100644 index 00000000000..235a35d2791 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/LocationMapper.java @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.net.URI; +import javax.annotation.CheckForNull; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.core.sarif.Location; +import org.sonar.core.sarif.PhysicalLocation; +import org.sonar.core.sarif.Result; + +import static java.util.Objects.requireNonNull; +import static org.sonar.api.utils.Preconditions.checkArgument; + +@ScannerSide +public class LocationMapper { + + private final SensorContext sensorContext; + private final RegionMapper regionMapper; + + LocationMapper(SensorContext sensorContext, RegionMapper regionMapper) { + this.sensorContext = sensorContext; + this.regionMapper = regionMapper; + } + + NewIssueLocation fillIssueInProjectLocation(Result result, NewIssueLocation newIssueLocation) { + return newIssueLocation + .message(getResultMessageOrThrow(result)) + .on(sensorContext.project()); + } + + @CheckForNull + NewIssueLocation fillIssueInFileLocation(Result result, NewIssueLocation newIssueLocation, Location location) { + newIssueLocation.message(getResultMessageOrThrow(result)); + PhysicalLocation physicalLocation = location.getPhysicalLocation(); + + String fileUri = getFileUriOrThrow(location); + InputFile file = findFile(sensorContext, fileUri); + if (file == null) { + return null; + } + newIssueLocation.on(file); + regionMapper.mapRegion(physicalLocation.getRegion(), file).ifPresent(newIssueLocation::at); + return newIssueLocation; + } + + private static String getResultMessageOrThrow(Result result) { + requireNonNull(result.getMessage(), "No messages found for issue thrown by rule " + result.getRuleId()); + return requireNonNull(result.getMessage().getText(), "No text found for messages in issue thrown by rule " + result.getRuleId()); + } + + private static String getFileUriOrThrow(Location location) { + PhysicalLocation physicalLocation = location.getPhysicalLocation(); + checkArgument(physicalLocation != null + && physicalLocation.getArtifactLocation() != null + && physicalLocation.getArtifactLocation().getUri() != null, + "The field location.physicalLocation.artifactLocation.uri is not set."); + return physicalLocation.getArtifactLocation().getUri(); + } + + @CheckForNull + private static InputFile findFile(SensorContext context, String filePath) { + FilePredicates predicates = context.fileSystem().predicates(); + return context.fileSystem().inputFile(predicates.or( + predicates.hasURI(URI.create(filePath)), predicates.hasPath(filePath) + )); + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RegionMapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RegionMapper.java new file mode 100644 index 00000000000..35001e0aabf --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RegionMapper.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.core.sarif.Region; + +@ScannerSide +public class RegionMapper { + + Optional<TextRange> mapRegion(@Nullable Region region, InputFile file) { + if (region == null) { + return Optional.empty(); + } + int startLine = Objects.requireNonNull(region.getStartLine(), "No start line defined for the region."); + Integer endLine = region.getEndLine(); + if (endLine != null) { + int startColumn = Optional.ofNullable(region.getStartColumn()).orElse(1); + int endColumn = Optional.ofNullable(region.getEndColumn()) + .orElseGet(() -> file.selectLine(endLine).end().lineOffset()); + return Optional.of(file.newRange(startLine, startColumn, endLine, endColumn)); + } else { + return Optional.of(file.selectLine(startLine)); + } + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/ResultMapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/ResultMapper.java new file mode 100644 index 00000000000..4bef14c9af1 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/ResultMapper.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rules.RuleType; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.core.sarif.Location; +import org.sonar.core.sarif.Result; + +import static java.util.Objects.requireNonNull; + +@ScannerSide +public class ResultMapper { + + private static final Map<String, Severity> SEVERITY_MAPPING = ImmutableMap.<String, Severity>builder() + .put("error", Severity.CRITICAL) + .put("warning", Severity.MAJOR) + .put("note", Severity.MINOR) + .put("none", Severity.INFO) + .build(); + + @VisibleForTesting + static final Severity DEFAULT_SEVERITY = Severity.MAJOR; + private static final RuleType DEFAULT_TYPE = RuleType.VULNERABILITY; + + private final SensorContext sensorContext; + private final LocationMapper locationMapper; + + ResultMapper(SensorContext sensorContext, LocationMapper locationMapper) { + this.sensorContext = sensorContext; + this.locationMapper = locationMapper; + } + + NewExternalIssue mapResult(String driverName, Result result) { + NewExternalIssue newExternalIssue = sensorContext.newExternalIssue(); + newExternalIssue.type(DEFAULT_TYPE); + newExternalIssue.engineId(driverName); + newExternalIssue.severity(toSonarQubeSeverity(result.getLevel())); + newExternalIssue.ruleId(requireNonNull(result.getRuleId(), "No ruleId found for issue thrown by driver " + driverName)); + + mapLocations(result, newExternalIssue); + return newExternalIssue; + } + + private static Severity toSonarQubeSeverity(@Nullable String level) { + return SEVERITY_MAPPING.getOrDefault(level, DEFAULT_SEVERITY); + } + + private void mapLocations(Result result, NewExternalIssue newExternalIssue) { + NewIssueLocation newIssueLocation = newExternalIssue.newLocation(); + if (result.getLocations().isEmpty()) { + newExternalIssue.at(locationMapper.fillIssueInProjectLocation(result, newIssueLocation)); + } else { + Location firstLocation = result.getLocations().iterator().next(); + NewIssueLocation primaryLocation = fillFileOrProjectLocation(result, newIssueLocation, firstLocation); + newExternalIssue.at(primaryLocation); + } + } + + private NewIssueLocation fillFileOrProjectLocation(Result result, NewIssueLocation newIssueLocation, Location firstLocation) { + return Optional.ofNullable(locationMapper.fillIssueInFileLocation(result, newIssueLocation, firstLocation)) + .orElseGet(() -> locationMapper.fillIssueInProjectLocation(result, newIssueLocation)); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java new file mode 100644 index 00000000000..623c04d5705 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.util.List; +import java.util.Optional; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.sarif.Result; +import org.sonar.core.sarif.Run; + +import static java.util.stream.Collectors.toList; +import static org.sonar.api.utils.Preconditions.checkArgument; + +@ScannerSide +public class RunMapper { + private static final Logger LOG = Loggers.get(RunMapper.class); + + private final ResultMapper resultMapper; + + RunMapper(ResultMapper resultMapper) { + this.resultMapper = resultMapper; + } + + List<NewExternalIssue> mapRun(Run run) { + String driverName = getToolDriverNameOrThrow(run); + return run.getResults().stream() + .map(result -> toNewExternalIssue(driverName, result)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + + private static String getToolDriverNameOrThrow(Run run) { + checkArgument(run.getTool() != null + && run.getTool().getDriver() != null + && run.getTool().getDriver().getName() != null, + "The run does not have a tool driver name defined."); + return run.getTool().getDriver().getName(); + } + + private Optional<NewExternalIssue> toNewExternalIssue(String driverName, Result result) { + try { + return Optional.of(resultMapper.mapResult(driverName, result)); + } catch (Exception exception) { + LOG.warn("Failed to import an issue raised by tool {}, error: {}", driverName, exception.getMessage()); + return Optional.empty(); + } + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/Sarif210Importer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/Sarif210Importer.java new file mode 100644 index 00000000000..9e88be77a18 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/Sarif210Importer.java @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import org.sonar.core.sarif.Sarif210; + +public interface Sarif210Importer { + void importSarif(Sarif210 sarif210); +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensor.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensor.java new file mode 100644 index 00000000000..cb95123ed9f --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensor.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.sarif.Sarif210; +import org.sonar.core.sarif.SarifSerializer; + +@ScannerSide +public class SarifIssuesImportSensor implements Sensor { + + private static final Logger LOG = Loggers.get(SarifIssuesImportSensor.class); + static final String SARIF_REPORT_PATHS_PROPERTY_KEY = "sonar.sarifReportPaths"; + + private final SarifSerializer sarifSerializer; + private final Sarif210Importer sarifImporter; + private final Configuration config; + + public SarifIssuesImportSensor(SarifSerializer sarifSerializer, Sarif210Importer sarifImporter, Configuration config) { + this.sarifSerializer = sarifSerializer; + this.sarifImporter = sarifImporter; + this.config = config; + } + + public static List<PropertyDefinition> properties() { + return Collections.singletonList( + PropertyDefinition.builder(SARIF_REPORT_PATHS_PROPERTY_KEY) + .name("SARIF report paths") + .description("List of comma-separated paths (absolute or relative) containing a SARIF report with issues created by external rule engines.") + .category(CoreProperties.CATEGORY_EXTERNAL_ISSUES) + .onQualifiers(Qualifiers.PROJECT) + .build()); + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Import external issues report from SARIF file.") + .onlyWhenConfiguration(c -> c.hasKey(SARIF_REPORT_PATHS_PROPERTY_KEY)); + } + + @Override + public void execute(SensorContext context) { + Set<String> reportPaths = loadReportPaths(); + for (String reportPath : reportPaths) { + try { + LOG.debug("Importing SARIF issues from '{}'", reportPath); + Path reportFilePath = context.fileSystem().resolvePath(reportPath).toPath(); + Sarif210 sarifReport = sarifSerializer.deserialize(reportFilePath); + sarifImporter.importSarif(sarifReport); + } catch (Exception exception) { + LOG.warn("Failed to process SARIF report from file '{}', error '{}'", reportPath, exception.getMessage()); + } + } + } + + private Set<String> loadReportPaths() { + return Arrays.stream(config.getStringArray(SARIF_REPORT_PATHS_PROPERTY_KEY)).collect(Collectors.toSet()); + } +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210ImporterTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210ImporterTest.java new file mode 100644 index 00000000000..8493eb091d8 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210ImporterTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.util.List; +import java.util.Set; +import junit.framework.TestCase; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.core.sarif.Run; +import org.sonar.core.sarif.Sarif210; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultSarif210ImporterTest extends TestCase { + + @Mock + private RunMapper runMapper; + + @Rule + public LogTester logTester = new LogTester(); + + @InjectMocks + DefaultSarif210Importer sarif210Importer; + + @Test + public void importSarif_shouldDelegateRunMapping_toRunMapper() { + Sarif210 sarif210 = mock(Sarif210.class); + + Run run1 = mock(Run.class); + Run run2 = mock(Run.class); + when(sarif210.getRuns()).thenReturn(Set.of(run1, run2)); + + NewExternalIssue issue1run1 = mock(NewExternalIssue.class); + NewExternalIssue issue2run1 = mock(NewExternalIssue.class); + NewExternalIssue issue1run2 = mock(NewExternalIssue.class); + when(runMapper.mapRun(run1)).thenReturn(List.of(issue1run1, issue2run1)); + when(runMapper.mapRun(run2)).thenReturn(List.of(issue1run2)); + + sarif210Importer.importSarif(sarif210); + + verify(issue1run1).save(); + verify(issue2run1).save(); + verify(issue1run2).save(); + } + + @Test + public void importSarif_whenExceptionThrownByRunMapper_shouldLogAndContinueProcessing() { + Sarif210 sarif210 = mock(Sarif210.class); + + Run run1 = mock(Run.class); + Run run2 = mock(Run.class); + when(sarif210.getRuns()).thenReturn(Set.of(run1, run2)); + + Exception testException = new RuntimeException("test"); + when(runMapper.mapRun(run1)).thenThrow(testException); + NewExternalIssue issue1run2 = mock(NewExternalIssue.class); + when(runMapper.mapRun(run2)).thenReturn(List.of(issue1run2)); + + sarif210Importer.importSarif(sarif210); + + assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Failed to import a sarif run, error: " + testException.getMessage()); + verify(issue1run2).save(); + } + + @Test + public void importSarif_whenGetRunsReturnNull_shouldFailWithProperMessage() { + Sarif210 sarif210 = mock(Sarif210.class); + + when(sarif210.getRuns()).thenReturn(null); + + assertThatNullPointerException() + .isThrownBy(() -> sarif210Importer.importSarif(sarif210)) + .withMessage("The runs section of the Sarif report is null"); + + verifyNoInteractions(runMapper); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/LocationMapperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/LocationMapperTest.java new file mode 100644 index 00000000000..be6f703643a --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/LocationMapperTest.java @@ -0,0 +1,171 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.net.URI; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.scanner.fs.InputProject; +import org.sonar.core.sarif.Location; +import org.sonar.core.sarif.Result; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LocationMapperTest { + + private static final String TEST_MESSAGE = "test message"; + private static final String URI_TEST = "URI_TEST"; + private static final String EXPECTED_MESSAGE_URI_MISSING = "The field location.physicalLocation.artifactLocation.uri is not set."; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private SensorContext sensorContext; + + @Mock + private RegionMapper regionMapper; + + @InjectMocks + private LocationMapper locationMapper; + + @Mock + private NewIssueLocation newIssueLocation; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Result result; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Location location; + + @Mock + private InputFile inputFile; + + @Before + public void setup() { + when(newIssueLocation.message(any())).thenReturn(newIssueLocation); + when(newIssueLocation.on(any())).thenReturn(newIssueLocation); + when(newIssueLocation.at(any())).thenReturn(newIssueLocation); + when(sensorContext.project()).thenReturn(mock(InputProject.class)); + + when(result.getMessage().getText()).thenReturn(TEST_MESSAGE); + + when(location.getPhysicalLocation().getArtifactLocation().getUri()).thenReturn(URI_TEST); + + FilePredicate filePredicate = mock(FilePredicate.class); + FilePredicates predicates = sensorContext.fileSystem().predicates(); + when(predicates.or( + predicates.hasURI(URI.create(URI_TEST)), predicates.hasPath(URI_TEST) + )).thenReturn(filePredicate); + + when(sensorContext.fileSystem().inputFile(filePredicate)).thenReturn(inputFile); + + } + + @Test + public void fillIssueInProjectLocation_shouldFillRelevantFields() { + NewIssueLocation actualIssueLocation = locationMapper.fillIssueInProjectLocation(result, newIssueLocation); + + assertThat(actualIssueLocation).isEqualTo(newIssueLocation); + verify(newIssueLocation).message(TEST_MESSAGE); + verify(newIssueLocation).on(sensorContext.project()); + verifyNoMoreInteractions(newIssueLocation); + } + + @Test + public void fillIssueInFileLocation_whenFileNotFound_returnsNull() { + when(sensorContext.fileSystem().inputFile(any())).thenReturn(null); + + NewIssueLocation actualIssueLocation = locationMapper.fillIssueInFileLocation(result, newIssueLocation, location); + + assertThat(actualIssueLocation).isNull(); + } + + @Test + public void fillIssueInFileLocation_whenMapRegionReturnsNull_onlyFillsSimpleFields() { + when(regionMapper.mapRegion(location.getPhysicalLocation().getRegion(), inputFile)) + .thenReturn(Optional.empty()); + + NewIssueLocation actualIssueLocation = locationMapper.fillIssueInFileLocation(result, newIssueLocation, location); + + assertThat(actualIssueLocation).isSameAs(newIssueLocation); + verify(newIssueLocation).message(TEST_MESSAGE); + verify(newIssueLocation).on(inputFile); + verifyNoMoreInteractions(newIssueLocation); + } + + @Test + public void fillIssueInFileLocation_whenMapRegionReturnsRegion_callsAt() { + TextRange textRange = mock(TextRange.class); + when(regionMapper.mapRegion(location.getPhysicalLocation().getRegion(), inputFile)) + .thenReturn(Optional.of(textRange)); + + NewIssueLocation actualIssueLocation = locationMapper.fillIssueInFileLocation(result, newIssueLocation, location); + + assertThat(actualIssueLocation).isSameAs(newIssueLocation); + verify(newIssueLocation).message(TEST_MESSAGE); + verify(newIssueLocation).on(inputFile); + verify(newIssueLocation).at(textRange); + verifyNoMoreInteractions(newIssueLocation); + } + + @Test + public void fillIssueInFileLocation_ifNullUri_throws() { + when(location.getPhysicalLocation().getArtifactLocation().getUri()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> locationMapper.fillIssueInFileLocation(result, newIssueLocation, location)) + .withMessage(EXPECTED_MESSAGE_URI_MISSING); + } + + @Test + public void fillIssueInFileLocation_ifNullArtifactLocation_throws() { + when(location.getPhysicalLocation().getArtifactLocation()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> locationMapper.fillIssueInFileLocation(result, newIssueLocation, location)) + .withMessage(EXPECTED_MESSAGE_URI_MISSING); + } + + @Test + public void fillIssueInFileLocation_ifNullPhysicalLocation_throws() { + when(location.getPhysicalLocation().getArtifactLocation()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> locationMapper.fillIssueInFileLocation(result, newIssueLocation, location)) + .withMessage(EXPECTED_MESSAGE_URI_MISSING); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RegionMapperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RegionMapperTest.java new file mode 100644 index 00000000000..27a973a15cb --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RegionMapperTest.java @@ -0,0 +1,143 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.fs.internal.DefaultIndexedFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.Metadata; +import org.sonar.core.sarif.Region; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RegionMapperTest { + private static final int LINE_END_OFFSET = 10; + private static final DefaultInputFile INPUT_FILE = new DefaultInputFile(new DefaultIndexedFile("ABCDE", Paths.get("module"), "relative/path", null), + f -> f.setMetadata(generateMetadata())); + + + private static Metadata generateMetadata() { + Metadata metadata = mock(Metadata.class); + when(metadata.lines()).thenReturn(100); + when(metadata.originalLineStartOffsets()).thenReturn(IntStream.range(0, 100).toArray()); + when(metadata.originalLineEndOffsets()).thenReturn(IntStream.range(0, 100).map(i -> i + LINE_END_OFFSET).toArray()); + return metadata; + } + + @Mock + private Region region; + + @InjectMocks + private RegionMapper regionMapper; + + @Test + public void mapRegion_whenNullRegion_returnsEmpty() { + assertThat(regionMapper.mapRegion(null, INPUT_FILE)).isEmpty(); + } + + @Test + public void mapRegion_whenStartLineIsNull_shouldThrow() { + when(region.getStartLine()).thenReturn(null); + + assertThatNullPointerException() + .isThrownBy(() -> regionMapper.mapRegion(region, INPUT_FILE)) + .withMessage("No start line defined for the region."); + } + + @Test + public void mapRegion_whenAllCoordinatesDefined() { + Region fullRegion = mockRegion(1, 2, 3, 4); + + Optional<TextRange> optTextRange = regionMapper.mapRegion(fullRegion, INPUT_FILE); + + assertThat(optTextRange).isPresent(); + TextRange textRange = optTextRange.get(); + assertThat(textRange.start().line()).isEqualTo(fullRegion.getStartLine()); + assertThat(textRange.start().lineOffset()).isEqualTo(fullRegion.getStartColumn()); + assertThat(textRange.end().line()).isEqualTo(fullRegion.getEndLine()); + assertThat(textRange.end().lineOffset()).isEqualTo(fullRegion.getEndColumn()); + } + + @Test + public void mapRegion_whenStartEndLinesDefined() { + Region fullRegion = mockRegion(null, null, 3, 8); + + Optional<TextRange> optTextRange = regionMapper.mapRegion(fullRegion, INPUT_FILE); + + assertThat(optTextRange).isPresent(); + TextRange textRange = optTextRange.get(); + assertThat(textRange.start().line()).isEqualTo(fullRegion.getStartLine()); + assertThat(textRange.start().lineOffset()).isEqualTo(1); + assertThat(textRange.end().line()).isEqualTo(fullRegion.getEndLine()); + assertThat(textRange.end().lineOffset()).isEqualTo(LINE_END_OFFSET); + } + + @Test + public void mapRegion_whenStartEndLinesDefinedAndStartColumn() { + Region fullRegion = mockRegion(8, null, 3, 8); + + Optional<TextRange> optTextRange = regionMapper.mapRegion(fullRegion, INPUT_FILE); + + assertThat(optTextRange).isPresent(); + TextRange textRange = optTextRange.get(); + assertThat(textRange.start().line()).isEqualTo(fullRegion.getStartLine()); + assertThat(textRange.start().lineOffset()).isEqualTo(fullRegion.getStartColumn()); + assertThat(textRange.end().line()).isEqualTo(fullRegion.getEndLine()); + assertThat(textRange.end().lineOffset()).isEqualTo(LINE_END_OFFSET); + } + + @Test + public void mapRegion_whenStartEndLinesDefinedAndEndColumn() { + Region fullRegion = mockRegion(null, 8, 3, 8); + + Optional<TextRange> optTextRange = regionMapper.mapRegion(fullRegion, INPUT_FILE); + + assertThat(optTextRange).isPresent(); + TextRange textRange = optTextRange.get(); + assertThat(textRange.start().line()).isEqualTo(fullRegion.getStartLine()); + assertThat(textRange.start().lineOffset()).isEqualTo(1); + assertThat(textRange.end().line()).isEqualTo(fullRegion.getEndLine()); + assertThat(textRange.end().lineOffset()).isEqualTo(fullRegion.getEndLine()); + } + + private static Region mockRegion(@Nullable Integer startColumn, @Nullable Integer endColumn, @Nullable Integer startLine, @Nullable Integer endLine) { + Region region = mock(Region.class); + when(region.getStartColumn()).thenReturn(startColumn); + when(region.getEndColumn()).thenReturn(endColumn); + when(region.getStartLine()).thenReturn(startLine); + when(region.getEndLine()).thenReturn(endLine); + return region; + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/ResultMapperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/ResultMapperTest.java new file mode 100644 index 00000000000..3e1d9a4172f --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/ResultMapperTest.java @@ -0,0 +1,165 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.api.batch.rule.Severity; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rules.RuleType; +import org.sonar.core.sarif.Location; +import org.sonar.core.sarif.Result; + +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class ResultMapperTest { + + private static final String RULE_ID = "test_rules_id"; + private static final String DRIVER_NAME = "driverName"; + + @Mock + private LocationMapper locationMapper; + + @Mock + private SensorContext sensorContext; + + @Mock + private NewExternalIssue newExternalIssue; + + @Mock + private NewIssueLocation newExternalIssueLocation; + + @Mock + private Result result; + + @InjectMocks + ResultMapper resultMapper; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + when(result.getRuleId()).thenReturn(RULE_ID); + when(sensorContext.newExternalIssue()).thenReturn(newExternalIssue); + when(locationMapper.fillIssueInFileLocation(any(), any(), any())).thenReturn(newExternalIssueLocation); + when(locationMapper.fillIssueInProjectLocation(any(), any())).thenReturn(newExternalIssueLocation); + when(newExternalIssue.newLocation()).thenReturn(newExternalIssueLocation); + } + + @Test + public void mapResult_mapsSimpleFieldsCorrectly() { + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + + verify(newExternalIssue).type(RuleType.VULNERABILITY); + verify(newExternalIssue).engineId(DRIVER_NAME); + verify(newExternalIssue).severity(ResultMapper.DEFAULT_SEVERITY); + verify(newExternalIssue).ruleId(RULE_ID); + } + + @Test + public void mapResult_ifRuleIdMissing_fails() { + when(result.getRuleId()).thenReturn(null); + assertThatNullPointerException() + .isThrownBy(() -> resultMapper.mapResult(DRIVER_NAME, result)) + .withMessage("No ruleId found for issue thrown by driver driverName"); + } + + @Test + public void mapResult_whenLocationExists_createsFileLocation() { + Location location = mock(Location.class); + when(result.getLocations()).thenReturn(Set.of(location)); + + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + + verify(locationMapper).fillIssueInFileLocation(result, newExternalIssueLocation, location); + verifyNoMoreInteractions(locationMapper); + verify(newExternalIssue).at(newExternalIssueLocation); + verify(newExternalIssue, never()).addLocation(any()); + verify(newExternalIssue, never()).addFlow(any()); + } + + @Test + public void mapResult_whenLocationExistsButLocationMapperReturnsNull_createsProjectLocation() { + Location location = mock(Location.class); + when(result.getLocations()).thenReturn(Set.of(location)); + when(locationMapper.fillIssueInFileLocation(any(), any(), any())).thenReturn(null); + + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + + verify(locationMapper).fillIssueInProjectLocation(result, newExternalIssueLocation); + verify(newExternalIssue).at(newExternalIssueLocation); + verify(newExternalIssue, never()).addLocation(any()); + verify(newExternalIssue, never()).addFlow(any()); + } + + @Test + public void mapResult_whenLocationNotFound_createsProjectLocation() { + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + + verify(locationMapper).fillIssueInProjectLocation(result, newExternalIssueLocation); + verifyNoMoreInteractions(locationMapper); + verify(newExternalIssue).at(newExternalIssueLocation); + verify(newExternalIssue, never()).addLocation(any()); + verify(newExternalIssue, never()).addFlow(any()); + } + + @Test + public void mapResult_mapsErrorLevel_toCriticalSeverity() { + when(result.getLevel()).thenReturn("error"); + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + verify(newExternalIssue).severity(Severity.CRITICAL); + } + + @DataProvider + public static Object[][] level_severity_mapping() { + return new Object[][] { + {"error", Severity.CRITICAL}, + {"warning", Severity.MAJOR}, + {"note", Severity.MINOR}, + {"none", Severity.INFO}, + {"anything else", ResultMapper.DEFAULT_SEVERITY}, + }; + } + + @Test + @UseDataProvider("level_severity_mapping") + public void mapResult_mapsCorrectlyLevelToSeverity(String level, Severity severity) { + when(result.getLevel()).thenReturn(level); + NewExternalIssue newExternalIssue = resultMapper.mapResult(DRIVER_NAME, result); + verify(newExternalIssue).severity(severity); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java new file mode 100644 index 00000000000..c44b765d312 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.util.List; +import java.util.Set; +import junit.framework.TestCase; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.batch.sensor.issue.NewExternalIssue; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.core.sarif.Result; +import org.sonar.core.sarif.Run; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RunMapperTest extends TestCase { + + private static final String TEST_DRIVER = "Test driver"; + + @Mock + private ResultMapper resultMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Run run; + + @Rule + public LogTester logTester = new LogTester(); + + @InjectMocks + private RunMapper runMapper; + + @Test + public void mapRun_delegatesToMapResult() { + when(run.getTool().getDriver().getName()).thenReturn(TEST_DRIVER); + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + when(run.getResults()).thenReturn(Set.of(result1, result2)); + + NewExternalIssue externalIssue1 = mockMappedResult(result1); + NewExternalIssue externalIssue2 = mockMappedResult(result2); + + List<NewExternalIssue> newExternalIssues = runMapper.mapRun(run); + + assertThat(newExternalIssues) + .containsOnly(externalIssue1, externalIssue2); + } + + @Test + public void mapRun_ifExceptionThrownByResultMapper_logsThemAndContinueProcessing() { + when(run.getTool().getDriver().getName()).thenReturn(TEST_DRIVER); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + when(run.getResults()).thenReturn(Set.of(result1, result2)); + + NewExternalIssue externalIssue2 = mockMappedResult(result2); + + when(resultMapper.mapResult(TEST_DRIVER, result1)).thenThrow(new IllegalArgumentException("test")); + + List<NewExternalIssue> newExternalIssues = runMapper.mapRun(run); + + assertThat(newExternalIssues) + .containsExactly(externalIssue2); + + assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Failed to import an issue raised by tool Test driver, error: test"); + } + + @Test + public void mapRun_failsIfToolNotSet() { + when(run.getTool()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> runMapper.mapRun(run)) + .withMessage("The run does not have a tool driver name defined."); + } + + @Test + public void mapRun_failsIfDriverNotSet() { + when(run.getTool().getDriver()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> runMapper.mapRun(run)) + .withMessage("The run does not have a tool driver name defined."); + } + + @Test + public void mapRun_failsIfDriverNameIsNotSet() { + when(run.getTool().getDriver().getName()).thenReturn(null); + + assertThatIllegalArgumentException() + .isThrownBy(() -> runMapper.mapRun(run)) + .withMessage("The run does not have a tool driver name defined."); + } + + private NewExternalIssue mockMappedResult(Result result) { + NewExternalIssue externalIssue = mock(NewExternalIssue.class); + when(resultMapper.mapResult(TEST_DRIVER, result)).thenReturn(externalIssue); + return externalIssue; + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensorTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensorTest.java new file mode 100644 index 00000000000..8d6d10db945 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensorTest.java @@ -0,0 +1,135 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.externalissue.sarif; + +import java.nio.file.Path; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.core.sarif.Sarif210; +import org.sonar.core.sarif.SarifSerializer; + +import static org.mockito.Mockito.doThrow; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SarifIssuesImportSensorTest { + + private static final String FILE_1 = "path/to/sarif/file.sarif"; + private static final String FILE_2 = "path/to/sarif/file2.sarif"; + private static final String SARIF_REPORT_PATHS_PARAM = FILE_1 + "," + FILE_2; + + @Mock + private SarifSerializer sarifSerializer; + @Mock + private Sarif210Importer sarifImporter; + + private MapSettings sensorSettings; + + @Before + public void before() { + sensorSettings = new MapSettings(); + } + + @Rule + public LogTester logTester = new LogTester(); + + private SensorContextTester sensorContext = SensorContextTester.create(Path.of(".")); + + @Test + public void execute_single_files() { + sensorSettings.setProperty("sonar.sarifReportPaths", FILE_1); + + Sarif210 sarifReport = mockReport(FILE_1); + + SarifIssuesImportSensor sensor = new SarifIssuesImportSensor(sarifSerializer, sarifImporter, sensorSettings.asConfig()); + sensor.execute(sensorContext); + + verify(sarifImporter).importSarif(sarifReport); + } + + @Test + public void execute_multiple_files() { + + sensorSettings.setProperty("sonar.sarifReportPaths", SARIF_REPORT_PATHS_PARAM); + + Sarif210 sarifReport1 = mockReport(FILE_1); + Sarif210 sarifReport2 = mockReport(FILE_2); + + SarifIssuesImportSensor sensor = new SarifIssuesImportSensor(sarifSerializer, sarifImporter, sensorSettings.asConfig()); + sensor.execute(sensorContext); + + verify(sarifImporter).importSarif(sarifReport1); + verify(sarifImporter).importSarif(sarifReport2); + } + + @Test + public void skip_report_when_import_fails() { + sensorSettings.setProperty("sonar.sarifReportPaths", SARIF_REPORT_PATHS_PARAM); + + Sarif210 sarifReport1 = mockReport(FILE_1); + Sarif210 sarifReport2 = mockReport(FILE_2); + + doThrow(new NullPointerException("import failed")).when(sarifImporter).importSarif(sarifReport1); + + SarifIssuesImportSensor sensor = new SarifIssuesImportSensor(sarifSerializer, sarifImporter, sensorSettings.asConfig()); + sensor.execute(sensorContext); + + verify(sarifImporter).importSarif(sarifReport2); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Failed to process SARIF report from file 'path/to/sarif/file.sarif', error 'import failed'"); + } + + @Test + public void skip_report_when_deserialization_fails() { + sensorSettings.setProperty("sonar.sarifReportPaths", SARIF_REPORT_PATHS_PARAM); + + failDeserializingReport(FILE_1); + Sarif210 sarifReport2 = mockReport(FILE_2); + + SarifIssuesImportSensor sensor = new SarifIssuesImportSensor(sarifSerializer, sarifImporter, sensorSettings.asConfig()); + sensor.execute(sensorContext); + + verify(sarifImporter).importSarif(sarifReport2); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Failed to process SARIF report from file 'path/to/sarif/file.sarif', error 'deserialization failed'"); + + } + + private Sarif210 mockReport(String path) { + Sarif210 report = mock(Sarif210.class); + Path reportFilePath = sensorContext.fileSystem().resolvePath(path).toPath(); + when(sarifSerializer.deserialize(reportFilePath)).thenReturn(report); + return report; + } + + private void failDeserializingReport(String path) { + Path reportFilePath = sensorContext.fileSystem().resolvePath(path).toPath(); + when(sarifSerializer.deserialize(reportFilePath)).thenThrow(new NullPointerException("deserialization failed")); + } +} |