diff options
Diffstat (limited to 'server/sonar-webserver-es')
7 files changed, 262 insertions, 62 deletions
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 72b3bd272f0..6b2c826f2c9 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 @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -54,6 +55,7 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.HasAggregations; +import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter; @@ -77,12 +79,13 @@ import org.sonar.api.issue.IssueStatus; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.Severity; import org.sonar.api.rules.CleanCodeAttributeCategory; -import org.sonar.core.rule.RuleType; import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version; 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.rule.RuleType; import org.sonar.server.es.EsClient; import org.sonar.server.es.EsUtils; import org.sonar.server.es.SearchOptions; @@ -120,10 +123,10 @@ import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termsQuery; import static org.elasticsearch.search.aggregations.AggregationBuilders.filters; import static org.elasticsearch.search.aggregations.AggregationBuilders.reverseNested; -import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT; -import static org.sonar.core.rule.RuleType.VULNERABILITY; import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_DEFAULT_VALUE; import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_ENABLED; +import static org.sonar.core.rule.RuleType.SECURITY_HOTSPOT; +import static org.sonar.core.rule.RuleType.VULNERABILITY; import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE; import static org.sonar.server.es.searchrequest.TopAggregationDefinition.NON_STICKY; @@ -145,6 +148,7 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.IMPACT_SOFTWARE_QUAL import static org.sonar.server.issue.index.IssueIndex.Facet.ISSUE_STATUSES; import static org.sonar.server.issue.index.IssueIndex.Facet.LANGUAGES; import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_ASVS_40; +import static org.sonar.server.issue.index.IssueIndex.Facet.OWASP_MOBILE_TOP_10_2024; 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; @@ -185,6 +189,7 @@ import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_LINE import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_NEW_STATUS; import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_ASVS_40; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024; 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; @@ -224,6 +229,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SOFT import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_MOBILE_TOP_10_2024; 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; @@ -256,7 +262,6 @@ public class IssueIndex { private static final String ISSUES_WITH_SECURITY_IMPACT = "issues_with_security_impact"; private static final String AGG_IMPACT_SEVERITIES = "impact_severities"; 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_DISTRIBUTION = "distribution"; private static final BoolQueryBuilder NON_RESOLVED_VULNERABILITIES_FILTER = boolQuery() @@ -266,10 +271,6 @@ public class IssueIndex { .filter(nestedQuery(FIELD_ISSUE_IMPACTS, termsQuery(FIELD_ISSUE_IMPACT_SOFTWARE_QUALITY, SoftwareQuality.SECURITY.name()), ScoreMode.Avg)) .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION)); - private static final BoolQueryBuilder IN_REVIEW_HOTSPOTS_FILTER = boolQuery() - .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())) - .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_IN_REVIEW)) - .mustNot(existsQuery(FIELD_ISSUE_RESOLUTION)); private static final BoolQueryBuilder TO_REVIEW_HOTSPOTS_FILTER = boolQuery() .filter(termQuery(FIELD_ISSUE_TYPE, SECURITY_HOTSPOT.name())) .filter(termQuery(FIELD_ISSUE_STATUS, Issue.STATUS_TO_REVIEW)) @@ -313,6 +314,7 @@ public class IssueIndex { 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_ASVS_40(PARAM_OWASP_ASVS_40, FIELD_ISSUE_OWASP_ASVS_40, STICKY, DEFAULT_FACET_SIZE), + OWASP_MOBILE_TOP_10_2024(PARAM_OWASP_MOBILE_TOP_10_2024, FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, 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), STIG_ASD_V5R3(PARAM_STIG_ASD_V5R3, FIELD_ISSUE_STIG_ASD_V5R3, STICKY, DEFAULT_FACET_SIZE), @@ -531,6 +533,7 @@ public class IssueIndex { addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_32, PCI_DSS_32, query.pciDss32(), filters); addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_40, PCI_DSS_40, query.pciDss40(), filters); addOwaspAsvsFilter(FIELD_ISSUE_OWASP_ASVS_40, OWASP_ASVS_40, query, filters); + addSecurityCategoryFilter(FIELD_ISSUE_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, query.owaspMobileTop10For2024(), 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_STIG_ASD_V5R3, STIG_ASD_V5R3, query.stigAsdV5R3(), filters); @@ -543,9 +546,8 @@ public class IssueIndex { addImpactFilters(query, filters); addComponentRelatedFilters(query, filters); addDatesFilter(filters, query); - addCreatedAfterByProjectsFilter(filters, query); + addNewCodeByProjectsFilter(filters, query); addNewCodeReferenceFilter(filters, query); - addNewCodeReferenceFilterByProjectsFilter(filters, query); return filters; } @@ -869,33 +871,23 @@ public class IssueIndex { if (newCodeOnReference != null) { filters.addFilter( FIELD_ISSUE_NEW_CODE_REFERENCE, new SimpleFieldFilterScope(FIELD_ISSUE_NEW_CODE_REFERENCE), - termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true)); - } - } - - private static void addNewCodeReferenceFilterByProjectsFilter(AllFilters allFilters, IssueQuery query) { - Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids(); - BoolQueryBuilder boolQueryBuilder = boolQuery(); - - if (!newCodeOnReferenceByProjectUuids.isEmpty()) { - - newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery() - .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid)) - .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true)))); - - allFilters.addFilter("__is_new_code_reference_by_project_uuids", - new SimpleFieldFilterScope("newCodeReferenceByProjectUuids"), boolQueryBuilder); + termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, newCodeOnReference)); } } - private static void addCreatedAfterByProjectsFilter(AllFilters allFilters, IssueQuery query) { + private static void addNewCodeByProjectsFilter(AllFilters allFilters, IssueQuery query) { Map<String, PeriodStart> createdAfterByProjectUuids = query.createdAfterByProjectUuids(); BoolQueryBuilder boolQueryBuilder = boolQuery(); createdAfterByProjectUuids.forEach((projectOrProjectBranchUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery() .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid)) .filter(rangeQuery(FIELD_ISSUE_FUNC_CREATED_AT).from(createdAfterDate.date().getTime(), createdAfterDate.inclusive())))); - allFilters.addFilter("__created_after_by_project_uuids", new SimpleFieldFilterScope("createdAfterByProjectUuids"), boolQueryBuilder); + Collection<String> newCodeOnReferenceByProjectUuids = query.newCodeOnReferenceByProjectUuids(); + newCodeOnReferenceByProjectUuids.forEach(projectOrProjectBranchUuid -> boolQueryBuilder.should(boolQuery() + .filter(termQuery(FIELD_ISSUE_BRANCH_UUID, projectOrProjectBranchUuid)) + .filter(termQuery(FIELD_ISSUE_NEW_CODE_REFERENCE, true)))); + + allFilters.addFilter("__new_code_by_project_uuids", new SimpleFieldFilterScope("newCodeByProjectUuids"), boolQueryBuilder); } private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) { @@ -925,6 +917,7 @@ public class IssueIndex { addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_32, PCI_DSS_32, options, aggregationHelper, esRequest, query.pciDss32().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_40, PCI_DSS_40, options, aggregationHelper, esRequest, query.pciDss40().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_OWASP_ASVS_40, OWASP_ASVS_40, options, aggregationHelper, esRequest, query.owaspAsvs40().toArray()); + addSecurityCategoryFacetIfNeeded(PARAM_OWASP_MOBILE_TOP_10_2024, OWASP_MOBILE_TOP_10_2024, options, aggregationHelper, esRequest, query.owaspMobileTop10For2024().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10, OWASP_TOP_10, options, aggregationHelper, esRequest, query.owaspTop10().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_OWASP_TOP_10_2021, OWASP_TOP_10_2021, options, aggregationHelper, esRequest, query.owaspTop10For2021().toArray()); addSecurityCategoryFacetIfNeeded(PARAM_STIG_ASD_V5R3, STIG_ASD_V5R3, options, aggregationHelper, esRequest, query.stigAsdV5R3().toArray()); @@ -1301,7 +1294,7 @@ public class IssueIndex { } private static SecurityStandardCategoryStatistics emptyCweStatistics(String rule) { - return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null); + return new SecurityStandardCategoryStatistics(rule, 0, OptionalInt.of(1), 0, 0, 1, null, null, Map.of()); } public List<SecurityStandardCategoryStatistics> getSonarSourceReport(String projectUuid, boolean isViewOrApp, boolean includeCwe) { @@ -1347,6 +1340,17 @@ public class IssueIndex { return searchWithLevelDistribution(request, version.label(), Integer.toString(level)); } + public List<SecurityStandardCategoryStatistics> getOwaspMobileTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspMobileTop10Version version) { + SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); + IntStream.rangeClosed(1, 10).mapToObj(i -> "m" + i) + .forEach(owaspMobileCategory -> request.aggregation( + newSecurityReportSubAggregations( + AggregationBuilders.filter(owaspMobileCategory, boolQuery().filter(termQuery(version.prefix(), owaspMobileCategory))), + includeCwe, + null))); + return search(request, includeCwe, version.label()); + } + public List<SecurityStandardCategoryStatistics> getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe, OwaspTop10Version version) { SearchSourceBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i) @@ -1446,10 +1450,11 @@ public class IssueIndex { Aggregation severitiesAggregations = ((ParsedFilter) categoryBucket.getAggregations().get(AGG_VULNERABILITIES)).getAggregations().get(AGG_SEVERITIES); - CountAndRating countAndRating = getCountAndRating(severitiesAggregations); - long vulnerabilities = countAndRating.getCount(); + SeverityAggregationDetails severityAggregationDetails = getSeverityDetails(severitiesAggregations); + long vulnerabilities = severityAggregationDetails.getCount(); // Worst severity having at least one issue - OptionalInt severityRating = countAndRating.getRating(); + OptionalInt severityRating = severityAggregationDetails.getRating(); + Map<String, Long> severityDistribution = severityAggregationDetails.getDistribution(); long toReviewSecurityHotspots = ((ParsedValueCount) ((ParsedFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) .getValue(); @@ -1460,32 +1465,39 @@ public class IssueIndex { Integer securityReviewRating = computeRating(percent.orElse(null)).getIndex(); return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots, - reviewedSecurityHotspots, securityReviewRating, children, version); + reviewedSecurityHotspots, securityReviewRating, children, version, severityDistribution); } - private CountAndRating getCountAndRating(Aggregation severitiesAggregations) { + private SeverityAggregationDetails getSeverityDetails(Aggregation severitiesAggregations) { + List<? extends Terms.Bucket> severityBuckets; + long vulnerabilities; + OptionalInt severityRating; if (isMQRMode()) { - List<? extends Terms.Bucket> severityBuckets = + severityBuckets = ((ParsedStringTerms) ((ParsedFilter) ((ParsedNested) severitiesAggregations).getAggregations().get(ISSUES_WITH_SECURITY_IMPACT)).getAggregations().get(AGG_IMPACT_SEVERITIES)).getBuckets(); - long vulnerabilities = + vulnerabilities = severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum(); // Worst severity having at least one issue - OptionalInt severityRating = severityBuckets.stream() + severityRating = severityBuckets.stream() .filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0) .mapToInt(b -> org.sonar.api.issue.impact.Severity.valueOf(b.getKeyAsString()).ordinal() + 1) .max(); - return new CountAndRating(vulnerabilities, severityRating); } else { - List<? extends Terms.Bucket> severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets(); - long vulnerabilities = + severityBuckets = ((ParsedStringTerms) severitiesAggregations).getBuckets(); + vulnerabilities = severityBuckets.stream().mapToLong(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue()).sum(); // Worst severity having at least one issue - OptionalInt severityRating = severityBuckets.stream() + severityRating = severityBuckets.stream() .filter(b -> ((ParsedValueCount) b.getAggregations().get(AGG_COUNT)).getValue() != 0) .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1) .max(); - return new CountAndRating(vulnerabilities, severityRating); } + Map<String, Long> severityDistribution = severityBuckets.stream() + .collect(Collectors.toMap( + e -> e.getKeyAsString().toLowerCase(Locale.US), + MultiBucketsAggregation.Bucket::getDocCount + )); + return new SeverityAggregationDetails(vulnerabilities, severityRating, severityDistribution); } private AggregationBuilder newSecurityReportSubAggregations(AggregationBuilder categoriesAggs, String securityStandardVersionPrefix) { @@ -1523,9 +1535,6 @@ public class IssueIndex { .subAggregation(AggregationBuilders.filter(AGG_TO_REVIEW_SECURITY_HOTSPOTS, TO_REVIEW_HOTSPOTS_FILTER) .subAggregation( AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))) - .subAggregation(AggregationBuilders.filter(AGG_IN_REVIEW_SECURITY_HOTSPOTS, IN_REVIEW_HOTSPOTS_FILTER) - .subAggregation( - AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))) .subAggregation(AggregationBuilders.filter(AGG_REVIEWED_SECURITY_HOTSPOTS, REVIEWED_HOTSPOTS_FILTER) .subAggregation( AggregationBuilders.count(AGG_COUNT).field(FIELD_ISSUE_KEY))); @@ -1569,7 +1578,6 @@ public class IssueIndex { componentFilter .should(getNonResolvedIssuesOrNonResolvedSecurityImpactQueryBuilderBasedOnMode()) .should(TO_REVIEW_HOTSPOTS_FILTER) - .should(IN_REVIEW_HOTSPOTS_FILTER) .should(REVIEWED_HOTSPOTS_FILTER) .minimumShouldMatch(1)) .size(0); @@ -1580,13 +1588,15 @@ public class IssueIndex { } - private static class CountAndRating { + private static class SeverityAggregationDetails { private long count; private OptionalInt rating; + private Map<String, Long> distribution; - public CountAndRating(long count, OptionalInt rating) { + public SeverityAggregationDetails(long count, OptionalInt rating, Map<String, Long> distribution) { this.count = count; this.rating = rating; + this.distribution = distribution; } public long getCount() { @@ -1596,5 +1606,9 @@ public class IssueIndex { public OptionalInt getRating() { return rating; } + + public Map<String, Long> getDistribution() { + return distribution; + } } } 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 ddf06536376..9a20bac4775 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 @@ -74,6 +74,7 @@ public class IssueQuery { private final Collection<String> languages; private final Collection<String> tags; private final Collection<String> types; + private final Collection<String> owaspMobileTop10For2024; private final Collection<String> owaspTop10; private final Collection<String> pciDss32; private final Collection<String> pciDss40; @@ -129,6 +130,7 @@ public class IssueQuery { this.pciDss40 = defaultCollection(builder.pciDss40); this.owaspAsvs40 = defaultCollection(builder.owaspAsvs40); this.owaspAsvsLevel = builder.owaspAsvsLevel; + this.owaspMobileTop10For2024 = defaultCollection(builder.owaspMobileTop10For2024); this.owaspTop10 = defaultCollection(builder.owaspTop10); this.owaspTop10For2021 = defaultCollection(builder.owaspTop10For2021); this.stigAsdV5R3 = defaultCollection(builder.stigAsdV5R3); @@ -256,6 +258,10 @@ public class IssueQuery { return Optional.ofNullable(owaspAsvsLevel); } + public Collection<String> owaspMobileTop10For2024() { + return owaspMobileTop10For2024; + } + public Collection<String> owaspTop10() { return owaspTop10; } @@ -402,6 +408,7 @@ public class IssueQuery { private Collection<String> pciDss40; private Collection<String> owaspAsvs40; private Integer owaspAsvsLevel; + private Collection<String> owaspMobileTop10For2024; private Collection<String> owaspTop10; private Collection<String> owaspTop10For2021; private Collection<String> stigAsdV5R3; @@ -556,6 +563,11 @@ public class IssueQuery { return this; } + public Builder owaspMobileTop10For2024(@Nullable Collection<String> o) { + this.owaspMobileTop10For2024 = o; + return this; + } + public Builder owaspTop10(@Nullable Collection<String> o) { this.owaspTop10 = o; return this; diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java index e105cd9d175..39e1189e5a6 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java @@ -150,6 +150,7 @@ public class IssueQueryFactory { .pciDss40(request.getPciDss40()) .owaspAsvs40(request.getOwaspAsvs40()) .owaspAsvsLevel(request.getOwaspAsvsLevel()) + .owaspMobileTop10For2024(request.getOwaspMobileTop10For2024()) .owaspTop10(request.getOwaspTop10()) .owaspTop10For2021(request.getOwaspTop10For2021()) .stigAsdR5V3(request.getStigAsdV5R3()) @@ -167,6 +168,8 @@ public class IssueQueryFactory { List<ComponentDto> allComponents = new ArrayList<>(); boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents); addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request); + // SONAR-25108 + unsetMainBranch(builder, issueKeys != null && !issueKeys.isEmpty(), allComponents, request); setCreatedAfterFromRequest(dbSession, builder, request, allComponents, timeZone); String sort = request.getSort(); @@ -266,18 +269,22 @@ public class IssueQueryFactory { ComponentDto component = componentUuids.iterator().next(); if (!QUALIFIERS_WITHOUT_LEAK_PERIOD.contains(component.qualifier()) && request.getPullRequest() == null) { - Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, component); - if (!snapshot.isEmpty() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) { - builder.newCodeOnReference(true); - return; - } - // if last analysis has no period date, then no issue should be considered new. - Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(snapshot); - setCreatedAfterFromDates(builder, createdAfterFromSnapshot, null, false); + setInNewCodePeriod(dbSession, builder, component.uuid()); } } } + private void setInNewCodePeriod(DbSession dbSession, IssueQuery.Builder builder, String componentUuid) { + Optional<SnapshotDto> snapshot = getLastAnalysis(dbSession, componentUuid); + if (!snapshot.isEmpty() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) { + builder.newCodeOnReference(true); + return; + } + // if last analysis has no period date, then no issue should be considered new. + Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(snapshot); + setCreatedAfterFromDates(builder, createdAfterFromSnapshot, null, false); + } + private static boolean notInNewCodePeriod(SearchRequest request) { Boolean inNewCodePeriod = request.getInNewCodePeriod(); inNewCodePeriod = Boolean.TRUE.equals(inNewCodePeriod); @@ -298,8 +305,8 @@ public class IssueQueryFactory { .isPresent(); } - private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, ComponentDto component) { - return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); + private Optional<SnapshotDto> getLastAnalysis(DbSession dbSession, String componentUuid) { + return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, componentUuid); } private List<SnapshotDto> getLastAnalysis(DbSession dbSession, Set<String> projectUuids) { @@ -511,4 +518,12 @@ public class IssueQueryFactory { builder.mainBranch(branchDto.isMain()); } } + + private static void unsetMainBranch(IssueQuery.Builder builder, boolean hasIssueKey, List<ComponentDto> components, SearchRequest request) { + var pullRequest = request.getPullRequest(); + var branch = request.getBranch(); + if ((components.isEmpty() || UNKNOWN_COMPONENT.equals(components.get(0)) || (pullRequest == null && branch == null)) && hasIssueKey) { + builder.mainBranch(null); + } + } } diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java index 23daad4fed0..e0fa9ca61cc 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java @@ -456,7 +456,7 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon { } @Test - void filter_by_new_code_reference_branches() { + void filter_by_new_reference_branches() { ComponentDto project1 = db.components().insertPrivateProject().getMainBranchComponent(); IssueDoc project1Issue1 = newDocForProject(project1).setIsNewCodeReference(true); IssueDoc project1Issue2 = newDocForProject(project1).setIsNewCodeReference(false); @@ -474,14 +474,20 @@ class IssueIndexFiltersTest extends IssueIndexTestCommon { IssueDoc project2Branch1Issue1 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(false); IssueDoc project2Branch1Issue2 = newDoc(project2Branch1, project2.uuid()).setIsNewCodeReference(true); + ComponentDto project3 = db.components().insertPrivateProject().getMainBranchComponent(); + ComponentDto project3Branch1 = db.components().insertProjectBranch(project2); + IssueDoc project3Issue1 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(1000L)); + IssueDoc project3Issue2 = newDoc(project3Branch1, project3.uuid()).setFuncCreationDate(new Date(2000L)); + indexIssues(project1Issue1, project1Issue2, project2Issue1, project2Issue2, - project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2); + project1Branch1Issue1, project1Branch1Issue2, project2Branch1Issue1, project2Branch1Issue2, project3Issue1, project3Issue2); // Search for issues of project 1 branch 1 and project 2 branch 1 that are new code on a branch using reference for new code assertThatSearchReturnsOnly(IssueQuery.builder() .mainBranch(false) - .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())), - project1Branch1Issue2.key(), project2Branch1Issue2.key()); + .newCodeOnReferenceByProjectUuids(Set.of(project1Branch1.uuid(), project2Branch1.uuid())) + .createdAfterByProjectUuids(Map.of(project3Branch1.uuid(), new IssueQuery.PeriodStart(new Date(1500), false))), + project1Branch1Issue2.key(), project2Branch1Issue2.key(), project3Issue2.key()); } @Test 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 0eee21734c2..0c002a4e13c 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 @@ -30,7 +30,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueStatus; import org.sonar.api.rule.Severity; +import org.sonar.api.server.rule.RulesDefinition; import org.sonar.core.rule.RuleType; import org.sonar.api.server.rule.RulesDefinition.StigVersion; import org.sonar.db.component.ComponentDto; @@ -51,6 +53,7 @@ import static org.sonar.api.issue.impact.Severity.MEDIUM; import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY; import static org.sonar.api.server.rule.RulesDefinition.OwaspAsvsVersion; +import static org.sonar.api.server.rule.RulesDefinition.OwaspMobileTop10Version.Y2024; import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2017; import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021; import static org.sonar.api.server.rule.RulesDefinition.PciDssVersion; @@ -357,6 +360,31 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon { tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5)); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void getOwaspMobileTop10For2024Report_aggregation_with_cwe(boolean mqrMode) { + doReturn(Optional.of(mqrMode)).when(config).getBoolean(MULTI_QUALITY_MODE_ENABLED); + List<SecurityStandardCategoryStatistics> owaspTop10Report = indexIssuesAndAssertOwaspMobile2024Report(true); + + Map<String, List<SecurityStandardCategoryStatistics>> cweByOwasp = owaspTop10Report.stream() + .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren)); + + assertThat(cweByOwasp.get("m1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, + SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating) + .containsExactlyInAnyOrder( + tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1), + tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1), + tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5)); + assertThat(cweByOwasp.get("m3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, + SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating) + .containsExactlyInAnyOrder( + tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1), + tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 1), + tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 5)); + } + private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) { ComponentDto project = newPrivateProjectDto(); indexIssues( @@ -545,6 +573,41 @@ class IssueIndexSecurityReportsTest extends IssueIndexTestCommon { return owaspTop10Report; } + private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspMobile2024Report(boolean includeCwe) { + ComponentDto project = newPrivateProjectDto(); + indexIssues( + newDocForProject("openvul1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, MEDIUM)).setStatus(IssueStatus.OPEN.name()) + .setSeverity(Severity.MAJOR), + newDocForProject("openvul2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setCwe(List.of("123")).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, LOW)).setStatus(IssueStatus.OPEN.name()) + .setSeverity(Severity.MINOR), + newDocForProject("notowaspvul", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setImpacts(Map.of(SECURITY, HIGH)).setStatus(IssueStatus.OPEN.toString()) + .setSeverity(Severity.CRITICAL), + newDocForProject("toreviewhotspot1", project).setOwaspMobileTop10For2024(asList("m1", "m3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_TO_REVIEW), + newDocForProject("toreviewhotspot2", project).setOwaspMobileTop10For2024(asList("m3", "m6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), + newDocForProject("reviewedHotspot", project).setOwaspMobileTop10For2024(asList("m3", "m8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) + .setResolution(Issue.RESOLUTION_FIXED), + newDocForProject("notowasphotspot", project).setOwaspMobileTop10For2024(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); + + List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspMobileTop10Report(project.uuid(), false, includeCwe, Y2024); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabilityRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, + SecurityStandardCategoryStatistics::getReviewedSecurityHotspots, SecurityStandardCategoryStatistics::getSecurityReviewRating) + .containsExactlyInAnyOrder( + tuple("m1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 5), + tuple("m2", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("m3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* reviewedHotspot */, 4), + tuple("m4", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("m5", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("m6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 5), + tuple("m7", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("m8", 0L, OptionalInt.empty(), 0L, 1L /* reviewedHotspot */, 1), + tuple("m9", 0L, OptionalInt.empty(), 0L, 0L, 1), + tuple("m10", 0L, OptionalInt.empty(), 0L, 0L, 1)); + return owaspTop10Report; + } + @ParameterizedTest @ValueSource(booleans = {true, false}) void getPciDssReport_aggregation_on_portfolio(boolean mqrMode) { diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java index eb5c05b4ab6..c62293eb309 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java @@ -25,6 +25,7 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Map; import org.junit.Rule; import org.junit.Test; @@ -741,4 +742,84 @@ public class IssueQueryFactoryTest { .hasMessageContaining("'unknown-date' cannot be parsed as either a date or date+time"); } + @Test + public void when_issue_keys_provided_with_no_component_should_not_have_main_branch() { + SearchRequest request = new SearchRequest() + .setIssues(List.of("issue-key-1", "issue-key-2")); + + IssueQuery query = underTest.create(request); + + assertThat(query.isMainBranch()).isNull(); + } + + @Test + public void when_issue_keys_and_component_provided_should_have_main_branch_set() { + // Create a project with main branch + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + String branchName = DEFAULT_MAIN_BRANCH_NAME; + + // Request with issue keys and main branch + SearchRequest request = new SearchRequest() + .setIssues(List.of("issue-key-1", "issue-key-2")) + .setComponentKeys(List.of(mainBranch.getKey())) + .setBranch(branchName); + + IssueQuery query = underTest.create(request); + + // Should unset main branch since issue keys are provided + assertThat(query.isMainBranch()).isTrue(); + assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid()); + } + + @Test + public void when_no_issue_keys_provided_should_default_to_main_branch() { + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + String branchName = DEFAULT_MAIN_BRANCH_NAME; + + SearchRequest request = new SearchRequest() + .setComponentKeys(List.of(mainBranch.getKey())) + .setBranch(branchName); + + IssueQuery query = underTest.create(request); + + assertThat(query.isMainBranch()).isTrue(); + assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid()); + } + + @Test + public void when_component_is_non_main_branch_should_not_default_to_main_branch() { + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + String branchName = "feature-branch"; + ComponentDto branch = db.components().insertProjectBranch(mainBranch, b -> b.setKey(branchName)); + + SearchRequest request = new SearchRequest() + .setComponentKeys(List.of(branch.getKey())) + .setBranch(branchName); + + IssueQuery query = underTest.create(request); + + assertThat(query.isMainBranch()).isFalse(); + assertThat(query.branchUuid()).isEqualTo(branch.uuid()); + } + + @Test + public void when_empty_issue_keys_list_provided_should_default_to_main_branch() { + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + String branchName = DEFAULT_MAIN_BRANCH_NAME; + + SearchRequest request = new SearchRequest() + .setIssues(Collections.emptyList()) + .setComponentKeys(List.of(mainBranch.getKey())) + .setBranch(branchName); + + IssueQuery query = underTest.create(request); + + assertThat(query.isMainBranch()).isTrue(); + assertThat(query.branchUuid()).isEqualTo(mainBranch.uuid()); + } + } 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 c8b9870d547..979e493da47 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 @@ -128,6 +128,15 @@ class IssueQueryTest { } @Test + void build_owasp_mobile_query() { + IssueQuery query = IssueQuery.builder() + .owaspMobileTop10For2024(List.of("m5", "m6")) + .build(); + + assertThat(query.owaspMobileTop10For2024()).containsOnly("m5", "m6"); + } + + @Test void build_stig_query() { IssueQuery query = IssueQuery.builder() .stigAsdR5V3(List.of("V-222400", "V-222401")) |