Browse Source

SONAR-12725 drop manual vulnerabilities

* drop manual vulnerabilities
* remove issues `from_hotspot` column usage
tags/8.2.0.32929
Jacek 4 years ago
parent
commit
114efaae21
26 changed files with 380 additions and 406 deletions
  1. 0
    12
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
  2. 5
    5
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java
  3. 0
    66
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
  4. 0
    12
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
  5. 8
    13
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
  6. 0
    2
      server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
  7. 0
    44
      server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java
  8. 2
    1
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81.java
  9. 68
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpots.java
  10. 1
    1
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81Test.java
  11. 140
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpotsTest.java
  12. 54
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpotsTest/schema.sql
  13. 0
    36
      server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IsManualVulnerability.java
  14. 2
    2
      server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IsNotHotspot.java
  15. 19
    45
      server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
  16. 2
    123
      server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java
  17. 2
    2
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/SetTypeAction.java
  18. 4
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
  19. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
  20. 1
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
  21. 0
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
  22. 1
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java
  23. 4
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
  24. 62
    24
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
  25. 0
    11
      sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
  26. 3
    1
      sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java

+ 0
- 12
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java View File

@@ -76,7 +76,6 @@ public class IssueLifecycle {
issue.setCreationDate(changeContext.date());
issue.setUpdateDate(changeContext.date());
issue.setEffort(debtCalculator.calculate(issue));
issue.setIsFromHotspot(rule.getType() == RuleType.SECURITY_HOTSPOT);
setType(issue, rule);
setStatus(issue, rule);
}
@@ -165,20 +164,9 @@ public class IssueLifecycle {
// In case issue was moved from module or folder to the root project
raw.setChanged(true);
}
raw.setIsFromHotspot(rule.getType() == RuleType.SECURITY_HOTSPOT);
setType(raw, rule);
copyFields(raw, base);
base.changes().forEach(raw::addChange);
if (raw.isFromHotspot() != base.isFromHotspot()) {
// This is to force DB update of the issue
raw.setChanged(true);
}
if (raw.isFromHotspot() && !base.isFromHotspot()) {
// First analysis after rule type was changed to security_hotspot. Issue will be reset to an open hotspot
updater.setType(raw, RuleType.SECURITY_HOTSPOT, changeContext);
updater.setStatus(raw, Issue.STATUS_TO_REVIEW, changeContext);
updater.setResolution(raw, null, changeContext);
}

if (base.manualSeverity()) {
raw.setManualSeverity(true);

+ 5
- 5
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java View File

@@ -80,7 +80,7 @@ public class ComponentIssuesLoaderTest {
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
Date issueDate = addDays(NOW, -10);
IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL).setIsFromHotspot(false));
IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 3), 20));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
@@ -100,7 +100,7 @@ public class ComponentIssuesLoaderTest {
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
Date issueDate = addDays(NOW, -10);
IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL).setIsFromHotspot(false));
IssueDto issue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(issueDate, 10));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 2), null));
db.issues().insertFieldDiffs(issue, newToClosedDiffsWithLine(addDays(issueDate, 1), 30));
@@ -120,9 +120,9 @@ public class ComponentIssuesLoaderTest {
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
RuleDefinitionDto rule = db.rules().insert(t -> t.setType(CODE_SMELL));
Date issueDate = addDays(NOW, -10);
IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL).setIsFromHotspot(false));
IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
IssueDto issueNoCloseDate = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIsFromHotspot(false));
IssueDto issueNoCloseDate = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED));
db.issues().insertFieldDiffs(issueNoCloseDate, newToClosedDiffsWithLine(issueDate, 10));
when(system2.now()).thenReturn(NOW.getTime());

@@ -198,7 +198,7 @@ public class ComponentIssuesLoaderTest {
};
IssueDto[] issues = Arrays.stream(issueDates)
.map(issueDate -> {
IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL).setIsFromHotspot(false));
IssueDto closedIssue = db.issues().insert(rule, project, file, t -> t.setStatus(STATUS_CLOSED).setIssueCloseDate(issueDate).setType(CODE_SMELL));
db.issues().insertFieldDiffs(closedIssue, newToClosedDiffsWithLine(issueDate, 10));
return closedIssue;
})

+ 0
- 66
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java View File

@@ -87,7 +87,6 @@ public class IssueLifecycleTest {
assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
assertThat(issue.isNew()).isTrue();
assertThat(issue.isCopied()).isFalse();
assertThat(issue.isFromHotspot()).isFalse();
}

@Test
@@ -107,7 +106,6 @@ public class IssueLifecycleTest {
assertThat(issue.effort()).isEqualTo(DEFAULT_DURATION);
assertThat(issue.isNew()).isTrue();
assertThat(issue.isCopied()).isFalse();
assertThat(issue.isFromHotspot()).isTrue();
}

@Test
@@ -303,70 +301,6 @@ public class IssueLifecycleTest {
verify(updater).setPastLocations(raw, issueLocations);
}

@Test
public void mergeExistingOpenIssue_vulnerability_changed_to_hotspot_should_be_to_review() {
rule.setType(RuleType.SECURITY_HOTSPOT);
DefaultIssue raw = new DefaultIssue()
.setNew(true)
.setKey("RAW_KEY")
.setRuleKey(XOO_X1)
.setCreationDate(parseDate("2015-10-01"))
.setUpdateDate(parseDate("2015-10-02"))
.setCloseDate(parseDate("2015-10-03"));

DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder()
.setTextRange(DbCommons.TextRange.newBuilder()
.setStartLine(10)
.setEndLine(12)
.build())
.build();
DefaultIssue base = new DefaultIssue()
.setKey("BASE_KEY")
.setType(RuleType.VULNERABILITY)
// First analysis before rule was changed to hotspot
.setIsFromHotspot(false)
.setCreationDate(parseDate("2015-01-01"))
.setUpdateDate(parseDate("2015-01-02"))
.setResolution(RESOLUTION_FALSE_POSITIVE)
.setStatus(STATUS_RESOLVED)
.setSeverity(BLOCKER)
.setAssigneeUuid("base assignee uuid")
.setAuthorLogin("base author")
.setTags(newArrayList("base tag"))
.setSelectedAt(1000L)
.setLine(10)
.setMessage("message")
.setGap(15d)
.setEffort(Duration.create(15L))
.setManualSeverity(false)
.setLocations(issueLocations);

when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);

