]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12726 add startup warning when hotspot rule desc can't be parsed
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 18 Dec 2019 17:04:50 +0000 (18:04 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:31 +0000 (20:46 +0100)
server/sonar-server-common/src/main/java/org/sonar/server/rule/HotspotRuleDescription.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java
server/sonar-server-common/src/test/java/org/sonar/server/rule/HotspotRuleDescriptionTest.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.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/HotspotRuleDescriptionTest.java [deleted file]

diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/HotspotRuleDescription.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/HotspotRuleDescription.java
new file mode 100644 (file)
index 0000000..f66e193
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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.rule;
+
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleForIndexingDto;
+
+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();
+    return from(description);
+  }
+
+  public static HotspotRuleDescription from(RuleForIndexingDto dto) {
+    return from(dto.getDescription());
+  }
+
+  private static HotspotRuleDescription from(@Nullable String description) {
+    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);
+  }
+
+  public boolean isComplete() {
+    return risk != null && vulnerable != null && fixIt != null;
+  }
+  @Override
+  public String toString() {
+    return "HotspotRuleDescription{" +
+      "risk='" + risk + '\'' +
+      ", vulnerable='" + vulnerable + '\'' +
+      ", fixIt='" + fixIt + '\'' +
+      '}';
+  }
+
+}
index d5ff6345cb610b996145fb8f39908d661eea51f3..4a4dac17eca3cf17560672167a78b3fb14deb497 100644 (file)
@@ -26,6 +26,8 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Stream;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
@@ -42,6 +44,7 @@ import org.sonar.server.es.IndexingListener;
 import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ResilientIndexer;
