]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12717 a rule must map to a single SQ Security Category
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 29 Nov 2019 14:30:26 +0000 (15:30 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:25 +0000 (20:46 +0100)
if it's not the case, only one is taken into account
a WARN log is displayed at startup to indicate rules wich do not comply

server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java
server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndexer.java
server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java
server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java
server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java

index 1a9eec23d0f4dfb409e17c3057018b0016316f54..8680eb6a2f49928b7931fe11f7dfaf5b8933dc99 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
 import org.sonar.server.es.BaseDoc;
 import org.sonar.server.permission.index.AuthorizationDoc;
+import org.sonar.server.security.SecurityStandards;
 
 import static org.sonar.server.issue.index.IssueIndexDefinition.TYPE_ISSUE;
 
@@ -315,12 +316,13 @@ public class IssueDoc extends BaseDoc {
   }
 
   @CheckForNull
-  public Collection<String> getSonarSourceSecurityCategories() {
-    return getNullableField(IssueIndexDefinition.FIELD_ISSUE_SONARSOURCE_SECURITY);
+  public SecurityStandards.SQCategory getSonarSourceSecurityCategory() {
+    String key = getNullableField(IssueIndexDefinition.FIELD_ISSUE_SONARSOURCE_SECURITY);
+    return SecurityStandards.SQCategory.fromKey(key).orElse(null);
   }
 
-  public IssueDoc setSonarSourceSecurityCategories(@Nullable Collection<String> c) {
-    setField(IssueIndexDefinition.FIELD_ISSUE_SONARSOURCE_SECURITY, c);
+  public IssueDoc setSonarSourceSecurityCategory(@Nullable SecurityStandards.SQCategory c) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_SONARSOURCE_SECURITY, c == null ? null : c.getKey());
     return this;
   }
 }
index 84670cf12cf03f5a196ab1122c084428a39cd936..0095f0c050d1766d0b44185cdb8608572fcc4e2d 100644 (file)
@@ -39,10 +39,8 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.ResultSetIterator;
 import org.sonar.server.security.SecurityStandards;
-import org.sonar.server.security.SecurityStandards.SQCategory;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.stream.Collectors.toList;
 import static org.sonar.api.utils.DateUtils.longToDate;
 import static org.sonar.db.DatabaseUtils.getLong;
 import static org.sonar.db.rule.RuleDefinitionDto.deserializeSecurityStandardsString;
@@ -235,7 +233,7 @@ class IssueIteratorForSingleChunk implements IssueIterator {
       doc.setOwaspTop10(securityStandards.getOwaspTop10());
       doc.setCwe(securityStandards.getCwe());
       doc.setSansTop25(securityStandards.getSansTop25());
-      doc.setSonarSourceSecurityCategories(securityStandards.getSq().stream().map(SQCategory::getKey).collect(toList()));
+      doc.setSonarSourceSecurityCategory(securityStandards.getSqCategory());
       return doc;
     }
 
index cf5102367f2b89fa480bbca361af8625ef9b9ce0..cc9630bb5422cb0d3dd31772d4669faeeffc8d3b 100644 (file)
@@ -38,7 +38,6 @@ import org.sonar.server.es.BaseDoc;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.security.SecurityStandards.SQCategory;
 
-import static java.util.stream.Collectors.toList;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE;
 
 
@@ -188,12 +187,13 @@ public class RuleDoc extends BaseDoc {
   }
 
   @CheckForNull
