From: Sébastien Lesaint Date: Wed, 18 Dec 2019 17:04:50 +0000 (+0100) Subject: SONAR-12726 add startup warning when hotspot rule desc can't be parsed X-Git-Tag: 8.2.0.32929~179 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=266881788f7ce7c582cb1021c36133509abb3c87;p=sonarqube.git SONAR-12726 add startup warning when hotspot rule desc can't be parsed --- 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 index 00000000000..f66e1938293 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/HotspotRuleDescription.java @@ -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 = "

Ask Yourself Whether

"; + String fixItTitle = "

Recommended Secure Coding Practices

"; + 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 getRisk() { + return ofNullable(risk); + } + + public Optional getVulnerable() { + return ofNullable(vulnerable); + } + + public Optional 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 + '\'' + + '}'; + } + +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java index d5ff6345cb6..4a4dac17eca 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java @@ -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 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 index 00000000000..511a516b293 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/rule/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.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\" +" + + "

Ask Yourself Whether

\n" + + "bar\n" + + "

Recommended Secure Coding Practices

\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\" +" + + "

" + whiteSpaceBefore + "Ask Yourself Whether" + whiteSpaceAfter + "

\n" + + "bar\n" + + "

" + whiteSpaceBefore + "Recommended Secure Coding Practices\" + whiteSpaceAfter + \"

\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( + "

Ask Yourself Whether

\n" + + "bar\n" + + "

Recommended Secure Coding Practices

\n" + + "foo"); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk()).isEmpty(); + assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nbar"); + assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\nfoo"); + } + + @Test + public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() { + RuleDefinitionDto dto = RuleTesting.newRule().setDescription( + "bar\n" + + "

Recommended Secure Coding Practices

\n" + + "foo"); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk().get()).isEqualTo("bar"); + assertThat(result.getVulnerable()).isEmpty(); + assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\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" + + "

Ask Yourself Whether

\n" + + noContent + + "

Recommended Secure Coding Practices

\n" + + "foo"); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk().get()).isEqualTo("bar"); + assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

"); + assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\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" + + "

Ask Yourself Whether

\n" + + "bar" + + "

Recommended Secure Coding Practices

\n" + + noContent); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk().get()).isEqualTo("bar"); + assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nbar"); + assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

"); + } + + @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" + + "

Ask Yourself Whether

