Browse Source

SONAR-12962 Compute new Security Review measures on Projects

tags/8.2.0.32929
Julien Lancelot 4 years ago
parent
commit
49c4ddbfef
12 changed files with 541 additions and 63 deletions
  1. 5
    5
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
  2. 123
    0
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewMeasuresVisitor.java
  3. 5
    7
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorForPortfoliosAndApplications.java
  4. 283
    0
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewMeasuresVisitorTest.java
  5. 10
    27
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorForPortfoliosAndApplicationsTest.java
  6. 38
    7
      server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java
  7. 1
    1
      server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java
  8. 10
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java
  9. 10
    6
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java
  10. 18
    5
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java
  11. 4
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  12. 34
    5
      sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java

+ 5
- 5
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java View File

@@ -70,21 +70,21 @@ import org.sonar.ce.task.projectanalysis.issue.IssueTrackingDelegator;
import org.sonar.ce.task.projectanalysis.issue.IssueVisitors;
import org.sonar.ce.task.projectanalysis.issue.IssuesRepositoryVisitor;
import org.sonar.ce.task.projectanalysis.issue.LoadComponentUuidsHavingOpenIssuesVisitor;
import org.sonar.ce.task.projectanalysis.issue.ReferenceBranchTrackerExecution;
import org.sonar.ce.task.projectanalysis.issue.MovedIssueVisitor;
import org.sonar.ce.task.projectanalysis.issue.NewEffortAggregator;
import org.sonar.ce.task.projectanalysis.issue.PullRequestTrackerExecution;
import org.sonar.ce.task.projectanalysis.issue.ReferenceBranchTrackerExecution;
import org.sonar.ce.task.projectanalysis.issue.RemoveProcessedComponentsVisitor;
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.PullRequestTrackerExecution;
import org.sonar.ce.task.projectanalysis.issue.SiblingsIssueMerger;
import org.sonar.ce.task.projectanalysis.issue.SiblingsIssuesLoader;
import org.sonar.ce.task.projectanalysis.issue.TrackerBaseInputFactory;
import org.sonar.ce.task.projectanalysis.issue.TrackerExecution;
import org.sonar.ce.task.projectanalysis.issue.TrackerReferenceBranchInputFactory;
import org.sonar.ce.task.projectanalysis.issue.TrackerRawInputFactory;
import org.sonar.ce.task.projectanalysis.issue.TrackerReferenceBranchInputFactory;
import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
import org.sonar.ce.task.projectanalysis.issue.commonrule.BranchCoverageRule;
import org.sonar.ce.task.projectanalysis.issue.commonrule.CommentDensityRule;
@@ -113,7 +113,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.qualitymodel.SecurityReviewMeasuresVisitor;
import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderImpl;
import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl;
import org.sonar.ce.task.projectanalysis.scm.ScmInfoDbLoader;
@@ -268,7 +268,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
NewMaintainabilityMeasuresVisitor.class,
ReliabilityAndSecurityRatingMeasuresVisitor.class,
NewReliabilityAndSecurityRatingMeasuresVisitor.class,
SecurityReviewRatingVisitor.class,
SecurityReviewMeasuresVisitor.class,
LastCommitVisitor.class,
MeasureComputersVisitor.class,


+ 123
- 0
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewMeasuresVisitor.java View File

@@ -0,0 +1,123 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.sonar.api.ce.measure.Issue;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.PathAwareVisitor;
import org.sonar.ce.task.projectanalysis.component.PathAwareVisitorAdapter;
import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesRepository;
import org.sonar.ce.task.projectanalysis.measure.Measure;
import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
import org.sonar.ce.task.projectanalysis.measure.RatingMeasures;
import org.sonar.ce.task.projectanalysis.metric.Metric;
import org.sonar.ce.task.projectanalysis.metric.MetricRepository;

import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY;
import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
import static org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit.FILE;
import static org.sonar.core.issue.DefaultIssue.STATUS_TO_REVIEW;
import static org.sonar.server.security.SecurityReviewRating.computePercent;
import static org.sonar.server.security.SecurityReviewRating.computeRating;

