]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12131 Compute Security Review Rating measures on projects
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 5 Jun 2019 09:51:31 +0000 (11:51 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 14 Jun 2019 18:21:10 +0000 (20:21 +0200)
* Compute Security Review Rating measures on projects
* Live update Security Review Rating measures

server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepository.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/measure/MeasureRepositoryRule.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java
server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java

index 99358168cc2766790416adb3cc1acb9e599f24d4..f507563326b1848e1b5cc1258b42bce2a9e4716a 100644 (file)
@@ -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,
 
index 4d6aeba7a766a8183b373bd722e89aa25fcfbafd..57d3d529bf3fa55a4bdabd1e0044bf570214b977 100644 (file)
@@ -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.
    * <p>
-   * 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.
    * </p>
    *
    * @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 <strong>not</strong>
-   * 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<Measure> getRawMeasure(Component component, Metric metric);
 
   /**
    * Returns the {@link Measure}s for the specified {@link Component} and the specified {@link Metric}.
-   * <p>
-   * 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)}.
-   * </p>
    */
   Set<Measure> getRawMeasures(Component component, Metric metric);
 
   /**
    * Returns the {@link Measure}s for the specified {@link Component} mapped by their metric key.
-   * <p>
-   * 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)}.
-   * </p>
    */
   SetMultimap<String, Measure> 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 (file)
index 0000000..6e5da70
--- /dev/null
@@ -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<Measure> nclocMeasure = measureRepository.getRawMeasure(project, nclocMetric);
+    Optional<Measure> 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()));
+  }
+
+}
index d83245d86acc825585b3bb0e6a7b47b2a80312cc..2c5791510242dca533145706799c703dd26637a3 100644 (file)
@@ -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<InternalKey, Measure> baseMeasures = new HashMap<>();
   private final Map<InternalKey, Measure> rawMeasures = new HashMap<>();
   private final Map<InternalKey, Measure> initialRawMeasures = new HashMap<>();
-  private Collection<Component> loadedAsRawComponents;
-  private Collection<Metric> loadedAsRawMetrics;
-  private final Predicate<Map.Entry<InternalKey, Measure>> isAddedMeasure = new Predicate<Map.Entry<InternalKey, Measure>>() {
-    @Override
-    public boolean apply(@Nonnull Map.Entry<InternalKey, Measure> input) {
-      return !initialRawMeasures.containsKey(input.getKey())
-        || !MeasureRepoEntry.deepEquals(input.getValue(), initialRawMeasures.get(input.getKey()));
-    }
-  };
+  private final Predicate<Map.Entry<InternalKey, Measure>> 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<Component> getComponentsLoadedAsRaw() {
-    return loadedAsRawComponents;
-  }
-
-  public Collection<Metric> getMetricsLoadedAsRaw() {
-    return loadedAsRawMetrics;
-  }
-
   @Override
   public Optional<Measure> getRawMeasure(Component component, Metric metric) {
     return Optional.ofNullable(rawMeasures.get(new InternalKey(component, metric)));
   }
 
-  public Optional<Measure> getRawRuleMeasure(Component component, Metric metric, int ruleId) {
-    return Optional.ofNullable(rawMeasures.get(new InternalKey(component, metric)));
-  }
-
   @Override
   public Set<Measure> 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 (file)
index 0000000..ee8e616
--- /dev/null
@@ -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 (file)
index 0000000..70b81d7
--- /dev/null
@@ -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 (file)
index 0000000..d84e62a
--- /dev/null
@@ -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<Object[]> 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);
+  }
+
+}
index f3b900c1ccefe7466e28896f0eb2cb9474ba8ea3..e7bb34eecbb1d0ddb85208f47138b082c63f739d 100644 (file)
@@ -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))),
 
index 7a916eb75328f5f03c79579ce278af206ed3207f..5051f4305f71d8986781d3d99bb22bc0d87bd66f 100644 (file)
@@ -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()
index ebf9c0aa1996e212e51a3ad20fc1b1b4f62b2d9c..b5cd959f4ea0815294a22f661470e990b092f257 100644 (file)
@@ -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
index 6a7d13c32b77c75cbd1feef3a0354ecb16eb8a3f..4b2a1605da780dcd20b6e58d170940d118b45eb4 100644 (file)
@@ -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<Integer> 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