+import org.sonar.server.rule.HotspotRuleDescription;
 import org.sonar.server.security.SecurityStandards;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -55,6 +58,8 @@ import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE_EXTENSIO
 import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDERING;
 
 public class RuleIndexer implements ResilientIndexer {
+  private static final Logger LOG = Loggers.get(RuleIndexer.class);
+
   private final EsClient esClient;
   private final DbClient dbClient;
 
@@ -195,7 +200,7 @@ public class RuleIndexer implements ResilientIndexer {
   private RuleDoc ruleDocOf(RuleForIndexingDto dto) {
     SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(dto.getSecurityStandards());
     if (!securityStandards.getIgnoredSQCategories().isEmpty()) {
-      Loggers.get(RuleIndexer.class).warn(
+      LOG.warn(
         "Rule {} with CWEs '{}' maps to multiple SQ Security Categories: {}",
         dto.getRuleKey(),
         String.join(", ", securityStandards.getCwe()),
@@ -204,9 +209,23 @@ public class RuleIndexer implements ResilientIndexer {
           .sorted(SQ_CATEGORY_KEYS_ORDERING)
           .collect(joining(", ")));
     }
+    if (dto.getTypeAsRuleType() == RuleType.SECURITY_HOTSPOT) {
+      HotspotRuleDescription ruleDescription = HotspotRuleDescription.from(dto);
+      if (!ruleDescription.isComplete()) {
+        LOG.warn(
+          "Description of Security Hotspot Rule {} can't be fully parsed: What is the risk?={}, Are you vulnerable?={}, How to fix it={}",
+          dto.getRuleKey(),
+          toOkMissing(ruleDescription.getRisk()), toOkMissing(ruleDescription.getVulnerable()),
+          toOkMissing(ruleDescription.getFixIt()));
+      }
+    }
     return RuleDoc.of(dto, securityStandards);
   }
 
+  private static String toOkMissing(Optional<String> field) {
+    return field.map(t -> "ok").orElse("missing");
+  }
+
   private BulkIndexer createBulkIndexer(Size bulkSize, IndexingListener listener) {
     return new BulkIndexer(esClient, TYPE_RULE, bulkSize, listener);
   }
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
new file mode 100644 (file)
index 0000000..511a516
--- /dev/null
@@ -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.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.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>");
+  }
+}
index beae75e6e245bd940b53d35ad52c033127c94076..5bba89e4db357f054853025e19bffbcd0bab8bde 100644 (file)
@@ -28,6 +28,7 @@ import java.util.Random;
 import java.util.Set;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
+import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,6 +56,7 @@ import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toSet;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE;
@@ -65,6 +67,11 @@ import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDER
 @RunWith(DataProviderRunner.class)
 public class RuleIndexerTest {
 
+  public static final String VALID_HOTSPOT_RULE_DESCRIPTION = "acme\n" +
+    "<h2>Ask Yourself Whether</h2>\n" +
+    "bar\n" +
+    "<h2>Recommended Secure Coding Practices</h2>\n" +
+    "foo";
   @Rule
   public EsTester es = EsTester.create();
   @Rule
@@ -181,7 +188,10 @@ public class RuleIndexerTest {
       .flatMap(t -> CWES_BY_SQ_CATEGORY.get(t).stream().map(e -> "cwe:" + e))
       .collect(toSet());
     SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(standards);
-    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule().setType(RuleType.SECURITY_HOTSPOT).setSecurityStandards(standards));
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setSecurityStandards(standards)
+      .setDescription(VALID_HOTSPOT_RULE_DESCRIPTION));
     OrganizationDto organization = dbTester.organizations().insert();
     underTest.commitAndIndex(dbTester.getSession(), rule.getId(), organization);
 
@@ -211,4 +221,82 @@ public class RuleIndexerTest {
       {sqCategory1, sqCategory2}
     };
   }
+
+  @Test
+  @UseDataProvider("nullEmptyOrNoTitleDescription")
+  public void log_a_warning_when_hotspot_rule_description_is_null_or_empty_or_has_none_of_the_key_titles(@Nullable String description) {
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setDescription(description));
+    OrganizationDto organization = dbTester.organizations().insert();
+    underTest.commitAndIndex(dbTester.getSession(), rule.getId(), organization);
+
+    assertThat(logTester.getLogs()).hasSize(1);
+    assertThat(logTester.logs(LoggerLevel.WARN).get(0))
+      .isEqualTo(format(
+        "Description of Security Hotspot Rule %s can't be fully parsed: What is the risk?=missing, Are you vulnerable?=missing, How to fix it=missing",
+        rule.getKey()));
+  }
+
+  @DataProvider
+  public static Object[][] nullEmptyOrNoTitleDescription() {
+    return new Object[][] {
+      {null},
+      {""},
+      {"   "},
+      {randomAlphabetic(30)}
+    };
+  }
+
+  @Test
+  public void log_a_warning_when_hotspot_rule_description_is_missing_fixIt_tab_content() {
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setDescription("bar\n" +
+        "<h2>Ask Yourself Whether</h2>\n" +
+        "foo"));
+    OrganizationDto organization = dbTester.organizations().insert();
+    underTest.commitAndIndex(dbTester.getSession(), rule.getId(), organization);
+
+    assertThat(logTester.getLogs()).hasSize(1);
+    assertThat(logTester.logs(LoggerLevel.WARN).get(0))
+      .isEqualTo(format(
+        "Description of Security Hotspot Rule %s can't be fully parsed: What is the risk?=ok, Are you vulnerable?=ok, How to fix it=missing",
+        rule.getKey()));
+  }
+
+  @Test
+  public void log_a_warning_when_hotspot_rule_description_is_missing_risk_tab_content() {
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setDescription("<h2>Ask Yourself Whether</h2>\n" +
+        "bar\n" +
+        "<h2>Recommended Secure Coding Practices</h2>\n" +
+        "foo"));
+    OrganizationDto organization = dbTester.organizations().insert();
+    underTest.commitAndIndex(dbTester.getSession(), rule.getId(), organization);
+
+    assertThat(logTester.getLogs()).hasSize(1);
+    assertThat(logTester.logs(LoggerLevel.WARN).get(0))
+      .isEqualTo(format(
+        "Description of Security Hotspot Rule %s can't be fully parsed: What is the risk?=missing, Are you vulnerable?=ok, How to fix it=ok",
+        rule.getKey()));
+  }
+
+  @Test
+  public void log_a_warning_when_hotspot_rule_description_is_missing_vulnerable_tab_content() {
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRule()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setDescription("bar\n" +
+        "<h2>Recommended Secure Coding Practices</h2>\n" +
+        "foo"));
+    OrganizationDto organization = dbTester.organizations().insert();
+    underTest.commitAndIndex(dbTester.getSession(), rule.getId(), organization);
+
+    assertThat(logTester.getLogs()).hasSize(1);
+    assertThat(logTester.logs(LoggerLevel.WARN).get(0))
+      .isEqualTo(format(
+        "Description of Security Hotspot Rule %s can't be fully parsed: What is the risk?=ok, Are you vulnerable?=missing, How to fix it=ok",
+        rule.getKey()));
+  }
 }
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
deleted file mode 100644 (file)
index 205bc72..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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 + '\'' +
-      '}';
-  }
-
-}
index 21ab159e1859f10e6f324e07cae0730b3cf2c57d..2bf315ca1655289b8a804e2dc32757e143de4a47 100644 (file)
@@ -43,6 +43,7 @@ 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;
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
deleted file mode 100644 (file)
index 1a35b06..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * 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>");
-  }
-}