RenameDaysBeforeDeletingInactiveSLBSetting.class) | RenameDaysBeforeDeletingInactiveSLBSetting.class) | ||||
.add(3112, "Migrate short and long living branches types to common BRANCH type", MigrateSlbsAndLlbsToCommonType.class) | .add(3112, "Migrate short and long living branches types to common BRANCH type", MigrateSlbsAndLlbsToCommonType.class) | ||||
.add(3113, "Migrate short and long living branches types to common BRANCH type in ce tasks table", | .add(3113, "Migrate short and long living branches types to common BRANCH type in ce tasks table", | ||||
MigrateSlbsAndLlbsToCommonTypeInCeTasks.class); | |||||
MigrateSlbsAndLlbsToCommonTypeInCeTasks.class) | |||||
.add(3114, "Drop 'In Review' Security Hotspots status ", DropSecurityHotSpotsInReviewStatus.class); | |||||
} | } | ||||
} | } |
/* | |||||
* 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.server.platform.db.migration.version.v81; | |||||
import java.sql.SQLException; | |||||
import org.sonar.api.utils.System2; | |||||
import org.sonar.db.Database; | |||||
import org.sonar.server.platform.db.migration.step.DataChange; | |||||
import org.sonar.server.platform.db.migration.step.MassUpdate; | |||||
public class DropSecurityHotSpotsInReviewStatus extends DataChange { | |||||
private System2 system; | |||||
public DropSecurityHotSpotsInReviewStatus(Database db, System2 system) { | |||||
super(db); | |||||
this.system = system; | |||||
} | |||||
@Override | |||||
protected void execute(Context context) throws SQLException { | |||||
MassUpdate massUpdate = context.prepareMassUpdate(); | |||||
massUpdate.select("select id,kee from issues where status = 'IN_REVIEW'"); | |||||
massUpdate.update("update issues set status = 'TO_REVIEW' where id = ? and status = 'IN_REVIEW'"); | |||||
massUpdate.update("insert into issue_changes(issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date) " + | |||||
"VALUES(?, 'diff', 'status=IN_REVIEW|TO_REVIEW', ?, ?, ?)"); | |||||
massUpdate.execute((row, update, updateIndex) -> { | |||||
if (updateIndex == 0) { | |||||
update.setLong(1, row.getLong(1)); | |||||
} else if (updateIndex == 1) { | |||||
long currentTime = system.now(); | |||||
update.setString(1, row.getString(2)) | |||||
.setLong(2, currentTime) | |||||
.setLong(3, currentTime) | |||||
.setLong(4, currentTime); | |||||
} | |||||
return true; | |||||
}); | |||||
} | |||||
} |
@Test | @Test | ||||
public void verify_migration_count() { | public void verify_migration_count() { | ||||
verifyMigrationCount(underTest, 14); | |||||
verifyMigrationCount(underTest, 15); | |||||
} | } | ||||
} | } |
/* | |||||
* 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.server.platform.db.migration.version.v81; | |||||
import java.sql.SQLException; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
import java.util.function.Function; | |||||
import java.util.stream.Collectors; | |||||
import java.util.stream.IntStream; | |||||
import org.junit.Rule; | |||||
import org.junit.Test; | |||||
import org.junit.rules.ExpectedException; | |||||
import org.sonar.api.utils.System2; | |||||
import org.sonar.db.CoreDbTester; | |||||
import org.sonar.server.platform.db.migration.step.DataChange; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
import static org.sonar.api.issue.Issue.STATUSES; | |||||
public class DropSecurityHotSpotsInReviewStatusTest { | |||||
private final static String ISSUES_TABLE_NAME = "issues"; | |||||
private final static int NUMBER_OF_ISSUES_IN_REVIEW = 3; | |||||
@Rule | |||||
public CoreDbTester db = CoreDbTester.createForSchema(DropSecurityHotSpotsInReviewStatusTest.class, "schema.sql"); | |||||
@Rule | |||||
public ExpectedException expectedException = ExpectedException.none(); | |||||
private System2 system2 = System2.INSTANCE; | |||||
private DataChange underTest = new DropSecurityHotSpotsInReviewStatus(db.database(), system2); | |||||
@Test | |||||
public void should_change_IN_REVIEW_statuses_only() throws SQLException { | |||||
Map<Integer, String> statuses = IntStream.range(0, STATUSES.size()) | |||||
.boxed() | |||||
.collect(Collectors.toMap(Function.identity(), STATUSES::get)); | |||||
statuses.forEach(this::insertIssue); | |||||
int startIndex = STATUSES.size(); | |||||
int endIndex = startIndex + NUMBER_OF_ISSUES_IN_REVIEW; | |||||
IntStream.range(startIndex, endIndex).forEach(value -> insertIssue(value, "IN_REVIEW")); | |||||
underTest.execute(); | |||||
IntStream.range(startIndex, endIndex).forEach(this::assertIssueChanged); | |||||
statuses.forEach(this::assertIssueNotChanged); | |||||
// should not fail if executed twice | |||||
underTest.execute(); | |||||
} | |||||
@Test | |||||
public void should_not_fail_if_no_issues() throws SQLException { | |||||
underTest.execute(); | |||||
assertThat(db.countRowsOfTable("issues")).isEqualTo(0); | |||||
} | |||||
private void assertIssueChanged(int issueId) { | |||||
List<Map<String, Object>> row = db.select(String.format("select status from issues where kee = '%s'", "issue-key-" + issueId)); | |||||
assertThat(row).hasSize(1); | |||||
assertThat(row.get(0).get("STATUS")) | |||||
.isEqualTo("TO_REVIEW"); | |||||
List<Map<String, Object>> changelogRows = db.select(String.format("select change_type, change_data, created_at, updated_at, issue_change_creation_date" + | |||||
" from issue_changes where issue_key = '%s'", "issue-key-" + issueId)); | |||||
assertThat(changelogRows).hasSize(1); | |||||
Map<String, Object> changelogRow = changelogRows.get(0); | |||||
assertThat(changelogRow.get("CHANGE_TYPE")).isEqualTo("diff"); | |||||
assertThat(changelogRow.get("CHANGE_DATA")).isEqualTo("status=IN_REVIEW|TO_REVIEW"); | |||||
assertThat(changelogRow.get("CREATED_AT")).isNotNull(); | |||||
assertThat(changelogRow.get("UPDATED_AT")).isNotNull(); | |||||
assertThat(changelogRow.get("ISSUE_CHANGE_CREATION_DATE")).isNotNull(); | |||||
} | |||||
private void assertIssueNotChanged(int issueId, String expectedStatus) { | |||||
List<Map<String, Object>> row = db.select(String.format("select status from issues where kee = '%s'", "issue-key-" + issueId)); | |||||
assertThat(row).hasSize(1); | |||||
assertThat(row.get(0).get("STATUS")) | |||||
.isEqualTo(expectedStatus); | |||||
List<Map<String, Object>> changelogRows = db.select(String.format("select change_type, change_data, created_at, updated_at, issue_change_creation_date" + | |||||
" from issue_changes where issue_key = '%s'", "issue-key-" + issueId)); | |||||
assertThat(changelogRows).isEmpty(); | |||||
} | |||||
private void insertIssue(int issueId, String status) { | |||||
db.executeInsert(ISSUES_TABLE_NAME, | |||||
"kee", "issue-key-" + issueId, | |||||
"status", status, | |||||
"manual_severity", false); | |||||
} | |||||
} |
CREATE TABLE "ISSUES"( | |||||
"ID" BIGINT NOT NULL AUTO_INCREMENT (1,1), | |||||
"KEE" VARCHAR(50) NOT NULL, | |||||
"RULE_ID" INTEGER, | |||||
"SEVERITY" VARCHAR(10), | |||||
"MANUAL_SEVERITY" BOOLEAN NOT NULL, | |||||
"MESSAGE" VARCHAR(4000), | |||||
"LINE" INTEGER, | |||||
"GAP" DOUBLE, | |||||
"STATUS" VARCHAR(20), | |||||
"RESOLUTION" VARCHAR(20), | |||||
"CHECKSUM" VARCHAR(1000), | |||||
"REPORTER" VARCHAR(255), | |||||
"ASSIGNEE" VARCHAR(255), | |||||
"AUTHOR_LOGIN" VARCHAR(255), | |||||
"ACTION_PLAN_KEY" VARCHAR(50), | |||||
"ISSUE_ATTRIBUTES" VARCHAR(4000), | |||||
"EFFORT" INTEGER, | |||||
"CREATED_AT" BIGINT, | |||||
"UPDATED_AT" BIGINT, | |||||
"ISSUE_CREATION_DATE" BIGINT, | |||||
"ISSUE_UPDATE_DATE" BIGINT, | |||||
"ISSUE_CLOSE_DATE" BIGINT, | |||||
"TAGS" VARCHAR(4000), | |||||
"COMPONENT_UUID" VARCHAR(50), | |||||
"PROJECT_UUID" VARCHAR(50), | |||||
"LOCATIONS" BLOB, | |||||
"ISSUE_TYPE" TINYINT, | |||||
"FROM_HOTSPOT" BOOLEAN | |||||
); | |||||
ALTER TABLE "ISSUES" ADD CONSTRAINT "PK_ISSUES" PRIMARY KEY("ID"); | |||||
CREATE INDEX "ISSUES_ASSIGNEE" ON "ISSUES"("ASSIGNEE"); | |||||
CREATE INDEX "ISSUES_COMPONENT_UUID" ON "ISSUES"("COMPONENT_UUID"); | |||||
CREATE INDEX "ISSUES_CREATION_DATE" ON "ISSUES"("ISSUE_CREATION_DATE"); | |||||
CREATE UNIQUE INDEX "ISSUES_KEE" ON "ISSUES"("KEE"); | |||||
CREATE INDEX "ISSUES_PROJECT_UUID" ON "ISSUES"("PROJECT_UUID"); | |||||
CREATE INDEX "ISSUES_RESOLUTION" ON "ISSUES"("RESOLUTION"); | |||||
CREATE INDEX "ISSUES_RULE_ID" ON "ISSUES"("RULE_ID"); | |||||
CREATE INDEX "ISSUES_UPDATED_AT" ON "ISSUES"("UPDATED_AT"); | |||||
CREATE TABLE "ISSUE_CHANGES"( | |||||
"ID" BIGINT NOT NULL AUTO_INCREMENT (1,1), | |||||
"KEE" VARCHAR(50), | |||||
"ISSUE_KEY" VARCHAR(50) NOT NULL, | |||||
"USER_LOGIN" VARCHAR(255), | |||||
"CHANGE_TYPE" VARCHAR(20), | |||||
"CHANGE_DATA" CLOB(2147483647), | |||||
"CREATED_AT" BIGINT, | |||||
"UPDATED_AT" BIGINT, | |||||
"ISSUE_CHANGE_CREATION_DATE" BIGINT | |||||
); | |||||
ALTER TABLE "ISSUE_CHANGES" ADD CONSTRAINT "PK_ISSUE_CHANGES" PRIMARY KEY("ID"); | |||||
CREATE INDEX "ISSUE_CHANGES_ISSUE_KEY" ON "ISSUE_CHANGES"("ISSUE_KEY"); | |||||
CREATE INDEX "ISSUE_CHANGES_KEE" ON "ISSUE_CHANGES"("KEE"); |
private final String category; | private final String category; | ||||
private final long vulnerabilities; | private final long vulnerabilities; | ||||
private final OptionalInt vulnerabiliyRating; | private final OptionalInt vulnerabiliyRating; | ||||
private final long inReviewSecurityHotspots; | |||||
private final long toReviewSecurityHotspots; | private final long toReviewSecurityHotspots; | ||||
private final long reviewedSecurityHotspots; | private final long reviewedSecurityHotspots; | ||||
private final List<SecurityStandardCategoryStatistics> children; | private final List<SecurityStandardCategoryStatistics> children; | ||||
private long activeRules; | private long activeRules; | ||||
private long totalRules; | private long totalRules; | ||||
public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long inReviewSecurityHotspots, long toReviewSecurityHotspots, | |||||
public SecurityStandardCategoryStatistics(String category, long vulnerabilities, OptionalInt vulnerabiliyRating, long toReviewSecurityHotspots, | |||||
long reviewedSecurityHotspots, @Nullable List<SecurityStandardCategoryStatistics> children) { | long reviewedSecurityHotspots, @Nullable List<SecurityStandardCategoryStatistics> children) { | ||||
this.category = category; | this.category = category; | ||||
this.vulnerabilities = vulnerabilities; | this.vulnerabilities = vulnerabilities; | ||||
this.vulnerabiliyRating = vulnerabiliyRating; | this.vulnerabiliyRating = vulnerabiliyRating; | ||||
this.inReviewSecurityHotspots = inReviewSecurityHotspots; | |||||
this.toReviewSecurityHotspots = toReviewSecurityHotspots; | this.toReviewSecurityHotspots = toReviewSecurityHotspots; | ||||
this.reviewedSecurityHotspots = reviewedSecurityHotspots; | this.reviewedSecurityHotspots = reviewedSecurityHotspots; | ||||
this.children = children; | this.children = children; | ||||
return vulnerabiliyRating; | return vulnerabiliyRating; | ||||
} | } | ||||
public long getInReviewSecurityHotspots() { | |||||
return inReviewSecurityHotspots; | |||||
} | |||||
public long getToReviewSecurityHotspots() { | public long getToReviewSecurityHotspots() { | ||||
return toReviewSecurityHotspots; | return toReviewSecurityHotspots; | ||||
} | } |
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | ||||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | import static org.sonar.api.issue.Issue.STATUS_CLOSED; | ||||
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | ||||
import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW; | |||||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | import static org.sonar.api.issue.Issue.STATUS_OPEN; | ||||
import static org.sonar.api.issue.Issue.STATUS_REOPENED; | import static org.sonar.api.issue.Issue.STATUS_REOPENED; | ||||
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | ||||
public void start() { | public void start() { | ||||
StateMachine.Builder builder = StateMachine.builder() | StateMachine.Builder builder = StateMachine.builder() | ||||
.states(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED, | .states(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED, | ||||
STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED); | |||||
STATUS_TO_REVIEW, STATUS_REVIEWED); | |||||
buildManualTransitions(builder); | buildManualTransitions(builder); | ||||
buildAutomaticTransitions(builder); | buildAutomaticTransitions(builder); | ||||
buildSecurityHotspotTransitions(builder); | buildSecurityHotspotTransitions(builder); | ||||
private static void buildSecurityHotspotTransitions(StateMachine.Builder builder) { | private static void buildSecurityHotspotTransitions(StateMachine.Builder builder) { | ||||
builder | builder | ||||
.transition(Transition.builder(DefaultTransitions.SET_AS_IN_REVIEW) | |||||
.from(STATUS_TO_REVIEW).to(STATUS_IN_REVIEW) | |||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | |||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | |||||
.build()) | |||||
.transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED) | .transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED) | ||||
.from(STATUS_TO_REVIEW).to(STATUS_REVIEWED) | .from(STATUS_TO_REVIEW).to(STATUS_REVIEWED) | ||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | .conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | ||||
.functions(new SetResolution(RESOLUTION_FIXED)) | .functions(new SetResolution(RESOLUTION_FIXED)) | ||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | ||||
.build()) | .build()) | ||||
.transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED) | |||||
.from(STATUS_IN_REVIEW).to(STATUS_REVIEWED) | |||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | |||||
.functions(new SetResolution(RESOLUTION_FIXED)) | |||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | |||||
.build()) | |||||
.transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED) | .transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED) | ||||
.from(STATUS_OPEN).to(STATUS_REVIEWED) | .from(STATUS_OPEN).to(STATUS_REVIEWED) | ||||
.conditions(new HasType(RuleType.VULNERABILITY), IsManualVulnerability.INSTANCE) | .conditions(new HasType(RuleType.VULNERABILITY), IsManualVulnerability.INSTANCE) | ||||
.functions(new SetResolution(null), new SetType(RuleType.VULNERABILITY)) | .functions(new SetResolution(null), new SetType(RuleType.VULNERABILITY)) | ||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | ||||
.build()) | .build()) | ||||
.transition(Transition.builder(DefaultTransitions.OPEN_AS_VULNERABILITY) | |||||
.from(STATUS_IN_REVIEW).to(STATUS_OPEN) | |||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | |||||
.functions(new SetType(RuleType.VULNERABILITY)) | |||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | |||||
.build()) | |||||
.transition(Transition.builder(DefaultTransitions.OPEN_AS_VULNERABILITY) | .transition(Transition.builder(DefaultTransitions.OPEN_AS_VULNERABILITY) | ||||
.from(STATUS_TO_REVIEW).to(STATUS_OPEN) | .from(STATUS_TO_REVIEW).to(STATUS_OPEN) | ||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | .conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | ||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | .requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | ||||
.build()) | .build()) | ||||
.transition(Transition.builder(DefaultTransitions.RESET_AS_TO_REVIEW) | |||||
.from(STATUS_IN_REVIEW).to(STATUS_TO_REVIEW) | |||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | |||||
.functions(new SetResolution(null)) | |||||
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN) | |||||
.build()) | |||||
.transition(Transition.builder(DefaultTransitions.RESET_AS_TO_REVIEW) | .transition(Transition.builder(DefaultTransitions.RESET_AS_TO_REVIEW) | ||||
.from(STATUS_REVIEWED).to(STATUS_TO_REVIEW) | .from(STATUS_REVIEWED).to(STATUS_TO_REVIEW) | ||||
.conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | .conditions(new HasType(RuleType.SECURITY_HOTSPOT)) | ||||
.functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE) | .functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE) | ||||
.automatic() | .automatic() | ||||
.build()) | .build()) | ||||
.transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION) | |||||
.from(STATUS_IN_REVIEW).to(STATUS_CLOSED) | |||||
.conditions(IsBeingClosed.INSTANCE, new HasType(RuleType.SECURITY_HOTSPOT)) | |||||
.functions(SetClosed.INSTANCE, SetCloseDate.INSTANCE) | |||||
.automatic() | |||||
.build()) | |||||
.transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION) | .transition(Transition.builder(AUTOMATIC_CLOSE_TRANSITION) | ||||
.from(STATUS_REVIEWED).to(STATUS_CLOSED) | .from(STATUS_REVIEWED).to(STATUS_CLOSED) | ||||
.conditions(IsBeingClosed.INSTANCE, new HasType(RuleType.SECURITY_HOTSPOT)) | .conditions(IsBeingClosed.INSTANCE, new HasType(RuleType.SECURITY_HOTSPOT)) |
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | ||||
import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED; | import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED; | ||||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | import static org.sonar.api.issue.Issue.STATUS_CLOSED; | ||||
import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW; | |||||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | import static org.sonar.api.issue.Issue.STATUS_OPEN; | ||||
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | ||||
import static org.sonar.api.issue.Issue.STATUS_REVIEWED; | import static org.sonar.api.issue.Issue.STATUS_REVIEWED; | ||||
@RunWith(DataProviderRunner.class) | @RunWith(DataProviderRunner.class) | ||||
public class IssueWorkflowForSecurityHotspotsTest { | public class IssueWorkflowForSecurityHotspotsTest { | ||||
private static final String[] ALL_STATUSES_LEADING_TO_CLOSED = new String[] {STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_RESOLVED}; | |||||
private static final String[] ALL_STATUSES_LEADING_TO_CLOSED = new String[] {STATUS_TO_REVIEW, STATUS_RESOLVED}; | |||||
private static final String[] SUPPORTED_RESOLUTIONS_FOR_UNCLOSING = new String[] {RESOLUTION_FIXED, RESOLUTION_REMOVED}; | private static final String[] SUPPORTED_RESOLUTIONS_FOR_UNCLOSING = new String[] {RESOLUTION_FIXED, RESOLUTION_REMOVED}; | ||||
List<Transition> transitions = underTest.outTransitions(issue); | List<Transition> transitions = underTest.outTransitions(issue); | ||||
assertThat(keys(transitions)).containsExactlyInAnyOrder("setinreview", "resolveasreviewed", "openasvulnerability"); | |||||
} | |||||
@Test | |||||
public void list_out_transitions_in_status_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue().setType(RuleType.SECURITY_HOTSPOT).setStatus(STATUS_IN_REVIEW); | |||||
List<Transition> transitions = underTest.outTransitions(issue); | |||||
assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "openasvulnerability", "resetastoreview"); | |||||
assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "openasvulnerability"); | |||||
} | } | ||||
@Test | @Test | ||||
assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "resetastoreview"); | assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "resetastoreview"); | ||||
} | } | ||||
@Test | |||||
public void set_as_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue() | |||||
.setType(RuleType.SECURITY_HOTSPOT) | |||||
.setIsFromHotspot(true) | |||||
.setStatus(STATUS_TO_REVIEW); | |||||
boolean result = underTest.doManualTransition(issue, DefaultTransitions.SET_AS_IN_REVIEW, IssueChangeContext.createUser(new Date(), "USER1")); | |||||
assertThat(result).isTrue(); | |||||
assertThat(issue.getStatus()).isEqualTo(STATUS_IN_REVIEW); | |||||
assertThat(issue.resolution()).isNull(); | |||||
} | |||||
@Test | @Test | ||||
public void resolve_as_reviewed_from_to_review() { | public void resolve_as_reviewed_from_to_review() { | ||||
underTest.start(); | underTest.start(); | ||||
assertThat(issue.resolution()).isEqualTo(RESOLUTION_FIXED); | assertThat(issue.resolution()).isEqualTo(RESOLUTION_FIXED); | ||||
} | } | ||||
@Test | |||||
public void resolve_as_reviewed_from_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue() | |||||
.setType(RuleType.SECURITY_HOTSPOT) | |||||
.setIsFromHotspot(true) | |||||
.setStatus(STATUS_IN_REVIEW) | |||||
.setResolution(null); | |||||
boolean result = underTest.doManualTransition(issue, DefaultTransitions.RESOLVE_AS_REVIEWED, IssueChangeContext.createUser(new Date(), "USER1")); | |||||
assertThat(result).isTrue(); | |||||
assertThat(issue.getStatus()).isEqualTo(STATUS_REVIEWED); | |||||
assertThat(issue.resolution()).isEqualTo(RESOLUTION_FIXED); | |||||
} | |||||
@Test | |||||
public void open_as_vulnerability_from_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue() | |||||
.setType(RuleType.SECURITY_HOTSPOT) | |||||
.setIsFromHotspot(true) | |||||
.setStatus(STATUS_IN_REVIEW) | |||||
.setResolution(null); | |||||
boolean result = underTest.doManualTransition(issue, DefaultTransitions.OPEN_AS_VULNERABILITY, IssueChangeContext.createUser(new Date(), "USER1")); | |||||
assertThat(result).isTrue(); | |||||
assertThat(issue.type()).isEqualTo(RuleType.VULNERABILITY); | |||||
assertThat(issue.getStatus()).isEqualTo(Issue.STATUS_OPEN); | |||||
assertThat(issue.resolution()).isNull(); | |||||
} | |||||
@Test | @Test | ||||
public void open_as_vulnerability_from_to_review() { | public void open_as_vulnerability_from_to_review() { | ||||
underTest.start(); | underTest.start(); | ||||
assertThat(issue.resolution()).isNull(); | assertThat(issue.resolution()).isNull(); | ||||
} | } | ||||
@Test | |||||
public void reset_as_to_review_from_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue() | |||||
.setType(RuleType.SECURITY_HOTSPOT) | |||||
.setIsFromHotspot(true) | |||||
.setStatus(STATUS_IN_REVIEW) | |||||
.setResolution(null); | |||||
boolean result = underTest.doManualTransition(issue, DefaultTransitions.RESET_AS_TO_REVIEW, IssueChangeContext.createUser(new Date(), "USER1")); | |||||
assertThat(result).isTrue(); | |||||
assertThat(issue.type()).isEqualTo(RuleType.SECURITY_HOTSPOT); | |||||
assertThat(issue.getStatus()).isEqualTo(STATUS_TO_REVIEW); | |||||
assertThat(issue.resolution()).isNull(); | |||||
} | |||||
@Test | @Test | ||||
public void reset_as_to_review_from_opened_as_vulnerability() { | public void reset_as_to_review_from_opened_as_vulnerability() { | ||||
underTest.start(); | underTest.start(); | ||||
assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND)); | assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND)); | ||||
} | } | ||||
@Test | |||||
public void automatically_close_resolved_security_hotspots_in_status_in_review() { | |||||
underTest.start(); | |||||
DefaultIssue issue = new DefaultIssue() | |||||
.setType(RuleType.SECURITY_HOTSPOT) | |||||
.setResolution(null) | |||||
.setStatus(STATUS_IN_REVIEW) | |||||
.setNew(false) | |||||
.setBeingClosed(true); | |||||
Date now = new Date(); | |||||
underTest.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); | |||||
assertThat(issue.resolution()).isEqualTo(RESOLUTION_FIXED); | |||||
assertThat(issue.status()).isEqualTo(STATUS_CLOSED); | |||||
assertThat(issue.closeDate()).isNotNull(); | |||||
assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND)); | |||||
} | |||||
@Test | @Test | ||||
public void automatically_close_resolved_security_hotspots_in_status_reviewed() { | public void automatically_close_resolved_security_hotspots_in_status_reviewed() { | ||||
underTest.start(); | underTest.start(); |
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | ||||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | import static org.sonar.api.issue.Issue.STATUS_CLOSED; | ||||
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | ||||
import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW; | |||||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | import static org.sonar.api.issue.Issue.STATUS_OPEN; | ||||
import static org.sonar.api.issue.Issue.STATUS_REOPENED; | import static org.sonar.api.issue.Issue.STATUS_REOPENED; | ||||
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | ||||
// issues statuses | // issues statuses | ||||
expectedStatus.addAll(Arrays.asList(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED)); | expectedStatus.addAll(Arrays.asList(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED)); | ||||
// hostpots statuses | // hostpots statuses | ||||
expectedStatus.addAll(Arrays.asList(STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)); | |||||
expectedStatus.addAll(Arrays.asList(STATUS_TO_REVIEW, STATUS_REVIEWED)); | |||||
assertThat(underTest.statusKeys()).containsExactlyInAnyOrder(expectedStatus.toArray(new String[]{})); | assertThat(underTest.statusKeys()).containsExactlyInAnyOrder(expectedStatus.toArray(new String[]{})); | ||||
} | } |
long toReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) | long toReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get(AGG_TO_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) | ||||
.getValue(); | .getValue(); | ||||
long inReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get(AGG_IN_REVIEW_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) | |||||
.getValue(); | |||||
long reviewedSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get(AGG_REVIEWED_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) | long reviewedSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get(AGG_REVIEWED_SECURITY_HOTSPOTS)).getAggregations().get(AGG_COUNT)) | ||||
.getValue(); | .getValue(); | ||||
return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, inReviewSecurityHotspots, toReviewSecurityHotspots, | |||||
return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots, | |||||
reviewedSecurityHotspots, children); | reviewedSecurityHotspots, children); | ||||
} | } | ||||
import org.junit.Rule; | import org.junit.Rule; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.junit.rules.ExpectedException; | import org.junit.rules.ExpectedException; | ||||
import org.sonar.api.impl.utils.TestSystem2; | |||||
import org.sonar.api.issue.Issue; | import org.sonar.api.issue.Issue; | ||||
import org.sonar.api.rule.Severity; | import org.sonar.api.rule.Severity; | ||||
import org.sonar.api.rules.RuleType; | import org.sonar.api.rules.RuleType; | ||||
import org.sonar.api.utils.System2; | import org.sonar.api.utils.System2; | ||||
import org.sonar.api.impl.utils.TestSystem2; | |||||
import org.sonar.db.DbTester; | import org.sonar.db.DbTester; | ||||
import org.sonar.db.component.ComponentDto; | import org.sonar.db.component.ComponentDto; | ||||
import org.sonar.db.organization.OrganizationDto; | import org.sonar.db.organization.OrganizationDto; | ||||
assertThat(cweByOwasp.get("a1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | assertThat(cweByOwasp.get("a1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | ||||
SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | ||||
SecurityStandardCategoryStatistics::getInReviewSecurityHotspots, SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
.containsExactlyInAnyOrder( | .containsExactlyInAnyOrder( | ||||
tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), | |||||
tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), | |||||
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); | |||||
tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L), | |||||
tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L), | |||||
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L)); | |||||
assertThat(cweByOwasp.get("a3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | assertThat(cweByOwasp.get("a3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | ||||
SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | ||||
SecurityStandardCategoryStatistics::getInReviewSecurityHotspots, SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
.containsExactlyInAnyOrder( | .containsExactlyInAnyOrder( | ||||
tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), | |||||
tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 1L /* toReviewHotspot */, 0L), | |||||
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); | |||||
tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L), | |||||
tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L/* toReviewHotspot */, 0L), | |||||
tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L)); | |||||
} | } | ||||
private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) { | private List<SecurityStandardCategoryStatistics> indexIssuesAndAssertOwaspReport(boolean includeCwe) { | ||||
OrganizationDto org = newOrganizationDto(); | OrganizationDto org = newOrganizationDto(); | ||||
ComponentDto project = newPrivateProjectDto(org); | ComponentDto project = newPrivateProjectDto(org); | ||||
indexIssues( | indexIssues( | ||||
newDoc("openvul1", project).setOwaspTop10(asList("a1", "a3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR), | |||||
newDoc("openvul2", project).setOwaspTop10(asList("a3", "a6")).setCwe(asList("123")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED).setSeverity(Severity.MINOR), | |||||
newDoc("openvul1", project).setOwaspTop10(asList("a1", "a3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) | |||||
.setSeverity(Severity.MAJOR), | |||||
newDoc("openvul2", project).setOwaspTop10(asList("a3", "a6")).setCwe(asList("123")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) | |||||
.setSeverity(Severity.MINOR), | |||||
newDoc("notowaspvul", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | newDoc("notowaspvul", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | ||||
newDoc("toreviewhotspot1", project).setOwaspTop10(asList("a1", "a3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot1", project).setOwaspTop10(asList("a1", "a3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT) | |||||
.setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot2", project).setOwaspTop10(asList("a3", "a6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | newDoc("toreviewhotspot2", project).setOwaspTop10(asList("a3", "a6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | ||||
newDoc("inreviewhotspot", project).setOwaspTop10(asList("a5", "a3")).setCwe(asList("456")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_IN_REVIEW), | |||||
newDoc("reviewedHotspot", project).setOwaspTop10(asList("a3", "a8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("reviewedHotspot", project).setOwaspTop10(asList("a3", "a8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) | |||||
.setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("notowasphotspot", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | newDoc("notowasphotspot", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | ||||
List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, includeCwe); | List<SecurityStandardCategoryStatistics> owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, includeCwe); | ||||
assertThat(owaspTop10Report) | assertThat(owaspTop10Report) | ||||
.extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | ||||
SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | ||||
SecurityStandardCategoryStatistics::getInReviewSecurityHotspots, SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
.containsExactlyInAnyOrder( | .containsExactlyInAnyOrder( | ||||
tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 0L), | |||||
tuple("a2", 0L, OptionalInt.empty(), 0L, 0L, 0L), | |||||
tuple("a3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* inReviewHotspot */, 1L /* reviewedHotspot */), | |||||
tuple("a4", 0L, OptionalInt.empty(), 0L, 0L, 0L), | |||||
tuple("a5", 0L, OptionalInt.empty(), 0L, 1L/* inReviewHotspot */, 0L), | |||||
tuple("a6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L, 0L), | |||||
tuple("a7", 0L, OptionalInt.empty(), 0L, 0L, 0L), | |||||
tuple("a8", 0L, OptionalInt.empty(), 0L, 0L, 1L /* reviewedHotspot */), | |||||
tuple("a9", 0L, OptionalInt.empty(), 0L, 0L, 0L), | |||||
tuple("a10", 0L, OptionalInt.empty(), 0L, 0L, 0L)); | |||||
tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L), | |||||
tuple("a2", 0L, OptionalInt.empty(), 0L, 0L), | |||||
tuple("a3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* reviewedHotspot */), | |||||
tuple("a4", 0L, OptionalInt.empty(), 0L, 0L), | |||||
tuple("a5", 0L, OptionalInt.empty(), 0L, 0L), | |||||
tuple("a6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* toreviewhotspot2 */, 0L), | |||||
tuple("a7", 0L, OptionalInt.empty(), 0L, 0L), | |||||
tuple("a8", 0L, OptionalInt.empty(), 0L, 1L /* reviewedHotspot */), | |||||
tuple("a9", 0L, OptionalInt.empty(), 0L, 0L), | |||||
tuple("a10", 0L, OptionalInt.empty(), 0L, 0L)); | |||||
return owaspTop10Report; | return owaspTop10Report; | ||||
} | } | ||||
.setResolution(Issue.RESOLUTION_FIXED) | .setResolution(Issue.RESOLUTION_FIXED) | ||||
.setSeverity(Severity.BLOCKER), | .setSeverity(Severity.BLOCKER), | ||||
newDoc("notsansvul", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | newDoc("notsansvul", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | ||||
newDoc("toreviewhotspot1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) | |||||
.setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) | |||||
.setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("inReviewHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_IN_REVIEW), | newDoc("inReviewHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_IN_REVIEW), | ||||
newDoc("reviewedHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("reviewedHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) | |||||
.setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("notowasphotspot", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | newDoc("notowasphotspot", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | ||||
List<SecurityStandardCategoryStatistics> sansTop25Report = underTest.getSansTop25Report(project.uuid(), false, false); | List<SecurityStandardCategoryStatistics> sansTop25Report = underTest.getSansTop25Report(project.uuid(), false, false); | ||||
assertThat(sansTop25Report) | assertThat(sansTop25Report) | ||||
.extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | ||||
SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | ||||
SecurityStandardCategoryStatistics::getInReviewSecurityHotspots, SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
.containsExactlyInAnyOrder( | .containsExactlyInAnyOrder( | ||||
tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 0L), | |||||
tuple(SANS_TOP_25_RISKY_RESOURCE, 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, 1L /* inReviewHotspot */,1L /* reviewedHotspot */), | |||||
tuple(SANS_TOP_25_POROUS_DEFENSES, 1L /* openvul2 */, OptionalInt.of(2)/* MINOR = B */, 1L/* openhotspot2 */, 0L, 0L)); | |||||
tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L), | |||||
tuple(SANS_TOP_25_RISKY_RESOURCE, 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* toreviewhotspot1,toreviewhotspot2 */, | |||||
1L /* reviewedHotspot */), | |||||
tuple(SANS_TOP_25_POROUS_DEFENSES, 1L /* openvul2 */, OptionalInt.of(2)/* MINOR = B */, 1L/* openhotspot2 */, 0L)); | |||||
assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); | assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); | ||||
} | } | ||||
.setResolution(Issue.RESOLUTION_FIXED) | .setResolution(Issue.RESOLUTION_FIXED) | ||||
.setSeverity(Severity.BLOCKER), | .setSeverity(Severity.BLOCKER), | ||||
newDoc("notsansvul", project2).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | newDoc("notsansvul", project2).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), | ||||
newDoc("toreviewhotspot1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("inReviewHotspot", project1).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_IN_REVIEW), | |||||
newDoc("reviewedHotspot", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("toreviewhotspot1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) | |||||
.setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("toreviewhotspot2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) | |||||
.setStatus(Issue.STATUS_TO_REVIEW), | |||||
newDoc("reviewedHotspot", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED) | |||||
.setResolution(Issue.RESOLUTION_FIXED), | |||||
newDoc("notowasphotspot", project1).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | newDoc("notowasphotspot", project1).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW)); | ||||
indexView(portfolio1.uuid(), singletonList(project1.uuid())); | indexView(portfolio1.uuid(), singletonList(project1.uuid())); | ||||
assertThat(sansTop25Report) | assertThat(sansTop25Report) | ||||
.extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, | ||||
SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, | ||||
SecurityStandardCategoryStatistics::getInReviewSecurityHotspots, SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
SecurityStandardCategoryStatistics::getReviewedSecurityHotspots) | |||||
.containsExactlyInAnyOrder( | .containsExactlyInAnyOrder( | ||||
tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L, 0L), | |||||
tuple(SANS_TOP_25_RISKY_RESOURCE, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L/* toreviewhotspot1 */, 1L /* inReviewHotspot */, 0L), | |||||
tuple(SANS_TOP_25_POROUS_DEFENSES, 0L, OptionalInt.empty(), 0L, 0L, 0L)); | |||||
tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* toreviewhotspot1 */, 0L), | |||||
tuple(SANS_TOP_25_RISKY_RESOURCE, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L/* toreviewhotspot1 */, 0L), | |||||
tuple(SANS_TOP_25_POROUS_DEFENSES, 0L, OptionalInt.empty(), 0L, 0L)); | |||||
assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); | assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); | ||||
} | } |
"The transitions involving security hotspots require the permission 'Administer Security Hotspot'.") | "The transitions involving security hotspots require the permission 'Administer Security Hotspot'.") | ||||
.setSince("3.6") | .setSince("3.6") | ||||
.setChangelog( | .setChangelog( | ||||
new Change("8.1", SET_AS_IN_REVIEW + " transition has been deprecated"), | |||||
new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY, RESET_AS_TO_REVIEW)), | new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY, RESET_AS_TO_REVIEW)), | ||||
new Change("7.3", "added transitions for security hotspots"), | new Change("7.3", "added transitions for security hotspots"), | ||||
new Change("6.5", "the database ids of the components are removed from the response"), | new Change("6.5", "the database ids of the components are removed from the response"), |
PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS) | PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS) | ||||
.setSince("3.6") | .setSince("3.6") | ||||
.setChangelog( | .setChangelog( | ||||
new Change("8.1", format("Status %s for Security Hotspots has been deprecated", STATUS_IN_REVIEW)), | |||||
new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)), | new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)), | ||||
new Change("7.8", "Security hotspots are returned by default"), | new Change("7.8", "Security hotspots are returned by default"), | ||||
new Change("7.7", format("Value '%s' in parameter '%s' is deprecated, please use '%s' instead", DEPRECATED_PARAM_AUTHORS, FACETS, PARAM_AUTHOR)), | new Change("7.7", format("Value '%s' in parameter '%s' is deprecated, please use '%s' instead", DEPRECATED_PARAM_AUTHORS, FACETS, PARAM_AUTHOR)), |
import org.junit.Test; | import org.junit.Test; | ||||
import org.junit.rules.ExpectedException; | import org.junit.rules.ExpectedException; | ||||
import org.mockito.ArgumentCaptor; | import org.mockito.ArgumentCaptor; | ||||
import org.sonar.api.impl.utils.TestSystem2; | |||||
import org.sonar.api.rules.RuleType; | import org.sonar.api.rules.RuleType; | ||||
import org.sonar.api.server.ws.WebService; | import org.sonar.api.server.ws.WebService; | ||||
import org.sonar.api.utils.System2; | import org.sonar.api.utils.System2; | ||||
import org.sonar.api.impl.utils.TestSystem2; | |||||
import org.sonar.db.DbClient; | import org.sonar.db.DbClient; | ||||
import org.sonar.db.DbTester; | import org.sonar.db.DbTester; | ||||
import org.sonar.db.component.BranchType; | import org.sonar.db.component.BranchType; | ||||
import static org.mockito.Mockito.mock; | import static org.mockito.Mockito.mock; | ||||
import static org.mockito.Mockito.verify; | import static org.mockito.Mockito.verify; | ||||
import static org.mockito.Mockito.verifyZeroInteractions; | import static org.mockito.Mockito.verifyZeroInteractions; | ||||
import static org.sonar.api.issue.DefaultTransitions.SET_AS_IN_REVIEW; | |||||
import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_REVIEWED; | |||||
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | ||||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | import static org.sonar.api.issue.Issue.STATUS_CLOSED; | ||||
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | ||||
import static org.sonar.api.issue.Issue.STATUS_IN_REVIEW; | |||||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | import static org.sonar.api.issue.Issue.STATUS_OPEN; | ||||
import static org.sonar.api.issue.Issue.STATUS_REVIEWED; | |||||
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; | import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; | ||||
import static org.sonar.api.rule.Severity.MAJOR; | import static org.sonar.api.rule.Severity.MAJOR; | ||||
import static org.sonar.api.rule.Severity.MINOR; | import static org.sonar.api.rule.Severity.MINOR; | ||||
BulkChangeWsResponse response = call(builder() | BulkChangeWsResponse response = call(builder() | ||||
.setIssues(singletonList(issue.getKey())) | .setIssues(singletonList(issue.getKey())) | ||||
.setDoTransition(SET_AS_IN_REVIEW) | |||||
.setDoTransition(RESOLVE_AS_REVIEWED) | |||||
.setSendNotifications(true) | .setSendNotifications(true) | ||||
.build()); | .build()); | ||||
assertThat(builder.getIssues()).hasSize(1); | assertThat(builder.getIssues()).hasSize(1); | ||||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | ChangedIssue changedIssue = builder.getIssues().iterator().next(); | ||||
assertThat(changedIssue.getKey()).isEqualTo(issue.getKey()); | assertThat(changedIssue.getKey()).isEqualTo(issue.getKey()); | ||||
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_IN_REVIEW); | |||||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||||
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_REVIEWED); | |||||
assertThat(changedIssue.getNewResolution()).hasValue(RESOLUTION_FIXED); | |||||
assertThat(changedIssue.getAssignee()).isEmpty(); | assertThat(changedIssue.getAssignee()).isEmpty(); | ||||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | ||||
assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user))); | assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user))); |
.executeProtobuf(SearchWsResponse.class); | .executeProtobuf(SearchWsResponse.class); | ||||
Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L) | Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L) | ||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("IN_REVIEW", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
assertThat(response.getFacets().getFacetsList()) | assertThat(response.getFacets().getFacetsList()) | ||||
.extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) | .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) | ||||
.executeProtobuf(SearchWsResponse.class); | .executeProtobuf(SearchWsResponse.class); | ||||
Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 10L).put("CONFIRMED", 0L) | Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 10L).put("CONFIRMED", 0L) | ||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("IN_REVIEW", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
assertThat(response.getFacets().getFacetsList()) | assertThat(response.getFacets().getFacetsList()) | ||||
.extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) | .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) | ||||
// Assignees contains one additional element : it's the empty string that will return number of unassigned issues | // Assignees contains one additional element : it's the empty string that will return number of unassigned issues | ||||
tuple("assignees", 101), | tuple("assignees", 101), | ||||
// Following facets returned fixed number of elements | // Following facets returned fixed number of elements | ||||
tuple("statuses", 8), | |||||
tuple("statuses", 7), | |||||
tuple("resolutions", 5), | tuple("resolutions", 5), | ||||
tuple("severities", 5), | tuple("severities", 5), | ||||
tuple("types", 4)); | tuple("types", 4)); | ||||
.executeProtobuf(SearchWsResponse.class); | .executeProtobuf(SearchWsResponse.class); | ||||
Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L) | Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L) | ||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("IN_REVIEW", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
.put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).put("TO_REVIEW", 0L).put("REVIEWED", 0L).build(); | |||||
assertThat(response.getFacets().getFacetsList()) | assertThat(response.getFacets().getFacetsList()) | ||||
.extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) | .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount))) |
String WONT_FIX = "wontfix"; | String WONT_FIX = "wontfix"; | ||||
/** | /** | ||||
* @since 7.8 | |||||
* @deprecated since 8.1, transition has no effect | |||||
*/ | */ | ||||
String SET_AS_IN_REVIEW = "setinreview"; | String SET_AS_IN_REVIEW = "setinreview"; | ||||
String STATUS_TO_REVIEW = "TO_REVIEW"; | String STATUS_TO_REVIEW = "TO_REVIEW"; | ||||
/** | /** | ||||
* @since 7.8 | |||||
* @deprecated since 8.1, status has been mapped as `TO_REVIEW` | |||||
*/ | */ | ||||
@Deprecated | |||||
String STATUS_IN_REVIEW = "IN_REVIEW"; | String STATUS_IN_REVIEW = "IN_REVIEW"; | ||||
/** | /** | ||||
* @since 4.4 | * @since 4.4 | ||||
*/ | */ | ||||
List<String> STATUSES = unmodifiableList(asList(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED, | List<String> STATUSES = unmodifiableList(asList(STATUS_OPEN, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_RESOLVED, STATUS_CLOSED, | ||||
STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)); | |||||
STATUS_TO_REVIEW, STATUS_REVIEWED)); | |||||
/** | /** | ||||
* Unique generated key. It looks like "d2de809c-1512-4ae2-9f34-f5345c9f1a13". | * Unique generated key. It looks like "d2de809c-1512-4ae2-9f34-f5345c9f1a13". |
optional string category = 1; | optional string category = 1; | ||||
optional int64 vulnerabilities = 2; | optional int64 vulnerabilities = 2; | ||||
optional int64 vulnerabilityRating = 3; | optional int64 vulnerabilityRating = 3; | ||||
optional int64 inReviewSecurityHotspots = 4; | |||||
optional int64 toReviewSecurityHotspots = 5; | optional int64 toReviewSecurityHotspots = 5; | ||||
optional int64 reviewedSecurityHotspots = 6; | optional int64 reviewedSecurityHotspots = 6; | ||||
repeated CweStatistics distribution = 7; | repeated CweStatistics distribution = 7; | ||||
optional string cwe = 1; | optional string cwe = 1; | ||||
optional int64 vulnerabilities = 2; | optional int64 vulnerabilities = 2; | ||||
optional int64 vulnerabilityRating = 3; | optional int64 vulnerabilityRating = 3; | ||||
optional int64 inReviewSecurityHotspots = 4; | |||||
optional int64 toReviewSecurityHotspots = 5; | optional int64 toReviewSecurityHotspots = 5; | ||||
optional int64 reviewedSecurityHotspots = 6; | optional int64 reviewedSecurityHotspots = 6; | ||||
optional int64 activeRules = 7; | optional int64 activeRules = 7; |