public class SecurityReviewMeasuresVisitor extends PathAwareVisitorAdapter<SecurityReviewMeasuresVisitor.Counter> {

private final ComponentIssuesRepository componentIssuesRepository;
private final MeasureRepository measureRepository;
private final Metric securityReviewRatingMetric;
private final Metric securityHotspotsReviewedMetric;

public SecurityReviewMeasuresVisitor(ComponentIssuesRepository componentIssuesRepository, MeasureRepository measureRepository, MetricRepository metricRepository) {
super(FILE, POST_ORDER, SecurityReviewMeasuresVisitor.CounterFactory.INSTANCE);
this.componentIssuesRepository = componentIssuesRepository;
this.measureRepository = measureRepository;
this.securityReviewRatingMetric = metricRepository.getByKey(SECURITY_REVIEW_RATING_KEY);
this.securityHotspotsReviewedMetric = metricRepository.getByKey(SECURITY_HOTSPOTS_REVIEWED_KEY);
}

@Override
public void visitProject(Component project, Path<SecurityReviewMeasuresVisitor.Counter> path) {
computeMeasure(project, path);
}

@Override
public void visitDirectory(Component directory, PathAwareVisitor.Path<SecurityReviewMeasuresVisitor.Counter> path) {
computeMeasure(directory, path);
}

@Override
public void visitFile(Component file, PathAwareVisitor.Path<SecurityReviewMeasuresVisitor.Counter> path) {
computeMeasure(file, path);
}

private void computeMeasure(Component component, PathAwareVisitor.Path<SecurityReviewMeasuresVisitor.Counter> path) {
componentIssuesRepository.getIssues(component)
.stream()
.filter(issue -> issue.type().equals(SECURITY_HOTSPOT))
.forEach(issue -> path.current().processHotspot(issue));

Double percent = computePercent(path.current().hotspotsToReview, path.current().hotspotsReviewed);
measureRepository.add(component, securityHotspotsReviewedMetric, Measure.newMeasureBuilder().create(percent, securityHotspotsReviewedMetric.getDecimalScale()));
measureRepository.add(component, securityReviewRatingMetric, RatingMeasures.get(computeRating(percent)));

if (!path.isRoot()) {
path.parent().add(path.current());
}
}

static final class Counter {
private long hotspotsReviewed;
private long hotspotsToReview;

private Counter() {
// prevents instantiation
}

void processHotspot(Issue issue) {
if (issue.status().equals(STATUS_REVIEWED)) {
hotspotsReviewed++;
} else if (issue.status().equals(STATUS_TO_REVIEW)) {
hotspotsToReview++;
}
}

void add(Counter otherCounter) {
hotspotsReviewed += otherCounter.hotspotsReviewed;
hotspotsToReview += otherCounter.hotspotsToReview;
}
}

private static final class CounterFactory extends PathAwareVisitorAdapter.SimpleStackElementFactory<SecurityReviewMeasuresVisitor.Counter> {
public static final SecurityReviewMeasuresVisitor.CounterFactory INSTANCE = new SecurityReviewMeasuresVisitor.CounterFactory();

private CounterFactory() {
// prevents instantiation
}

@Override
public SecurityReviewMeasuresVisitor.Counter createForAny(Component component) {
return new SecurityReviewMeasuresVisitor.Counter();
}
}

}

server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitor.java → server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorForPortfoliosAndApplications.java View File

@@ -34,18 +34,16 @@ 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.Component.Type.PROJECT;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.SUBVIEW;