underTest.mergeExistingOpenIssue(raw, base);

assertThat(raw.isNew()).isFalse();
assertThat(raw.key()).isEqualTo("BASE_KEY");
assertThat(raw.creationDate()).isEqualTo(base.creationDate());
assertThat(raw.updateDate()).isEqualTo(base.updateDate());
assertThat(raw.assignee()).isEqualTo("base assignee uuid");
assertThat(raw.authorLogin()).isEqualTo("base author");
assertThat(raw.tags()).containsOnly("base tag");
assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
assertThat(raw.selectedAt()).isEqualTo(1000L);
assertThat(raw.isFromHotspot()).isTrue();
assertThat(raw.isChanged()).isTrue();

verify(updater).setType(raw, RuleType.SECURITY_HOTSPOT, issueChangeContext);
verify(updater).setStatus(raw, STATUS_TO_REVIEW, issueChangeContext);
verify(updater).setResolution(raw, null, issueChangeContext);
verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext);
verify(updater).setPastLine(raw, 10);
verify(updater).setPastMessage(raw, "message", issueChangeContext);
verify(updater).setPastEffort(raw, Duration.create(15L), issueChangeContext);
verify(updater).setPastLocations(raw, issueLocations);
}

@Test
public void mergeExistingOpenIssue_with_manual_severity() {
DefaultIssue raw = new DefaultIssue()

+ 0
- 12
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java View File

@@ -122,7 +122,6 @@ public final class IssueDto implements Serializable {
.setRuleId(ruleId)
.setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule())
.setExternal(issue.isFromExternalRuleEngine())
.setIsFromHotspot(issue.isFromHotspot())
.setTags(issue.tags())
.setComponentUuid(issue.componentUuid())
.setComponentKey(issue.componentKey())
@@ -171,7 +170,6 @@ public final class IssueDto implements Serializable {
.setAuthorLogin(issue.authorLogin())
.setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule())
.setExternal(issue.isFromExternalRuleEngine())
.setIsFromHotspot(issue.isFromHotspot())
.setTags(issue.tags())
.setComponentUuid(issue.componentUuid())
.setComponentKey(issue.componentKey())
@@ -489,15 +487,6 @@ public final class IssueDto implements Serializable {
return this;
}

public boolean isFromHotspot() {
return isFromHotspot;
}

public IssueDto setIsFromHotspot(boolean value) {
isFromHotspot = value;
return this;
}

public String getComponentKey() {
return componentKey;
}
@@ -742,7 +731,6 @@ public final class IssueDto implements Serializable {
issue.setSelectedAt(selectedAt);
issue.setLocations(parseLocations());
issue.setIsFromExternalRuleEngine(isExternal);
issue.setIsFromHotspot(isFromHotspot);
return issue;
}
}

+ 8
- 13
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml View File

@@ -38,8 +38,7 @@
p.path as filePath,
root.kee as projectKey,
i.project_uuid as projectUuid,
i.issue_type as type,
i.from_hotspot as "isFromHotspot"
i.issue_type as type
</sql>

<sql id="sortColumn">
@@ -96,8 +95,7 @@
p.scope,
p.organization_uuid as "organizationUuid",
i.tags,
i.issue_type as "issueType",
i.from_hotspot as "isFromHotspot"
i.issue_type as "issueType"
</sql>


