summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-12-03 16:50:08 +0100
committerSonarTech <sonartech@sonarsource.com>2020-01-13 20:46:26 +0100
commit751fd7ffdb00dcf77e6fa982e6d3428dd7a6c564 (patch)
tree160bc506c3cf6b93c6ed6fb173b40010a94b7f75
parent71eed309ddc5258a3d0bf548f20445cecf9aa148 (diff)
downloadsonarqube-751fd7ffdb00dcf77e6fa982e6d3428dd7a6c564.tar.gz
sonarqube-751fd7ffdb00dcf77e6fa982e6d3428dd7a6c564.zip
SONAR-12718 add WS api/hotspots/show
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.java123
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java47
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java34
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java123
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TextRangeResponseFormatter.java83
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java4
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java56
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotRuleDescriptionTest.java352
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java2
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java35
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java364
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java2
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java3
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java3
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java3
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java3
-rw-r--r--sonar-ws/src/main/protobuf/ws-hotspots.proto55
19 files changed, 1187 insertions, 112 deletions
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.java
new file mode 100644
index 00000000000..205bc723dbc
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.hotspot.ws;
+
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.db.rule.RuleDefinitionDto;
+
+import static java.lang.Character.isWhitespace;
+import static java.util.Optional.ofNullable;
+
+public class HotspotRuleDescription {
+ private static final HotspotRuleDescription NO_DESCRIPTION = new HotspotRuleDescription(null, null, null);
+
+ @CheckForNull
+ private final String risk;
+ @CheckForNull
+ private final String vulnerable;
+ @CheckForNull
+ private final String fixIt;
+
+ private HotspotRuleDescription(@Nullable String risk, @Nullable String vulnerable, @Nullable String fixIt) {
+ this.risk = risk;
+ this.vulnerable = vulnerable;
+ this.fixIt = fixIt;
+ }
+
+ public static HotspotRuleDescription from(RuleDefinitionDto dto) {
+ String description = dto.getDescription();
+ if (description == null) {
+ return NO_DESCRIPTION;
+ }
+
+ String vulnerableTitle = "<h2>Ask Yourself Whether</h2>";
+ String fixItTitle = "<h2>Recommended Secure Coding Practices</h2>";
+ int vulnerableTitlePosition = description.indexOf(vulnerableTitle);
+ int fixItTitlePosition = description.indexOf(fixItTitle);
+ if (vulnerableTitlePosition == -1 && fixItTitlePosition == -1) {
+ return NO_DESCRIPTION;
+ }
+
+ if (vulnerableTitlePosition == -1) {
+ return new HotspotRuleDescription(
+ trimingSubstring(description, 0, fixItTitlePosition),
+ null,
+ trimingSubstring(description, fixItTitlePosition, description.length())
+ );
+ }
+ if (fixItTitlePosition == -1) {
+ return new HotspotRuleDescription(
+ trimingSubstring(description, 0, vulnerableTitlePosition),
+ trimingSubstring(description, vulnerableTitlePosition, description.length()),
+ null
+ );
+ }
+ return new HotspotRuleDescription(
+ trimingSubstring(description, 0, vulnerableTitlePosition),
+ trimingSubstring(description, vulnerableTitlePosition, fixItTitlePosition),
+ trimingSubstring(description, fixItTitlePosition, description.length())
+ );
+ }
+
+ @CheckForNull
+ private static String trimingSubstring(String description, int beginIndex, int endIndex) {
+ if (beginIndex == endIndex) {
+ return null;
+ }
+
+ int trimmedBeginIndex = beginIndex;
+ while (trimmedBeginIndex < endIndex && isWhitespace(description.charAt(trimmedBeginIndex))) {
+ trimmedBeginIndex++;
+ }
+ int trimmedEndIndex = endIndex;
+ while (trimmedEndIndex > 0 && trimmedEndIndex > trimmedBeginIndex && isWhitespace(description.charAt(trimmedEndIndex - 1))) {
+ trimmedEndIndex--;
+ }
+ if (trimmedBeginIndex == trimmedEndIndex) {
+ return null;
+ }
+
+ return description.substring(trimmedBeginIndex, trimmedEndIndex);
+ }
+
+ public Optional<String> getRisk() {
+ return ofNullable(risk);
+ }
+
+ public Optional<String> getVulnerable() {
+ return ofNullable(vulnerable);
+ }
+
+ public Optional<String> getFixIt() {
+ return ofNullable(fixIt);
+ }
+
+ @Override
+ public String toString() {
+ return "HotspotRuleDescription{" +
+ "risk='" + risk + '\'' +
+ ", vulnerable='" + vulnerable + '\'' +
+ ", fixIt='" + fixIt + '\'' +
+ '}';
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java
new file mode 100644
index 00000000000..0ac88897685
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsResponseFormatter.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.hotspot.ws;
+
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonarqube.ws.Hotspots;
+
+import static java.util.Optional.ofNullable;
+
+public class HotspotWsResponseFormatter {
+ private final DefaultOrganizationProvider defaultOrganizationProvider;
+
+ public HotspotWsResponseFormatter(DefaultOrganizationProvider defaultOrganizationProvider) {
+ this.defaultOrganizationProvider = defaultOrganizationProvider;
+ }
+
+ Hotspots.Component formatComponent(Hotspots.Component.Builder builder, ComponentDto component) {
+ builder
+ .clear()
+ .setOrganization(defaultOrganizationProvider.get().getKey())
+ .setKey(component.getKey())
+ .setQualifier(component.qualifier())
+ .setName(component.name())
+ .setLongName(component.longName());
+ ofNullable(component.path()).ifPresent(builder::setPath);
+ return builder.build();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
index e04dd6d3cce..2a437420b81 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
@@ -25,8 +25,9 @@ public class HotspotsWsModule extends Module {
@Override
protected void configureModule() {
add(
+ HotspotWsResponseFormatter.class,
SearchAction.class,
- HotspotsWs.class
- );
+ ShowAction.class,
+ HotspotsWs.class);
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
index 021901007cf..250278316d8 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java
@@ -51,13 +51,12 @@ import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.index.IssueQuery;
-import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.security.SecurityStandards;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Hotspots;
+import org.sonarqube.ws.Hotspots.SearchWsResponse;
-import static com.google.common.base.Strings.nullToEmpty;
import static java.util.Optional.ofNullable;
import static org.sonar.api.server.ws.WebService.Param.PAGE;
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
@@ -68,6 +67,7 @@ import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.WsUtils.nullToEmpty;
public class SearchAction implements HotspotsWsAction {
private static final String PARAM_PROJECT_KEY = "projectKey";
@@ -75,13 +75,14 @@ public class SearchAction implements HotspotsWsAction {
private final DbClient dbClient;
private final UserSession userSession;
private final IssueIndex issueIndex;
- private final DefaultOrganizationProvider defaultOrganizationProvider;
+ private final HotspotWsResponseFormatter responseFormatter;
- public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, DefaultOrganizationProvider defaultOrganizationProvider) {
+ public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex,
+ HotspotWsResponseFormatter responseFormatter) {
this.dbClient = dbClient;
this.userSession = userSession;
this.issueIndex = issueIndex;
- this.defaultOrganizationProvider = defaultOrganizationProvider;
+ this.responseFormatter = responseFormatter;
}
@Override
@@ -187,8 +188,8 @@ public class SearchAction implements HotspotsWsAction {
}
}
- private Hotspots.SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
- Hotspots.SearchWsResponse.Builder responseBuilder = Hotspots.SearchWsResponse.newBuilder();
+ private SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
+ SearchWsResponse.Builder responseBuilder = SearchWsResponse.newBuilder();
formatPaging(searchResponseData, responseBuilder);
if (!searchResponseData.isEmpty()) {
formatHotspots(searchResponseData, responseBuilder);
@@ -197,7 +198,7 @@ public class SearchAction implements HotspotsWsAction {
return responseBuilder.build();
}
- private void formatPaging(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ private void formatPaging(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
Paging paging = searchResponseData.getPaging();
Common.Paging.Builder pagingBuilder = Common.Paging.newBuilder()
.setPageIndex(paging.pageIndex())
@@ -207,13 +208,13 @@ public class SearchAction implements HotspotsWsAction {
responseBuilder.setPaging(pagingBuilder.build());
}
- private static void formatHotspots(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ private void formatHotspots(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
if (orderedHotspots.isEmpty()) {
return;
}
- Hotspots.Hotspot.Builder builder = Hotspots.Hotspot.newBuilder();
+ SearchWsResponse.Hotspot.Builder builder = SearchWsResponse.Hotspot.newBuilder();
for (IssueDto hotspot : orderedHotspots) {
RuleDefinitionDto rule = searchResponseData.getRule(hotspot.getRuleKey())
// due to join with table Rule when retrieving data from Issues, this can't happen
@@ -244,7 +245,7 @@ public class SearchAction implements HotspotsWsAction {
}
}
- private void formatComponents(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ private void formatComponents(SearchResponseData searchResponseData, SearchWsResponse.Builder responseBuilder) {
Set<ComponentDto> components = searchResponseData.getComponents();
if (components.isEmpty()) {
return;
@@ -252,16 +253,7 @@ public class SearchAction implements HotspotsWsAction {
Hotspots.Component.Builder builder = Hotspots.Component.newBuilder();
for (ComponentDto component : components) {
- builder
- .clear()
- .setOrganization(defaultOrganizationProvider.get().getKey())
- .setKey(component.getKey())
- .setQualifier(component.qualifier())
- .setName(component.name())
- .setLongName(component.longName());
- ofNullable(component.path()).ifPresent(builder::setPath);
-
- responseBuilder.addComponents(builder.build());
+ responseBuilder.addComponents(responseFormatter.formatComponent(builder, component));
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
index f384e4c2872..862f73f53ac 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
@@ -19,6 +19,9 @@
*/
package org.sonar.server.hotspot.ws;
+import java.util.Objects;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
@@ -28,10 +31,20 @@ import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueDto;
+import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.security.SecurityStandards;
import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Hotspots;
+import org.sonarqube.ws.Hotspots.ShowWsResponse;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.nullToEmpty;
import static java.lang.String.format;
+import static java.util.Optional.ofNullable;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
public class ShowAction implements HotspotsWsAction {
@@ -39,10 +52,14 @@ public class ShowAction implements HotspotsWsAction {
private final DbClient dbClient;
private final UserSession userSession;
+ private final HotspotWsResponseFormatter responseFormatter;
+ private final TextRangeResponseFormatter textRangeFormatter;
- public ShowAction(DbClient dbClient, UserSession userSession) {
+ public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter) {
this.dbClient = dbClient;
this.userSession = userSession;
+ this.responseFormatter = responseFormatter;
+ this.textRangeFormatter = textRangeFormatter;
}
@Override
@@ -50,8 +67,9 @@ public class ShowAction implements HotspotsWsAction {
WebService.NewAction action = controller
.createAction("show")
.setHandler(this)
- .setDescription("Provides the details of a Security Hotpot.")
- .setSince("8.1");
+ .setDescription("Provides the details of a Security Hotspot.")
+ .setSince("8.1")
+ .setInternal(true);
action.createParam(PARAM_HOTSPOT_KEY)
.setDescription("Key of the Security Hotspot")
@@ -66,12 +84,103 @@ public class ShowAction implements HotspotsWsAction {
String hotspotKey = request.mandatoryParam(PARAM_HOTSPOT_KEY);
try (DbSession dbSession = dbClient.openSession(false)) {
IssueDto hotspot = dbClient.issueDao().selectByKey(dbSession, hotspotKey)
- .orElseThrow(() -> new NotFoundException(format("Hotspot with key '%s' does not exist", hotspotKey)));
- ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, hotspot.getProjectUuid())
- .orElseThrow(() -> new NotFoundException(format("Project with uuid '%s' does not exist", hotspot.getProjectUuid())));
- userSession.checkComponentPermission(UserRole.USER, project);
+ .filter(t -> t.getType() == RuleType.SECURITY_HOTSPOT.getDbConstant())
+ .orElseThrow(() -> new NotFoundException(format("Hotspot '%s' does not exist", hotspotKey)));
+ Components components = loadComponents(dbSession, hotspot);
+ RuleDefinitionDto rule = loadRule(dbSession, hotspot);
+ ShowWsResponse.Builder responseBuilder = ShowWsResponse.newBuilder();
+ formatHotspot(responseBuilder, hotspot);
+ formatComponents(components, responseBuilder);
+ formatRule(responseBuilder, rule);
+ formatTextRange(hotspot, responseBuilder);
+
+ writeProtobuf(responseBuilder.build(), request, response);
}
}
+
+ private void formatHotspot(ShowWsResponse.Builder builder, IssueDto hotspot) {
+ builder.setKey(hotspot.getKey());
+ ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
+ // FIXME resolution field will be added later
+ // ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
+ ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
+ builder.setMessage(nullToEmpty(hotspot.getMessage()));
+ ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
+ // FIXME Filter author only if user is member of the organization (as done in issues/search WS)
+ // if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) {
+ builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
+ // }
+ builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
+ builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
+ }
+
+ private void formatComponents(Components components, ShowWsResponse.Builder responseBuilder) {
+ responseBuilder
+ .setProject(responseFormatter.formatComponent(Hotspots.Component.newBuilder(), components.getProject()))
+ .setComponent(responseFormatter.formatComponent(Hotspots.Component.newBuilder(), components.getComponent()));
+ }
+
+ private void formatRule(ShowWsResponse.Builder responseBuilder, RuleDefinitionDto ruleDefinitionDto) {
+ SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(ruleDefinitionDto.getSecurityStandards());
+ SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory();
+ HotspotRuleDescription hotspotRuleDescription = HotspotRuleDescription.from(ruleDefinitionDto);
+ Hotspots.Rule.Builder ruleBuilder = Hotspots.Rule.newBuilder()
+ .setKey(ruleDefinitionDto.getKey().toString())
+ .setName(nullToEmpty(ruleDefinitionDto.getName()))
+ .setSecurityCategory(sqCategory.getKey())
+ .setVulnerabilityProbability(sqCategory.getVulnerability().name());
+ hotspotRuleDescription.getVulnerable().ifPresent(ruleBuilder::setVulnerabilityDescription);
+ hotspotRuleDescription.getRisk().ifPresent(ruleBuilder::setRiskDescription);
+ hotspotRuleDescription.getFixIt().ifPresent(ruleBuilder::setFixRecommendations);
+ responseBuilder.setRule(ruleBuilder.build());
+ }
+
+ private void formatTextRange(IssueDto hotspot, ShowWsResponse.Builder responseBuilder) {
+ textRangeFormatter.formatTextRange(hotspot, responseBuilder::setTextRange);
+ }
+
+ private RuleDefinitionDto loadRule(DbSession dbSession, IssueDto hotspot) {
+ RuleKey ruleKey = hotspot.getRuleKey();
+ return dbClient.ruleDao().selectDefinitionByKey(dbSession, ruleKey)
+ .orElseThrow(() -> new NotFoundException(format("Rule '%s' does not exist", ruleKey)));
+ }
+
+ private Components loadComponents(DbSession dbSession, IssueDto hotspot) {
+ String projectUuid = hotspot.getProjectUuid();
+ String componentUuid = hotspot.getComponentUuid();
+ checkArgument(projectUuid != null, "Hotspot '%s' has no project", hotspot.getKee());
+ checkArgument(componentUuid != null, "Hotspot '%s' has no component", hotspot.getKee());
+
+ ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, projectUuid)
+ .orElseThrow(() -> new NotFoundException(format("Project with uuid '%s' does not exist", projectUuid)));
+ userSession.checkComponentPermission(UserRole.USER, project);
+
+ boolean hotspotOnProject = Objects.equals(projectUuid, componentUuid);
+ ComponentDto component = hotspotOnProject ? project
+ : dbClient.componentDao().selectByUuid(dbSession, componentUuid)
+ .orElseThrow(() -> new NotFoundException(format("Component with uuid '%s' does not exist", componentUuid)));
+
+ return new Components(project, component);
+ }
+
+ private static final class Components {
+ private final ComponentDto project;
+ private final ComponentDto component;
+
+ private Components(ComponentDto project, ComponentDto component) {
+ this.project = project;
+ this.component = component;
+ }
+
+ public ComponentDto getProject() {
+ return project;
+ }
+
+ public ComponentDto getComponent() {
+ return component;
+ }
+ }
+
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TextRangeResponseFormatter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TextRangeResponseFormatter.java
new file mode 100644
index 00000000000..b1eacc1d724
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/TextRangeResponseFormatter.java
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue;
+
+import java.util.Map;
+import java.util.function.Consumer;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonarqube.ws.Common;
+
+import static java.util.Optional.ofNullable;
+
+public class TextRangeResponseFormatter {
+
+ public void formatTextRange(IssueDto dto, Consumer<Common.TextRange> rangeConsumer) {
+ DbIssues.Locations locations = dto.parseLocations();
+ if (locations == null) {
+ return;
+ }
+ formatTextRange(locations, rangeConsumer);
+ }
+
+ public void formatTextRange(DbIssues.Locations locations, Consumer<Common.TextRange> rangeConsumer) {
+ if (locations.hasTextRange()) {
+ DbCommons.TextRange textRange = locations.getTextRange();
+ rangeConsumer.accept(convertTextRange(textRange).build());
+ }
+ }
+
+ public Common.Location formatLocation(DbIssues.Location source, String issueComponent, Map<String, ComponentDto> componentsByUuid) {
+ Common.Location.Builder target = Common.Location.newBuilder();
+ if (source.hasMsg()) {
+ target.setMsg(source.getMsg());
+ }
+ if (source.hasTextRange()) {
+ DbCommons.TextRange sourceRange = source.getTextRange();
+ Common.TextRange.Builder targetRange = convertTextRange(sourceRange);
+ target.setTextRange(targetRange);
+ }
+ if (source.hasComponentId()) {
+ ofNullable(componentsByUuid.get(source.getComponentId())).ifPresent(c -> target.setComponent(c.getKey()));
+ } else {
+ target.setComponent(issueComponent);
+ }
+ return target.build();
+ }
+
+ private static Common.TextRange.Builder convertTextRange(DbCommons.TextRange sourceRange) {
+ Common.TextRange.Builder targetRange = Common.TextRange.newBuilder();
+ if (sourceRange.hasStartLine()) {
+ targetRange.setStartLine(sourceRange.getStartLine());
+ }
+ if (sourceRange.hasStartOffset()) {
+ targetRange.setStartOffset(sourceRange.getStartOffset());
+ }
+ if (sourceRange.hasEndLine()) {
+ targetRange.setEndLine(sourceRange.getEndLine());
+ }
+ if (sourceRange.hasEndOffset()) {
+ targetRange.setEndOffset(sourceRange.getEndOffset());
+ }
+ return targetRange;
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
index b21e4728d6f..1103eee6efa 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
@@ -21,6 +21,7 @@ package org.sonar.server.issue.ws;
import org.sonar.core.platform.Module;
import org.sonar.server.issue.AvatarResolverImpl;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.TransitionService;
@@ -45,6 +46,7 @@ public class IssueWsModule extends Module {
IssuesWs.class,
AvatarResolverImpl.class,
SearchResponseLoader.class,
+ TextRangeResponseFormatter.class,
SearchResponseFormat.class,
OperationResponseWriter.class,
AddCommentAction.class,
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
index fad0639372c..fff99e6c79c 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
@@ -82,6 +82,10 @@ public class SearchResponseData {
return componentsByUuid.get(uuid);
}
+ public Map<String, ComponentDto> getComponentsByUuid() {
+ return componentsByUuid;
+ }
+
public List<UserDto> getUsers() {
return new ArrayList<>(usersByUuid.values());
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
index 362fe7f428d..8b628b17185 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
@@ -37,14 +37,13 @@ import org.sonar.api.utils.Paging;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueChangeDto;
import org.sonar.db.issue.IssueDto;
-import org.sonar.db.protobuf.DbCommons;
import org.sonar.db.protobuf.DbIssues;
-import org.sonar.db.protobuf.DbIssues.Location;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.user.UserDto;
import org.sonar.markdown.Markdown;
import org.sonar.server.es.Facets;
import org.sonar.server.issue.AvatarResolver;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.issue.workflow.Transition;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Issues;
@@ -78,11 +77,14 @@ public class SearchResponseFormat {
private final Durations durations;
private final Languages languages;
private final AvatarResolver avatarFactory;
+ private final TextRangeResponseFormatter textRangeFormatter;
- public SearchResponseFormat(Durations durations, Languages languages, AvatarResolver avatarFactory) {
+ public SearchResponseFormat(Durations durations, Languages languages, AvatarResolver avatarFactory,
+ TextRangeResponseFormatter textRangeFormatter) {
this.durations = durations;
this.languages = languages;
this.avatarFactory = avatarFactory;
+ this.textRangeFormatter = textRangeFormatter;
}
SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging, Facets facets) {
@@ -219,57 +221,19 @@ public class SearchResponseFormat {
return ruleKey.repository().replace(EXTERNAL_RULE_REPO_PREFIX, "");
}
- private static void completeIssueLocations(IssueDto dto, Issue.Builder issueBuilder, SearchResponseData data) {
+ private void completeIssueLocations(IssueDto dto, Issue.Builder issueBuilder, SearchResponseData data) {
DbIssues.Locations locations = dto.parseLocations();
if (locations == null) {
return;
}
- if (locations.hasTextRange()) {
- DbCommons.TextRange textRange = locations.getTextRange();
- issueBuilder.setTextRange(convertTextRange(textRange));
- }
+ textRangeFormatter.formatTextRange(locations, issueBuilder::setTextRange);
for (DbIssues.Flow flow : locations.getFlowList()) {
Common.Flow.Builder targetFlow = Common.Flow.newBuilder();
- for (Location flowLocation : flow.getLocationList()) {
- targetFlow.addLocations(convertLocation(issueBuilder, flowLocation, data));
+ for (DbIssues.Location flowLocation : flow.getLocationList()) {
+ targetFlow.addLocations(textRangeFormatter.formatLocation(flowLocation, issueBuilder.getComponent(), data.getComponentsByUuid()));
}
- issueBuilder.addFlows(targetFlow);
- }
- }
-
- private static Common.Location convertLocation(Issue.Builder issueBuilder, Location source, SearchResponseData data) {
- Common.Location.Builder target = Common.Location.newBuilder();
- if (source.hasMsg()) {
- target.setMsg(source.getMsg());
- }
- if (source.hasTextRange()) {
- DbCommons.TextRange sourceRange = source.getTextRange();
- Common.TextRange.Builder targetRange = convertTextRange(sourceRange);
- target.setTextRange(targetRange);
- }
- if (source.hasComponentId()) {
- ofNullable(data.getComponentByUuid(source.getComponentId())).ifPresent(c -> target.setComponent(c.getKey()));
- } else {
- target.setComponent(issueBuilder.getComponent());
- }
- return target.build();
- }
-
- private static Common.TextRange.Builder convertTextRange(DbCommons.TextRange sourceRange) {
- Common.TextRange.Builder targetRange = Common.TextRange.newBuilder();
- if (sourceRange.hasStartLine()) {
- targetRange.setStartLine(sourceRange.getStartLine());
- }
- if (sourceRange.hasStartOffset()) {
- targetRange.setStartOffset(sourceRange.getStartOffset());
- }
- if (sourceRange.hasEndLine()) {
- targetRange.setEndLine(sourceRange.getEndLine());
- }
- if (sourceRange.hasEndOffset()) {
- targetRange.setEndOffset(sourceRange.getEndOffset());
+ issueBuilder.addFlows(targetFlow.build());
}
- return targetRange;
}
private static void formatIssueTransitions(SearchResponseData data, Issue.Builder wsIssue, IssueDto dto) {
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotRuleDescriptionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotRuleDescriptionTest.java
new file mode 100644
index 00000000000..1a35b069c1b
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotRuleDescriptionTest.java
@@ -0,0 +1,352 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.hotspot.ws;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(DataProviderRunner.class)
+public class HotspotRuleDescriptionTest {
+ @Test
+ public void parse_returns_all_empty_fields_when_no_description() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(null);
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @Test
+ @UseDataProvider("noContentVariants")
+ public void parse_returns_all_empty_fields_when_empty_description(String noContent) {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription("");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @Test
+ public void parse_ignores_titles_if_not_h2() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "acme\" +" +
+ "<h1>Ask Yourself Whether</h1>\n" +
+ "bar\n" +
+ "<h1>Recommended Secure Coding Practices</h1>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @Test
+ @UseDataProvider("whiteSpaceBeforeAndAfterCombinations")
+ public void parse_does_not_trim_content_of_h2_titles(String whiteSpaceBefore, String whiteSpaceAfter) {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "acme\" +" +
+ "<h2>" + whiteSpaceBefore + "Ask Yourself Whether" + whiteSpaceAfter + "</h2>\n" +
+ "bar\n" +
+ "<h2>" + whiteSpaceBefore + "Recommended Secure Coding Practices\" + whiteSpaceAfter + \"</h2>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @DataProvider
+ public static Object[][] whiteSpaceBeforeAndAfterCombinations() {
+ String whiteSpace = " ";
+ String noWithSpace = "";
+ return new Object[][] {
+ {noWithSpace, whiteSpace},
+ {whiteSpace, noWithSpace},
+ {whiteSpace, whiteSpace}
+ };
+ }
+
+ @Test
+ @UseDataProvider("descriptionsWithoutTitles")
+ public void parse_return_null_fields_when_desc_contains_neither_title(String description) {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(description);
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @DataProvider
+ public static Object[][] descriptionsWithoutTitles() {
+ return new Object[][] {
+ {""},
+ {randomAlphabetic(123)},
+ {"bar\n" +
+ "acme\n" +
+ "foo"}
+ };
+ }
+
+ @Test
+ public void parse_return_null_risk_when_desc_starts_with_ask_yourself_title() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "<h2>Ask Yourself Whether</h2>\n" +
+ "bar\n" +
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk()).isEmpty();
+ assertThat(result.getVulnerable().get()).isEqualTo("<h2>Ask Yourself Whether</h2>\nbar");
+ assertThat(result.getFixIt().get()).isEqualTo("<h2>Recommended Secure Coding Practices</h2>\nfoo");
+ }
+
+ @Test
+ public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "bar\n" +
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk().get()).isEqualTo("bar");
+ assertThat(result.getVulnerable()).isEmpty();
+ assertThat(result.getFixIt().get()).isEqualTo("<h2>Recommended Secure Coding Practices</h2>\nfoo");
+ }
+
+ @Test
+ @UseDataProvider("noContentVariants")
+ public void parse_returns_vulnerable_with_only_title_when_no_content_between_titles(String noContent) {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "bar\n" +
+ "<h2>Ask Yourself Whether</h2>\n" +
+ noContent +
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk().get()).isEqualTo("bar");
+ assertThat(result.getVulnerable().get()).isEqualTo("<h2>Ask Yourself Whether</h2>");
+ assertThat(result.getFixIt().get()).isEqualTo("<h2>Recommended Secure Coding Practices</h2>\nfoo");
+ }
+
+ @Test
+ @UseDataProvider("noContentVariants")
+ public void parse_returns_fixIt_with_only_title_when_no_content_after_Recommended_Secure_Coding_Practices_title(String noContent) {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "bar\n" +
+ "<h2>Ask Yourself Whether</h2>\n" +
+ "bar" +
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ noContent);
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk().get()).isEqualTo("bar");
+ assertThat(result.getVulnerable().get()).isEqualTo("<h2>Ask Yourself Whether</h2>\nbar");
+ assertThat(result.getFixIt().get()).isEqualTo("<h2>Recommended Secure Coding Practices</h2>");
+ }
+
+ @DataProvider
+ public static Object[][] noContentVariants() {
+ return new Object[][] {
+ {""},
+ {"\n"},
+ {" \n "},
+ {"\t\n \n"},
+ };
+ }
+
+ @Test
+ public void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "bar\n" +
+ "<h2>Ask Yourself Whether</h2>\n" +
+ "foo");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk().get()).isEqualTo("bar");
+ assertThat(result.getVulnerable().get()).isEqualTo("<h2>Ask Yourself Whether</h2>\nfoo");
+ assertThat(result.getFixIt()).isEmpty();
+ }
+
+ @Test
+ public void parse_returns_regular_description() {
+ RuleDefinitionDto dto = RuleTesting.newRule().setDescription(
+ "<p>Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:</p>\n" +
+ "<ul>\n" +
+ " <li> <a href=\"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0269\">CVE-2018-0269</a> </li>\n" +
+ " <li> <a href=\"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14460\">CVE-2017-14460</a> </li>\n" +
+ "</ul>\n" +
+ "<p>Applications that enable CORS will effectively relax the same-origin policy in browsers, which is in place to prevent AJAX requests to hosts other\n" +
+ "than the one showing in the browser address bar. Being too permissive, CORS can potentially allow an attacker to gain access to sensitive\n" +
+ "information.</p>\n" +
+ "<p>This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.</p>\n" +
+ "<h2>Ask Yourself Whether</h2>\n" +
+ "<ul>\n" +
+ " <li> Any URLs responding with <code>Access-Control-Allow-Origin: *</code> include sensitive content. </li>\n" +
+ " <li> Any domains specified in <code>Access-Control-Allow-Origin</code> headers are checked against a whitelist. </li>\n" +
+ "</ul>\n" +
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ "<ul>\n" +
+ " <li> The <code>Access-Control-Allow-Origin</code> header should be set only on specific URLs that require access from other domains. Don't enable\n" +
+ " the header on the entire domain. </li>\n" +
+ " <li> Don't rely on the <code>Origin</code> header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" +
+ " the <code>Origin</code> domain (including protocol) is allowed before returning it back in the <code>Access-Control-Allow-Origin</code> header.\n" +
+ " </li>\n" +
+ " <li> Use <code>Access-Control-Allow-Origin: *</code> only if your application absolutely requires it, for example in the case of an open/public API.\n" +
+ " For such endpoints, make sure that there is no sensitive content or information included in the response. </li>\n" +
+ "</ul>\n" +
+ "<h2>Sensitive Code Example</h2>\n" +
+ "<pre>\n" +
+ "// === Java Servlet ===\n" +
+ "@Override\n" +
+ "protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n" +
+ " resp.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n" +
+ " resp.setHeader(\"Access-Control-Allow-Origin\", \"http://localhost:8080\"); // Questionable\n" +
+ " resp.setHeader(\"Access-Control-Allow-Credentials\", \"true\"); // Questionable\n" +
+ " resp.setHeader(\"Access-Control-Allow-Methods\", \"GET\"); // Questionable\n" +
+ " resp.getWriter().write(\"response\");\n" +
+ "}\n" +
+ "</pre>\n" +
+ "<pre>\n" +
+ "// === Spring MVC Controller annotation ===\n" +
+ "@CrossOrigin(origins = \"http://domain1.com\") // Questionable\n" +
+ "@RequestMapping(\"\")\n" +
+ "public class TestController {\n" +
+ " public String home(ModelMap model) {\n" +
+ " model.addAttribute(\"message\", \"ok \");\n" +
+ " return \"view\";\n" +
+ " }\n" +
+ "\n" +
+ " @CrossOrigin(origins = \"http://domain2.com\") // Questionable\n" +
+ " @RequestMapping(value = \"/test1\")\n" +
+ " public ResponseEntity&lt;String&gt; test1() {\n" +
+ " return ResponseEntity.ok().body(\"ok\");\n" +
+ " }\n" +
+ "}\n" +
+ "</pre>\n" +
+ "<h2>See</h2>\n" +
+ "<ul>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/Top_10-2017_A6-Security_Misconfiguration\">OWASP Top 10 2017 Category A6</a> - Security\n" +
+ " Misconfiguration </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Cross_Origin_Resource_Sharing\">OWASP HTML5 Security Cheat Sheet</a> - Cross\n" +
+ " Origin Resource Sharing </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/CORS_OriginHeaderScrutiny\">OWASP CORS OriginHeaderScrutiny</a> </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/CORS_RequestPreflighScrutiny\">OWASP CORS RequestPreflighScrutiny</a> </li>\n" +
+ " <li> <a href=\"https://cwe.mitre.org/data/definitions/346.html\">MITRE, CWE-346</a> - Origin Validation Error </li>\n" +
+ " <li> <a href=\"https://cwe.mitre.org/data/definitions/942.html\">MITRE, CWE-942</a> - Overly Permissive Cross-domain Whitelist </li>\n" +
+ " <li> <a href=\"https://www.sans.org/top25-software-errors/#cat3\">SANS Top 25</a> - Porous Defenses </li>\n" +
+ "</ul>");
+
+ HotspotRuleDescription result = HotspotRuleDescription.from(dto);
+
+ assertThat(result.getRisk().get()).isEqualTo(
+ "<p>Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:</p>\n" +
+ "<ul>\n" +
+ " <li> <a href=\"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0269\">CVE-2018-0269</a> </li>\n" +
+ " <li> <a href=\"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14460\">CVE-2017-14460</a> </li>\n" +
+ "</ul>\n" +
+ "<p>Applications that enable CORS will effectively relax the same-origin policy in browsers, which is in place to prevent AJAX requests to hosts other\n" +
+ "than the one showing in the browser address bar. Being too permissive, CORS can potentially allow an attacker to gain access to sensitive\n" +
+ "information.</p>\n" +
+ "<p>This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.</p>");
+ assertThat(result.getVulnerable().get()).isEqualTo(
+ "<h2>Ask Yourself Whether</h2>\n" +
+ "<ul>\n" +
+ " <li> Any URLs responding with <code>Access-Control-Allow-Origin: *</code> include sensitive content. </li>\n" +
+ " <li> Any domains specified in <code>Access-Control-Allow-Origin</code> headers are checked against a whitelist. </li>\n" +
+ "</ul>");
+ assertThat(result.getFixIt().get()).isEqualTo(
+ "<h2>Recommended Secure Coding Practices</h2>\n" +
+ "<ul>\n" +
+ " <li> The <code>Access-Control-Allow-Origin</code> header should be set only on specific URLs that require access from other domains. Don't enable\n" +
+ " the header on the entire domain. </li>\n" +
+ " <li> Don't rely on the <code>Origin</code> header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" +
+ " the <code>Origin</code> domain (including protocol) is allowed before returning it back in the <code>Access-Control-Allow-Origin</code> header.\n" +
+ " </li>\n" +
+ " <li> Use <code>Access-Control-Allow-Origin: *</code> only if your application absolutely requires it, for example in the case of an open/public API.\n" +
+ " For such endpoints, make sure that there is no sensitive content or information included in the response. </li>\n" +
+ "</ul>\n" +
+ "<h2>Sensitive Code Example</h2>\n" +
+ "<pre>\n" +
+ "// === Java Servlet ===\n" +
+ "@Override\n" +
+ "protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n" +
+ " resp.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n" +
+ " resp.setHeader(\"Access-Control-Allow-Origin\", \"http://localhost:8080\"); // Questionable\n" +
+ " resp.setHeader(\"Access-Control-Allow-Credentials\", \"true\"); // Questionable\n" +
+ " resp.setHeader(\"Access-Control-Allow-Methods\", \"GET\"); // Questionable\n" +
+ " resp.getWriter().write(\"response\");\n" +
+ "}\n" +
+ "</pre>\n" +
+ "<pre>\n" +
+ "// === Spring MVC Controller annotation ===\n" +
+ "@CrossOrigin(origins = \"http://domain1.com\") // Questionable\n" +
+ "@RequestMapping(\"\")\n" +
+ "public class TestController {\n" +
+ " public String home(ModelMap model) {\n" +
+ " model.addAttribute(\"message\", \"ok \");\n" +
+ " return \"view\";\n" +
+ " }\n" +
+ "\n" +
+ " @CrossOrigin(origins = \"http://domain2.com\") // Questionable\n" +
+ " @RequestMapping(value = \"/test1\")\n" +
+ " public ResponseEntity&lt;String&gt; test1() {\n" +
+ " return ResponseEntity.ok().body(\"ok\");\n" +
+ " }\n" +
+ "}\n" +
+ "</pre>\n" +
+ "<h2>See</h2>\n" +
+ "<ul>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/Top_10-2017_A6-Security_Misconfiguration\">OWASP Top 10 2017 Category A6</a> - Security\n" +
+ " Misconfiguration </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet#Cross_Origin_Resource_Sharing\">OWASP HTML5 Security Cheat Sheet</a> - Cross\n" +
+ " Origin Resource Sharing </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/CORS_OriginHeaderScrutiny\">OWASP CORS OriginHeaderScrutiny</a> </li>\n" +
+ " <li> <a href=\"https://www.owasp.org/index.php/CORS_RequestPreflighScrutiny\">OWASP CORS RequestPreflighScrutiny</a> </li>\n" +
+ " <li> <a href=\"https://cwe.mitre.org/data/definitions/346.html\">MITRE, CWE-346</a> - Origin Validation Error </li>\n" +
+ " <li> <a href=\"https://cwe.mitre.org/data/definitions/942.html\">MITRE, CWE-942</a> - Overly Permissive Cross-domain Whitelist </li>\n" +
+ " <li> <a href=\"https://www.sans.org/top25-software-errors/#cat3\">SANS Top 25</a> - Porous Defenses </li>\n" +
+ "</ul>");
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
index 197753a60a4..a9c839b56df 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
@@ -30,7 +30,7 @@ public class HotspotsWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new HotspotsWsModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 4);
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java
index 2e173802409..d1a1fc45ef9 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java
@@ -97,8 +97,9 @@ public class SearchActionTest {
private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient));
private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+ private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
- private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, defaultOrganizationProvider);
+ private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, responseFormatter);
private WsActionTester actionTester = new WsActionTester(underTest);
@Test
@@ -200,7 +201,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots)
.filter(t -> !t.getKey().equals(hotspotWithoutRule.getKey()))
.map(IssueDto::getKey)
@@ -250,7 +251,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getComponentsList())
.extracting(Component::getKey)
@@ -274,7 +275,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(Hotspots.SearchWsResponse.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getComponentsList())
.extracting(Component::getKey)
@@ -302,7 +303,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(responseProject1.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(Collectors.toList()));
assertThat(responseProject1.getComponentsList())
.extracting(Component::getKey)
@@ -312,7 +313,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(responseProject2.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new));
assertThat(responseProject2.getComponentsList())
.extracting(Component::getKey)
@@ -339,7 +340,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsOnly(unresolvedHotspot.getKey(), badlyClosedHotspot.getKey());
}
@@ -363,7 +364,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList()).hasSize(1);
- Hotspots.Hotspot actual = response.getHotspots(0);
+ SearchWsResponse.Hotspot actual = response.getHotspots(0);
assertThat(actual.getComponent()).isEqualTo(file.getKey());
assertThat(actual.getProject()).isEqualTo(project.getKey());
assertThat(actual.getStatus()).isEqualTo(hotspot.getStatus());
@@ -392,7 +393,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList()).hasSize(1);
- Hotspots.Hotspot actual = response.getHotspots(0);
+ Hotspots.SearchWsResponse.Hotspot actual = response.getHotspots(0);
assertThat(actual.getSecurityCategory()).isEqualTo(expected.getKey());
assertThat(actual.getVulnerabilityProbability()).isEqualTo(expected.getVulnerability().name());
}
@@ -434,7 +435,7 @@ public class SearchActionTest {
assertThat(response.getHotspotsList())
.hasSize(1);
- Hotspots.Hotspot actual = response.getHotspots(0);
+ SearchWsResponse.Hotspot actual = response.getHotspots(0);
assertThat(actual.hasStatus()).isFalse();
// FIXME resolution field will be added later
// assertThat(actual.hasResolution()).isFalse();
@@ -463,7 +464,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey());
assertThat(response.getComponentsList()).hasSize(3);
assertThat(response.getComponentsList())
@@ -523,7 +524,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsExactly(expectedHotspotKeys);
}
@@ -556,7 +557,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsExactly(expectedHotspotKeys);
}
@@ -579,7 +580,7 @@ public class SearchActionTest {
SearchWsResponse response = request.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsExactly(hotspots.stream().limit(100).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getPaging().getTotal()).isEqualTo(hotspots.size());
assertThat(response.getPaging().getPageIndex()).isEqualTo(1);
@@ -623,7 +624,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsExactly(hotspots.stream().skip(2 * pageSize).limit(pageSize).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getPaging().getTotal()).isEqualTo(hotspots.size());
assertThat(response.getPaging().getPageIndex()).isEqualTo(3);
@@ -635,7 +636,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.containsExactly(hotspots.stream().skip(3 * pageSize).limit(pageSize).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getPaging().getTotal()).isEqualTo(hotspots.size());
assertThat(response.getPaging().getPageIndex()).isEqualTo(4);
@@ -648,7 +649,7 @@ public class SearchActionTest {
.executeProtobuf(SearchWsResponse.class);
assertThat(response.getHotspotsList())
- .extracting(Hotspots.Hotspot::getKey)
+ .extracting(SearchWsResponse.Hotspot::getKey)
.isEmpty();
assertThat(response.getPaging().getTotal()).isEqualTo(hotspots.size());
assertThat(response.getPaging().getPageIndex()).isEqualTo(emptyPage);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java
new file mode 100644
index 00000000000..9f394b96e9e
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java
@@ -0,0 +1,364 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.hotspot.ws;
+
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueTesting;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.issue.TextRangeResponseFormatter;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.security.SecurityStandards;
+import org.sonar.server.security.SecurityStandards.SQCategory;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Hotspots;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+@RunWith(DataProviderRunner.class)
+public class ShowActionTest {
+ private static final Random RANDOM = new Random();
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ @Rule
+ public EsTester es = EsTester.create();
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+ private DbClient dbClient = dbTester.getDbClient();
+ private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
+
+ private TextRangeResponseFormatter commonFormatter = new TextRangeResponseFormatter();
+ private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
+
+ private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, commonFormatter);
+ private WsActionTester actionTester = new WsActionTester(underTest);
+
+ @Test
+ public void ws_is_internal() {
+ assertThat(actionTester.getDef().isInternal()).isTrue();
+ }
+
+ @Test
+ public void fails_with_IAE_if_parameter_hotspot_is_missing() {
+ TestRequest request = actionTester.newRequest();
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("The 'hotspot' parameter is missing");
+ }
+
+ @Test
+ public void fails_with_NotFoundException_if_hotspot_does_not_exist() {
+ String key = randomAlphabetic(12);
+ TestRequest request = actionTester.newRequest()
+ .setParam("hotspot", key);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("Hotspot '%s' does not exist", key);
+ }
+
+ @Test
+ @UseDataProvider("ruleTypesButHotspot")
+ public void fails_with_NotFoundException_if_issue_is_not_a_hotspot(RuleType ruleType) {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(ruleType);
+ IssueDto notAHotspot = dbTester.issues().insertIssue(IssueTesting.newIssue(rule, project, file).setType(ruleType));
+ TestRequest request = newRequest(notAHotspot);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("Hotspot '%s' does not exist", notAHotspot.getKey());
+ }
+
+ @DataProvider
+ public static Object[][] ruleTypesButHotspot() {
+ return Arrays.stream(RuleType.values())
+ .filter(t -> t != SECURITY_HOTSPOT)
+ .map(t -> new Object[] {t})
+ .toArray(Object[][]::new);
+ }
+
+ @Test
+ public void fails_with_ForbiddenException_if_project_is_private_and_not_allowed() {
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+ TestRequest request = newRequest(hotspot);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(ForbiddenException.class)
+ .hasMessage("Insufficient privileges");
+ }
+
+ @Test
+ public void succeeds_on_public_project() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ assertThat(response.getKey()).isEqualTo(hotspot.getKey());
+ }
+
+ @Test
+ public void succeeds_on_private_project_with_permission() {
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ userSessionRule.registerComponents(project);
+ userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ assertThat(response.getKey()).isEqualTo(hotspot.getKey());
+ }
+
+ @Test
+ public void returns_hotspot_component_and_rule() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ assertThat(response.getKey()).isEqualTo(hotspot.getKey());
+ verifyComponent(response.getComponent(), file);
+ verifyComponent(response.getProject(), project);
+ verifyRule(response.getRule(), rule);
+ assertThat(response.hasTextRange()).isFalse();
+ }
+
+ @Test
+ public void returns_no_textrange_when_locations_have_none() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule)
+ .setLocations(DbIssues.Locations.newBuilder().build()));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ assertThat(response.hasTextRange()).isFalse();
+ }
+
+ @Test
+ @UseDataProvider("randomTextRangeValues")
+ public void returns_textRange(int startLine, int endLine, int startOffset, int endOffset) {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule)
+ .setLocations(DbIssues.Locations.newBuilder()
+ .setTextRange(DbCommons.TextRange.newBuilder()
+ .setStartLine(startLine)
+ .setEndLine(endLine)
+ .setStartOffset(startOffset)
+ .setEndOffset(endOffset)
+ .build())
+ .build()));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+ assertThat(response.hasTextRange()).isTrue();
+ Common.TextRange textRange = response.getTextRange();
+ assertThat(textRange.getStartLine()).isEqualTo(startLine);
+ assertThat(textRange.getEndLine()).isEqualTo(endLine);
+ assertThat(textRange.getStartOffset()).isEqualTo(startOffset);
+ assertThat(textRange.getEndOffset()).isEqualTo(endOffset);
+ }
+
+ @DataProvider
+ public static Object[][] randomTextRangeValues() {
+ int startLine = RANDOM.nextInt(200);
+ int endLine = RANDOM.nextInt(200);
+ int startOffset = RANDOM.nextInt(200);
+ int endOffset = RANDOM.nextInt(200);
+ return new Object[][] {
+ {startLine, endLine, startOffset, endOffset}
+ };
+ }
+
+ @Test
+ public void returns_textRange_missing_fields() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule)
+ .setLocations(DbIssues.Locations.newBuilder()
+ .setTextRange(DbCommons.TextRange.newBuilder().build())
+ .build()));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ assertThat(response.hasTextRange()).isTrue();
+ Common.TextRange textRange = response.getTextRange();
+ assertThat(textRange.hasStartLine()).isFalse();
+ assertThat(textRange.hasEndLine()).isFalse();
+ assertThat(textRange.hasStartOffset()).isFalse();
+ assertThat(textRange.hasEndOffset()).isFalse();
+ }
+
+ @Test
+ @UseDataProvider("allSQCategoryAndVulnerabilityProbability")
+ public void returns_securityCategory_and_vulnerabilityProbability_of_rule(Set<String> standards,
+ SQCategory expected) {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT, t -> t.setSecurityStandards(standards));
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule)
+ .setLocations(DbIssues.Locations.newBuilder()
+ .setTextRange(DbCommons.TextRange.newBuilder().build())
+ .build()));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+ Hotspots.Rule wsRule = response.getRule();
+ assertThat(wsRule.getSecurityCategory()).isEqualTo(expected.getKey());
+ assertThat(wsRule.getVulnerabilityProbability()).isEqualTo(expected.getVulnerability().name());
+ }
+
+ @DataProvider
+ public static Object[][] allSQCategoryAndVulnerabilityProbability() {
+ Stream<Object[]> allButOthers = SecurityStandards.CWES_BY_SQ_CATEGORY
+ .entrySet()
+ .stream()
+ .map(t -> new Object[] {
+ t.getValue().stream().map(s -> "cwe:" + s).collect(Collectors.toSet()),
+ t.getKey()
+ });
+ Stream<Object[]> others = Stream.of(
+ new Object[] {Collections.emptySet(), SQCategory.OTHERS},
+ new Object[] {ImmutableSet.of("foo", "bar", "acme"), SQCategory.OTHERS});
+ return Stream.concat(allButOthers, others)
+ .toArray(Object[][]::new);
+ }
+
+ @Test
+ public void returns_project_twice_when_hotspot_on_project() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, project, rule)
+ .setLocations(DbIssues.Locations.newBuilder()
+ .setTextRange(DbCommons.TextRange.newBuilder().build())
+ .build()));
+
+ Hotspots.ShowWsResponse response = newRequest(hotspot)
+ .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+ verifyComponent(response.getProject(), project);
+ verifyComponent(response.getComponent(), project);
+ }
+
+ public void verifyRule(Hotspots.Rule wsRule, RuleDefinitionDto dto) {
+ assertThat(wsRule.getKey()).isEqualTo(dto.getKey().toString());
+ assertThat(wsRule.getName()).isEqualTo(dto.getName());
+ assertThat(wsRule.getSecurityCategory()).isEqualTo(SQCategory.OTHERS.getKey());
+ assertThat(wsRule.getVulnerabilityProbability()).isEqualTo(SQCategory.OTHERS.getVulnerability().name());
+ }
+
+ private static void verifyComponent(Hotspots.Component wsComponent, ComponentDto dto) {
+ assertThat(wsComponent.getKey()).isEqualTo(dto.getKey());
+ if (dto.path() == null) {
+ assertThat(wsComponent.hasPath()).isFalse();
+ } else {
+ assertThat(wsComponent.getPath()).isEqualTo(dto.path());
+ }
+ assertThat(wsComponent.getQualifier()).isEqualTo(dto.qualifier());
+ assertThat(wsComponent.getName()).isEqualTo(dto.name());
+ assertThat(wsComponent.getLongName()).isEqualTo(dto.longName());
+ }
+
+ private static IssueDto newHotspot(ComponentDto project, ComponentDto file, RuleDefinitionDto rule) {
+ return IssueTesting.newIssue(rule, project, file).setType(SECURITY_HOTSPOT);
+ }
+
+ private TestRequest newRequest(IssueDto hotspot) {
+ return actionTester.newRequest()
+ .setParam("hotspot", hotspot.getKey());
+ }
+
+ private RuleDefinitionDto newRule(RuleType ruleType) {
+ return newRule(ruleType, t -> {
+ });
+ }
+
+ private RuleDefinitionDto newRule(RuleType ruleType, Consumer<RuleDefinitionDto> populate) {
+ RuleDefinitionDto ruleDefinition = RuleTesting.newRule()
+ .setType(ruleType);
+ populate.accept(ruleDefinition);
+ dbTester.rules().insert(ruleDefinition);
+ return ruleDefinition;
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
index bb67997b4d0..4e41efcfd6f 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
@@ -30,7 +30,7 @@ public class IssueWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new IssueWsModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 28);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 29);
}
}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
index f4e85478c72..9902e74ee9b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
@@ -37,6 +37,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.es.EsTester;
import org.sonar.server.issue.AvatarResolverImpl;
import org.sonar.server.issue.IssueFieldsSetter;
@@ -105,7 +106,7 @@ public class SearchActionComponentsTest {
private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
private Languages languages = new Languages();
- private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
+ private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl(), new TextRangeResponseFormatter());
private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java
index 0981dccbb84..4cc89cf1b10 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java
@@ -37,6 +37,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.StartupIndexer;
import org.sonar.server.issue.AvatarResolverImpl;
@@ -87,7 +88,7 @@ public class SearchActionFacetsTest {
private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(db.getDbClient(), Clock.systemUTC(), userSession);
private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, db.getDbClient(), new TransitionService(userSession, null));
private Languages languages = new Languages();
- private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
+ private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl(), new TextRangeResponseFormatter());
private WsActionTester ws = new WsActionTester(
new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
index daa6ab4e4c2..3d1b890861b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
@@ -51,6 +51,7 @@ import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleTesting;
import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.StartupIndexer;
@@ -120,7 +121,7 @@ public class SearchActionTest {
private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
private Languages languages = new Languages();
- private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
+ private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl(), new TextRangeResponseFormatter());
private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
new MapSettings().asConfig(), System2.INSTANCE, dbClient));
private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
index 9443753ccdb..8d71785e450 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
@@ -37,6 +37,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.TextRangeResponseFormatter;
import org.sonar.server.es.EsTester;
import org.sonar.server.issue.AvatarResolverImpl;
import org.sonar.server.issue.IssueFieldsSetter;
@@ -77,7 +78,7 @@ public class SearchActionTestOnSonarCloud {
private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
private Languages languages = new Languages();
- private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
+ private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl(), new TextRangeResponseFormatter());
private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
private SearchAction underTest = new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto
index e6650472d04..cef59629a8e 100644
--- a/sonar-ws/src/main/protobuf/ws-hotspots.proto
+++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto
@@ -31,23 +31,42 @@ message SearchWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated Hotspot hotspots = 2;
repeated Component components = 3;
+
+ message Hotspot {
+ optional string key = 1;
+ optional string component = 2;
+ optional string project = 3;
+ optional string securityCategory = 4;
+ optional string vulnerabilityProbability = 5;
+ optional string status = 6;
+ // FIXME resolution field will be added later
+ // optional string resolution = 7;
+ optional int32 line = 8;
+ optional string message = 9;
+ optional string assignee = 10;
+ optional string author = 11;
+ optional string creationDate = 12;
+ optional string updateDate = 13;
+ }
}
-message Hotspot {
+// Response of GET api/hotspots/show
+message ShowWsResponse {
optional string key = 1;
- optional string component = 2;
- optional string project = 3;
- optional string securityCategory = 4;
- optional string vulnerabilityProbability = 5;
- optional string status = 6;
+ optional Component component = 2;
+ optional Component project = 3;
+ optional Rule rule = 4;
+ optional string status = 5;
// FIXME resolution field will be added later
-// optional string resolution = 7;
- optional int32 line = 8;
- optional string message = 9;
- optional string assignee = 10;
- optional string author = 11;
- optional string creationDate = 12;
- optional string updateDate = 13;
+// optional string resolution = 6;
+ optional int32 line = 7;
+ optional string message = 8;
+ optional string assignee = 9;
+ // SCM login of the committer who introduced the issue
+ optional string author = 10;
+ optional string creationDate = 11;
+ optional string updateDate = 12;
+ optional sonarqube.ws.commons.TextRange textRange = 13;
}
message Component {
@@ -58,3 +77,13 @@ message Component {
optional string longName = 5;
optional string path = 6;
}
+
+message Rule {
+ optional string key = 1;
+ optional string name = 2;
+ optional string securityCategory = 3;
+ optional string vulnerabilityProbability = 4;
+ optional string riskDescription = 5;
+ optional string vulnerabilityDescription = 6;
+ optional string fixRecommendations = 7;
+}