public class SecurityReviewRatingVisitor extends TypeAwareVisitorAdapter {
public class SecurityReviewRatingVisitorForPortfoliosAndApplications 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(new CrawlerDepthLimit.Builder(PROJECT).withViewsMaxDepth(SUBVIEW), Order.POST_ORDER);
public SecurityReviewRatingVisitorForPortfoliosAndApplications(MeasureRepository measureRepository, MetricRepository metricRepository) {
super(CrawlerDepthLimit.SUBVIEW, Order.POST_ORDER);
this.measureRepository = measureRepository;
this.nclocMetric = metricRepository.getByKey(NCLOC_KEY);
this.securityHostspotsMetric = metricRepository.getByKey(SECURITY_HOTSPOTS_KEY);
@@ -54,7 +52,7 @@ public class SecurityReviewRatingVisitor extends TypeAwareVisitorAdapter {

@Override
public void visitProject(Component project) {
computeMeasure(project);
// Do nothing
}

@Override
@@ -75,7 +73,7 @@ public class SecurityReviewRatingVisitor extends TypeAwareVisitorAdapter {
}
int ncloc = nclocMeasure.get().getIntValue();
int securityHotspots = securityHostspotsMeasure.get().getIntValue();
Rating rating = SecurityReviewRating.compute(ncloc, securityHotspots);
Rating rating = SecurityReviewRating.computeForPortfolios(ncloc, securityHotspots);
measureRepository.add(component, securityReviewRatingMetric, RatingMeasures.get(rating));
}


+ 283
- 0
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewMeasuresVisitorTest.java View File

@@ -0,0 +1,283 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.rules.RuleType;
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.issue.ComponentIssuesRepositoryRule;
import org.sonar.ce.task.projectanalysis.issue.FillComponentIssuesVisitorRule;
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.core.issue.DefaultIssue;
import org.sonar.core.util.Uuids;
import org.sonar.server.measure.Rating;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED;
import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_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.api.rule.Severity.MAJOR;
import static org.sonar.api.rule.Severity.MINOR;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
import static org.sonar.core.issue.DefaultIssue.STATUS_REVIEWED;
import static org.sonar.core.issue.DefaultIssue.STATUS_TO_REVIEW;
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;

public class SecurityReviewMeasuresVisitorTest {

private static final int PROJECT_REF = 1;
private static final int ROOT_DIR_REF = 12;
private static final int DIRECTORY_REF = 123;
private static final int FILE_1_REF = 1231;
private static final int FILE_2_REF = 1232;

static final Component ROOT_PROJECT = builder(Component.Type.PROJECT, PROJECT_REF).setKey("project")
.addChildren(
builder(DIRECTORY, ROOT_DIR_REF).setKey("dir")
.addChildren(
builder(DIRECTORY, DIRECTORY_REF).setKey("directory")
.addChildren(
builder(FILE, FILE_1_REF).setKey("file1").build(),
builder(FILE, FILE_2_REF).setKey("file2").build())
.build())
.build())
.build();

@Rule
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
@Rule
public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
.add(SECURITY_REVIEW_RATING)
.add(SECURITY_HOTSPOTS_REVIEWED);
@Rule
public ComponentIssuesRepositoryRule componentIssuesRepositoryRule = new ComponentIssuesRepositoryRule(treeRootHolder);
@Rule
public FillComponentIssuesVisitorRule fillComponentIssuesVisitorRule = new FillComponentIssuesVisitorRule(componentIssuesRepositoryRule, treeRootHolder);
@Rule
public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);

private VisitorsCrawler underTest = new VisitorsCrawler(asList(fillComponentIssuesVisitorRule,
new SecurityReviewMeasuresVisitor(componentIssuesRepositoryRule, measureRepository, metricRepository)));

@Test
public void compute_measures_when_100_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_SAFE),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, A, 100.0);
verifyMeasures(FILE_2_REF, A, 100.0);
verifyMeasures(DIRECTORY_REF, A, 100.0);
verifyMeasures(ROOT_DIR_REF, A, 100.0);
verifyMeasures(PROJECT_REF, A, 100.0);
}

@Test
public void compute_measures_when_more_than_80_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, A, 100.0);
verifyMeasures(FILE_2_REF, A, 80.0);
verifyMeasures(DIRECTORY_REF, A, 87.5);
verifyMeasures(ROOT_DIR_REF, A, 87.5);
verifyMeasures(PROJECT_REF, A, 87.5);
}

@Test
public void compute_measures_when_more_than_70_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, A, 100.0);
verifyMeasures(FILE_2_REF, B, 71.4);
verifyMeasures(DIRECTORY_REF, B, 75.0);
verifyMeasures(ROOT_DIR_REF, B, 75.0);
verifyMeasures(PROJECT_REF, B, 75.0);
}

@Test
public void compute_measures_when_more_than_50_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, C, 50.0);
verifyMeasures(FILE_2_REF, C, 60.0);
verifyMeasures(DIRECTORY_REF, C, 57.1);
verifyMeasures(ROOT_DIR_REF, C, 57.1);
verifyMeasures(PROJECT_REF, C, 57.1);
}

@Test
public void compute_measures_when_more_30_than_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, D, 33.3);
verifyMeasures(FILE_2_REF, D, 40.0);
verifyMeasures(DIRECTORY_REF, D, 37.5);
verifyMeasures(ROOT_DIR_REF, D, 37.5);
verifyMeasures(PROJECT_REF, D, 37.5);
}

