diff options
Diffstat (limited to 'server')
7 files changed, 130 insertions, 28 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java index 484b03f1918..903fe411831 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/SecurityStandardCategoryStatistics.java @@ -20,6 +20,7 @@ package org.sonar.server.issue.index; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import javax.annotation.Nullable; @@ -38,9 +39,11 @@ public class SecurityStandardCategoryStatistics { private boolean hasMoreRules; private final Optional<String> version; private Optional<String> level = Optional.empty(); + private final Map<String, Long> severityDistribution; public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long toReviewSecurityHotspots, - long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version) { + long reviewedSecurityHotspots, Integer securityReviewRating, @Nullable List<SecurityStandardCategoryStatistics> children, @Nullable String version, + Map<String, Long> severityDistribution) { this.category = category; this.vulnerabilities = vulnerabilities; this.vulnerabilityRating = vulnerabiliyRating; @@ -50,6 +53,7 @@ public class SecurityStandardCategoryStatistics { this.children = children; this.version = Optional.ofNullable(version); this.hasMoreRules = false; + this.severityDistribution = severityDistribution; } public SecurityStandardCategoryStatistics withModifiedVulnerabilities( @@ -71,7 +75,8 @@ public class SecurityStandardCategoryStatistics { this.getReviewedSecurityHotspots(), this.getSecurityReviewRating(), this.getChildren(), - this.getVersion().orElse(null)); + this.getVersion().orElse(null), + this.getSeverityDistribution()); } public String getCategory() { @@ -140,4 +145,8 @@ public class SecurityStandardCategoryStatistics { public void setHasMoreRules(boolean hasMoreRules) { this.hasMoreRules = hasMoreRules; } + + public Map<String, Long> getSeverityDistribution() { + return severityDistribution; + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java index 389d1697c5f..3da617a1be9 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/codequalityissue/CodeQualityIssueWorkflowDefinition.java @@ -26,6 +26,7 @@ import org.sonar.api.server.ServerSide; import org.sonar.issue.workflow.statemachine.StateMachine; import org.sonar.issue.workflow.statemachine.Transition; +import static java.util.function.Predicate.not; import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE; import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED; @@ -56,6 +57,9 @@ public class CodeQualityIssueWorkflowDefinition { private static final Consumer<CodeQualityIssueWorkflowActions> UNSET_RESOLUTION = CodeQualityIssueWorkflowActions::unsetResolution; private static final Consumer<CodeQualityIssueWorkflowActions> RESTORE_RESOLUTION = CodeQualityIssueWorkflowActions::restoreResolution; + private static final Predicate<CodeQualityIssueWorkflowEntity> AUTOMATIC_REOPEN_PREDICATE = not(CodeQualityIssueWorkflowEntity::isBeingClosed) + .and(issue -> issue.hasAnyResolution(RESOLUTION_FIXED)); + private final StateMachine<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions> machine; public CodeQualityIssueWorkflowDefinition() { @@ -181,9 +185,7 @@ public class CodeQualityIssueWorkflowDefinition { // Reopen issues that are marked as resolved but that are still alive. .transition(Transition.<CodeQualityIssueWorkflowEntity, CodeQualityIssueWorkflowActions>builder("automaticreopen") .from(STATUS_RESOLVED).to(STATUS_REOPENED) - .conditions( - Predicate.not(CodeQualityIssueWorkflowEntity::isBeingClosed), - i -> i.hasAnyResolution(RESOLUTION_FIXED)) + .conditions(AUTOMATIC_REOPEN_PREDICATE) .actions(UNSET_RESOLUTION, UNSET_CLOSE_DATE) .automatic() .build()) @@ -227,6 +229,8 @@ public class CodeQualityIssueWorkflowDefinition { .from(STATUS_RESOLVED) .to(STATUS_REOPENED) .conditions( + // We check this first condition to avoid overlap with the automaticreopen transition. + not(AUTOMATIC_REOPEN_PREDICATE), CodeQualityIssueWorkflowEntity::isTaintVulnerability, CodeQualityIssueWorkflowEntity::locationsChanged) .actions( diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java index 36638caf954..a23c45cd248 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/SecurityStandardCategoryStatisticsTest.java @@ -20,6 +20,7 @@ package org.sonar.server.issue.index; import java.util.ArrayList; +import java.util.Map; import java.util.OptionalInt; import org.junit.Test; @@ -33,7 +34,7 @@ public class SecurityStandardCategoryStatisticsTest { public void hasMoreRules_default_false() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 0, empty(), 0, - 0, 5, null, null + 0, 5, null, null, Map.of() ); assertThat(standardCategoryStatistics.hasMoreRules()).isFalse(); } @@ -42,7 +43,7 @@ public class SecurityStandardCategoryStatisticsTest { public void hasMoreRules_is_updatable() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 0, empty(), 0, - 0, 5, null, null + 0, 5, null, null, Map.of() ); standardCategoryStatistics.setHasMoreRules(true); assertThat(standardCategoryStatistics.hasMoreRules()).isTrue(); @@ -52,7 +53,7 @@ public class SecurityStandardCategoryStatisticsTest { public void test_getters() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 1, empty(), 0, - 0, 5, new ArrayList<>(), "version" + 0, 5, new ArrayList<>(), "version", Map.of() ).setLevel("1"); standardCategoryStatistics.setActiveRules(3); @@ -71,13 +72,14 @@ public class SecurityStandardCategoryStatisticsTest { assertThat(standardCategoryStatistics.getVersion().get()).contains("version"); assertThat(standardCategoryStatistics.getLevel().get()).contains("1"); assertThat(standardCategoryStatistics.hasMoreRules()).isFalse(); + assertThat(standardCategoryStatistics.getSeverityDistribution()).isEmpty(); } @Test public void withModifiedVulnerabilities() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 1, empty(), 0, - 0, 5, null, null + 0, 5, null, null, Map.of() ); SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3); @@ -91,7 +93,7 @@ public class SecurityStandardCategoryStatisticsTest { public void withModifiedVulnerabilities_noNewRating() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 1, OptionalInt.of(1), 0, - 0, 5, null, null + 0, 5, null, null, Map.of() ); SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, null); @@ -105,7 +107,7 @@ public class SecurityStandardCategoryStatisticsTest { public void withModifiedVulnerabilities_usesLowestRating() { SecurityStandardCategoryStatistics standardCategoryStatistics = new SecurityStandardCategoryStatistics( "cat", 1, OptionalInt.of(5), 0, - 0, 5, null, null + 0, 5, null, null, Map.of() ); SecurityStandardCategoryStatistics modified = standardCategoryStatistics.withModifiedVulnerabilities(2, 3); diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java index 386b3666c26..956ef000a0e 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForCodeQualityIssuesTest.java @@ -60,6 +60,7 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.issue.Issue.STATUS_REOPENED; import static org.sonar.api.issue.Issue.STATUS_RESOLVED; import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByScanBuilder; +import static org.sonar.core.rule.RuleType.VULNERABILITY; class IssueWorkflowForCodeQualityIssuesTest { @@ -455,6 +456,27 @@ class IssueWorkflowForCodeQualityIssuesTest { assertThat(issue.assignee()).isNull(); } + @Test + void doManualTransition_shouldUseAutomaticReopenTransitionOnTaintVulnerability_whenMarkedAsResolvedButStillAlive() { + DefaultIssue issue = new DefaultIssue() + .setKey("issue_key") + .setRuleKey(RuleKey.of("xoo", "S001")) + .setStatus(STATUS_RESOLVED) + .setResolution(RESOLUTION_FIXED) + .setLocationsChanged(true) + .setNew(false) + .setType(VULNERABILITY) + .setBeingClosed(false); + when(taintChecker.isTaintVulnerability(issue)) + .thenReturn(true); + + underTest.doAutomaticTransition(issue, issueChangeContextByScanBuilder(new Date()).build()); + + assertThat(issue.issueStatus()).isEqualTo(IssueStatus.OPEN); + List<DefaultIssueComment> issueComments = issue.defaultIssueComments(); + assertThat(issueComments).isEmpty(); + } + private static DefaultIssue newClosedIssue(String resolution) { return new DefaultIssue() .setKey("ABCDE") 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 fd408c6c26a..9235bc57b1a 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; @@ -1292,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) { @@ -1448,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(); @@ -1462,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) { @@ -1578,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() { @@ -1594,5 +1606,9 @@ public class IssueIndex { public OptionalInt getRating() { return rating; } + + public Map<String, Long> getDistribution() { + return distribution; + } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java index dd20b9c78c8..048487d44ba 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java @@ -106,7 +106,7 @@ public class BuiltInQProfileRepositoryImpl implements BuiltInQProfileRepository .collect(Collectors.toSet()); checkState(languagesWithoutBuiltInQProfiles.isEmpty(), "The following languages have no built-in quality profiles: %s", - languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join("", languagesWithoutBuiltInQProfiles)); + languagesWithoutBuiltInQProfiles.isEmpty() ? "" : String.join(", ", languagesWithoutBuiltInQProfiles)); } private Map<String, Map<String, BuiltInQualityProfile>> validateAndClean(BuiltInQualityProfilesDefinition.Context context) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java new file mode 100644 index 00000000000..2f76e5f9435 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImplTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.qualityprofile.builtin; + +import org.junit.jupiter.api.Test; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.db.DbClient; +import org.sonar.server.rule.ServerRuleFinder; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class BuiltInQProfileRepositoryImplTest { + @Test + void initializationWithoutQualityProfiles() { + DbClient dbClient = mock(DbClient.class); + ServerRuleFinder ruleFinder = mock(ServerRuleFinder.class); + Languages languages = mock(Languages.class); + Language java = mock(Language.class); + Language kotlin = mock(Language.class); + + when(languages.all()).thenReturn(new Language[]{ java, kotlin }); + when(java.getKey()).thenReturn("java"); + when(kotlin.getKey()).thenReturn("kotlin"); + + BuiltInQProfileRepositoryImpl repository = new BuiltInQProfileRepositoryImpl(dbClient, ruleFinder, languages); + + assertThatCode(repository::initialize).hasMessage("The following languages have no built-in quality profiles: java, kotlin"); + } +} |