-  public Collection<String> getSonarSourceSecurityCategories() {
-    return getNullableField(RuleIndexDefinition.FIELD_RULE_SONARSOURCE_SECURITY);
+  public SQCategory getSonarSourceSecurityCategory() {
+    String key = getNullableField(RuleIndexDefinition.FIELD_RULE_SONARSOURCE_SECURITY);
+    return SQCategory.fromKey(key).orElse(null);
   }
 
-  public RuleDoc setSonarSourceSecurityCategories(@Nullable Collection<String> c) {
-    setField(RuleIndexDefinition.FIELD_RULE_SONARSOURCE_SECURITY, c);
+  public RuleDoc setSonarSourceSecurityCategory(@Nullable SQCategory sqCategory) {
+    setField(RuleIndexDefinition.FIELD_RULE_SONARSOURCE_SECURITY, sqCategory == null ? null : sqCategory.getKey());
     return this;
   }
 
@@ -269,8 +269,7 @@ public class RuleDoc extends BaseDoc {
     return ReflectionToStringBuilder.toString(this);
   }
 
-  public static RuleDoc of(RuleForIndexingDto dto) {
-    SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(dto.getSecurityStandards());
+  public static RuleDoc of(RuleForIndexingDto dto, SecurityStandards securityStandards) {
     RuleDoc ruleDoc = new RuleDoc()
       .setId(dto.getId())
       .setKey(dto.getRuleKey().toString())
@@ -282,7 +281,7 @@ public class RuleDoc extends BaseDoc {
       .setCwe(securityStandards.getCwe())
       .setOwaspTop10(securityStandards.getOwaspTop10())
       .setSansTop25(securityStandards.getSansTop25())
-      .setSonarSourceSecurityCategories(securityStandards.getSq().stream().map(SQCategory::getKey).collect(toList()))
+      .setSonarSourceSecurityCategory(securityStandards.getSqCategory())
       .setName(dto.getName())
       .setRuleKey(dto.getPluginRuleKey())
       .setSeverity(dto.getSeverityAsString())
index 52f25eadd9520c3f35e8f3d8677d1b0b3693655f..d5ff6345cb610b996145fb8f39908d661eea51f3 100644 (file)
@@ -25,12 +25,15 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
+import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.es.RuleExtensionId;
 import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.rule.RuleForIndexingDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
@@ -39,16 +42,19 @@ 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.security.SecurityStandards;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Stream.concat;
 import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE_EXTENSION;
+import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDERING;
 
 public class RuleIndexer implements ResilientIndexer {
-
   private final EsClient esClient;
   private final DbClient dbClient;
 
@@ -71,7 +77,7 @@ public class RuleIndexer implements ResilientIndexer {
       // index all definitions and system extensions
       if (uninitializedIndexTypes.contains(TYPE_RULE)) {
         dbClient.ruleDao().scrollIndexingRules(dbSession, dto -> {
-          bulk.add(RuleDoc.of(dto).toIndexRequest());
+          bulk.add(ruleDocOf(dto).toIndexRequest());
           bulk.add(RuleExtensionDoc.of(dto).toIndexRequest());
         });
       }
@@ -142,7 +148,7 @@ public class RuleIndexer implements ResilientIndexer {
 
     dbClient.ruleDao().scrollIndexingRulesByKeys(dbSession, ruleIds,
       r -> {
-        bulkIndexer.add(RuleDoc.of(r).toIndexRequest());
+        bulkIndexer.add(ruleDocOf(r).toIndexRequest());
         bulkIndexer.add(RuleExtensionDoc.of(r).toIndexRequest());
         ruleIds.remove(r.getId());
       });
@@ -186,6 +192,21 @@ public class RuleIndexer implements ResilientIndexer {
     return Optional.of(bulkIndexer.stop());
   }
 
+  private RuleDoc ruleDocOf(RuleForIndexingDto dto) {
+    SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(dto.getSecurityStandards());
+    if (!securityStandards.getIgnoredSQCategories().isEmpty()) {
+      Loggers.get(RuleIndexer.class).warn(
+        "Rule {} with CWEs '{}' maps to multiple SQ Security Categories: {}",
+        dto.getRuleKey(),
+        String.join(", ", securityStandards.getCwe()),
+        concat(Stream.of(securityStandards.getSqCategory()), securityStandards.getIgnoredSQCategories().stream())
+          .map(SecurityStandards.SQCategory::getKey)
+          .sorted(SQ_CATEGORY_KEYS_ORDERING)
+          .collect(joining(", ")));
+    }
+    return RuleDoc.of(dto, securityStandards);
+  }
+
   private BulkIndexer createBulkIndexer(Size bulkSize, IndexingListener listener) {
     return new BulkIndexer(esClient, TYPE_RULE, bulkSize, listener);
   }
index b99b5ec541cb5a7e97d41a0467fc6cab25e57675..267a48bb2e7bb40130dfd1442c1c4065d61c25f0 100644 (file)
@@ -22,18 +22,24 @@ package org.sonar.server.security;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
-import org.sonar.core.util.stream.MoreCollectors;
 
 import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
 import static java.util.Collections.singleton;
+import static java.util.Collections.singletonList;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
 import static org.sonar.server.security.SecurityStandards.VulnerabilityProbability.HIGH;
 import static org.sonar.server.security.SecurityStandards.VulnerabilityProbability.LOW;
 import static org.sonar.server.security.SecurityStandards.VulnerabilityProbability.MEDIUM;
@@ -85,6 +91,7 @@ public final class SecurityStandards {
     FILE_MANIPULATION("file-manipulation", LOW),
     OTHERS("others", LOW);
 
+    private static final Map<String, SQCategory> SQ_CATEGORY_BY_KEY = stream(values()).collect(uniqueIndex(SQCategory::getKey));
     private final String key;
     private final VulnerabilityProbability vulnerability;
 
@@ -100,6 +107,10 @@ public final class SecurityStandards {
     public VulnerabilityProbability getVulnerability() {
       return vulnerability;
     }
+
+    public static Optional<SQCategory> fromKey(@Nullable String key) {
+      return Optional.ofNullable(key).map(SQ_CATEGORY_BY_KEY::get);
+    }
   }
 
   public static final Map<SQCategory, Set<String>> CWES_BY_SQ_CATEGORY = ImmutableMap.<SQCategory, Set<String>>builder()
@@ -123,21 +134,23 @@ public final class SecurityStandards {
     .put(SQCategory.INSECURE_CONF, ImmutableSet.of("102", "215", "311", "315", "346", "614", "489", "942"))
     .put(SQCategory.FILE_MANIPULATION, ImmutableSet.of("97", "73"))
     .build();
-  public static final Ordering<SQCategory> SQ_CATEGORY_ORDERING = Ordering.explicit(Arrays.stream(SQCategory.values()).collect(Collectors.toList()));
-  public static final Ordering<String> SQ_CATEGORY_KEYS_ORDERING = Ordering.explicit(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList()));
+  private static final Ordering<SQCategory> SQ_CATEGORY_ORDERING = Ordering.explicit(stream(SQCategory.values()).collect(Collectors.toList()));
+  public static final Ordering<String> SQ_CATEGORY_KEYS_ORDERING = Ordering.explicit(stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList()));
 
   private final Set<String> standards;
   private final Set<String> cwe;
   private final Set<String> owaspTop10;
   private final Set<String> sansTop25;
-  private final Set<SQCategory> sq;
+  private final SQCategory sqCategory;
+  private final Set<SQCategory> ignoredSQCategories;
 
-  private SecurityStandards(Set<String> standards, Set<String> cwe, Set<String> owaspTop10, Set<String> sansTop25, Set<SQCategory> sq) {
+  private SecurityStandards(Set<String> standards, Set<String> cwe, Set<String> owaspTop10, Set<String> sansTop25, SQCategory sqCategory, Set<SQCategory> ignoredSQCategories) {
     this.standards = standards;
     this.cwe = cwe;
     this.owaspTop10 = owaspTop10;
     this.sansTop25 = sansTop25;
-    this.sq = sq;
+    this.sqCategory = sqCategory;
+    this.ignoredSQCategories = ignoredSQCategories;
   }
 
   public Set<String> getStandards() {
@@ -156,33 +169,42 @@ public final class SecurityStandards {
     return sansTop25;
   }
 
-  public Set<SQCategory> getSq() {
-    return sq;
+  public SQCategory getSqCategory() {
+    return sqCategory;
+  }
+
+  public Set<SQCategory> getIgnoredSQCategories() {
+    return ignoredSQCategories;
   }
 
+  /**
+   * @throws IllegalStateException if {@code securityStandards} maps to multiple {@link SQCategory SQCategories}
+   */
   public static SecurityStandards fromSecurityStandards(Set<String> securityStandards) {
     Set<String> standards = securityStandards.stream()
       .filter(Objects::nonNull)
-      .collect(MoreCollectors.toSet());
-    Set<String> owaspTop10 = toOwaspTop10(standards);
+      .collect(toSet());
     Set<String> cwe = toCwe(standards);
+    Set<String> owaspTop10 = toOwaspTop10(standards);
     Set<String> sansTop25 = toSansTop25(cwe);
-    Set<SQCategory> sq = toSQCategories(cwe);
-    return new SecurityStandards(standards, cwe, owaspTop10, sansTop25, sq);
+    List<SQCategory> sq = toSortedSQCategories(cwe);
+    SQCategory sqCategory = sq.iterator().next();
+    Set<SQCategory> ignoredSQCategories = sq.stream().skip(1).collect(Collectors.toSet());
+    return new SecurityStandards(standards, cwe, owaspTop10, sansTop25, sqCategory, ignoredSQCategories);
   }
 
   private static Set<String> toOwaspTop10(Set<String> securityStandards) {
     return securityStandards.stream()
       .filter(s -> s.startsWith(OWASP_TOP10_PREFIX))
       .map(s -> s.substring(OWASP_TOP10_PREFIX.length()))
-      .collect(MoreCollectors.toSet());
+      .collect(toSet());
   }
 
   private static Set<String> toCwe(Collection<String> securityStandards) {
     Set<String> result = securityStandards.stream()
       .filter(s -> s.startsWith(CWE_PREFIX))
       .map(s -> s.substring(CWE_PREFIX.length()))
-      .collect(MoreCollectors.toSet());
+      .collect(toSet());
     return result.isEmpty() ? singleton(UNKNOWN_STANDARD) : result;
   }
 
@@ -191,15 +213,16 @@ public final class SecurityStandards {
       .keySet()
       .stream()
       .filter(k -> cwe.stream().anyMatch(CWES_BY_SANS_TOP_25.get(k)::contains))
-      .collect(MoreCollectors.toSet());
+      .collect(toSet());
   }
 
-  private static Set<SQCategory> toSQCategories(Collection<String> cwe) {
-    Set<SQCategory> result = CWES_BY_SQ_CATEGORY
+  private static List<SQCategory> toSortedSQCategories(Collection<String> cwe) {
+    List<SQCategory> result = CWES_BY_SQ_CATEGORY
       .keySet()
       .stream()
       .filter(k -> cwe.stream().anyMatch(CWES_BY_SQ_CATEGORY.get(k)::contains))
-      .collect(MoreCollectors.toSet());
-    return result.isEmpty() ? singleton(SQCategory.OTHERS) : result;
+      .sorted(SQ_CATEGORY_ORDERING)
+      .collect(toList());
+    return result.isEmpty() ? singletonList(SQCategory.OTHERS) : result;
   }
 }
index 066e8162610b9889d43e7dfcd225b28f6681beb8..abaefc7c63a79926d4e9383ac03232d21aebbf4e 100644 (file)
@@ -139,7 +139,7 @@ public class IssueIndexerTest {
     assertThat(doc.getCwe()).containsExactlyInAnyOrder(SecurityStandards.UNKNOWN_STANDARD);
     assertThat(doc.getOwaspTop10()).isEmpty();
     assertThat(doc.getSansTop25()).isEmpty();
-    assertThat(doc.getSonarSourceSecurityCategories()).containsOnly(SQCategory.OTHERS.getKey());
+    assertThat(doc.getSonarSourceSecurityCategory()).isEqualTo(SQCategory.OTHERS);
   }
 
   @Test
index 17721d0634d31015aa0668890c53013e741a79f9..beae75e6e245bd940b53d35ad52c033127c94076 100644 (file)
 package org.sonar.server.rule.index;
 
 import com.google.common.collect.ImmutableSet;
-import java.util.stream.Collectors;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.EnumSet;
+import java.util.Random;
+import java.util.Set;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
@@ -37,22 +46,31 @@ import org.sonar.db.rule.RuleDto.Scope;
 import org.sonar.db.rule.RuleMetadataDto;
 import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.security.SecurityStandards;
+import org.sonar.server.security.SecurityStandards.SQCategory;
 
 import static com.google.common.collect.Sets.newHashSet;
+import static java.lang.String.format;
 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.assertj.core.api.Assertions.assertThat;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE_EXTENSION;
+import static org.sonar.server.security.SecurityStandards.CWES_BY_SQ_CATEGORY;
+import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDERING;
 
+@RunWith(DataProviderRunner.class)
 public class RuleIndexerTest {
 
   @Rule
   public EsTester es = EsTester.create();
-
   @Rule
   public DbTester dbTester = DbTester.create();
+  @Rule
+  public LogTester logTester = new LogTester();
 
   private DbClient dbClient = dbTester.getDbClient();
   private final RuleIndexer underTest = new RuleIndexer(es.client(), dbClient);
@@ -122,7 +140,7 @@ public class RuleIndexerTest {
         .get()
         .getHits()
         .getHits()[0]
-        .getId()).isEqualTo(doc.getId());
+          .getId()).isEqualTo(doc.getId());
   }
 
   @Test
@@ -149,10 +167,48 @@ public class RuleIndexerTest {
 
   @Test
   public void index_long_rule_description() {
-    String description = IntStream.range(0, 100000).map(i -> i % 100).mapToObj(Integer::toString).collect(Collectors.joining(" "));
+    String description = IntStream.range(0, 100000).map(i -> i % 100).mapToObj(Integer::toString).collect(joining(" "));
     RuleDefinitionDto rule = dbTester.rules().insert(r -> r.setDescription(description));
     underTest.commitAndIndex(dbTester.getSession(), rule.getId());
 
     assertThat(es.countDocuments(TYPE_RULE)).isEqualTo(1);
   }
+
+  @Test
+  @UseDataProvider("twoDifferentCategoriesButOTHERS")
+  public void log_a_warning_if_hotspot_rule_maps_to_multiple_SQCategories(SQCategory sqCategory1, SQCategory sqCategory2) {
+    Set<String> standards = Stream.of(sqCategory1, sqCategory2)
+      .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));
+    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(
+        "Rule %s with CWEs '%s' maps to multiple SQ Security Categories: %s",
+        rule.getKey(),
+        String.join(", ", securityStandards.getCwe()),
+        ImmutableSet.of(sqCategory1, sqCategory2).stream()
+          .map(SQCategory::getKey)
+          .sorted(SQ_CATEGORY_KEYS_ORDERING)
+          .collect(joining(", "))));
+  }
+
+  @DataProvider
+  public static Object[][] twoDifferentCategoriesButOTHERS() {
+    EnumSet<SQCategory> sqCategories = EnumSet.allOf(SQCategory.class);
+    sqCategories.remove(SQCategory.OTHERS);
+
+    // pick two random categories
+    Random random = new Random();
+    SQCategory sqCategory1 = sqCategories.toArray(new SQCategory[0])[random.nextInt(sqCategories.size())];
+    sqCategories.remove(sqCategory1);
+    SQCategory sqCategory2 = sqCategories.toArray(new SQCategory[0])[random.nextInt(sqCategories.size())];
+    return new Object[][] {
+      {sqCategory1, sqCategory2}
+    };
+  }
 }
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java
new file mode 100644 (file)
index 0000000..930830d
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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.security;
+
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.sonar.server.security.SecurityStandards.SQCategory;
+
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.toSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.security.SecurityStandards.CWES_BY_SQ_CATEGORY;
+import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDERING;
+import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
+
+public class SecurityStandardsTest {
+  @Test
+  public void fromSecurityStandards_from_empty_set_has_SQCategory_OTHERS() {
+    SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+    assertThat(securityStandards.getStandards()).isEmpty();
+    assertThat(securityStandards.getSqCategory()).isEqualTo(SQCategory.OTHERS);
+    assertThat(securityStandards.getIgnoredSQCategories()).isEmpty();
+  }
+
+  @Test
+  public void fromSecurityStandards_from_empty_set_has_unkwown_cwe_standard() {
+    SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+    assertThat(securityStandards.getStandards()).isEmpty();
+    assertThat(securityStandards.getCwe()).containsOnly("unknown");
+  }
+
+  @Test
+  public void fromSecurityStandards_from_empty_set_has_no_OwaspTop10_standard() {
+    SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+    assertThat(securityStandards.getStandards()).isEmpty();
+    assertThat(securityStandards.getOwaspTop10()).isEmpty();
+  }
+
+  @Test
+  public void fromSecurityStandards_from_empty_set_has_no_SansTop25_standard() {
+    SecurityStandards securityStandards = fromSecurityStandards(emptySet());
+
+    assertThat(securityStandards.getStandards()).isEmpty();
+    assertThat(securityStandards.getSansTop25()).isEmpty();
+  }
+
+  @Test
+  public void fromSecurityStandards_finds_SQCategory_from_any_if_the_mapped_CWE_standard() {
+    CWES_BY_SQ_CATEGORY.forEach((sqCategory, cwes) -> {
+      cwes.forEach(cwe -> {
+        SecurityStandards securityStandards = fromSecurityStandards(singleton("cwe:" + cwe));
+
+        assertThat(securityStandards.getSqCategory()).isEqualTo(sqCategory);
+      });
+    });
+  }
+
+  @Test
+  public void fromSecurityStandards_finds_SQCategory_from_multiple_of_the_mapped_CWE_standard() {
+    CWES_BY_SQ_CATEGORY.forEach((sqCategory, cwes) -> {
+      SecurityStandards securityStandards = fromSecurityStandards(cwes.stream().map(t -> "cwe:" + t).collect(toSet()));
+
+      assertThat(securityStandards.getSqCategory()).isEqualTo(sqCategory);
+    });
+  }
+
+  @Test
+  public void fromSecurityStandards_finds_SQCategory_first_in_order_when_CWEs_map_to_multiple_SQCategories() {
+    EnumSet<SQCategory> sqCategories = EnumSet.allOf(SQCategory.class);
+    sqCategories.remove(SQCategory.OTHERS);
+
+    while (!sqCategories.isEmpty()) {
+      SQCategory expected = sqCategories.stream().min(SQ_CATEGORY_KEYS_ORDERING.onResultOf(SQCategory::getKey)).get();
+      SQCategory[] expectedIgnored = sqCategories.stream().filter(t -> t != expected).toArray(SQCategory[]::new);
+
+      Set<String> cwes = sqCategories.stream()
+        .flatMap(t -> CWES_BY_SQ_CATEGORY.get(t).stream().map(e -> "cwe:" + e))
+        .collect(Collectors.toSet());
+      SecurityStandards securityStandards = fromSecurityStandards(cwes);
+
+      assertThat(securityStandards.getSqCategory()).isEqualTo(expected);
+      assertThat(securityStandards.getIgnoredSQCategories()).containsOnly(expectedIgnored);
+
+      sqCategories.remove(expected);
+    }
+  }
+}
index 149197a0eb58f63a993da9624d2d6f1ff897bf5b..f93d5101fb87adb3155abef35723a1343e52f273 100644 (file)
@@ -236,10 +236,7 @@ public class SearchAction implements HotspotsWsAction {
     Hotspots.Rule.Builder ruleBuilder = Hotspots.Rule.newBuilder();
     for (RuleDefinitionDto rule : rules) {
       SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(rule.getSecurityStandards());
-      SecurityStandards.SQCategory sqCategory = securityStandards.getSq()
-        .stream()
-        .min(SecurityStandards.SQ_CATEGORY_ORDERING)
-        .orElse(SecurityStandards.SQCategory.OTHERS);
+      SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory();
       ruleBuilder
         .clear()
         .setKey(rule.getKey().toString())