@Test
public void compute_measures_when_less_than_30_percent_hotspots_reviewed() {
treeRootHolder.setRoot(ROOT_PROJECT);
fillComponentIssuesVisitorRule.setIssues(FILE_1_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED),
// Should not be taken into account
newIssue());
fillComponentIssuesVisitorRule.setIssues(FILE_2_REF,
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newHotspot(STATUS_TO_REVIEW, null),
newIssue());

underTest.visit(ROOT_PROJECT);

verifyMeasures(FILE_1_REF, D, 33.3);
verifyMeasures(FILE_2_REF, E, 0.0);
verifyMeasures(DIRECTORY_REF, E, 16.7);
verifyMeasures(ROOT_DIR_REF, E, 16.7);
verifyMeasures(PROJECT_REF, E, 16.7);
}

@Test
public void compute_A_rating_and_100_percent_when_no_hotspot() {
treeRootHolder.setRoot(ROOT_PROJECT);

underTest.visit(ROOT_PROJECT);

verifyMeasures(PROJECT_REF, A, 100.0);
}

private void verifyMeasures(int componentRef, Rating expectedReviewRating, double expectedHotspotsReviewed) {
verifySecurityReviewRating(componentRef, expectedReviewRating);
verifySecurityHotspotsReviewed(componentRef, expectedHotspotsReviewed);
}

private void verifySecurityReviewRating(int componentRef, Rating rating) {
Measure measure = measureRepository.getAddedRawMeasure(componentRef, SECURITY_REVIEW_RATING_KEY).get();
assertThat(measure.getIntValue()).isEqualTo(rating.getIndex());
assertThat(measure.getData()).isEqualTo(rating.name());
}

private void verifySecurityHotspotsReviewed(int componentRef, double percent) {
assertThat(measureRepository.getAddedRawMeasure(componentRef, SECURITY_HOTSPOTS_REVIEWED_KEY).get().getDoubleValue()).isEqualTo(percent);
}

private static DefaultIssue newHotspot(String status, @Nullable String resolution) {
return new DefaultIssue()
.setKey(Uuids.create())
.setSeverity(MINOR)
.setStatus(status)
.setResolution(resolution)
.setType(RuleType.SECURITY_HOTSPOT);
}

private static DefaultIssue newIssue() {
return new DefaultIssue()
.setKey(Uuids.create())
.setSeverity(MAJOR)
.setType(RuleType.BUG);
}

}

server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorTest.java → server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualitymodel/SecurityReviewRatingVisitorForPortfoliosAndApplicationsTest.java View File

@@ -38,15 +38,11 @@ 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;
import static org.sonar.server.measure.Rating.B;
import static org.sonar.server.measure.Rating.C;

public class SecurityReviewRatingVisitorTest {

private static final int PROJECT_REF = 1;
private static final Component PROJECT = builder(Component.Type.PROJECT, PROJECT_REF).setKey("project").build();
public class SecurityReviewRatingVisitorForPortfoliosAndApplicationsTest {

private static final int PORTFOLIO_REF = 10;
private static final int SUB_PORTFOLIO_1_REF = 11;
@@ -74,20 +70,7 @@ public class SecurityReviewRatingVisitorTest {
@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(C.getIndex());
assertThat(measure.getData()).isEqualTo(C.name());
}
private VisitorsCrawler underTest = new VisitorsCrawler(singletonList(new SecurityReviewRatingVisitorForPortfoliosAndApplications(measureRepository, metricRepository)));

@Test
public void compute_security_review_rating_on_portfolio() {
@@ -121,21 +104,21 @@ public class SecurityReviewRatingVisitorTest {

@Test
public void compute_nothing_when_no_ncloc() {
treeRootHolder.setRoot(PROJECT);
measureRepository.addRawMeasure(PROJECT_REF, SECURITY_HOTSPOTS_KEY, newMeasureBuilder().create(2));
treeRootHolder.setRoot(PORTFOLIO);
measureRepository.addRawMeasure(PORTFOLIO_REF, SECURITY_HOTSPOTS_KEY, newMeasureBuilder().create(2));

underTest.visit(PROJECT);
underTest.visit(PORTFOLIO);

assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, SECURITY_REVIEW_RATING_KEY)).isEmpty();
assertThat(measureRepository.getAddedRawMeasure(PORTFOLIO_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));
treeRootHolder.setRoot(PORTFOLIO);
measureRepository.addRawMeasure(PORTFOLIO_REF, NCLOC_KEY, newMeasureBuilder().create(1000));

underTest.visit(PROJECT);
underTest.visit(PORTFOLIO);

assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, SECURITY_REVIEW_RATING_KEY)).isEmpty();
assertThat(measureRepository.getAddedRawMeasure(PORTFOLIO_REF, SECURITY_REVIEW_RATING_KEY)).isEmpty();
}
}