@@ -105,7 +103,7 @@
INSERT INTO issues (kee, rule_id, severity, manual_severity,
message, line, locations, gap, effort, status, tags,
resolution, checksum, assignee, author_login, issue_attributes, issue_creation_date, issue_update_date,
issue_close_date, created_at, updated_at, component_uuid, project_uuid, issue_type, from_hotspot)
issue_close_date, created_at, updated_at, component_uuid, project_uuid, issue_type)
VALUES (#{kee,jdbcType=VARCHAR}, #{ruleId,jdbcType=INTEGER},
#{severity,jdbcType=VARCHAR},
#{manualSeverity,jdbcType=BOOLEAN}, #{message,jdbcType=VARCHAR}, #{line,jdbcType=INTEGER},
@@ -118,7 +116,7 @@
#{issueAttributes,jdbcType=VARCHAR},
#{issueCreationTime,jdbcType=BIGINT},#{issueUpdateTime,jdbcType=BIGINT}, #{issueCloseTime,jdbcType=BIGINT},
#{createdAt,jdbcType=BIGINT}, #{updatedAt,jdbcType=BIGINT},
#{componentUuid,jdbcType=VARCHAR}, #{projectUuid,jdbcType=VARCHAR}, #{type,jdbcType=INTEGER}, #{isFromHotspot,jdbcType=BOOLEAN})
#{componentUuid,jdbcType=VARCHAR}, #{projectUuid,jdbcType=VARCHAR}, #{type,jdbcType=INTEGER})
</insert>

<!--
@@ -145,8 +143,7 @@
issue_update_date=#{issueUpdateTime,jdbcType=BIGINT},
issue_close_date=#{issueCloseTime,jdbcType=BIGINT},
updated_at=#{updatedAt,jdbcType=BIGINT},
issue_type=#{type,jdbcType=INTEGER},
from_hotspot=#{isFromHotspot,jdbcType=BOOLEAN}
issue_type=#{type,jdbcType=INTEGER}
where kee = #{kee}
</update>

@@ -175,8 +172,7 @@
issue_update_date=#{issueUpdateTime,jdbcType=BIGINT},
issue_close_date=#{issueCloseTime,jdbcType=BIGINT},
updated_at=#{updatedAt,jdbcType=BIGINT},
issue_type=#{type,jdbcType=INTEGER},
from_hotspot=#{isFromHotspot,jdbcType=BOOLEAN}
issue_type=#{type,jdbcType=INTEGER}
where kee = #{kee} and updated_at &lt;= #{selectedAt}
</update>

@@ -213,7 +209,7 @@
(r.is_external is NULL or r.is_external = ${_false}) and
i.component_uuid = #{componentUuid,jdbcType=VARCHAR} and
i.status &lt;&gt; 'CLOSED' and
i.issue_type &lt;&gt; 4 and (i.from_hotspot is NULL or i.from_hotspot = ${_false})
i.issue_type &lt;&gt; 4
</select>

<select id="scrollClosedByComponentUuid" resultType="Issue" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
@@ -237,7 +233,6 @@
and i.issue_close_date is not null
and i.issue_close_date >= #{closeDateAfter,jdbcType=BIGINT}
and i.issue_type &lt;&gt; 4
and (i.from_hotspot is null or i.from_hotspot = ${_false})
order by
i.kee, ic.issue_change_creation_date desc
</select>
@@ -323,7 +318,7 @@
i.project_uuid = #{projectUuid, jdbcType=VARCHAR} and
p.module_uuid_path like #{likeModuleUuidPath, jdbcType=VARCHAR} escape '/' and
i.status &lt;&gt; 'CLOSED' and
i.issue_type &lt;&gt; 4 and (i.from_hotspot is NULL or i.from_hotspot = ${_false})
i.issue_type &lt;&gt; 4
</select>

<select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map">

+ 0
- 2
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java View File

@@ -142,7 +142,6 @@ public class IssueDaoTest {
IssueDto openIssueOnProject = db.issues().insert(rule, project, project, i -> i.setStatus("OPEN").setResolution(null).setType(randomRuleTypeExceptHotspot()));

IssueDto securityHotspot = db.issues().insert(rule, project, file, i -> i.setType(RuleType.SECURITY_HOTSPOT));
IssueDto manualVulnerability = db.issues().insert(rule, project, file, i -> i.setType(RuleType.VULNERABILITY).setIsFromHotspot(true));

RuleDefinitionDto external = db.rules().insert(ruleDefinitionDto -> ruleDefinitionDto.setIsExternal(true));
IssueDto issueFromExteralruleOnFile = db.issues().insert(external, project, file, i -> i.setKee("ON_FILE_FROM_EXTERNAL").setType(randomRuleTypeExceptHotspot()));
@@ -174,7 +173,6 @@ public class IssueDaoTest {
i -> i.setStatus("OPEN").setResolution(null).setType(randomRuleTypeExceptHotspot()));

IssueDto securityHotspot = db.issues().insert(rule, project, file, i -> i.setType(RuleType.SECURITY_HOTSPOT));
IssueDto manualVulnerability = db.issues().insert(rule, project, file, i -> i.setType(RuleType.VULNERABILITY).setIsFromHotspot(true));

RuleDefinitionDto external = db.rules().insert(ruleDefinitionDto -> ruleDefinitionDto.setIsExternal(true));
IssueDto issueFromExteralruleOnFile = db.issues().insert(external, project, file, i -> i.setKee("ON_FILE_FROM_EXTERNAL").setType(randomRuleTypeExceptHotspot()));

+ 0
- 44
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java View File

@@ -340,27 +340,6 @@ public class IssueMapperTest {
.containsOnly(tuple(issue.getKey(), issueChange.getChangeData()));
}

@Test
public void scrollClosedByComponentUuid_returns_closed_issues_without_isHotspot_flag() {
RuleType ruleType = randomSupportedRuleType();
OrganizationDto organization = dbTester.organizations().insert();
ComponentDto component = randomComponent(organization);
IssueDto noHotspotFlagIssue = insertNewClosedIssue(component, ruleType);
IssueChangeDto noFlagIssueChange = insertToClosedDiff(noHotspotFlagIssue);
manuallySetToNullFromHotpotsColumn(noHotspotFlagIssue);
IssueDto issue = insertNewClosedIssue(component, ruleType);
IssueChangeDto issueChange = insertToClosedDiff(issue);

RecorderResultHandler resultHandler = new RecorderResultHandler();
underTest.scrollClosedByComponentUuid(component.uuid(), NO_FILTERING_ON_CLOSE_DATE, resultHandler);

assertThat(resultHandler.issues)
.extracting(IssueDto::getKey, t -> t.getClosedChangeData().get())
.containsOnly(
tuple(issue.getKey(), issueChange.getChangeData()),
tuple(noHotspotFlagIssue.getKey(), noFlagIssueChange.getChangeData()));
}

@Test
public void scrollClosedByComponentUuid_does_not_return_closed_issues_without_close_date() {
RuleType ruleType = randomSupportedRuleType();
@@ -428,29 +407,6 @@ public class IssueMapperTest {
.containsOnly(issues[3].getKey(), issues[1].getKey(), issues[2].getKey(), issues[0].getKey());
}

private void manuallySetToNullFromHotpotsColumn(IssueDto fromHostSpotIssue) {
dbTester.executeUpdateSql("update issues set from_hotspot = null where kee = '" + fromHostSpotIssue.getKey() + "'");
dbTester.commit();
}

@Test
@UseDataProvider("closedIssuesSupportedRuleTypes")
public void scrollClosedByComponentUuid_does_not_return_closed_issues_with_isHotspot_flag_true(RuleType ruleType) {
OrganizationDto organization = dbTester.organizations().insert();
ComponentDto component = randomComponent(organization);
IssueDto fromHostSpotIssue = insertNewClosedIssue(component, ruleType, t -> t.setIsFromHotspot(true));
insertToClosedDiff(fromHostSpotIssue);
IssueDto issue = insertNewClosedIssue(component, ruleType);
IssueChangeDto issueChange = insertToClosedDiff(issue);

RecorderResultHandler resultHandler = new RecorderResultHandler();
underTest.scrollClosedByComponentUuid(component.uuid(), NO_FILTERING_ON_CLOSE_DATE, resultHandler);

assertThat(resultHandler.issues)
.extracting(IssueDto::getKey, t -> t.getClosedChangeData().get())
.containsOnly(tuple(issue.getKey(), issueChange.getChangeData()));
}

@Test
@UseDataProvider("closedIssuesSupportedRuleTypes")
public void scrollClosedByComponentUuid_return_one_row_per_status_diff_to_CLOSED_sorted_by_most_recent_creation_date_first(RuleType ruleType) {

+ 2
- 1
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81.java View File

@@ -43,6 +43,7 @@ public class DbVersion81 implements DbVersion {
.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",
MigrateSlbsAndLlbsToCommonTypeInCeTasks.class)
.add(3114, "Drop 'In Review' Security Hotspots status ", DropSecurityHotSpotsInReviewStatus.class);
.add(3114, "Drop 'In Review' Security Hotspots status ", DropSecurityHotSpotsInReviewStatus.class)
.add(3115, "Migrate Manual Vulnerabilities to Security Hotspots ", MigrateManualVulnerabilitiesToSecurityHotSpots.class);
}
}

+ 68
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpots.java View File

@@ -0,0 +1,68 @@
/*
* 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;

import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
import static org.sonar.api.rules.RuleType.VULNERABILITY;

public class MigrateManualVulnerabilitiesToSecurityHotSpots extends DataChange {
private System2 system;

public MigrateManualVulnerabilitiesToSecurityHotSpots(Database db, System2 system) {
super(db);
this.system = system;
}

@Override
protected void execute(Context context) throws SQLException {
MassUpdate updateIssues = context.prepareMassUpdate();
updateIssues.select("select id, kee, project_uuid, component_uuid from issues where from_hotspot = ? and issue_type = ?")
.setBoolean(1, true)
.setInt(2, 3);
updateIssues.update("update issues set issue_type = ?, status = ? where id = ? and from_hotspot = ? and issue_type = ?");
updateIssues.update("insert into issue_changes(issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date) " +
"VALUES(?, ?, ?, ?, ?, ?)");

updateIssues.execute((row, update, updateIndex) -> {
if (updateIndex == 0) {
update.setInt(1, 4)
.setString(2, STATUS_TO_REVIEW)
.setLong(3, row.getLong(1))
.setBoolean(4, true)
.setInt(5, VULNERABILITY.getDbConstant());
} else if (updateIndex == 1) {
long currentTime = system.now();
update.setString(1, row.getString(2))
.setString(2, "diff")
.setString(3, "type=VULNERABILITY|SECURITY_HOTSPOT,status=OPEN|TO_REVIEW")
.setLong(4, currentTime)
.setLong(5, currentTime)
.setLong(6, currentTime);
}
return true;
});
}
}

+ 1
- 1
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81Test.java View File

@@ -36,7 +36,7 @@ public class DbVersion81Test {

@Test
public void verify_migration_count() {
verifyMigrationCount(underTest, 15);
verifyMigrationCount(underTest, 16);
}

}

+ 140
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpotsTest.java View File

@@ -0,0 +1,140 @@
/*
* 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
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.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.rules.RuleType.VULNERABILITY;

public class MigrateManualVulnerabilitiesToSecurityHotSpotsTest {

private final static String ISSUES_TABLE_NAME = "issues";
private final static int TOTAL_NUMBER_OF_ISSUES = 9;

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(MigrateManualVulnerabilitiesToSecurityHotSpotsTest.class, "schema.sql");
@Rule
public ExpectedException expectedException = ExpectedException.none();

private System2 system2 = System2.INSTANCE;

private DataChange underTest = new MigrateManualVulnerabilitiesToSecurityHotSpots(db.database(), system2);

@Test
public void should_migrate_manual_vulnerabilities_only() throws SQLException {
Random random = new Random();
List<Integer> range = IntStream.range(0, TOTAL_NUMBER_OF_ISSUES).boxed()
.collect(Collectors.toCollection(ArrayList::new));
Collections.shuffle(range);

insertIssue(range.get(0), CODE_SMELL.getDbConstant(), random.nextBoolean());
insertIssue(range.get(1), BUG.getDbConstant(), random.nextBoolean());
insertIssue(range.get(2), VULNERABILITY.getDbConstant(), false);
insertIssue(range.get(3), SECURITY_HOTSPOT.getDbConstant(), random.nextBoolean());
insertIssue(range.get(4), -1, random.nextBoolean());

insertIssue(range.get(5), VULNERABILITY.getDbConstant(), true);
insertIssue(range.get(6), VULNERABILITY.getDbConstant(), true);
insertIssue(range.get(7), VULNERABILITY.getDbConstant(), true);
insertIssue(range.get(8), VULNERABILITY.getDbConstant(), true);

underTest.execute();

assertIssueNotChanged(range.get(0), CODE_SMELL.getDbConstant());
assertIssueNotChanged(range.get(1), BUG.getDbConstant());
assertIssueNotChanged(range.get(2), VULNERABILITY.getDbConstant());
assertIssueNotChanged(range.get(3), SECURITY_HOTSPOT.getDbConstant());
assertIssueNotChanged(range.get(4), -1);

assertIssueChanged(range.get(5));
assertIssueChanged(range.get(6));
assertIssueChanged(range.get(7));
assertIssueChanged(range.get(8));

// 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("type=VULNERABILITY|SECURITY_HOTSPOT,status=OPEN|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, int expectedType) {
List<Map<String, Object>> row = db.select(String.format("select issue_type, status from issues where kee = '%s'", "issue-key-" + issueId));
assertThat(row).hasSize(1);

Map<String, Object> issueData = row.get(0);
assertThat(issueData.get("STATUS"))
.isNull();
assertThat(issueData.get("ISSUE_TYPE"))
.isEqualTo(expectedType);

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, int issueType, boolean fromHotspot) {
db.executeInsert(ISSUES_TABLE_NAME,
"kee", "issue-key-" + issueId,
"issue_type", issueType,
"from_hotspot", fromHotspot,
"manual_severity", false);
}

}

+ 54
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateManualVulnerabilitiesToSecurityHotSpotsTest/schema.sql View File

@@ -0,0 +1,54 @@
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");

+ 0
- 36
server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IsManualVulnerability.java View File

@@ -1,36 +0,0 @@
/*
* 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.issue.workflow;

import org.sonar.api.issue.Issue;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;

/**
* The vulnerability originally come from a hotspot that was moved to vulnerability by a security auditor.
*/
enum IsManualVulnerability implements Condition {
INSTANCE;

@Override
public boolean matches(Issue issue) {
return ((DefaultIssue) issue).type() == RuleType.VULNERABILITY && ((DefaultIssue) issue).isFromHotspot();
}
}

server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IsNotHotspotNorManualVulnerability.java → server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IsNotHotspot.java View File

@@ -23,11 +23,11 @@ import org.sonar.api.issue.Issue;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;

enum IsNotHotspotNorManualVulnerability implements Condition {
enum IsNotHotspot implements Condition {
INSTANCE;

@Override
public boolean matches(Issue issue) {
return ((DefaultIssue) issue).type() != RuleType.SECURITY_HOTSPOT && !((DefaultIssue) issue).isFromHotspot();
return ((DefaultIssue) issue).type() != RuleType.SECURITY_HOTSPOT;
}
}

+ 19
- 45
server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java View File

@@ -75,31 +75,31 @@ public class IssueWorkflow implements Startable {
// confirm
.transition(Transition.builder(DefaultTransitions.CONFIRM)
.from(STATUS_OPEN).to(STATUS_CONFIRMED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(null))
.build())
.transition(Transition.builder(DefaultTransitions.CONFIRM)
.from(STATUS_REOPENED).to(STATUS_CONFIRMED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(null))
.build())

// resolve as fixed
.transition(Transition.builder(DefaultTransitions.RESOLVE)
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FIXED))
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.RESOLVE)
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FIXED))
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.RESOLVE)
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FIXED))
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
@@ -107,31 +107,31 @@ public class IssueWorkflow implements Startable {
// reopen
.transition(Transition.builder(DefaultTransitions.UNCONFIRM)
.from(STATUS_CONFIRMED).to(STATUS_REOPENED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(null))
.build())
.transition(Transition.builder(DefaultTransitions.REOPEN)
.from(STATUS_RESOLVED).to(STATUS_REOPENED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(null))
.build())

// resolve as false-positive
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
@@ -139,19 +139,19 @@ public class IssueWorkflow implements Startable {
// resolve as won't fix
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(STATUS_OPEN).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(STATUS_REOPENED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(STATUS_CONFIRMED).to(STATUS_RESOLVED)
.conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(IsNotHotspot.INSTANCE)
.functions(new SetResolution(RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build());
@@ -165,39 +165,13 @@ public class IssueWorkflow implements Startable {
.functions(new SetResolution(RESOLUTION_FIXED))
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.RESOLVE_AS_REVIEWED)
.from(STATUS_OPEN).to(STATUS_REVIEWED)
.conditions(new HasType(RuleType.VULNERABILITY), IsManualVulnerability.INSTANCE)
.functions(new SetType(RuleType.SECURITY_HOTSPOT), new SetResolution(RESOLUTION_FIXED))
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
.build())

.transition(Transition.builder(DefaultTransitions.OPEN_AS_VULNERABILITY)
.from(STATUS_REVIEWED).to(STATUS_OPEN)
.conditions(new HasType(RuleType.SECURITY_HOTSPOT))
.functions(new SetResolution(null), new SetType(RuleType.VULNERABILITY))
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.OPEN_AS_VULNERABILITY)
.from(STATUS_TO_REVIEW).to(STATUS_OPEN)
.conditions(new HasType(RuleType.SECURITY_HOTSPOT))
.functions(new SetType(RuleType.VULNERABILITY))
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
.build())

.transition(Transition.builder(DefaultTransitions.RESET_AS_TO_REVIEW)
.from(STATUS_REVIEWED).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)
.from(STATUS_OPEN).to(STATUS_TO_REVIEW)
.conditions(new HasType(RuleType.VULNERABILITY), IsManualVulnerability.INSTANCE)
.functions(new SetType(RuleType.SECURITY_HOTSPOT), new SetResolution(null))
.requiredProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN)
.build())
;
.build());
}

