From: Sébastien Lesaint Date: Fri, 29 Nov 2019 14:30:26 +0000 (+0100) Subject: SONAR-12717 a rule must map to a single SQ Security Category X-Git-Tag: 8.2.0.32929~216 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=f816ceb0a331f0d166857218c9422512eb15ae9d;p=sonarqube.git SONAR-12717 a rule must map to a single SQ Security Category 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 --- diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java index 1a9eec23d0f..8680eb6a2f4 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java @@ -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 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 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; } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java index 84670cf12cf..0095f0c050d 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java @@ -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; } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java index cf5102367f2..cc9630bb542 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleDoc.java @@ -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 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 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()) 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 52f25eadd95..d5ff6345cb6 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 @@ -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); } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java index b99b5ec541c..267a48bb2e7 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityStandards.java @@ -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 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 fromKey(@Nullable String key) { + return Optional.ofNullable(key).map(SQ_CATEGORY_BY_KEY::get); + } } public static final Map> CWES_BY_SQ_CATEGORY = ImmutableMap.>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 SQ_CATEGORY_ORDERING = Ordering.explicit(Arrays.stream(SQCategory.values()).collect(Collectors.toList())); - public static final Ordering SQ_CATEGORY_KEYS_ORDERING = Ordering.explicit(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList())); + private static final Ordering SQ_CATEGORY_ORDERING = Ordering.explicit(stream(SQCategory.values()).collect(Collectors.toList())); + public static final Ordering SQ_CATEGORY_KEYS_ORDERING = Ordering.explicit(stream(SQCategory.values()).map(SQCategory::getKey).collect(Collectors.toList())); private final Set standards; private final Set cwe; private final Set owaspTop10; private final Set sansTop25; - private final Set sq; + private final SQCategory sqCategory; + private final Set ignoredSQCategories; - private SecurityStandards(Set standards, Set cwe, Set owaspTop10, Set sansTop25, Set sq) { + private SecurityStandards(Set standards, Set cwe, Set owaspTop10, Set sansTop25, SQCategory sqCategory, Set ignoredSQCategories) { this.standards = standards; this.cwe = cwe; this.owaspTop10 = owaspTop10; this.sansTop25 = sansTop25; - this.sq = sq; + this.sqCategory = sqCategory; + this.ignoredSQCategories = ignoredSQCategories; } public Set getStandards() { @@ -156,33 +169,42 @@ public final class SecurityStandards { return sansTop25; } - public Set getSq() { - return sq; + public SQCategory getSqCategory() { + return sqCategory; + } + + public Set getIgnoredSQCategories() { + return ignoredSQCategories; } + /** + * @throws IllegalStateException if {@code securityStandards} maps to multiple {@link SQCategory SQCategories} + */ public static SecurityStandards fromSecurityStandards(Set securityStandards) { Set standards = securityStandards.stream() .filter(Objects::nonNull) - .collect(MoreCollectors.toSet()); - Set owaspTop10 = toOwaspTop10(standards); + .collect(toSet()); Set cwe = toCwe(standards); + Set owaspTop10 = toOwaspTop10(standards); Set sansTop25 = toSansTop25(cwe); - Set sq = toSQCategories(cwe); - return new SecurityStandards(standards, cwe, owaspTop10, sansTop25, sq); + List sq = toSortedSQCategories(cwe); + SQCategory sqCategory = sq.iterator().next(); + Set ignoredSQCategories = sq.stream().skip(1).collect(Collectors.toSet()); + return new SecurityStandards(standards, cwe, owaspTop10, sansTop25, sqCategory, ignoredSQCategories); } private static Set toOwaspTop10(Set 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 toCwe(Collection securityStandards) { Set 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 toSQCategories(Collection cwe) { - Set result = CWES_BY_SQ_CATEGORY + private static List toSortedSQCategories(Collection cwe) { + List 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; } } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java index 066e8162610..abaefc7c63a 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java @@ -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 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 17721d0634d..beae75e6e24 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 @@ -20,13 +20,22 @@ 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 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 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 index 00000000000..930830d3239 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityStandardsTest.java @@ -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 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 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); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java index 149197a0eb5..f93d5101fb8 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java @@ -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())