aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sonar-core/src/main/java/org/sonar/core/sarif/Region.java23
-rw-r--r--sonar-core/src/main/java/org/sonar/core/sarif/Result.java17
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java15
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210Importer.java66
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/LocationMapper.java89
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RegionMapper.java48
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/ResultMapper.java91
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/RunMapper.java70
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/Sarif210Importer.java26
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensor.java91
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/DefaultSarif210ImporterTest.java109
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/LocationMapperTest.java171
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RegionMapperTest.java143
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/ResultMapperTest.java165
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/RunMapperTest.java129
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/sarif/SarifIssuesImportSensorTest.java135
16 files changed, 1377 insertions, 11 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/sarif/Region.java b/sonar-core/src/main/java/org/sonar/core/sarif/Region.java
index e909fceb9c3..7b4173fce8d 100644
--- a/sonar-core/src/main/java/org/sonar/core/sarif/Region.java
+++ b/sonar-core/src/main/java/org/sonar/core/sarif/Region.java
@@ -20,18 +20,20 @@
package org.sonar.core.sarif;
import com.google.gson.annotations.SerializedName;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
public class Region {
@SerializedName("startLine")
- private final int startLine;
+ private final Integer startLine;
@SerializedName("endLine")
- private final int endLine;
+ private final Integer endLine;
@SerializedName("startColumn")
- private final int startColumn;
+ private final Integer startColumn;
@SerializedName("endColumn")
- private final int endColumn;
+ private final Integer endColumn;
- private Region(int startLine, int endLine, int startColumn, int endColumn) {
+ private Region(Integer startLine, @Nullable Integer endLine, @Nullable Integer startColumn, @Nullable Integer endColumn) {
this.startLine = startLine;
this.endLine = endLine;
this.startColumn = startColumn;
@@ -42,19 +44,22 @@ public class Region {
return new RegionBuilder();
}
- public int getStartLine() {
+ public Integer getStartLine() {
return startLine;
}
- public int getEndLine() {
+ @CheckForNull
+ public Integer getEndLine() {
return endLine;
}
- public int getStartColumn() {
+ @CheckForNull
+ public Integer getStartColumn() {
return startColumn;
}
- public int getEndColumn() {
+ @CheckForNull
+ public Integer getEndColumn() {
return endColumn;
}
diff --git a/sonar-core/src/main/java/org/sonar/core/sarif/Result.java b/sonar-core/src/main/java/org/sonar/core/sarif/Result.java
index 60f980de875..33a9d12618f 100644
--- a/sonar-core/src/main/java/org/sonar/core/sarif/Result.java
+++ b/sonar-core/src/main/java/org/sonar/core/sarif/Result.java
@@ -37,14 +37,17 @@ public class Result {
private final PartialFingerprints partialFingerprints;
@SerializedName("codeFlows")
private final List<CodeFlow> codeFlows;
+ @SerializedName("level")
+ private final String level;
private Result(String ruleId, String message, LinkedHashSet<Location> locations,
- @Nullable String primaryLocationLineHash, @Nullable List<CodeFlow> codeFlows) {
+ @Nullable String primaryLocationLineHash, @Nullable List<CodeFlow> codeFlows, @Nullable String level) {
this.ruleId = ruleId;
this.message = WrappedText.of(message);
this.locations = locations;
this.partialFingerprints = primaryLocationLineHash == null ? null : new PartialFingerprints(primaryLocationLineHash);
this.codeFlows = codeFlows == null ? null : List.copyOf(codeFlows);
+ this.level = level;
}
public String getRuleId() {
@@ -69,6 +72,10 @@ public class Result {
return codeFlows;
}
+ public String getLevel() {
+ return level;
+ }
+
public static ResultBuilder builder() {
return new ResultBuilder();
}
@@ -79,6 +86,7 @@ public class Result {
private LinkedHashSet<Location> locations;
private String hash;
private List<CodeFlow> codeFlows;
+ private String level;
private ResultBuilder() {
}
@@ -93,6 +101,11 @@ public class Result {
return this;
}
+ public ResultBuilder level(String level) {
+ this.level = level;
+ return this;
+ }
+
public ResultBuilder locations(Set<Location> locations) {
this.locations = new LinkedHashSet<>(locations);
return this;
@@ -109,7 +122,7 @@ public class Result {
}
public Result build() {
- return new Result(ruleId, message, locations, hash, codeFlows);
+ return new Result(ruleId, message, locations, hash, codeFlows, level);
}
}
}
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"));
+ }
+}