From 9cd44988c23e6533cbf34e5acc6d225e706f1707 Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Tue, 19 Jul 2022 18:08:17 +0200 Subject: [PATCH] SONAR-17061 add PCI DSS to security reports show API --- .../sonar/server/issue/index/IssueDoc.java | 28 ++++- .../issue/index/IssueIndexDefinition.java | 4 + .../index/IssueIteratorForSingleChunk.java | 2 + .../server/security/SecurityStandards.java | 33 ++++++ .../security/SecurityStandardsTest.java | 10 ++ .../sonar/server/issue/index/IssueIndex.java | 96 +++++++++++---- .../sonar/server/issue/index/IssueQuery.java | 24 ++++ .../index/IssueIndexSecurityReportsTest.java | 112 ++++++++++++++++++ .../server/issue/index/IssueQueryTest.java | 26 +++- .../ws/client/issue/IssuesWsParameters.java | 3 + sonar-ws/src/main/protobuf/ws-security.proto | 1 + 11 files changed, 307 insertions(+), 32 deletions(-) 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 5ee631b248c..ac194d590ab 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 @@ -276,13 +276,28 @@ public class IssueDoc extends BaseDoc { } @CheckForNull - public Collection getOwaspTop10() { - return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10); + public Collection getPciDss32() { + return getNullableField(IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32); + } + + public IssueDoc setPciDss32(@Nullable Collection o) { + setField(IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32, o); + return this; + } + + public IssueDoc setPciDss40(@Nullable Collection o) { + setField(IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_40, o); + return this; } @CheckForNull - public Collection getOwaspTop10For2021() { - return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021); + public Collection getPciDss40() { + return getNullableField(IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_40); + } + + @CheckForNull + public Collection getOwaspTop10() { + return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10); } public IssueDoc setOwaspTop10(@Nullable Collection o) { @@ -290,6 +305,11 @@ public class IssueDoc extends BaseDoc { return this; } + @CheckForNull + public Collection getOwaspTop10For2021() { + return getNullableField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021); + } + public IssueDoc setOwaspTop10For2021(@Nullable Collection o) { setField(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021, o); return this; diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java index d7dd7fbb541..a0bf4593815 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java @@ -96,6 +96,8 @@ public class IssueIndexDefinition implements IndexDefinition { public static final String FIELD_ISSUE_STATUS = "status"; public static final String FIELD_ISSUE_TAGS = "tags"; public static final String FIELD_ISSUE_TYPE = "type"; + public static final String FIELD_ISSUE_PCI_DSS_32 = "pciDss-3.2"; + public static final String FIELD_ISSUE_PCI_DSS_40 = "pciDss-4.0"; public static final String FIELD_ISSUE_OWASP_TOP_10 = "owaspTop10"; public static final String FIELD_ISSUE_OWASP_TOP_10_2021 = "owaspTop10-2021"; public static final String FIELD_ISSUE_SANS_TOP_25 = "sansTop25"; @@ -164,6 +166,8 @@ public class IssueIndexDefinition implements IndexDefinition { mapping.keywordFieldBuilder(FIELD_ISSUE_STATUS).disableNorms().addSubFields(SORTABLE_ANALYZER).build(); mapping.keywordFieldBuilder(FIELD_ISSUE_TAGS).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_TYPE).disableNorms().build(); + mapping.keywordFieldBuilder(FIELD_ISSUE_PCI_DSS_32).disableNorms().build(); + mapping.keywordFieldBuilder(FIELD_ISSUE_PCI_DSS_40).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_OWASP_TOP_10_2021).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ISSUE_SANS_TOP_25).disableNorms().build(); 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 743f962f618..9586ff3cb55 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 @@ -235,6 +235,8 @@ class IssueIteratorForSingleChunk implements IssueIterator { SecurityStandards.SQCategory sqCategory = securityStandards.getSqCategory(); doc.setOwaspTop10(securityStandards.getOwaspTop10()); doc.setOwaspTop10For2021(securityStandards.getOwaspTop10For2021()); + doc.setPciDss32(securityStandards.getPciDss32()); + doc.setPciDss40(securityStandards.getPciDss40()); doc.setCwe(securityStandards.getCwe()); doc.setSansTop25(securityStandards.getSansTop25()); doc.setSonarSourceSecurityCategory(sqCategory); 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 4569aeecf17..afb13b4271b 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 @@ -37,6 +37,8 @@ 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.api.server.rule.RulesDefinition.PciDssVersion.V3_2; +import static org.sonar.api.server.rule.RulesDefinition.PciDssVersion.V4_0; 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; @@ -54,6 +56,8 @@ public final class SecurityStandards { private static final String OWASP_TOP10_PREFIX = "owaspTop10:"; private static final String OWASP_TOP10_2021_PREFIX = "owaspTop10-2021:"; + private static final String PCI_DSS_32_PREFIX = V3_2.prefix() + ":"; + private static final String PCI_DSS_40_PREFIX = V4_0.prefix() + ":"; private static final String CWE_PREFIX = "cwe:"; // See https://www.sans.org/top25-software-errors private static final Set INSECURE_CWE = new HashSet<>(asList("89", "78", "79", "434", "352", "601")); @@ -159,6 +163,20 @@ public final class SecurityStandards { } } + public enum PciDss { + R1("1"), R2("2"), R3("3"), R4("4"), R5("5"), R6("6"), R7("7"), R8("8"), R9("9"), R10("10"), R11("11"), R12("12"); + + private final String category; + + PciDss(String category) { + this.category = category; + } + + public String category() { + return category; + } + } + public static final Map> CWES_BY_SQ_CATEGORY = ImmutableMap.>builder() .put(SQCategory.BUFFER_OVERFLOW, Set.of("119", "120", "131", "676", "788")) .put(SQCategory.SQL_INJECTION, Set.of("89", "564", "943")) @@ -207,6 +225,14 @@ public final class SecurityStandards { return cwe; } + public Set getPciDss32() { + return toPciDss(standards, PCI_DSS_32_PREFIX); + } + + public Set getPciDss40() { + return toPciDss(standards, PCI_DSS_40_PREFIX); + } + public Set getOwaspTop10() { return toOwaspTop10(standards, OWASP_TOP10_PREFIX); } @@ -250,6 +276,13 @@ public final class SecurityStandards { return new SecurityStandards(standards, cwe, sqCategory, ignoredSQCategories); } + private static Set toPciDss(Set securityStandards, String prefix) { + return securityStandards.stream() + .filter(s -> s.startsWith(prefix)) + .map(s -> s.substring(prefix.length())) + .collect(toSet()); + } + private static Set toOwaspTop10(Set securityStandards, String prefix) { return securityStandards.stream() .filter(s -> s.startsWith(prefix)) 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 index bfcf245dfe5..665d27fa357 100644 --- 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 @@ -19,10 +19,13 @@ */ package org.sonar.server.security; +import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.Test; +import org.sonar.server.security.SecurityStandards.PciDss; import org.sonar.server.security.SecurityStandards.SQCategory; import static java.util.Collections.emptySet; @@ -115,4 +118,11 @@ public class SecurityStandardsTest { sqCategories.remove(expected); } } + + @Test + public void pciDss_categories_check() { + List pciDssCategories = Arrays.stream(PciDss.values()).map(PciDss::category).collect(Collectors.toList()); + + assertThat(pciDssCategories).hasSize(12).containsExactly("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"); + } } diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 459a89763fb..d9c53eed857 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -69,6 +69,7 @@ import org.sonar.api.issue.Issue; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version; +import org.sonar.api.server.rule.RulesDefinition.PciDssVersion; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; import org.sonar.core.util.stream.MoreCollectors; @@ -89,6 +90,7 @@ import org.sonar.server.issue.index.IssueQuery.PeriodStart; import org.sonar.server.permission.index.AuthorizationDoc; import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.security.SecurityStandards; +import org.sonar.server.security.SecurityStandards.PciDss; import org.sonar.server.security.SecurityStandards.SQCategory; import org.sonar.server.user.UserSession; import org.sonar.server.view.index.ViewIndexDefinition; @@ -100,12 +102,12 @@ import static java.util.stream.Collectors.toList; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.existsQuery; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termsQuery; import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; import static org.sonar.api.rules.RuleType.VULNERABILITY; -import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021; import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds; import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; @@ -124,6 +126,8 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.FILES; import static org.sonar.server.issue.index.IssueIndex.Facet.LANGUAGES; import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10; import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_TOP_10_2021; +import static org.sonar.server.issue.index.IssueIndex.Facet.PCI_DSS_32; +import static org.sonar.server.issue.index.IssueIndex.Facet.PCI_DSS_40; import static org.sonar.server.issue.index.IssueIndex.Facet.PROJECT_UUIDS; import static org.sonar.server.issue.index.IssueIndex.Facet.RESOLUTIONS; import static org.sonar.server.issue.index.IssueIndex.Facet.RULES; @@ -153,6 +157,8 @@ import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_MODU import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10_2021; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_32; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PCI_DSS_40; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_RESOLUTION; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_RULE_UUID; @@ -183,6 +189,8 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10_2021; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_32; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PCI_DSS_40; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25; @@ -209,7 +217,7 @@ public class IssueIndex { private static final String AGG_TO_REVIEW_SECURITY_HOTSPOTS = "toReviewSecurityHotspots"; private static final String AGG_IN_REVIEW_SECURITY_HOTSPOTS = "inReviewSecurityHotspots"; private static final String AGG_REVIEWED_SECURITY_HOTSPOTS = "reviewedSecurityHotspots"; - private static final String AGG_CWES = "cwes"; + private static final String AGG_DISTRIBUTION = "distribution"; private static final BoolQueryBuilder NON_RESOLVED_VULNERABILITIES_FILTER = boolQuery() .filter(termQuery(FIELD_ISSUE_TYPE, VULNERABILITY.name())) .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION)); @@ -245,6 +253,8 @@ public class IssueIndex { DIRECTORIES(PARAM_DIRECTORIES, FIELD_ISSUE_DIRECTORY_PATH, STICKY, MAX_FACET_SIZE), ASSIGNEES(PARAM_ASSIGNEES, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, MAX_FACET_SIZE), ASSIGNED_TO_ME(FACET_ASSIGNED_TO_ME, FIELD_ISSUE_ASSIGNEE_UUID, STICKY, 1), + PCI_DSS_32(PARAM_PCI_DSS_32, FIELD_ISSUE_PCI_DSS_32, STICKY, DEFAULT_FACET_SIZE), + PCI_DSS_40(PARAM_PCI_DSS_40, FIELD_ISSUE_PCI_DSS_40, STICKY, DEFAULT_FACET_SIZE), OWASP_TOP_10(PARAM_OWASP_TOP_10, FIELD_ISSUE_OWASP_TOP_10, STICKY, DEFAULT_FACET_SIZE), OWASP_TOP_10_2021(PARAM_OWASP_TOP_10_2021, FIELD_ISSUE_OWASP_TOP_10_2021, STICKY, DEFAULT_FACET_SIZE), SANS_TOP_25(PARAM_SANS_TOP_25, FIELD_ISSUE_SANS_TOP_25, STICKY, DEFAULT_FACET_SIZE), @@ -445,6 +455,8 @@ public class IssueIndex { filters.addFilter(FIELD_ISSUE_STATUS, STATUSES.getFilterScope(), createTermsFilter(FIELD_ISSUE_STATUS, query.statuses())); // security category + addSecurityCategoryFilter(FIELD_ISSUE_PCI_DSS_32, PCI_DSS_32, query.pciDss32(), filters); + addSecurityCategoryFilter(FIELD_ISSUE_PCI_DSS_40, PCI_DSS_40, query.pciDss40(), filters); addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10, OWASP_TOP_10, query.owaspTop10(), filters); addSecurityCategoryFilter(FIELD_ISSUE_OWASP_TOP_10_2021, OWASP_TOP_10_2021, query.owaspTop10For2021(), filters); addSecurityCategoryFilter(FIELD_ISSUE_SANS_TOP_25, SANS_TOP_25, query.sansTop25(), filters); @@ -565,11 +577,11 @@ public class IssueIndex { private static RequestFiltersComputer newFilterComputer(SearchOptions options, AllFilters allFilters) { Collection facetNames = options.getFacets(); Set> facets = Stream.concat( - Stream.of(EFFORT_TOP_AGGREGATION), - facetNames.stream() - .map(FACETS_BY_NAME::get) - .filter(Objects::nonNull) - .map(Facet::getTopAggregationDef)) + Stream.of(EFFORT_TOP_AGGREGATION), + facetNames.stream() + .map(FACETS_BY_NAME::get) + .filter(Objects::nonNull) + .map(Facet::getTopAggregationDef)) .collect(MoreCollectors.toSet(facetNames.size())); return new RequestFiltersComputer(allFilters, facets); @@ -767,11 +779,11 @@ public class IssueIndex { RESOLUTIONS.getName(), RESOLUTIONS.getTopAggregationDef(), RESOLUTIONS.getNumberOfTerms(), NO_EXTRA_FILTER, t -> - // add aggregation of type "missing" to return count of unresolved issues in the facet - t.subAggregation( - addEffortAggregationIfNeeded(query, AggregationBuilders - .missing(RESOLUTIONS.getName() + FACET_SUFFIX_MISSING) - .field(RESOLUTIONS.getFieldName())))); + // add aggregation of type "missing" to return count of unresolved issues in the facet + t.subAggregation( + addEffortAggregationIfNeeded(query, AggregationBuilders + .missing(RESOLUTIONS.getName() + FACET_SUFFIX_MISSING) + .field(RESOLUTIONS.getFieldName())))); esRequest.aggregation(aggregation); } @@ -891,10 +903,10 @@ public class IssueIndex { ASSIGNED_TO_ME.getNumberOfTerms(), NO_EXTRA_FILTER, t -> - // add sub-aggregation to return issue count for current user - aggregationHelper.getSubAggregationHelper() - .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[]{uuid}) - .ifPresent(t::subAggregation)); + // add sub-aggregation to return issue count for current user + aggregationHelper.getSubAggregationHelper() + .buildSelectedItemsAggregation(ASSIGNED_TO_ME.getName(), ASSIGNED_TO_ME.getTopAggregationDef(), new String[] {uuid}) + .ifPresent(t::subAggregation)); esRequest.aggregation(aggregation); } } @@ -1084,31 +1096,56 @@ public class IssueIndex { return search(request, includeCwe, null); } + public List getPciDssReport(String projectUuid, boolean isViewOrApp, PciDssVersion version) { + SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); + Arrays.stream(PciDss.values()) + .forEach(pciDss -> request.aggregation( + newPciDssSecurityReportSubAggregations( + AggregationBuilders.filter(pciDss.category(), boolQuery().filter(prefixQuery(version.prefix(), pciDss.category() + "."))), version))); + return searchPciDss(request, version.label()); + } + public List getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspTop10Version version) { - String queryKey = version.equals(Y2021) ? FIELD_ISSUE_OWASP_TOP_10_2021 : FIELD_ISSUE_OWASP_TOP_10; SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i) .forEach(owaspCategory -> request.aggregation( newSecurityReportSubAggregations( - AggregationBuilders.filter(owaspCategory, boolQuery().filter(termQuery(queryKey, owaspCategory))), + AggregationBuilders.filter(owaspCategory, boolQuery().filter(termQuery(version.prefix(), owaspCategory))), includeCwe, null))); return search(request, includeCwe, version.label()); } - private List search(SearchSourceBuilder sourceBuilder, boolean includeCwe, @Nullable String version) { + private List searchPciDss(SearchSourceBuilder sourceBuilder, String version) { + SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType()) + .source(sourceBuilder); + SearchResponse response = client.search(request); + return response.getAggregations().asList().stream() + .map(c -> processPciDssSecurityReportIssueSearchResults((ParsedFilter) c, version)) + .collect(MoreCollectors.toList()); + } + + private List search(SearchSourceBuilder sourceBuilder, boolean includeDistribution, @Nullable String version) { SearchRequest request = EsClient.prepareSearch(TYPE_ISSUE.getMainType()) .source(sourceBuilder); SearchResponse response = client.search(request); return response.getAggregations().asList().stream() - .map(c -> processSecurityReportIssueSearchResults((ParsedFilter) c, includeCwe, version)) + .map(c -> processSecurityReportIssueSearchResults((ParsedFilter) c, includeDistribution, version)) .collect(MoreCollectors.toList()); } - private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(ParsedFilter categoryBucket, boolean includeCwe, String version) { + private static SecurityStandardCategoryStatistics processPciDssSecurityReportIssueSearchResults(ParsedFilter categoryFilter, String version) { + Stream stream = ((ParsedStringTerms) categoryFilter.getAggregations().get(AGG_DISTRIBUTION)).getBuckets().stream(); + var children = stream.filter(categoryBucket -> StringUtils.startsWith(categoryBucket.getKeyAsString(), categoryFilter.getName() + ".")) + .map(categoryBucket -> processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getKeyAsString(), null, null)).collect(toList()); + + return processSecurityReportCategorySearchResults(categoryFilter, categoryFilter.getName(), children, version); + } + + private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(ParsedFilter categoryBucket, boolean includeDistribution, String version) { List children = new ArrayList<>(); - if (includeCwe) { - Stream stream = ((ParsedStringTerms) categoryBucket.getAggregations().get(AGG_CWES)).getBuckets().stream(); + if (includeDistribution) { + Stream stream = ((ParsedStringTerms) categoryBucket.getAggregations().get(AGG_DISTRIBUTION)).getBuckets().stream(); children = stream.map(cweBucket -> processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null, null)).collect(toList()); } @@ -1138,10 +1175,21 @@ public class IssueIndex { reviewedSecurityHotspots, securityReviewRating, children, version); } + private static AggregationBuilder newPciDssSecurityReportSubAggregations(AggregationBuilder categoriesAggs, PciDssVersion version) { + AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs); + final TermsAggregationBuilder distributionAggregation = AggregationBuilders.terms(AGG_DISTRIBUTION) + .field(version.prefix()) + // 100 should be enough to display all the requirements per category. If not, the UI will be broken anyway + .size(MAX_FACET_SIZE); + categoriesAggs.subAggregation(addSecurityReportIssueCountAggregations(distributionAggregation)); + + return aggregationBuilder; + } + private static AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe, @Nullable Collection cwesInCategory) { AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs); if (includeCwe) { - final TermsAggregationBuilder cwesAgg = AggregationBuilders.terms(AGG_CWES) + final TermsAggregationBuilder cwesAgg = AggregationBuilders.terms(AGG_DISTRIBUTION) .field(FIELD_ISSUE_CWE) // 100 should be enough to display all CWEs. If not, the UI will be broken anyway .size(MAX_FACET_SIZE); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java index fb743517548..77fe8d8ab72 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java @@ -80,6 +80,8 @@ public class IssueQuery { private final Collection tags; private final Collection types; private final Collection owaspTop10; + private final Collection pciDss32; + private final Collection pciDss40; private final Collection owaspTop10For2021; private final Collection sansTop25; private final Collection cwe; @@ -119,6 +121,8 @@ public class IssueQuery { this.languages = defaultCollection(builder.languages); this.tags = defaultCollection(builder.tags); this.types = defaultCollection(builder.types); + this.pciDss32 = defaultCollection(builder.pciDss32); + this.pciDss40 = defaultCollection(builder.pciDss40); this.owaspTop10 = defaultCollection(builder.owaspTop10); this.owaspTop10For2021 = defaultCollection(builder.owaspTop10For2021); this.sansTop25 = defaultCollection(builder.sansTop25); @@ -213,6 +217,14 @@ public class IssueQuery { return types; } + public Collection pciDss32() { + return pciDss32; + } + + public Collection pciDss40() { + return pciDss40; + } + public Collection owaspTop10() { return owaspTop10; } @@ -333,6 +345,8 @@ public class IssueQuery { private Collection languages; private Collection tags; private Collection types; + private Collection pciDss32; + private Collection pciDss40; private Collection owaspTop10; private Collection owaspTop10For2021; private Collection sansTop25; @@ -448,6 +462,16 @@ public class IssueQuery { return this; } + public Builder pciDss32(@Nullable Collection o) { + this.pciDss32 = o; + return this; + } + + public Builder pciDss40(@Nullable Collection o) { + this.pciDss40 = o; + return this; + } + public Builder owaspTop10(@Nullable Collection o) { this.owaspTop10 = o; return this; diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java index bede12e3cdd..82bf992f1b8 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityReportsTest.java @@ -29,6 +29,7 @@ import org.sonar.api.impl.utils.TestSystem2; import org.sonar.api.issue.Issue; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.utils.System2; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; @@ -40,9 +41,11 @@ import org.sonar.server.tester.UserSessionRule; import org.sonar.server.view.index.ViewDoc; import org.sonar.server.view.index.ViewIndexer; +import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.Collections.singletonList; +import static java.util.Comparator.comparing; import static java.util.TimeZone.getTimeZone; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; @@ -206,6 +209,27 @@ public class IssueIndexSecurityReportsTest { .allMatch(category -> category.getChildren().isEmpty()); } + @Test + public void getPciDss32Report_aggregation() { + List pciDss32Report = indexIssuesAndAssertPciDss32Report(); + + assertThat(pciDss32Report) + .isNotEmpty(); + + assertThat(pciDss32Report.get(0).getChildren()).hasSize(2); + assertThat(pciDss32Report.get(1).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(2).getChildren()).hasSize(4); + assertThat(pciDss32Report.get(3).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(4).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(5).getChildren()).hasSize(2); + assertThat(pciDss32Report.get(6).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(7).getChildren()).hasSize(1); + assertThat(pciDss32Report.get(8).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(9).getChildren()).hasSize(1); + assertThat(pciDss32Report.get(10).getChildren()).isEmpty(); + assertThat(pciDss32Report.get(11).getChildren()).isEmpty(); + } + @Test public void getOwaspTop10Report_aggregation_with_cwe() { List owaspTop10Report = indexIssuesAndAssertOwaspReport(true); @@ -286,6 +310,47 @@ public class IssueIndexSecurityReportsTest { return owaspTop10Report; } + private List indexIssuesAndAssertPciDss32Report() { + ComponentDto project = newPrivateProjectDto(); + indexIssues( + newDoc("openvul1", project).setPciDss32(asList("1.2.0", "3.4.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.MAJOR), + newDoc("openvul2", project).setPciDss32(asList("3.3.2", "6.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("openvul3", project).setPciDss32(asList("10.1.2", "6.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("notpcidssvul", project).setPciDss32(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), + newDoc("toreviewhotspot1", project).setPciDss32(asList("1.3.0", "3.3.2")).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_TO_REVIEW), + newDoc("toreviewhotspot2", project).setPciDss32(asList("3.5.6", "6.4.5")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), + newDoc("reviewedHotspot", project).setPciDss32(asList("3.1.1", "8.6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) + .setResolution(Issue.RESOLUTION_FIXED), + newDoc("notpcidsshotspot", project).setPciDss32(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); + + List pciDssReport = underTest.getPciDssReport(project.uuid(), false, RulesDefinition.PciDssVersion.V3_2).stream() + .sorted(comparing(s -> parseInt(s.getCategory()))) + .collect(toList()); + assertThat(pciDssReport) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, + SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating) + .containsExactlyInAnyOrder( + tuple("1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 5), + tuple("2", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* reviewedHotspot */, 4), + tuple("4", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("5", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("6", 2L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 5), + tuple("7", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("8", 0L, OptionalInt.empty(), 0L, 1L /* reviewedHotspot */, 1), + tuple("9", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("10", 1L, OptionalInt.of(2), 0L, 0L, 1), + tuple("11", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("12", 0L, OptionalInt.empty(), 0L, 0L, 1)); + + return pciDssReport; + } + private List indexIssuesAndAssertOwasp2021Report(boolean includeCwe) { ComponentDto project = newPrivateProjectDto(); indexIssues( @@ -395,6 +460,53 @@ public class IssueIndexSecurityReportsTest { assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); } + @Test + public void getPciDssReport_aggregation_on_portfolio() { + ComponentDto portfolio1 = db.components().insertPrivateApplication(); + ComponentDto portfolio2 = db.components().insertPrivateApplication(); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + indexIssues( + newDoc("openvul1", project1).setPciDss32(asList("1.2.0", "3.4.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.MAJOR), + newDoc("openvul2", project2).setPciDss32(asList("3.3.2", "6.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("openvul3", project1).setPciDss32(asList("10.1.2", "6.5")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("notpcidssvul", project1).setPciDss32(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), + newDoc("toreviewhotspot1", project2).setPciDss32(asList("1.3.0", "3.3.2")).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_TO_REVIEW), + newDoc("toreviewhotspot2", project1).setPciDss32(asList("3.5.6", "6.4.5")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), + newDoc("reviewedHotspot", project2).setPciDss32(asList("3.1.1", "8.6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) + .setResolution(Issue.RESOLUTION_FIXED), + newDoc("notpcidsshotspot", project1).setPciDss32(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); + + indexView(portfolio1.uuid(), singletonList(project1.uuid())); + indexView(portfolio2.uuid(), singletonList(project2.uuid())); + + List pciDssReport = underTest.getPciDssReport(portfolio1.uuid(), true, RulesDefinition.PciDssVersion.V3_2).stream() + .sorted(comparing(s -> parseInt(s.getCategory()))) + .collect(toList()); + assertThat(pciDssReport) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, + SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating) + .containsExactlyInAnyOrder( + tuple("1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1), + tuple("2", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("3", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L/* toreviewhotspot2 */, 0L, 5), + tuple("4", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("5", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("6", 1L /* openvul3 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 5), + tuple("7", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("8", 0L, OptionalInt.empty(), 0L, 0L /* reviewedHotspot */, 1), + tuple("9", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("10", 1L /* openvul3 */, OptionalInt.of(2), 0L, 0L, 1), + tuple("11", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("12", 0L, OptionalInt.empty(), 0L, 0L, 1)); + } + @Test public void getCWETop25Report_aggregation() { ComponentDto project = newPrivateProjectDto(); diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java index 907e5b34bac..5215a89d36f 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java @@ -50,8 +50,6 @@ public class IssueQueryTest { .languages(List.of("xoo")) .tags(List.of("tag1", "tag2")) .types(List.of("RELIABILITY", "SECURITY")) - .owaspTop10(List.of("a1", "a2")) - .owaspTop10For2021(List.of("a3", "a4")) .sansTop25(List.of("insecure-interaction", "porous-defenses")) .cwe(List.of("12", "125")) .branchUuid("my_branch") @@ -76,8 +74,6 @@ public class IssueQueryTest { assertThat(query.languages()).containsOnly("xoo"); assertThat(query.tags()).containsOnly("tag1", "tag2"); assertThat(query.types()).containsOnly("RELIABILITY", "SECURITY"); - assertThat(query.owaspTop10()).containsOnly("a1", "a2"); - assertThat(query.owaspTop10For2021()).containsOnly("a3", "a4"); assertThat(query.sansTop25()).containsOnly("insecure-interaction", "porous-defenses"); assertThat(query.cwe()).containsOnly("12", "125"); assertThat(query.branchUuid()).isEqualTo("my_branch"); @@ -94,6 +90,28 @@ public class IssueQueryTest { assertThat(query.asc()).isTrue(); } + @Test + public void build_pci_dss_query() { + IssueQuery query = IssueQuery.builder() + .pciDss32(List.of("1.2.3", "3.2.1")) + .pciDss40(List.of("3.4.5", "5.6")) + .build(); + + assertThat(query.pciDss32()).containsOnly("1.2.3", "3.2.1"); + assertThat(query.pciDss40()).containsOnly("3.4.5", "5.6"); + } + + @Test + public void build_owasp_query() { + IssueQuery query = IssueQuery.builder() + .owaspTop10(List.of("a1", "a2")) + .owaspTop10For2021(List.of("a3", "a4")) + .build(); + + assertThat(query.owaspTop10()).containsOnly("a1", "a2"); + assertThat(query.owaspTop10For2021()).containsOnly("a3", "a4"); + } + @Test public void build_query_without_dates() { IssueQuery query = IssueQuery.builder() diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java index aee04974a89..9fb03b98b95 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java @@ -79,6 +79,9 @@ public class IssuesWsParameters { public static final String PARAM_LANGUAGES = "languages"; public static final String PARAM_TAGS = "tags"; public static final String PARAM_TYPES = "types"; + public static final String PARAM_PCI_DSS = "pciDss"; + public static final String PARAM_PCI_DSS_32 = "pciDss-3.2"; + public static final String PARAM_PCI_DSS_40 = "pciDss-4.0"; public static final String PARAM_OWASP_TOP_10 = "owaspTop10"; public static final String PARAM_OWASP_TOP_10_2021 = "owaspTop10-2021"; @Deprecated diff --git a/sonar-ws/src/main/protobuf/ws-security.proto b/sonar-ws/src/main/protobuf/ws-security.proto index 159e74094b3..da6870b886e 100644 --- a/sonar-ws/src/main/protobuf/ws-security.proto +++ b/sonar-ws/src/main/protobuf/ws-security.proto @@ -55,6 +55,7 @@ message CweStatistics { optional int64 activeRules = 8; optional int64 totalRules = 9; optional bool hasMoreRules = 10; + optional string category = 11; } -- 2.39.5