]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16397 update hotspot API to handle advanced rule descriptions
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Mon, 9 May 2022 10:10:19 +0000 (12:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 May 2022 20:02:59 +0000 (20:02 +0000)
server/sonar-server-common/src/test/java/org/sonar/server/rule/HotspotRuleDescriptionTest.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java
sonar-ws/src/main/protobuf/ws-hotspots.proto

diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/rule/HotspotRuleDescriptionTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/rule/HotspotRuleDescriptionTest.java
deleted file mode 100644 (file)
index 07f9045..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * 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.server.rule;
-
-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.RuleDto;
-import org.sonar.db.rule.RuleTesting;
-
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-
-@RunWith(DataProviderRunner.class)
-public class HotspotRuleDescriptionTest {
-
-  @Test
-  public void parse_returns_all_empty_fields_when_no_description() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection();
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).isEmpty();
-    assertThat(result.getVulnerable()).isEmpty();
-    assertThat(result.getFixIt()).isEmpty();
-  }
-
-  @Test
-  public void parse_returns_all_empty_fields_when_empty_description() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", ""));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).isEmpty();
-    assertThat(result.getVulnerable()).isEmpty();
-    assertThat(result.getFixIt()).isEmpty();
-  }
-
-  @Test
-  @UseDataProvider("descriptionsWithoutTitles")
-  public void parse_to_risk_description_fields_when_desc_contains_no_section(String description) {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", description));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(description);
-    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() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
-      createDefaultRuleDescriptionSection("uuid", (ASKATRISK + RECOMMENTEDCODINGPRACTICE)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).isEmpty();
-    assertThat(result.getVulnerable()).contains(ASKATRISK);
-    assertThat(result.getFixIt()).contains(RECOMMENTEDCODINGPRACTICE);
-  }
-
-  @Test
-  public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection()
-      .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + RECOMMENTEDCODINGPRACTICE)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(DESCRIPTION);
-    assertThat(result.getVulnerable()).isEmpty();
-    assertThat(result.getFixIt()).contains(RECOMMENTEDCODINGPRACTICE);
-  }
-
-  @Test
-  public void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection()
-      .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + ASKATRISK)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(DESCRIPTION);
-    assertThat(result.getVulnerable()).contains(ASKATRISK);
-    assertThat(result.getFixIt()).isEmpty();
-  }
-
-  @Test
-  public void parse_with_noncompliant_section_not_removed() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
-      createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + NONCOMPLIANTCODE + COMPLIANTCODE)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(DESCRIPTION);
-    assertThat(result.getVulnerable()).contains(NONCOMPLIANTCODE);
-    assertThat(result.getFixIt()).contains(COMPLIANTCODE);
-  }
-
-  @Test
-  public void parse_moved_noncompliant_code() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
-      createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + RECOMMENTEDCODINGPRACTICE + NONCOMPLIANTCODE + SEE)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(DESCRIPTION);
-
-    assertThat(result.getVulnerable()).contains(NONCOMPLIANTCODE);
-    assertThat(result.getFixIt()).contains(RECOMMENTEDCODINGPRACTICE + SEE);
-
-  }
-
-  @Test
-  public void parse_moved_sensitivecode_code() {
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
-      createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + ASKATRISK + RECOMMENTEDCODINGPRACTICE + SENSITIVECODE + SEE)));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk()).contains(DESCRIPTION);
-    assertThat(result.getVulnerable()).contains(ASKATRISK + SENSITIVECODE);
-    assertThat(result.getFixIt()).contains(RECOMMENTEDCODINGPRACTICE + SEE);
-  }
-
-  @Test
-  public void parse_custom_rule_description() {
-    String ruleDescription = "This is the custom rule description";
-    String exceptionsContent = "This the exceptions section content";
-    String askContent = "This is the ask section content";
-    String recommendedContent = "This is the recommended section content";
-
-    RuleDto dto = RuleTesting.newRuleWithoutDescriptionSection()
-      .setTemplateUuid("123")
-      .setDescriptionFormat(RuleDto.Format.MARKDOWN)
-      .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(
-        "uuid", ruleDescription + "\n"
-          + "== Exceptions" + "\n"
-          + exceptionsContent + "\n"
-          + "== Ask Yourself Whether" + "\n"
-          + askContent + "\n"
-          + "== Recommended Secure Coding Practices" + "\n"
-          + recommendedContent + "\n"
-      ));
-
-    HotspotRuleDescription result = HotspotRuleDescription.from(dto);
-
-    assertThat(result.getRisk().get()).hasToString(
-      ruleDescription + "<br/>"
-      + "<h2>Exceptions</h2>"
-      + exceptionsContent + "<br/>"
-    );
-    assertThat(result.getVulnerable().get()).hasToString(
-        "<h2>Ask Yourself Whether</h2>"
-        + askContent + "<br/>"
-    );
-    assertThat(result.getFixIt().get()).hasToString(
-      "<h2>Recommended Secure Coding Practices</h2>"
-        + recommendedContent + "<br/>"
-    );
-  }
-
-  /*
-   * Bunch of static constant to create rule description.
-   */
-  private static final String DESCRIPTION = "<p>The use of operators pairs ( <code>=+</code>, <code>=-</code> or <code>=!</code> ) where the reversed, single operator was meant (<code>+=</code>,\n"
-    +
-    "<code>-=</code> or <code>!=</code>) will compile and run, but not produce the expected results.</p>\n" +
-    "<p>This rule raises an issue when <code>=+</code>, <code>=-</code>, or <code>=!</code> is used without any spacing between the two operators and when\n" +
-    "there is at least one whitespace character after.</p>\n";
-  private static final String NONCOMPLIANTCODE = "<h2>Noncompliant Code Example</h2>\n" +
-    "<pre>Integer target = -5;\n" +
-    "Integer num = 3;\n" +
-    "\n" +
-    "target =- num;  // Noncompliant; target = -3. Is that really what's meant?\n" +
-    "target =+ num; // Noncompliant; target = 3\n" +
-    "</pre>\n";
-
-  private static final String COMPLIANTCODE = "<h2>Compliant Solution</h2>\n" +
-    "<pre>Integer target = -5;\n" +
-    "Integer num = 3;\n" +
-    "\n" +
-    "target = -num;  // Compliant; intent to assign inverse value of num is clear\n" +
-    "target += num;\n" +
-    "</pre>\n";
-
-  private static final String SEE = "<h2>See</h2>\n" +
-    "<ul>\n" +
-    "  <li> <a href=\"https://cwe.mitre.org/data/definitions/352.html\">MITRE, CWE-352</a> - Cross-Site Request Forgery (CSRF) </li>\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/Cross-Site_Request_Forgery_%28CSRF%29\">OWASP: Cross-Site Request Forgery</a> </li>\n" +
-    "  <li> <a href=\"https://www.sans.org/top25-software-errors/#cat1\">SANS Top 25</a> - Insecure Interaction Between Components </li>\n" +
-    "  <li> Derived from FindSecBugs rule <a href=\"https://find-sec-bugs.github.io/bugs.htm#SPRING_CSRF_PROTECTION_DISABLED\">SPRING_CSRF_PROTECTION_DISABLED</a> </li>\n" +
-    "  <li> <a href=\"https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#when-to-use-csrf-protection\">Spring Security\n" +
-    "  Official Documentation: When to use CSRF protection</a> </li>\n" +
-    "</ul>\n";
-
-  private static final String RECOMMENTEDCODINGPRACTICE = "<h2>Recommended Secure Coding Practices</h2>\n" +
-    "<ul>\n" +
-    "  <li> activate Spring Security's CSRF protection. </li>\n" +
-    "</ul>\n";
-
-  private static final String ASKATRISK = "<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";
-
-  private static final String SENSITIVECODE = "<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";
-}
index d9ea20f6388993b3f12038ef770f19bb8028e2d9..0658ce71649a7ce8a5a8a5478e86785f62a46571 100644 (file)
@@ -26,11 +26,11 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
@@ -42,15 +42,16 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.db.protobuf.DbIssues.Locations;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.markdown.Markdown;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.IssueChangeWSSupport;
 import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext;
 import org.sonar.server.issue.IssueChangeWSSupport.Load;
 import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.ws.UserResponseFormatter;