private static void buildAutomaticTransitions(StateMachine.Builder builder) {
@@ -243,7 +217,7 @@ public class IssueWorkflow implements Startable {
// Reopen issues that are marked as resolved but that are still alive.
.transition(Transition.builder("automaticreopen")
.from(STATUS_RESOLVED).to(STATUS_REOPENED)
.conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(RESOLUTION_FIXED), IsNotHotspotNorManualVulnerability.INSTANCE)
.conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(RESOLUTION_FIXED), IsNotHotspot.INSTANCE)
.functions(new SetResolution(null), UnsetCloseDate.INSTANCE)
.automatic()
.build())
@@ -253,7 +227,7 @@ public class IssueWorkflow implements Startable {
.conditions(
new PreviousStatusWas(STATUS_OPEN),
new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
IsNotHotspotNorManualVulnerability.INSTANCE)
IsNotHotspot.INSTANCE)
.functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
.automatic()
.build())
@@ -262,7 +236,7 @@ public class IssueWorkflow implements Startable {
.conditions(
new PreviousStatusWas(STATUS_REOPENED),
new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
IsNotHotspotNorManualVulnerability.INSTANCE)
IsNotHotspot.INSTANCE)
.functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
.automatic()
.build())
@@ -271,7 +245,7 @@ public class IssueWorkflow implements Startable {
.conditions(
new PreviousStatusWas(STATUS_CONFIRMED),
new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
IsNotHotspotNorManualVulnerability.INSTANCE)
IsNotHotspot.INSTANCE)
.functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
.automatic()
.build())
@@ -280,7 +254,7 @@ public class IssueWorkflow implements Startable {
.conditions(
new PreviousStatusWas(STATUS_RESOLVED),
new HasResolution(RESOLUTION_REMOVED, RESOLUTION_FIXED),
IsNotHotspotNorManualVulnerability.INSTANCE)
IsNotHotspot.INSTANCE)
.functions(RestoreResolutionFunction.INSTANCE, UnsetCloseDate.INSTANCE)
.automatic()
.build());