\n" + + "foo"); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk().get()).isEqualTo("bar"); + assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nfoo"); + assertThat(result.getFixIt()).isEmpty(); + } + + @Test + public void parse_returns_regular_description() { + RuleDefinitionDto dto = RuleTesting.newRule().setDescription( + "

Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:

\n" + + "\n" + + "

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.

\n" + + "

This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.

\n" + + "

Ask Yourself Whether

\n" + + "
    \n" + + "
  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • \n" + + "
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
  • \n" + + "
\n" + + "

Recommended Secure Coding Practices

\n" + + "
    \n" + + "
  • The Access-Control-Allow-Origin header should be set only on specific URLs that require access from other domains. Don't enable\n" + + " the header on the entire domain.
  • \n" + + "
  • Don't rely on the Origin header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" + + " the Origin domain (including protocol) is allowed before returning it back in the Access-Control-Allow-Origin header.\n" + + "
  • \n" + + "
  • Use Access-Control-Allow-Origin: * 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.
  • \n" + + "
\n" + + "

Sensitive Code Example

\n" + + "
\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" +
+        "
\n" + + "
\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<String> test1() {\n" +
+        "        return ResponseEntity.ok().body(\"ok\");\n" +
+        "    }\n" +
+        "}\n" +
+        "
\n" + + "

See

\n" + + ""); + + HotspotRuleDescription result = HotspotRuleDescription.from(dto); + + assertThat(result.getRisk().get()).isEqualTo( + "

Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:

\n" + + "\n" + + "

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.

\n" + + "

This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.

"); + assertThat(result.getVulnerable().get()).isEqualTo( + "

Ask Yourself Whether

\n" + + "
    \n" + + "
  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • \n" + + "
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
  • \n" + + "
"); + assertThat(result.getFixIt().get()).isEqualTo( + "

Recommended Secure Coding Practices

\n" + + "
    \n" + + "
  • The Access-Control-Allow-Origin header should be set only on specific URLs that require access from other domains. Don't enable\n" + + " the header on the entire domain.
  • \n" + + "
  • Don't rely on the Origin header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" + + " the Origin domain (including protocol) is allowed before returning it back in the Access-Control-Allow-Origin header.\n" + + "
  • \n" + + "
  • Use Access-Control-Allow-Origin: * 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.
  • \n" + + "
\n" + + "

Sensitive Code Example

\n" + + "
\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" +
+        "
\n" + + "
\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<String> test1() {\n" +
+        "        return ResponseEntity.ok().body(\"ok\");\n" +
+        "    }\n" +
+        "}\n" +
+        "
\n" + + "

See

\n" + + ""); + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java index beae75e6e24..5bba89e4db3 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java @@ -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" + + "

Ask Yourself Whether

\n" + + "bar\n" + + "

Recommended Secure Coding Practices

\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" + + "

Ask Yourself Whether

\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("

Ask Yourself Whether

\n" + + "bar\n" + + "

Recommended Secure Coding Practices

\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" + + "

Recommended Secure Coding Practices

\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 index 205bc723dbc..00000000000 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotRuleDescription.java +++ /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 = "

Ask Yourself Whether

"; - String fixItTitle = "

Recommended Secure Coding Practices

"; - 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 getRisk() { - return ofNullable(risk); - } - - public Optional getVulnerable() { - return ofNullable(vulnerable); - } - - public Optional 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/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 21ab159e185..2bf315ca165 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 @@ -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 index 1a35b069c1b..00000000000 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotRuleDescriptionTest.java +++ /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\" +" + - "

Ask Yourself Whether

\n" + - "bar\n" + - "

Recommended Secure Coding Practices

\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\" +" + - "

" + whiteSpaceBefore + "Ask Yourself Whether" + whiteSpaceAfter + "

\n" + - "bar\n" + - "

" + whiteSpaceBefore + "Recommended Secure Coding Practices\" + whiteSpaceAfter + \"

\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( - "

Ask Yourself Whether

\n" + - "bar\n" + - "

Recommended Secure Coding Practices

\n" + - "foo"); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk()).isEmpty(); - assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nbar"); - assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\nfoo"); - } - - @Test - public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() { - RuleDefinitionDto dto = RuleTesting.newRule().setDescription( - "bar\n" + - "

Recommended Secure Coding Practices

\n" + - "foo"); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk().get()).isEqualTo("bar"); - assertThat(result.getVulnerable()).isEmpty(); - assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\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" + - "

Ask Yourself Whether

\n" + - noContent + - "

Recommended Secure Coding Practices

\n" + - "foo"); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk().get()).isEqualTo("bar"); - assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

"); - assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

\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" + - "

Ask Yourself Whether

\n" + - "bar" + - "

Recommended Secure Coding Practices

\n" + - noContent); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk().get()).isEqualTo("bar"); - assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nbar"); - assertThat(result.getFixIt().get()).isEqualTo("

Recommended Secure Coding Practices

"); - } - - @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" + - "

Ask Yourself Whether

\n" + - "foo"); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk().get()).isEqualTo("bar"); - assertThat(result.getVulnerable().get()).isEqualTo("

Ask Yourself Whether

\nfoo"); - assertThat(result.getFixIt()).isEmpty(); - } - - @Test - public void parse_returns_regular_description() { - RuleDefinitionDto dto = RuleTesting.newRule().setDescription( - "

Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:

\n" + - "\n" + - "

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.

\n" + - "

This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.

\n" + - "

Ask Yourself Whether

\n" + - "
    \n" + - "
  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • \n" + - "
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
  • \n" + - "
\n" + - "

Recommended Secure Coding Practices

\n" + - "
    \n" + - "
  • The Access-Control-Allow-Origin header should be set only on specific URLs that require access from other domains. Don't enable\n" + - " the header on the entire domain.
  • \n" + - "
  • Don't rely on the Origin header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" + - " the Origin domain (including protocol) is allowed before returning it back in the Access-Control-Allow-Origin header.\n" + - "
  • \n" + - "
  • Use Access-Control-Allow-Origin: * 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.
  • \n" + - "
\n" + - "

Sensitive Code Example

\n" + - "
\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" +
-        "
\n" + - "
\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<String> test1() {\n" +
-        "        return ResponseEntity.ok().body(\"ok\");\n" +
-        "    }\n" +
-        "}\n" +
-        "
\n" + - "

See

\n" + - ""); - - HotspotRuleDescription result = HotspotRuleDescription.from(dto); - - assertThat(result.getRisk().get()).isEqualTo( - "

Enabling Cross-Origin Resource Sharing (CORS) is security-sensitive. For example, it has led in the past to the following vulnerabilities:

\n" + - "\n" + - "

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.

\n" + - "

This rule flags code that enables CORS or specifies any HTTP response headers associated with CORS. The goal is to guide security code reviews.

"); - assertThat(result.getVulnerable().get()).isEqualTo( - "

Ask Yourself Whether

\n" + - "
    \n" + - "
  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • \n" + - "
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
  • \n" + - "
"); - assertThat(result.getFixIt().get()).isEqualTo( - "

Recommended Secure Coding Practices

\n" + - "
    \n" + - "
  • The Access-Control-Allow-Origin header should be set only on specific URLs that require access from other domains. Don't enable\n" + - " the header on the entire domain.
  • \n" + - "
  • Don't rely on the Origin header blindly without validation as it could be spoofed by an attacker. Use a whitelist to check that\n" + - " the Origin domain (including protocol) is allowed before returning it back in the Access-Control-Allow-Origin header.\n" + - "
  • \n" + - "
  • Use Access-Control-Allow-Origin: * 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.
  • \n" + - "
\n" + - "

Sensitive Code Example

\n" + - "
\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" +
-        "
\n" + - "
\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<String> test1() {\n" +
-        "        return ResponseEntity.ok().body(\"ok\");\n" +
-        "    }\n" +
-        "}\n" +
-        "
\n" + - "

See

\n" + - ""); - } -}