+ 38
- 7
server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java View File

@@ -21,27 +21,58 @@ package org.sonar.server.security;

import org.sonar.server.measure.Rating;

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;

public class SecurityReviewRating {

private SecurityReviewRating() {
// Only static method
}

public static Rating compute(int ncloc, int securityHotspots) {
/**
* This code will be removed when updating computation of Security Review Rating for portfolios
*/
@Deprecated
public static Rating computeForPortfolios(int ncloc, int securityHotspots) {
if (ncloc == 0) {
return Rating.A;
return A;
}
double ratio = (double) securityHotspots * 1000d / (double) ncloc;
if (ratio <= 3d) {
return Rating.A;
return A;
} else if (ratio <= 10) {
return Rating.B;
return B;
} else if (ratio <= 15) {
return Rating.C;
return C;
} else if (ratio <= 25) {
return Rating.D;
return D;
} else {
return Rating.E;
return E;
}
}

public static Double computePercent(long hotspotsToReview, long hotspotsReviewed) {
long total = hotspotsToReview + hotspotsReviewed;
if (total == 0) {
return 100.0;
}
return hotspotsReviewed * 100.0 / total;
}

public static Rating computeRating(Double percent) {
if (percent >= 80.0) {
return A;
} else if (percent >= 70.0) {
return B;
} else if (percent >= 50.0) {
return C;
} else if (percent >= 30.0) {
return D;
}
return E;
}
}

+ 1
- 1
server/sonar-server-common/src/test/java/org/sonar/server/security/SecurityReviewRatingTest.java View File

@@ -62,7 +62,7 @@ public class SecurityReviewRatingTest {
@Test
@UseDataProvider("values")
public void compute_security_review_rating_on_project(int ncloc, int securityHotspots, Rating expectedRating) {
assertThat(SecurityReviewRating.compute(ncloc, securityHotspots)).isEqualTo(expectedRating);
assertThat(SecurityReviewRating.computeForPortfolios(ncloc, securityHotspots)).isEqualTo(expectedRating);
}

}

+ 10
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java View File

@@ -40,6 +40,7 @@ class IssueCounter {
private final Map<RuleType, Count> unresolvedByType = new EnumMap<>(RuleType.class);
private final Map<String, Count> byResolution = new HashMap<>();
private final Map<String, Count> byStatus = new HashMap<>();
private final Map<String, Count> hotspotsByStatus = new HashMap<>();
private final Count unresolved = new Count();

IssueCounter(Collection<IssueGroupDto> groups) {
@@ -51,6 +52,11 @@ class IssueCounter {
.computeIfAbsent(SECURITY_HOTSPOT, k -> new Count())
.add(group);
}
if (group.getStatus() != null) {
hotspotsByStatus
.computeIfAbsent(group.getStatus(), k -> new Count())
.add(group);
}
continue;
}
if (group.getResolution() == null) {
@@ -113,6 +119,10 @@ class IssueCounter {
return value(unresolved, onlyInLeak);
}

public long countHotspotsByStatus(String status, boolean onlyInLeak) {
return value(hotspotsByStatus.get(status), onlyInLeak);
}

private static long value(@Nullable Count count, boolean onlyInLeak) {
if (count == null) {
return 0;

+ 10
- 6
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java View File

@@ -28,10 +28,11 @@ 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;
import static org.sonar.server.security.SecurityReviewRating.computePercent;
import static org.sonar.server.security.SecurityReviewRating.computeRating;

public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory {

@@ -109,9 +110,12 @@ public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory
(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)),
(context, issues) -> context
.setValue(computeRating(computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))))),

new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false,
(context, issues) -> context
.setValue(computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false)))),