+ 2
- 123
server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowForSecurityHotspotsTest.java View File

@@ -31,7 +31,6 @@ import org.apache.commons.lang.time.DateUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.issue.Issue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;
@@ -44,7 +43,6 @@ 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_REMOVED;
import static org.sonar.api.issue.Issue.STATUS_CLOSED;
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_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
@@ -93,7 +91,7 @@ public class IssueWorkflowForSecurityHotspotsTest {

List<Transition> transitions = underTest.outTransitions(issue);

assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "openasvulnerability");
assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed");
}

@Test
@@ -103,17 +101,7 @@ public class IssueWorkflowForSecurityHotspotsTest {

List<Transition> transitions = underTest.outTransitions(issue);

assertThat(keys(transitions)).containsExactlyInAnyOrder("openasvulnerability", "resetastoreview");
}

@Test
public void list_out_vulnerability_transitions_in_status_open() {
underTest.start();
DefaultIssue issue = new DefaultIssue().setType(RuleType.VULNERABILITY).setResolution(RESOLUTION_FIXED).setStatus(STATUS_OPEN).setIsFromHotspot(true);

List<Transition> transitions = underTest.outTransitions(issue);

assertThat(keys(transitions)).containsExactlyInAnyOrder("resolveasreviewed", "resetastoreview");
assertThat(keys(transitions)).containsExactlyInAnyOrder("resetastoreview");
}

