@@ -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, | |||
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; |
@@ -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) { |
@@ -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 |
@@ -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 |
@@ -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(); | |||
// -------------------------------------------------------------------------------------------------------------------- |