new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))),
@@ -181,7 +185,7 @@ public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory
if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
return debt / devCost.get();
}
return 0d;
return 0.0;
}

private static double newDebtDensity(IssueMetricFormula.Context context) {
@@ -190,7 +194,7 @@ public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory
if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
return debt / devCost.get();
}
return 0d;
return 0.0;
}

private static double effortToReachMaintainabilityRatingA(IssueMetricFormula.Context context) {

+ 18
- 5
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java View File

@@ -120,16 +120,29 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.SECURITY_HOTSPOT).setCount(7),
// not hotspots
newGroup(RuleType.BUG).setCount(11))
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
}

@Test
public void test_security_review_rating() {
withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.A);
with(
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
.assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.B);

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 test_security_hotspots_reviewed() {
with(
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, 75.0);

withNoIssues()
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, 100.0);
}

@Test

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1820,6 +1820,7 @@ metric_domain.Maintainability=Maintainability
metric_domain.Releasability=Releasability
metric_domain.Reliability=Reliability
metric_domain.Security=Security
metric_domain.SecurityReview=Security Review
metric_domain.Issues=Issues
metric_domain.Duplications=Duplications
metric_domain.Coverage=Coverage
@@ -2224,6 +2225,9 @@ 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.security_hotspots_reviewed.description=Security Hotspots Reviewed
metric.security_hotspots_reviewed.name=Security Hotspots Reviewed
metric.security_hotspots_reviewed.extra_short_name=Hotspots Reviewed
metric.skipped_tests.description=Number of skipped unit tests
metric.skipped_tests.name=Skipped Unit Tests
metric.skipped_tests.short_name=Skipped

+ 34
- 5
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java View File

@@ -73,6 +73,11 @@ public final class CoreMetrics {
*/
public static String DOMAIN_SECURITY = "Security";

/**
* @since 8.2
*/
public static String DOMAIN_SECURITY_REVIEW = "SecurityReview";

/**
* @since 5.5
*/
@@ -1151,7 +1156,7 @@ public final class CoreMetrics {
.setDescription("Security Hotspots")
.setDirection(Metric.DIRECTION_WORST)
.setQualitative(false)
.setDomain(DOMAIN_SECURITY)
.setDomain(DOMAIN_SECURITY_REVIEW)
.setBestValue(0.0)
.setOptimizedBestValue(true)
.create();
@@ -1168,7 +1173,7 @@ public final class CoreMetrics {
.setDescription("New Security Hotspots")
.setDirection(Metric.DIRECTION_WORST)
.setQualitative(true)
.setDomain(DOMAIN_SECURITY)
.setDomain(DOMAIN_SECURITY_REVIEW)
.setBestValue(0.0)
.setOptimizedBestValue(true)
.setDeleteHistoricalData(true)
@@ -1506,6 +1511,13 @@ public final class CoreMetrics {
.setWorstValue(5.0)
.create();


// --------------------------------------------------------------------------------------------------------------------
//
// SECURITY REVIEW
//
// --------------------------------------------------------------------------------------------------------------------

/**
* @since 7.8
*/
@@ -1516,11 +1528,28 @@ public final class CoreMetrics {
*/
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)
.setDomain(DOMAIN_SECURITY_REVIEW)
.setDirection(Metric.DIRECTION_WORST)
.setQualitative(true)
.setBestValue(1d)
.setWorstValue(5d)
.setBestValue(1.0)
.setWorstValue(5.0)
.create();

/**
* @since 8.2
*/
public static final String SECURITY_HOTSPOTS_REVIEWED_KEY = "security_hotspots_reviewed";

/**
* @since 8.2
*/
public static final Metric<Integer> SECURITY_HOTSPOTS_REVIEWED = new Metric.Builder(SECURITY_HOTSPOTS_REVIEWED_KEY, "Security Hotspots Reviewed", Metric.ValueType.PERCENT)
.setDescription("Security Hotspots Reviewed")
.setDomain(DOMAIN_SECURITY_REVIEW)
.setDirection(Metric.DIRECTION_BETTER)
.setQualitative(true)
.setWorstValue(0.0)
.setBestValue(100.0)
.create();

// --------------------------------------------------------------------------------------------------------------------

Loading…
Cancel
Save