@Test
@@ -121,7 +109,6 @@ public class IssueWorkflowForSecurityHotspotsTest {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.SECURITY_HOTSPOT)
.setIsFromHotspot(true)
.setStatus(STATUS_TO_REVIEW);

boolean result = underTest.doManualTransition(issue, DefaultTransitions.RESOLVE_AS_REVIEWED, IssueChangeContext.createUser(new Date(), "USER1"));
@@ -131,46 +118,11 @@ public class IssueWorkflowForSecurityHotspotsTest {
assertThat(issue.resolution()).isEqualTo(RESOLUTION_FIXED);
}

@Test
public void open_as_vulnerability_from_to_review() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.SECURITY_HOTSPOT)
.setIsFromHotspot(true)
.setStatus(STATUS_TO_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
public void open_as_vulnerability_from_reviewed() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.SECURITY_HOTSPOT)
.setIsFromHotspot(true)
.setResolution(RESOLUTION_FIXED)
.setStatus(STATUS_REVIEWED);

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
public void reset_as_to_review_from_reviewed() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.SECURITY_HOTSPOT)
.setIsFromHotspot(true)
.setStatus(STATUS_REVIEWED)
.setResolution(RESOLUTION_FIXED);

@@ -181,22 +133,6 @@ public class IssueWorkflowForSecurityHotspotsTest {
assertThat(issue.resolution()).isNull();
}

@Test
public void reset_as_to_review_from_opened_as_vulnerability() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.VULNERABILITY)
.setIsFromHotspot(true)
.setStatus(STATUS_OPEN)
.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
public void automatically_close_resolved_security_hotspots_in_status_to_review() {
underTest.start();
@@ -235,26 +171,6 @@ public class IssueWorkflowForSecurityHotspotsTest {
assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND));
}

