From f6028da2015f64cb71146b1c8736e800c9ed7b54 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 5 Jun 2019 11:51:31 +0200 Subject: [PATCH] SONAR-12131 Compute Security Review Rating measures on projects * Compute Security Review Rating measures on projects * Live update Security Review Rating measures --- ...ProjectAnalysisTaskContainerPopulator.java | 4 +- .../measure/MeasureRepository.java | 18 +--- .../SecurityReviewRatingVisitor.java | 67 +++++++++++++ .../measure/MeasureRepositoryRule.java | 36 +------ .../SecurityReviewRatingVisitorTest.java | 95 +++++++++++++++++++ .../server/security/SecurityReviewRating.java | 48 ++++++++++ .../security/SecurityReviewRatingTest.java | 69 ++++++++++++++ .../live/IssueMetricFormulaFactoryImpl.java | 6 ++ .../IssueMetricFormulaFactoryImplTest.java | 9 ++ .../resources/org/sonar/l10n/core.properties | 3 + .../org/sonar/api/measures/CoreMetrics.java | 17 ++++ 11 files changed, 323 insertions(+), 49 deletions(-) create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java create mode 100644 server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java create mode 100644 server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java create mode 100644 server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 99358168cc2..f507563326b 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -77,9 +77,9 @@ import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryImpl; import org.sonar.ce.task.projectanalysis.issue.RuleTagsCopier; import org.sonar.ce.task.projectanalysis.issue.ScmAccountToUser; import org.sonar.ce.task.projectanalysis.issue.ScmAccountToUserLoader; +import org.sonar.ce.task.projectanalysis.issue.ShortBranchOrPullRequestTrackerExecution; import org.sonar.ce.task.projectanalysis.issue.SiblingsIssueMerger; import org.sonar.ce.task.projectanalysis.issue.SiblingsIssuesLoader; -import org.sonar.ce.task.projectanalysis.issue.ShortBranchOrPullRequestTrackerExecution; import org.sonar.ce.task.projectanalysis.issue.TrackerBaseInputFactory; import org.sonar.ce.task.projectanalysis.issue.TrackerExecution; import org.sonar.ce.task.projectanalysis.issue.TrackerMergeOrTargetBranchInputFactory; @@ -111,6 +111,7 @@ import org.sonar.ce.task.projectanalysis.qualitymodel.NewMaintainabilityMeasures import org.sonar.ce.task.projectanalysis.qualitymodel.NewReliabilityAndSecurityRatingMeasuresVisitor; import org.sonar.ce.task.projectanalysis.qualitymodel.RatingSettings; import org.sonar.ce.task.projectanalysis.qualitymodel.ReliabilityAndSecurityRatingMeasuresVisitor; +import org.sonar.ce.task.projectanalysis.qualitymodel.SecurityReviewRatingVisitor; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderImpl; import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl; import org.sonar.ce.task.projectanalysis.scm.ScmInfoDbLoader; @@ -265,6 +266,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop NewMaintainabilityMeasuresVisitor.class, ReliabilityAndSecurityRatingMeasuresVisitor.class, NewReliabilityAndSecurityRatingMeasuresVisitor.class, + SecurityReviewRatingVisitor.class, LastCommitVisitor.class, MeasureComputersVisitor.class, diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepository.java index 4d6aeba7a76..57d3d529bf3 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepository.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepository.java @@ -32,8 +32,7 @@ public interface MeasureRepository { * Retrieves the base measure (ie. the one currently existing in DB) for the specified {@link Component} for * the specified {@link MetricImpl} if it exists. *

- * This method searches for Measure which are specific to the Component and not associated to a rule or a - * characteristic. + * This method searches for Measure which are specific to the Component. *

* * @throws NullPointerException if either argument is {@code null} @@ -42,32 +41,23 @@ public interface MeasureRepository { /** * Retrieves the measure created during the current analysis for the specified {@link Component} for the specified - * {@link Metric} if it exists (ie. one created by the Compute Engine or the Batch) and which is not - * associated to a rule, a characteristic, or a developer. + * {@link Metric} if it exists (ie. one created by the Compute Engine or the Scanner). */ Optional getRawMeasure(Component component, Metric metric); /** * Returns the {@link Measure}s for the specified {@link Component} and the specified {@link Metric}. - *

- * Their will be one measure not associated to rules, characteristics or developers, the other ones will be associated to rules or to characteristics - * (see {@link Measure#equals(Object)}. - *

*/ Set getRawMeasures(Component component, Metric metric); /** * Returns the {@link Measure}s for the specified {@link Component} mapped by their metric key. - *

- * Their can be multiple measures for the same Metric but only one which has no rule nor characteristic, one with a - * specific ruleId and one with specific characteristicId (see {@link Measure#equals(Object)}. - *

*/ SetMultimap getRawMeasures(Component component); /** * Adds the specified measure for the specified Component and Metric. There can be no more than one measure for a - * specific combination of Component, Metric and association to a specific rule or characteristic. + * specific combination of Component, Metric. * * @throws NullPointerException if any of the arguments is null * @throws UnsupportedOperationException when trying to add a measure when one already exists for the specified Component/Metric paar @@ -76,7 +66,7 @@ public interface MeasureRepository { /** * Updates the specified measure for the specified Component and Metric. There can be no more than one measure for a - * specific combination of Component, Metric and association to a specific rule or characteristic. + * specific combination of Component, Metric. * * @throws NullPointerException if any of the arguments is null * @throws UnsupportedOperationException when trying to update a non existing measure diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java new file mode 100644 index 00000000000..6e5da70e2f1 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.ce.task.projectanalysis.qualitymodel; + +import java.util.Optional; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter; +import org.sonar.ce.task.projectanalysis.measure.Measure; +import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; +import org.sonar.ce.task.projectanalysis.metric.Metric; +import org.sonar.ce.task.projectanalysis.metric.MetricRepository; +import org.sonar.server.measure.Rating; +import org.sonar.server.security.SecurityReviewRating; + +import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY; +import static org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit.PROJECT; +import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder; + +public class SecurityReviewRatingVisitor extends TypeAwareVisitorAdapter { + + private final MeasureRepository measureRepository; + private final Metric nclocMetric; + private final Metric securityHostspotsMetric; + private final Metric securityReviewRatingMetric; + + public SecurityReviewRatingVisitor(MeasureRepository measureRepository, MetricRepository metricRepository) { + super(PROJECT, Order.POST_ORDER); + this.measureRepository = measureRepository; + this.nclocMetric = metricRepository.getByKey(NCLOC_KEY); + this.securityHostspotsMetric = metricRepository.getByKey(SECURITY_HOTSPOTS_KEY); + this.securityReviewRatingMetric = metricRepository.getByKey(SECURITY_REVIEW_RATING_KEY); + } + + @Override + public void visitProject(Component project) { + Optional nclocMeasure = measureRepository.getRawMeasure(project, nclocMetric); + Optional securityHostspotsMeasure = measureRepository.getRawMeasure(project, securityHostspotsMetric); + if (!nclocMeasure.isPresent() || !securityHostspotsMeasure.isPresent()) { + return; + } + int ncloc = nclocMeasure.get().getIntValue(); + int securityHotspots = securityHostspotsMeasure.get().getIntValue(); + Rating rating = SecurityReviewRating.compute(ncloc, securityHotspots); + measureRepository.add(project, securityReviewRatingMetric, newMeasureBuilder().create(rating.getIndex(), rating.name())); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepositoryRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepositoryRule.java index d83245d86ac..2c579151024 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepositoryRule.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepositoryRule.java @@ -23,7 +23,6 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -61,15 +60,8 @@ public class MeasureRepositoryRule extends ExternalResource implements MeasureRe private final Map baseMeasures = new HashMap<>(); private final Map rawMeasures = new HashMap<>(); private final Map initialRawMeasures = new HashMap<>(); - private Collection loadedAsRawComponents; - private Collection loadedAsRawMetrics; - private final Predicate> isAddedMeasure = new Predicate>() { - @Override - public boolean apply(@Nonnull Map.Entry input) { - return !initialRawMeasures.containsKey(input.getKey()) - || !MeasureRepoEntry.deepEquals(input.getValue(), initialRawMeasures.get(input.getKey())); - } - }; + private final Predicate> isAddedMeasure = input -> !initialRawMeasures.containsKey(input.getKey()) + || !MeasureRepoEntry.deepEquals(input.getValue(), initialRawMeasures.get(input.getKey())); private MeasureRepositoryRule(ComponentProvider componentProvider, @Nullable MetricRepositoryRule metricRepositoryRule) { this.componentProvider = componentProvider; @@ -95,18 +87,6 @@ public class MeasureRepositoryRule extends ExternalResource implements MeasureRe return new MeasureRepositoryRule(new TreeComponentProvider(treeRoot), requireNonNull(metricRepositoryRule)); } - public MeasureRepositoryRule addBaseMeasure(Component component, Metric metric, Measure measure) { - checkAndInitProvidersState(); - - InternalKey internalKey = new InternalKey(component, metric); - checkState(!baseMeasures.containsKey(internalKey), - format("Can not add a BaseMeasure twice for a Component (ref=%s) and Metric (key=%s)", getRef(component), metric.getKey())); - - baseMeasures.put(internalKey, measure); - - return this; - } - public MeasureRepositoryRule addBaseMeasure(int componentRef, String metricKey, Measure measure) { checkAndInitProvidersState(); @@ -188,23 +168,11 @@ public class MeasureRepositoryRule extends ExternalResource implements MeasureRe return Optional.ofNullable(baseMeasures.get(new InternalKey(component, metric))); } - public Collection getComponentsLoadedAsRaw() { - return loadedAsRawComponents; - } - - public Collection getMetricsLoadedAsRaw() { - return loadedAsRawMetrics; - } - @Override public Optional getRawMeasure(Component component, Metric metric) { return Optional.ofNullable(rawMeasures.get(new InternalKey(component, metric))); } - public Optional getRawRuleMeasure(Component component, Metric metric, int ruleId) { - return Optional.ofNullable(rawMeasures.get(new InternalKey(component, metric))); - } - @Override public Set getRawMeasures(Component component, Metric metric) { return from(filterKeys(rawMeasures, hasComponentRef(component)).entrySet()).filter(new MatchMetric(metric)).transform(ToMeasure.INSTANCE).toSet(); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java new file mode 100644 index 00000000000..ee8e616336d --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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.ce.task.projectanalysis.qualitymodel; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; +import org.sonar.ce.task.projectanalysis.component.VisitorsCrawler; +import org.sonar.ce.task.projectanalysis.measure.Measure; +import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule; +import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule; +import org.sonar.server.measure.Rating; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.measures.CoreMetrics.NCLOC; +import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING; +import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY; +import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder; +import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder; + +public class SecurityReviewRatingVisitorTest { + + private static final int PROJECT_REF = 1; + private static final Component PROJECT = builder(Component.Type.PROJECT, PROJECT_REF).setKey("project").build(); + + @Rule + public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule(); + + @Rule + public MetricRepositoryRule metricRepository = new MetricRepositoryRule() + .add(NCLOC) + .add(SECURITY_HOTSPOTS) + .add(SECURITY_REVIEW_RATING); + + @Rule + public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository); + + private VisitorsCrawler underTest = new VisitorsCrawler(singletonList(new SecurityReviewRatingVisitor(measureRepository, metricRepository))); + + @Test + public void compute_security_review_rating_on_project() { + treeRootHolder.setRoot(PROJECT); + measureRepository.addRawMeasure(PROJECT_REF, NCLOC_KEY, newMeasureBuilder().create(1000)); + measureRepository.addRawMeasure(PROJECT_REF, SECURITY_HOTSPOTS_KEY, newMeasureBuilder().create(12)); + + underTest.visit(PROJECT); + + Measure measure = measureRepository.getAddedRawMeasure(PROJECT_REF, SECURITY_REVIEW_RATING_KEY).get(); + assertThat(measure.getIntValue()).isEqualTo(Rating.C.getIndex()); + assertThat(measure.getData()).isEqualTo(Rating.C.name()); + } + + @Test + public void compute_nothing_when_no_ncloc() { + treeRootHolder.setRoot(PROJECT); + measureRepository.addRawMeasure(PROJECT_REF, SECURITY_HOTSPOTS_KEY, newMeasureBuilder().create(2)); + + underTest.visit(PROJECT); + + assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, SECURITY_REVIEW_RATING_KEY)).isEmpty(); + } + + @Test + public void compute_nothing_when_no_security_hotspot() { + treeRootHolder.setRoot(PROJECT); + measureRepository.addRawMeasure(PROJECT_REF, NCLOC_KEY, newMeasureBuilder().create(1000)); + + underTest.visit(PROJECT); + + assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, SECURITY_REVIEW_RATING_KEY)).isEmpty(); + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java new file mode 100644 index 00000000000..70b81d78275 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.security; + +import org.sonar.server.measure.Rating; + +public class SecurityReviewRating { + + private SecurityReviewRating() { + // Only static method + } + + public static Rating compute(int ncloc, int securityHotspots) { + if (ncloc == 0) { + return Rating.A; + } + double ratio = (double) securityHotspots * 1000d / (double) ncloc; + if (ratio <= 3d) { + return Rating.A; + } else if (ratio <= 10) { + return Rating.B; + } else if (ratio <= 15) { + return Rating.C; + } else if (ratio <= 25) { + return Rating.D; + } else { + return Rating.E; + } + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java new file mode 100644 index 00000000000..d84e62a17d1 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.security; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.server.measure.Rating; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.measure.Rating.A; +import static org.sonar.server.measure.Rating.B; +import static org.sonar.server.measure.Rating.C; +import static org.sonar.server.measure.Rating.D; +import static org.sonar.server.measure.Rating.E; + +@RunWith(DataProviderRunner.class) +public class SecurityReviewRatingTest { + + @DataProvider + public static Object[][] values() { + List res = new ArrayList<>(); + res.add(new Object[] {1000, 0, A}); + res.add(new Object[] {1000, 3, A}); + res.add(new Object[] {1000, 4, B}); + res.add(new Object[] {1000, 10, B}); + res.add(new Object[] {1000, 11, C}); + res.add(new Object[] {1000, 15, C}); + res.add(new Object[] {1000, 16, D}); + res.add(new Object[] {1000, 25, D}); + res.add(new Object[] {1000, 26, E}); + res.add(new Object[] {1000, 900, E}); + + res.add(new Object[] {0, 2, A}); + res.add(new Object[] {1001, 3, A}); + res.add(new Object[] {999, 3, B}); + res.add(new Object[] {Integer.MAX_VALUE, Integer.MAX_VALUE, E}); + return res.toArray(new Object[res.size()][3]); + } + + @Test + @UseDataProvider("values") + public void compute_security_review_rating_on_project(int ncloc, int securityHotspots, Rating expectedRating) { + assertThat(SecurityReviewRating.compute(ncloc, securityHotspots)).isEqualTo(expectedRating); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java index f3b900c1cce..e7bb34eecbb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java @@ -28,6 +28,7 @@ import org.sonar.api.measures.Metric; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.server.measure.Rating; +import org.sonar.server.security.SecurityReviewRating; import static java.util.Arrays.asList; import static org.sonar.server.measure.Rating.RATING_BY_SEVERITY; @@ -107,6 +108,11 @@ public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory new IssueMetricFormula(CoreMetrics.SECURITY_RATING, false, (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))), + new IssueMetricFormula(CoreMetrics.SECURITY_REVIEW_RATING, false, + (context, issues) -> context.setValue(SecurityReviewRating.compute(context.getValue(CoreMetrics.NCLOC).orElse(0d).intValue(), + context.getValue(CoreMetrics.SECURITY_HOTSPOTS).orElse(0d).intValue())), + asList(CoreMetrics.NCLOC, CoreMetrics.SECURITY_HOTSPOTS)), + new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true, (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))), diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java index 7a916eb7532..5051f4305f7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java @@ -123,6 +123,15 @@ public class IssueMetricFormulaFactoryImplTest { .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5); } + @Test + public void test_security_review_rating() { + withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.A); + + with(CoreMetrics.SECURITY_HOTSPOTS, 12.0) + .and(CoreMetrics.NCLOC, 1000.0) + .assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.C); + } + @Test public void count_unresolved_by_severity() { withNoIssues() diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ebf9c0aa199..b5cd959f4ea 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1988,6 +1988,9 @@ metric.security_rating.tooltip.E=Security rating is E when there is at least one metric.security_remediation_effort.description=Security remediation effort metric.security_remediation_effort.name=Security Remediation Effort metric.security_remediation_effort.extra_short_name=Remediation Effort +metric.security_review_rating.description=Security Review Rating +metric.security_review_rating.name=Security Review Rating +metric.security_review_rating.extra_short_name=Review Rating metric.skipped_tests.description=Number of skipped unit tests metric.skipped_tests.name=Skipped Unit Tests metric.skipped_tests.short_name=Skipped diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index 6a7d13c32b7..4b2a1605da7 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -1506,6 +1506,23 @@ public final class CoreMetrics { .setWorstValue(5.0) .create(); + /** + * @since 7.8 + */ + public static final String SECURITY_REVIEW_RATING_KEY = "security_review_rating"; + + /** + * @since 7.8 + */ + public static final Metric SECURITY_REVIEW_RATING = new Metric.Builder(SECURITY_REVIEW_RATING_KEY, "Security Review Rating", Metric.ValueType.RATING) + .setDescription("Security Review Rating") + .setDomain(DOMAIN_SECURITY) + .setDirection(Metric.DIRECTION_WORST) + .setQualitative(true) + .setBestValue(1d) + .setWorstValue(5d) + .create(); + // -------------------------------------------------------------------------------------------------------------------- // // FILE DATA -- 2.39.5