-import org.sonar.server.rule.HotspotRuleDescription;
 import org.sonar.server.security.SecurityStandards;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Hotspots;
@@ -64,8 +65,13 @@ import static com.google.common.collect.Sets.difference;
 import static java.lang.String.format;
 import static java.util.Collections.singleton;
 import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toMap;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class ShowAction implements HotspotsWsAction {
@@ -95,7 +101,9 @@ public class ShowAction implements HotspotsWsAction {
       .createAction("show")
       .setHandler(this)
       .setDescription("Provides the details of a Security Hotspot.")
-      .setSince("8.1");
+      .setSince("8.1")
+      .setChangelog(new Change("9.5", "The fields rule.riskDescription, rule.fixRecommendations, rule.vulnerabilityDescription of the response are deprecated."
+      + " /api/rules/show endpoint should be used to fetch rule descriptions."));
 
     action.createParam(PARAM_HOTSPOT_KEY)
       .setDescription("Key of the Security Hotspot")
@@ -173,14 +181,27 @@ public class ShowAction implements HotspotsWsAction {
       .setSecurityCategory(sqCategory.getKey())
       .setVulnerabilityProbability(sqCategory.getVulnerability().name());
 
-    HotspotRuleDescription hotspotRuleDescription = HotspotRuleDescription.from(ruleDto);
-    hotspotRuleDescription.getVulnerable().ifPresent(ruleBuilder::setVulnerabilityDescription);
-    hotspotRuleDescription.getRisk().ifPresent(ruleBuilder::setRiskDescription);
-    hotspotRuleDescription.getFixIt().ifPresent(ruleBuilder::setFixRecommendations);
-
+    Map<String, String> sectionKeyToContent = getSectionKeyToContent(ruleDto);
+    Optional.ofNullable(sectionKeyToContent.get(DEFAULT_KEY)).ifPresent(ruleBuilder::setVulnerabilityDescription);
+    Optional.ofNullable(sectionKeyToContent.get(ROOT_CAUSE_SECTION_KEY)).ifPresent(ruleBuilder::setVulnerabilityDescription);
+    Optional.ofNullable(sectionKeyToContent.get(ASSESS_THE_PROBLEM_SECTION_KEY)).ifPresent(ruleBuilder::setRiskDescription);
+    Optional.ofNullable(sectionKeyToContent.get(HOW_TO_FIX_SECTION_KEY)).ifPresent(ruleBuilder::setFixRecommendations);
     responseBuilder.setRule(ruleBuilder.build());
   }
 
+  private static Map<String, String> getSectionKeyToContent(RuleDto ruleDefinitionDto) {
+    return ruleDefinitionDto.getRuleDescriptionSectionDtos().stream()
+      .collect(toMap(RuleDescriptionSectionDto::getKey,
+        section -> getContentAndConvertToHtmlIfNecessary(ruleDefinitionDto.getDescriptionFormat(), section)));
+  }
+
+  private static String getContentAndConvertToHtmlIfNecessary(@Nullable RuleDto.Format descriptionFormat, RuleDescriptionSectionDto section) {
+    if (RuleDto.Format.MARKDOWN.equals(descriptionFormat)) {
+      return Markdown.convertToHtml(section.getContent());
+    }
+    return section.getContent();
+  }
+
   private void formatTextRange(ShowWsResponse.Builder hotspotBuilder, IssueDto hotspot) {
     textRangeFormatter.formatTextRange(hotspot, hotspotBuilder::setTextRange);
   }
@@ -221,7 +242,7 @@ public class ShowAction implements HotspotsWsAction {
     Map<String, ComponentDto> componentsByUuids = dbClient.componentDao().selectSubProjectsByComponentUuids(dbSession,
       componentUuids)
       .stream()
-      .collect(Collectors.toMap(ComponentDto::uuid, Function.identity(), (componentDto, componentDto2) -> componentDto2));
+      .collect(toMap(ComponentDto::uuid, Function.identity(), (componentDto, componentDto2) -> componentDto2));
 
     Set<String> componentUuidsToLoad = copyOf(difference(componentUuids, componentsByUuids.keySet()));
     if (!componentUuidsToLoad.isEmpty()) {
index ae7b290965acfdec48bbbe1447ed07663b105e94..7e4146efb2cdc080ed675d0cad0b9295dd1db890 100644 (file)
@@ -55,6 +55,7 @@ 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.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.rule.RuleTesting;
 import org.sonar.db.user.UserDto;
@@ -91,8 +92,14 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.INTRODUCTION_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
-import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
+import static org.sonar.db.rule.RuleDto.Format.HTML;
 import static org.sonar.db.rule.RuleDto.Format.MARKDOWN;
 
 @RunWith(DataProviderRunner.class)
@@ -427,6 +434,101 @@ public class ShowActionTest {
     };
   }
 
+
+  @Test
+  public void dispatch_description_sections_of_advanced_rule_in_relevant_field() {
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    userSessionRule.registerComponents(project);
+    userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+
+    RuleDescriptionSectionDto introductionSection = generateSectionWithKey(INTRODUCTION_SECTION_KEY);
+    RuleDescriptionSectionDto rootCauseSection = generateSectionWithKey(ROOT_CAUSE_SECTION_KEY);
+    RuleDescriptionSectionDto assesTheProblemSection = generateSectionWithKey(ASSESS_THE_PROBLEM_SECTION_KEY);
+    RuleDescriptionSectionDto resourcesSection = generateSectionWithKey(RESOURCES_SECTION_KEY);
+    RuleDescriptionSectionDto howToFixSection = generateSectionWithKey(HOW_TO_FIX_SECTION_KEY);
+    RuleDescriptionSectionDto dummySection = generateSectionWithKey("dummySection");
+
+    RuleDto rule = newRuleWithoutSection(SECURITY_HOTSPOT,
+      r -> r.addRuleDescriptionSectionDto(introductionSection)
+        .addRuleDescriptionSectionDto(rootCauseSection)
+        .addRuleDescriptionSectionDto(assesTheProblemSection)
+        .addRuleDescriptionSectionDto(resourcesSection)
+        .addRuleDescriptionSectionDto(howToFixSection)
+        .addRuleDescriptionSectionDto(dummySection)
+        .setDescriptionFormat(HTML));
+
+    IssueDto hotspot = dbTester.issues().insertHotspot(rule, project, file);
+    mockChangelogAndCommentsFormattingContext();
+
+    Hotspots.ShowWsResponse response = newRequest(hotspot)
+      .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+    assertThat(response.getRule().getVulnerabilityDescription()).isEqualTo(rootCauseSection.getContent());
+    assertThat(response.getRule().getRiskDescription()).isEqualTo(assesTheProblemSection.getContent());
+    assertThat(response.getRule().getFixRecommendations()).isEqualTo(howToFixSection.getContent());
+  }
+
+  @Test
+  public void fallbacks_to_default_section_in_case_of_legacy_rule() {
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    userSessionRule.registerComponents(project);
+    userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+
+    RuleDescriptionSectionDto introductionSection = generateSectionWithKey(DEFAULT_KEY);
+
+    RuleDto rule = newRuleWithoutSection(SECURITY_HOTSPOT,
+      r -> r.addRuleDescriptionSectionDto(introductionSection)
+       .setDescriptionFormat(HTML));
+
+    IssueDto hotspot = dbTester.issues().insertHotspot(rule, project, file);
+    mockChangelogAndCommentsFormattingContext();
+
+    Hotspots.ShowWsResponse response = newRequest(hotspot)
+      .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+    assertThat(response.getRule().getVulnerabilityDescription()).isEqualTo(introductionSection.getContent());
+    assertThat(response.getRule().getRiskDescription()).isEmpty();
+    assertThat(response.getRule().getFixRecommendations()).isEmpty();
+  }
+
+  @Test
+  public void ignore_default_section_if_root_cause_provided() {
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    userSessionRule.registerComponents(project);
+    userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+
+    RuleDescriptionSectionDto introductionSection = generateSectionWithKey(INTRODUCTION_SECTION_KEY);
+    RuleDescriptionSectionDto rootCauseSection = generateSectionWithKey(ROOT_CAUSE_SECTION_KEY);
+    RuleDescriptionSectionDto assesTheProblemSection = generateSectionWithKey(ASSESS_THE_PROBLEM_SECTION_KEY);
+
+    RuleDto rule = newRuleWithoutSection(SECURITY_HOTSPOT,
+      r -> r.addRuleDescriptionSectionDto(introductionSection)
+        .addRuleDescriptionSectionDto(rootCauseSection)
+        .addRuleDescriptionSectionDto(assesTheProblemSection)
+        .setDescriptionFormat(HTML));
+
+    IssueDto hotspot = dbTester.issues().insertHotspot(rule, project, file);
+    mockChangelogAndCommentsFormattingContext();
+
+    Hotspots.ShowWsResponse response = newRequest(hotspot)
+      .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+    assertThat(response.getRule().getVulnerabilityDescription()).isEqualTo(rootCauseSection.getContent());
+    assertThat(response.getRule().getRiskDescription()).isEqualTo(assesTheProblemSection.getContent());
+    assertThat(response.getRule().getFixRecommendations()).isEmpty();
+  }
+
+  private RuleDescriptionSectionDto generateSectionWithKey(String assessTheProblemSectionKey) {
+    return RuleDescriptionSectionDto.builder()
+      .uuid(uuidFactory.create())
+      .key(assessTheProblemSectionKey)
+      .content(randomAlphabetic(200))
+      .build();
+  }
+
   @Test
   public void returns_html_description_for_custom_rules() {
     ComponentDto project = dbTester.components().insertPrivateProject();
@@ -436,9 +538,15 @@ public class ShowActionTest {
 
     String description = "== Title\n<div>line1\nline2</div>";
 
+    RuleDescriptionSectionDto sectionDto = RuleDescriptionSectionDto.builder()
+      .uuid(uuidFactory.create())
+      .key(ASSESS_THE_PROBLEM_SECTION_KEY)
+      .content(description)
+      .build();
+
     RuleDto rule = newRuleWithoutSection(SECURITY_HOTSPOT,
       r -> r.setTemplateUuid("123")
-        .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), description))
+        .addRuleDescriptionSectionDto(sectionDto)
         .setDescriptionFormat(MARKDOWN));
 
     IssueDto hotspot = dbTester.issues().insertHotspot(rule, project, file);
@@ -450,6 +558,7 @@ public class ShowActionTest {
     assertThat(response.getRule().getRiskDescription()).isEqualTo("<h2>Title</h2>&lt;div&gt;line1<br/>line2&lt;/div&gt;");
   }
 
+
   @Test
   public void handles_null_description_for_custom_rules() {
     ComponentDto project = dbTester.components().insertPrivateProject();
index 4560baa7dcc1c02ebf81477fcbf29a4a82f253ad..41b89d19269b432fb4ba5e3c274824495951a3c0 100644 (file)
@@ -90,7 +90,7 @@ message Rule {
   optional string name = 2;
   optional string securityCategory = 3;
   optional string vulnerabilityProbability = 4;
-  optional string riskDescription = 5;
-  optional string vulnerabilityDescription = 6;
-  optional string fixRecommendations = 7;
+  optional string riskDescription = 5 [deprecated=true];
+  optional string vulnerabilityDescription = 6 [deprecated=true];
+  optional string fixRecommendations = 7 [deprecated=true];
 }