@Test
public void automatically_close_hotspots_opened_as_vulnerability() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setType(RuleType.VULNERABILITY)
.setResolution(null)
.setStatus(STATUS_OPEN)
.setIsFromHotspot(true)
.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
@UseDataProvider("allStatusesLeadingToClosed")
public void do_not_automatically_reopen_closed_issues_of_security_hotspots(String previousStatus) {
@@ -292,43 +208,6 @@ public class IssueWorkflowForSecurityHotspotsTest {
assertThat(issue.resolution()).isNull();
}

@Test
@UseDataProvider("allStatusesLeadingToClosed")
public void do_not_automatically_reopen_closed_issues_of_manual_vulnerability(String previousStatus) {
DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
.map(resolution -> {
DefaultIssue issue = newClosedIssue(resolution);
setStatusPreviousToClosed(issue, previousStatus);
issue.setIsFromHotspot(true);
return issue;
})
.toArray(DefaultIssue[]::new);
Date now = new Date();
underTest.start();

Arrays.stream(issues).forEach(issue -> {
underTest.doAutomaticTransition(issue, IssueChangeContext.createScan(now));

assertThat(issue.status()).isEqualTo(STATUS_CLOSED);
assertThat(issue.updateDate()).isNull();
});
}

@Test
public void do_not_allow_to_doManualTransition_when_condition_fails() {
underTest.start();
DefaultIssue issue = new DefaultIssue()
.setKey("ABCDE")
// Detect is only available on hotspot
.setType(RuleType.VULNERABILITY)
.setIsFromHotspot(false)
.setStatus(STATUS_OPEN)
.setResolution(null)
.setRuleKey(XOO_X1);

assertThat(underTest.doManualTransition(issue, DefaultTransitions.RESET_AS_TO_REVIEW, IssueChangeContext.createScan(new Date()))).isFalse();
}

private Collection<String> keys(List<Transition> transitions) {
return transitions.stream().map(Transition::key).collect(MoreCollectors.toList());
}

+ 2
- 2
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/SetTypeAction.java View File

@@ -22,10 +22,10 @@ package org.sonar.server.issue;
import java.util.Collection;
import java.util.Map;
import org.sonar.api.issue.Issue;
import org.sonar.server.issue.workflow.IsUnResolved;
import org.sonar.api.rules.RuleType;
import org.sonar.api.web.UserRole;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.server.issue.workflow.IsUnResolved;
import org.sonar.server.user.UserSession;

import static com.google.common.base.Preconditions.checkArgument;
@@ -47,7 +47,7 @@ public class SetTypeAction extends Action {
}

private boolean isCurrentUserIssueAdmin(Issue issue) {
return !((DefaultIssue) issue).isFromHotspot() && userSession.hasComponentUuidPermission(UserRole.ISSUE_ADMIN, issue.projectUuid());
return userSession.hasComponentUuidPermission(UserRole.ISSUE_ADMIN, issue.projectUuid());
}

@Override

+ 4
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java View File

@@ -80,7 +80,9 @@ import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static org.sonar.api.issue.DefaultTransitions.OPEN_AS_VULNERABILITY;
import static org.sonar.api.issue.DefaultTransitions.REOPEN;
import static org.sonar.api.issue.DefaultTransitions.SET_AS_IN_REVIEW;
import static org.sonar.api.rule.Severity.BLOCKER;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
@@ -143,6 +145,8 @@ public class BulkChangeAction implements IssuesWsAction {
"Requires authentication.")
.setSince("3.7")
.setChangelog(
new Change("8.1", OPEN_AS_VULNERABILITY + " transition is no more supported"),
new Change("8.1", SET_AS_IN_REVIEW + " transition is no more supported"),
new Change("6.3", "'actions' parameter is ignored"))
.setHandler(this)
.setResponseExample(getClass().getResource("bulk_change-example.json"))

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java View File

@@ -75,7 +75,8 @@ public class DoTransitionAction implements IssuesWsAction {
"The transitions involving security hotspots require the permission 'Administer Security Hotspot'.")
.setSince("3.6")
.setChangelog(
new Change("8.1", SET_AS_IN_REVIEW + " transition has been deprecated"),
new Change("8.1", OPEN_AS_VULNERABILITY + " transition is no more supported"),
new Change("8.1", SET_AS_IN_REVIEW + " transition is no more supported"),
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("6.5", "the database ids of the components are removed from the response"),

+ 1
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

@@ -199,6 +199,7 @@ public class SearchAction implements IssuesWsAction, Startable {
PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS)
.setSince("3.6")
.setChangelog(
new Change("8.1", "response field 'fromHotspot' has been deprecated and is no more populated"),
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", "Security hotspots are returned by default"),

+ 0
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java View File

@@ -188,7 +188,6 @@ public class SearchResponseFormat {
if (dto.isExternal()) {
issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey()));
}
issueBuilder.setFromHotspot(dto.isFromHotspot());
if (dto.getType() != RuleType.SECURITY_HOTSPOT.getDbConstant()) {
issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity()));
}

+ 1
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java View File

@@ -234,10 +234,8 @@ public class SearchResponseLoader {
}
availableActions.add(ASSIGN_KEY);
availableActions.add("set_tags");
if (!issue.isFromHotspot() && userSession.hasComponentPermission(ISSUE_ADMIN, project)) {
if (ruleType != RuleType.SECURITY_HOTSPOT && userSession.hasComponentPermission(ISSUE_ADMIN, project)) {
availableActions.add(SET_TYPE_KEY);
}
if ((ruleType != RuleType.SECURITY_HOTSPOT && userSession.hasComponentPermission(ISSUE_ADMIN, project))) {
availableActions.add(SET_SEVERITY_KEY);
}
return availableActions;

+ 4
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java View File

@@ -37,6 +37,7 @@ import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.user.UserSession;

import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
@@ -105,9 +106,11 @@ public class SetTypeAction implements IssuesWsAction {
private SearchResponseData setType(DbSession session, String issueKey, RuleType ruleType) {
IssueDto issueDto = issueFinder.getByKey(session, issueKey);
DefaultIssue issue = issueDto.toDefaultIssue();
if (issue.isFromHotspot()) {

if (SECURITY_HOTSPOT == issue.type()) {
throw new IllegalArgumentException("Changing type of a security hotspot is not permitted");
}

userSession.checkComponentUuidPermission(ISSUE_ADMIN, issue.projectUuid());

IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid());

+ 62
- 24
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java View File

@@ -19,12 +19,21 @@
*/
package org.sonar.server.issue.ws;

import com.google.common.collect.Sets;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
@@ -65,13 +74,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.rules.RuleType.VULNERABILITY;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
import static org.sonar.api.web.UserRole.USER;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.issue.IssueTesting.newDto;
import static org.sonar.db.rule.RuleTesting.newRuleDto;

@RunWith(DataProviderRunner.class)
public class SetTypeActionTest {

@Rule
@@ -102,34 +112,28 @@ public class SetTypeActionTest {
responseWriter, system2));

@Test
public void set_type() {
@UseDataProvider("allTypesFromToExceptHotspots")
public void set_type(RuleType from, RuleType to) {
long now = 1_999_777_234L;
when(system2.now()).thenReturn(now);
IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(CODE_SMELL));
IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(from));
setUserWithBrowseAndAdministerIssuePermission(issueDto);

call(issueDto.getKey(), BUG.name());
call(issueDto.getKey(), to.name());

verify(responseWriter).write(eq(issueDto.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class));
verifyContentOfPreloadedSearchResponseData(issueDto);
IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get();
assertThat(issueReloaded.getType()).isEqualTo(BUG.getDbConstant());
assertThat(issueReloaded.getType()).isEqualTo(to.getDbConstant());

assertThat(issueChangePostProcessor.calledComponents())
.extracting(ComponentDto::uuid)
.containsExactlyInAnyOrder(issueDto.getComponentUuid());
}

@Test
public void prevent_changing_type_security_hotspot() {
long now = 1_999_777_234L;
when(system2.now()).thenReturn(now);
IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(VULNERABILITY).setIsFromHotspot(true));
setUserWithBrowseAndAdministerIssuePermission(issueDto);

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Changing type of a security hotspot is not permitted");
call(issueDto.getKey(), BUG.name());
if (from != to) {
verifyContentOfPreloadedSearchResponseData(issueDto);
assertThat(issueChangePostProcessor.calledComponents())
.extracting(ComponentDto::uuid)
.containsExactlyInAnyOrder(issueDto.getComponentUuid());
} else {
assertThat(issueChangePostProcessor.wasCalled())
.isFalse();
}
}

@Test
@@ -174,12 +178,25 @@ public class SetTypeActionTest {
}

@Test
public void fail_when_missing_administer_issue_permission() {
IssueDto issueDto = issueDbTester.insertIssue();
@UseDataProvider("allTypesExceptSecurityHotspot")
public void fail_type_except_hotspot_when_missing_administer_issue_permission(RuleType type) {
IssueDto issueDto = issueDbTester.insertIssue(issue -> issue.setType(type));
logInAndAddProjectPermission("john", issueDto, USER);

expectedException.expect(ForbiddenException.class);
call(issueDto.getKey(), BUG.name());
call(issueDto.getKey(), type.name());
}

@Test
@UseDataProvider("allTypesExceptSecurityHotspot")
public void fail_if_trying_to_change_type_of_a_hotspot(RuleType type) {
long now = 1_999_777_234L;
when(system2.now()).thenReturn(now);
IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(SECURITY_HOTSPOT));
setUserWithBrowseAndAdministerIssuePermission(issueDto);

expectedException.expect(IllegalArgumentException.class);
call(issueDto.getKey(), type.name());
}

@Test
@@ -229,4 +246,25 @@ public class SetTypeActionTest {
.extracting(ComponentDto::uuid)
.containsOnly(issue.getComponentUuid(), issue.getProjectUuid());
}

@DataProvider
public static Object[][] allTypesExceptSecurityHotspot() {
return EnumSet.allOf(RuleType.class)
.stream()
.filter(ruleType -> SECURITY_HOTSPOT != ruleType)
.map(t -> new Object[] {t})
.toArray(Object[][]::new);
}

@DataProvider
public static Object[][] allTypesFromToExceptHotspots() {
Set<RuleType> set = EnumSet.allOf(RuleType.class)
.stream()
.filter(ruleType -> SECURITY_HOTSPOT != ruleType)
.collect(Collectors.toSet());
return Sets.cartesianProduct(set, set)
.stream()
.map(ruleTypes -> new Object[] {ruleTypes.get(0), ruleTypes.get(1)})
.toArray(Object[][]::new);
}
}

+ 0
- 11
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java View File

@@ -89,8 +89,6 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
private Date updateDate;
private Date closeDate;

private boolean isFromHotspot = false;

// Current changes
private FieldDiffs currentChange = null;

@@ -607,15 +605,6 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
}
}

public DefaultIssue setIsFromHotspot(boolean value) {
this.isFromHotspot = value;
return this;
}

public boolean isFromHotspot() {
return isFromHotspot;
}

public DefaultIssue setTags(Collection<String> tags) {
this.tags = new LinkedHashSet<>(tags);
return this;

+ 3
- 1
sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java View File

@@ -53,7 +53,9 @@ public interface DefaultTransitions {

/**
* @since 7.8
* @deprecated since 8.1, security hotspots can no longer be converted to vulnerabilities
*/
@Deprecated
String OPEN_AS_VULNERABILITY = "openasvulnerability";

/**
@@ -65,5 +67,5 @@ public interface DefaultTransitions {
* @since 4.4
*/
List<String> ALL = unmodifiableList(asList(CONFIRM, UNCONFIRM, REOPEN, RESOLVE, FALSE_POSITIVE, WONT_FIX, CLOSE,
SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY,RESET_AS_TO_REVIEW));
SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, RESET_AS_TO_REVIEW));
}

Loading…
Cancel
Save