]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10874 New issue transitions for security hotspots
authorJulien HENRY <julien.henry@sonarsource.com>
Wed, 13 Jun 2018 08:34:09 +0000 (10:34 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 4 Jul 2018 07:31:04 +0000 (09:31 +0200)
44 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleTypeCopier.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/Function.java
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/FunctionExecutor.java
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/HasType.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsManualVulnerability.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsNotHotspotNorManualVulnerability.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
server/sonar-server/src/main/java/org/sonar/server/issue/workflow/SetType.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
server/sonar-server/src/test/java/org/sonar/server/issue/SetSeverityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.tsx
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/IssueType.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
server/sonar-web/src/main/js/components/issue/types.js
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java
sonar-ws/src/main/protobuf/ws-issues.proto

index c1166f7d55a1dd647441f6ac2286c1f01d324b02..52855f4234bab0fbdb5a4d529b9e8b75b6870763 100644 (file)
@@ -23,7 +23,6 @@ import com.google.common.annotations.VisibleForTesting;
 import java.util.Date;
 import java.util.Optional;
 import org.sonar.api.issue.Issue;
-import org.sonar.api.rules.RuleType;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.FieldDiffs;
@@ -132,9 +131,6 @@ public class IssueLifecycle {
   public void mergeExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
     raw.setKey(base.key());
     raw.setNew(false);
-    if (raw.type() == RuleType.SECURITY_HOTSPOT) {
-      raw.setIsFromHotspot(true);
-    }
     copyFields(raw, base);
 
     if (base.manualSeverity()) {
index de0a30cd9dabd0c0858a2e53e41be6fa78b491e1..cecfee913509881738a50f0dc47776ec75e9d945 100644 (file)
@@ -33,13 +33,13 @@ public class RuleTypeCopier extends IssueVisitor {
 
   @Override
   public void onIssue(Component component, DefaultIssue issue) {
+    Rule rule = ruleRepository.getByKey(issue.ruleKey());
     if (issue.type() == null) {
-      Rule rule = ruleRepository.getByKey(issue.ruleKey());
       if (!rule.isExternal()) {
         // rule type should never be null for rules created by plugins (non-external rules)
         issue.setType(rule.getType());
       }
     }
-    issue.setIsFromHotspot(issue.type() == RuleType.SECURITY_HOTSPOT);
+    issue.setIsFromHotspot(rule.getType() == RuleType.SECURITY_HOTSPOT);
   }
 }
index 23de915307112a10c92b0c0631674eb55214026b..e76865e0c92fddf0ed671010bf9700b50e9f36d0 100644 (file)
@@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableMap;
 import java.util.Date;
 import org.junit.Rule;
 import org.junit.Test;
-import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
 import org.sonar.ce.task.projectanalysis.analysis.Branch;
@@ -85,38 +84,6 @@ public class IssueLifecycleTest {
     assertThat(issue.isCopied()).isFalse();
   }
 
-  @Test
-  public void set_fromHotspot_flag_for_existing_vulnerability() {
-    DefaultIssue raw = new DefaultIssue()
-      .setNew(true)
-      .setKey("RAW_KEY")
-      .setType(RuleType.SECURITY_HOTSPOT)
-      .setCreationDate(parseDate("2015-10-01"))
-      .setUpdateDate(parseDate("2015-10-02"))
-      .setCloseDate(parseDate("2015-10-03"));
-
-    DefaultIssue base = new DefaultIssue()
-      .setKey("BASE_KEY")
-      .setType(RuleType.VULNERABILITY)
-      .setResolution(RESOLUTION_FIXED)
-      .setStatus(STATUS_CLOSED)
-      .setSeverity(BLOCKER)
-      .setCreationDate(parseDate("2015-01-01"))
-      .setUpdateDate(parseDate("2015-01-02"))
-      .setCloseDate(parseDate("2015-01-03"));
-
-    underTest.mergeExistingOpenIssue(raw, base);
-
-    assertThat(raw.isNew()).isFalse();
-    assertThat(raw.key()).isNotNull();
-    assertThat(raw.key()).isEqualTo(base.key());
-    assertThat(raw.creationDate()).isEqualTo(base.creationDate());
-    assertThat(raw.updateDate()).isEqualTo(base.updateDate());
-    assertThat(raw.closeDate()).isEqualTo(base.closeDate());
-    assertThat(raw.type()).isEqualTo(RuleType.VULNERABILITY);
-    assertThat(raw.isFromHotspot()).isTrue();
-  }
-
   @Test
   public void mergeIssueFromShortLivingBranch() {
     DefaultIssue raw = new DefaultIssue()
index 90aa92e568b02e2dc09cad822d914797c940def6..5a2054cfab44bc3e9efba90e8ebdd279a531a118 100644 (file)
@@ -25,6 +25,7 @@ import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.sonar.api.issue.condition.IsUnResolved;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.db.DbClient;
@@ -51,7 +52,7 @@ public class AssignAction extends Action {
     super(ASSIGN_KEY);
     this.dbClient = dbClient;
     this.issueFieldsSetter = issueFieldsSetter;
-    super.setConditions(new IsUnResolved());
+    super.setConditions(new IsUnResolved(), issue -> ((DefaultIssue) issue).type() != RuleType.SECURITY_HOTSPOT);
   }
 
   @Override
index 3b905d1011fa51274da5d2e923df51cad2964345..cc7fe251dd81ff0220fced12a0b5f79a1c9bfc19 100644 (file)
@@ -21,14 +21,16 @@ package org.sonar.server.issue;
 
 import java.util.Collection;
 import java.util.Map;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.condition.IsUnResolved;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.server.user.UserSession;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 
 @ServerSide
 public class SetSeverityAction extends Action {
@@ -43,11 +45,12 @@ public class SetSeverityAction extends Action {
     super(SET_SEVERITY_KEY);
     this.issueUpdater = issueUpdater;
     this.userSession = userSession;
-    super.setConditions(new IsUnResolved(), issue -> isCurrentUserIssueAdmin(issue.projectUuid()));
+    super.setConditions(new IsUnResolved(), this::isCurrentUserIssueAdminOrSecurityAuditor);
   }
 
-  private boolean isCurrentUserIssueAdmin(String projectUuid) {
-    return userSession.hasComponentUuidPermission(UserRole.ISSUE_ADMIN, projectUuid);
+  private boolean isCurrentUserIssueAdminOrSecurityAuditor(Issue issue) {
+    DefaultIssue defaultIssue = (DefaultIssue) issue;
+    return ((defaultIssue.type() != RuleType.SECURITY_HOTSPOT && userSession.hasComponentUuidPermission(ISSUE_ADMIN, issue.projectUuid())));
   }
 
   @Override
index 193fe499247e65b188c5da1a1a4fe7be6560bb45..8b6d7f677596a0fbd7d1b79d2cee45d0ad089200 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.issue;
 
 import java.util.Collection;
 import java.util.Map;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.condition.IsUnResolved;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.web.UserRole;
@@ -42,11 +43,11 @@ public class SetTypeAction extends Action {
     super(SET_TYPE_KEY);
     this.issueUpdater = issueUpdater;
     this.userSession = userSession;
-    super.setConditions(new IsUnResolved(), issue -> isCurrentUserIssueAdmin(issue.projectUuid()));
+    super.setConditions(new IsUnResolved(), this::isCurrentUserIssueAdmin);
   }
 
-  private boolean isCurrentUserIssueAdmin(String projectUuid) {
-    return userSession.hasComponentUuidPermission(UserRole.ISSUE_ADMIN, projectUuid);
+  private boolean isCurrentUserIssueAdmin(Issue issue) {
+    return !((DefaultIssue) issue).isFromHotspot() && userSession.hasComponentUuidPermission(UserRole.ISSUE_ADMIN, issue.projectUuid());
   }
 
   @Override
index c289816ba5effaa369da78af4ecf100f2ffdb00c..77d24bc593ebfd2d0b1034e61feba1051d824b11 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.issue.workflow;
 
 import javax.annotation.Nullable;
 import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
 import org.sonar.db.user.UserDto;
 
 interface Function {
@@ -34,6 +35,8 @@ interface Function {
     Context setCloseDate(boolean b);
 
     Context setLine(@Nullable Integer line);
+
+    Context setType(@Nullable RuleType type);
   }
 
   void execute(Context context);
index bb76fad9de2296a6ea1054a6466571e5e1ed0dda..d92fa575d80cd5e20035b1440d8ad5ca4b0ed449 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.issue.workflow;
 import javax.annotation.Nullable;
 import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
@@ -86,5 +87,11 @@ public class FunctionExecutor {
       updater.setLine(issue, line);
       return this;
     }
+
+    @Override
+    public Function.Context setType(RuleType type) {
+      updater.setType(issue, type, changeContext);
+      return this;
+    }
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/HasType.java b/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/HasType.java
new file mode 100644 (file)
index 0000000..4767298
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 java.util.EnumSet;
+import java.util.Set;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.condition.Condition;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.DefaultIssue;
+
+import static java.util.Arrays.asList;
+
+public class HasType implements Condition {
+
+  private final Set<RuleType> types;
+
+  public HasType(RuleType first, RuleType... others) {
+    this.types = EnumSet.noneOf(RuleType.class);
+    this.types.add(first);
+    this.types.addAll(asList(others));
+  }
+
+  @Override
+  public boolean matches(Issue issue) {
+    return types.contains(((DefaultIssue) issue).type());
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsManualVulnerability.java b/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsManualVulnerability.java
new file mode 100644 (file)
index 0000000..7eadb25
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.issue.condition.Condition;
+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();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsNotHotspotNorManualVulnerability.java b/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/IsNotHotspotNorManualVulnerability.java
new file mode 100644 (file)
index 0000000..af9c359
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.issue.condition.Condition;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.DefaultIssue;
+
+enum IsNotHotspotNorManualVulnerability implements Condition {
+  INSTANCE;
+
+  @Override
+  public boolean matches(Issue issue) {
+    return ((DefaultIssue) issue).type() != RuleType.SECURITY_HOTSPOT && !((DefaultIssue) issue).isFromHotspot();
+  }
+}
index 0c22a8a09356106500e5f841f6699d33530c33c3..aec49fbf75e37147d3132d8b83002b78b4554e10 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.issue.DefaultTransitions;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.condition.HasResolution;
 import org.sonar.api.issue.condition.NotCondition;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssue;
@@ -57,52 +58,64 @@ public class IssueWorkflow implements Startable {
 
     buildManualTransitions(builder);
     buildAutomaticTransitions(builder);
+    buildSecurityHotspotTransitions(builder);
     machine = builder.build();
   }
 
   private static void buildManualTransitions(StateMachine.Builder builder) {
-    builder.transition(Transition.builder(DefaultTransitions.CONFIRM)
-      .from(Issue.STATUS_OPEN).to(Issue.STATUS_CONFIRMED)
-      .functions(new SetResolution(null))
-      .build())
+    builder
+      .transition(Transition.builder(DefaultTransitions.CONFIRM)
+        .from(Issue.STATUS_OPEN).to(Issue.STATUS_CONFIRMED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
+        .functions(new SetResolution(null))
+        .build())
       .transition(Transition.builder(DefaultTransitions.CONFIRM)
         .from(Issue.STATUS_REOPENED).to(Issue.STATUS_CONFIRMED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(null))
         .build())
       .transition(Transition.builder(DefaultTransitions.UNCONFIRM)
         .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_REOPENED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(null))
         .build())
       .transition(Transition.builder(DefaultTransitions.RESOLVE)
         .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FIXED))
         .build())
       .transition(Transition.builder(DefaultTransitions.RESOLVE)
         .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FIXED))
         .build())
       .transition(Transition.builder(DefaultTransitions.RESOLVE)
         .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FIXED))
         .build())
       .transition(Transition.builder(DefaultTransitions.REOPEN)
         .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(null))
         .build())
 
       // resolve as false-positive
       .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
         .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build())
       .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
         .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build())
       .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
         .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build())
@@ -110,19 +123,90 @@ public class IssueWorkflow implements Startable {
       // resolve as won't fix
       .transition(Transition.builder(DefaultTransitions.WONT_FIX)
         .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build())
       .transition(Transition.builder(DefaultTransitions.WONT_FIX)
         .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build())
       .transition(Transition.builder(DefaultTransitions.WONT_FIX)
         .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
         .requiredProjectPermission(UserRole.ISSUE_ADMIN)
         .build());
+  }
+
+  private static void buildSecurityHotspotTransitions(StateMachine.Builder builder) {
+    builder
+      .transition(Transition.builder(DefaultTransitions.DETECT)
+        .from(Issue.STATUS_OPEN).to(Issue.STATUS_OPEN)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
+        .functions(new SetType(RuleType.VULNERABILITY))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.DETECT)
+        .from(Issue.STATUS_REOPENED).to(Issue.STATUS_OPEN)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
+        .functions(new SetType(RuleType.VULNERABILITY))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.DETECT)
+        .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_OPEN)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(Issue.RESOLUTION_WONT_FIX))
+        .functions(new SetType(RuleType.VULNERABILITY), new SetResolution(null))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.DISMISS)
+        .from(Issue.STATUS_OPEN).to(Issue.STATUS_REOPENED)
+        .conditions(IsManualVulnerability.INSTANCE)
+        .functions(new SetType(RuleType.SECURITY_HOTSPOT))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.REQUEST_REVIEW)
+        .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+        .conditions(IsManualVulnerability.INSTANCE)
+        .functions(new SetType(RuleType.SECURITY_HOTSPOT), new SetResolution(Issue.RESOLUTION_FIXED))
+        .build())
+      .transition(Transition.builder(DefaultTransitions.REQUEST_REVIEW)
+        .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+        .conditions(IsManualVulnerability.INSTANCE)
+        .functions(new SetType(RuleType.SECURITY_HOTSPOT), new SetResolution(Issue.RESOLUTION_FIXED))
+        .build())
+      .transition(Transition.builder(DefaultTransitions.REJECT)
+        .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(Issue.RESOLUTION_FIXED))
+        .functions(new SetType(RuleType.VULNERABILITY), new SetResolution(null))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.ACCEPT)
+        .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_RESOLVED)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(Issue.RESOLUTION_FIXED))
+        .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.CLEAR)
+        .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
+        .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.CLEAR)
+        .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT))
+        .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build())
+      .transition(Transition.builder(DefaultTransitions.REOPEN_HOTSPOT)
+        .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
+        .conditions(new HasType(RuleType.SECURITY_HOTSPOT), new HasResolution(Issue.RESOLUTION_WONT_FIX))
+        .functions(new SetResolution(null))
+        .requiredProjectPermission(UserRole.ISSUE_ADMIN) // TODO need to check new permission
+        .build());
 
   }
 
@@ -157,7 +241,7 @@ public class IssueWorkflow implements Startable {
       // Reopen issues that are marked as resolved but that are still alive.
       .transition(Transition.builder("automaticreopen")
         .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
-        .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED))
+        .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED), IsNotHotspotNorManualVulnerability.INSTANCE)
         .functions(new SetResolution(null), new SetCloseDate(false))
         .automatic()
         .build());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/SetType.java b/server/sonar-server/src/main/java/org/sonar/server/issue/workflow/SetType.java
new file mode 100644 (file)
index 0000000..47f3487
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+
+public class SetType implements Function {
+  private final RuleType type;
+
+  public SetType(@Nullable RuleType type) {
+    this.type = type;
+  }
+
+  @Override
+  public void execute(Context context) {
+    context.setType(type);
+  }
+}
index ce8714515b8569d80e89f45a576311371af85821..a44ea87e12ace451ef5f63b58adc56dd99f107e3 100644 (file)
@@ -26,6 +26,7 @@ import java.util.Optional;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang.BooleanUtils;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -115,6 +116,7 @@ public class AssignAction implements IssuesWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       IssueDto issueDto = issueFinder.getByKey(dbSession, issueKey);
       DefaultIssue issue = issueDto.toDefaultIssue();
+      checkArgument(issue.type() != RuleType.SECURITY_HOTSPOT,"It is not allowed to assign a security hotspot");
       UserDto user = getUser(dbSession, login);
       if (user != null) {
         checkMembership(dbSession, issueDto, user);
index 17c2e938dfb1fe2e53ee42c2f9bb885054510fd9..37d5fda28f24f2b64248a988e972b7678492d411 100644 (file)
@@ -68,11 +68,13 @@ public class DoTransitionAction implements IssuesWsAction {
   public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction(ACTION_DO_TRANSITION)
       .setDescription("Do workflow transition on an issue. Requires authentication and Browse permission on project.<br/>" +
-        "The transitions '" + DefaultTransitions.WONT_FIX + "' and '" + DefaultTransitions.FALSE_POSITIVE + "' require the permission 'Administer Issues'.")
+        "The transitions '" + DefaultTransitions.WONT_FIX + "' and '" + DefaultTransitions.FALSE_POSITIVE + "' require the permission 'Administer Issues'.<br/>" +
+        "The transitions involving security hotspots (except '" + DefaultTransitions.REQUEST_REVIEW + "') require the permission 'Administer Security Hotspot'.")
       .setSince("3.6")
       .setChangelog(
         new Change("6.5", "the database ids of the components are removed from the response"),
-        new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."))
+        new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."),
+        new Change("7.3", "added transitions for security hotspots"))
       .setHandler(this)
       .setResponseExample(Resources.getResource(this.getClass(), "do_transition-example.json"))
       .setPost(true);
index 5fe1c04436f760cb0b96c09b7a3b3d539f76cfa3..f12eb753de2ba91a6f5df9395ba862bfabdea150 100644 (file)
@@ -173,7 +173,8 @@ public class SearchAction implements IssuesWsAction {
         new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and manual issue features)"),
         new Change("5.5", "response field 'debt' is renamed 'effort'"),
         new Change("7.2", "response field 'externalRuleEngine' added to issues that have been imported from an external rule engine"),
-        new Change("7.2", format("value '%s' in parameter '%s' is deprecated, it won't have any effect", SORT_BY_ASSIGNEE, Param.SORT)))
+        new Change("7.2", format("value '%s' in parameter '%s' is deprecated, it won't have any effect", SORT_BY_ASSIGNEE, Param.SORT)),
+        new Change("7.3", "response field 'fromHotspot' added to issues that are security hotspots"))
       .setResponseExample(getClass().getResource("search-example.json"));
 
     action.addPagingParams(100, MAX_LIMIT);
index b5acc861c56201e1fb4d1bc2ecf68e94de0402db..e39cbff71efbe93a1c323086fa1cd64fd5671aba 100644 (file)
@@ -144,7 +144,7 @@ public class SearchResponseData {
     }
   }
 
-  public void addActions(String issueKey, List<String> actions) {
+  public void addActions(String issueKey, Iterable<String> actions) {
     actionsByIssueKey.putAll(issueKey, actions);
   }
 
index 4bfbc23ff8dabd2a6ee6163d59657127791fcc3c..2bf22c04931bc92a6efec4251f17c8e1392eb619 100644 (file)
@@ -179,6 +179,7 @@ public class SearchResponseFormat {
     if (dto.isExternal()) {
       issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey()));
     }
+    issueBuilder.setFromHotspot(dto.isFromHotspot());
     issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity()));
     setNullable(data.getUserByUuid(dto.getAssigneeUuid()), assignee -> issueBuilder.setAssignee(assignee.getLogin()));
     setNullable(emptyToNull(dto.getResolution()), issueBuilder::setResolution);
index e66f427cebb7f8d9b845170965049f11dda4bd69..dfe4fea1788217ebb8f0765d6800b5cfcf0254b1 100644 (file)
@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
@@ -53,8 +54,8 @@ import org.sonarqube.ws.client.issue.IssuesWsParameters;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.ImmutableSet.copyOf;
-import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Sets.difference;
+import static com.google.common.collect.Sets.newHashSet;
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 import static java.util.Optional.ofNullable;
@@ -267,20 +268,25 @@ public class SearchResponseLoader {
     }
   }
 
-  private List<String> listAvailableActions(IssueDto issue, ComponentDto project) {
-    List<String> availableActions = newArrayList();
+  private Set<String> listAvailableActions(IssueDto issue, ComponentDto project) {
+    Set<String> availableActions = newHashSet();
     String login = userSession.getLogin();
     if (login == null) {
-      return Collections.emptyList();
+      return Collections.emptySet();
     }
+    RuleType ruleType = RuleType.valueOf(issue.getType());
     availableActions.add(COMMENT_KEY);
     if (issue.getResolution() != null) {
       return availableActions;
     }
-    availableActions.add(ASSIGN_KEY);
+    if (ruleType != RuleType.SECURITY_HOTSPOT) {
+      availableActions.add(ASSIGN_KEY);
+    }
     availableActions.add("set_tags");
-    if (userSession.hasComponentPermission(ISSUE_ADMIN, project)) {
+    if (!issue.isFromHotspot() && 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;
index 241b6be963f98f7f5ad2989124c5c9a043fb4706..5675ac9b7304ac788ad58182c094b3a8c96dd6d3 100644 (file)
@@ -106,6 +106,9 @@ 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()) {
+      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());
index bc5aedea1d19d3ffdd67c65ef9c7bb97987e74ed..1a02b737deab54b30ee081797dfc90ea2180dda0 100644 (file)
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.FieldDiffs;
 import org.sonar.core.issue.IssueChangeContext;
@@ -102,6 +103,16 @@ public class SetSeverityActionTest {
     assertThat(action.supports(issue.setResolution(Issue.RESOLUTION_FIXED))).isFalse();
   }
 
+  @Test
+  public void doesnt_support_security_hotspots() {
+    IssueDto issueDto = newIssue().setSeverity(MAJOR);
+    DefaultIssue issue = issueDto.toDefaultIssue();
+    setUserWithBrowseAndAdministerIssuePermission(issueDto);
+
+    assertThat(action.supports(issue.setType(RuleType.CODE_SMELL))).isTrue();
+    assertThat(action.supports(issue.setType(RuleType.SECURITY_HOTSPOT))).isFalse();
+  }
+
   @Test
   public void support_only_issues_with_issue_admin_permission() {
     IssueDto authorizedIssue = newIssue().setSeverity(MAJOR);
index e9a1efd5cfe535a1d3f935064d366e1a3948be9d..fa8786037e6b16c0bc0f445f5d323fcf8fe835c0 100644 (file)
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -197,6 +198,21 @@ public class AssignActionTest {
       .execute();
   }
 
+  @Test
+  public void fail_when_trying_to_assign_hotspot() {
+    IssueDto issueDto = db.issues().insertIssue(i -> i.setType(RuleType.SECURITY_HOTSPOT));
+    setUserWithBrowsePermission(issueDto);
+    UserDto arthur = insertUser("arthur");
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("It is not allowed to assign a security hotspot");
+
+    ws.newRequest()
+      .setParam("issue", issueDto.getKey())
+      .setParam("assignee", "arthur")
+      .execute();
+  }
+
   @Test
   public void fail_when_assignee_is_disabled() {
     IssueDto issue = newIssueWithBrowsePermission();
@@ -239,7 +255,7 @@ public class AssignActionTest {
   @Test
   public void fail_when_assignee_is_not_member_of_organization_of_project_issue() {
     OrganizationDto org = db.organizations().insert(organizationDto -> organizationDto.setKey("Organization key"));
-    IssueDto issueDto = db.issues().insertIssue(org);
+    IssueDto issueDto = db.issues().insertIssue(org, i -> i.setType(RuleType.CODE_SMELL));
     setUserWithBrowsePermission(issueDto);
     OrganizationDto otherOrganization = db.organizations().insert();
     UserDto assignee = db.users().insertUser("arthur");
@@ -261,11 +277,12 @@ public class AssignActionTest {
   }
 
   private IssueDto newIssue(String assignee) {
-      IssueDto issue = db.issues().insertIssue(
+    IssueDto issue = db.issues().insertIssue(
       issueDto -> issueDto
         .setAssigneeUuid(assignee)
         .setCreatedAt(PAST).setIssueCreationTime(PAST)
-        .setUpdatedAt(PAST).setIssueUpdateTime(PAST));
+        .setUpdatedAt(PAST).setIssueUpdateTime(PAST)
+        .setType(RuleType.CODE_SMELL));
     return issue;
   }
 
index d6fe59d9f61d6e823b4fa2b1a8a1d556d6167ebf..710fa2131abdb4c4422badfba416f47f518c4525 100644 (file)
@@ -64,6 +64,7 @@ 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.web.UserRole.ISSUE_ADMIN;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.core.util.Protobuf.setNullable;
@@ -118,6 +119,18 @@ public class SetTypeActionTest {
       .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());
+  }
+
   @Test
   public void insert_entry_in_changelog_when_setting_type() {
     IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(CODE_SMELL));
index 75ac42d3237e2923f007fbb404c367c79c333fa1..15639fc96c37db70f948dd9c01acf93be4ce392d 100644 (file)
@@ -231,6 +231,7 @@ export interface Issue {
   fromExternalRule?: boolean;
   key: string;
   flows: FlowLocation[][];
+  fromHotspot: boolean;
   line?: number;
   message: string;
   organization: string;
index 639ab681d9b45a85e1987fa8e8d8abb1866ce333..549928cb0ea22daa1f0cf5f28c54a343826679ee 100644 (file)
@@ -40,7 +40,8 @@ const issue = {
   status: '',
   type: '',
   secondaryLocations: [],
-  flows: []
+  flows: [],
+  fromHotspot: false
 };
 
 it('should render', () => {
index ee41d9fd30674d34fd3ed197aaaf94c0057310c1..744a5418ee6f74feb97e05fcdce948a3d9bedb6b 100644 (file)
@@ -14,6 +14,7 @@ exports[`should render 1`] = `
         "componentUuid": "",
         "creationDate": "",
         "flows": Array [],
+        "fromHotspot": false,
         "key": "",
         "message": "",
         "organization": "",
index c9dc513d817c9aecaa4e5eae6832022e7319ff8f..5fd8d5e434a27c3ecd5d7ecac6023e202faf864d 100644 (file)
@@ -30,6 +30,7 @@ const issueBase: Issue = {
   creationDate: '',
   key: '',
   flows: [],
+  fromHotspot: false,
   message: '',
   organization: '',
   project: '',
index b4fd9fc6e70e682089860c85b6653d212cce5440..d63bbf1a2bee378a4f583e3be06f07c1ad8240b0 100644 (file)
@@ -31,6 +31,7 @@ const issueBase: Issue = {
   creationDate: '',
   key: '',
   flows: [],
+  fromHotspot: false,
   message: '',
   organization: '',
   project: '',
index 7e9c9ec5bbe9b2095ded7dbc19fc4a37a4e3f6a1..bd621904bb9b19bb58df3d6d0172935235d9c19e 100644 (file)
@@ -30,6 +30,7 @@ const issueBase: Issue = {
   creationDate: '',
   key: '',
   flows: [],
+  fromHotspot: false,
   message: '',
   organization: '',
   project: '',
index dc0f5c922005558a06238ebbcffd93df43604a51..5546edb66459bdc4056d2653e3f6698c9000021d 100644 (file)
@@ -53,6 +53,7 @@ exports[`render code 1`] = `
           "componentUuid": "",
           "creationDate": "",
           "flows": Array [],
+          "fromHotspot": false,
           "key": "issue-1",
           "message": "",
           "organization": "",
@@ -74,6 +75,7 @@ exports[`render code 1`] = `
           "componentUuid": "",
           "creationDate": "",
           "flows": Array [],
+          "fromHotspot": false,
           "key": "issue-2",
           "message": "",
           "organization": "",
index d19a4e96ef064f529ca4a9dc74feec1380181c81..2fa14dfb1b326f1efae5c872b0d0f3dfb7c3780e 100644 (file)
@@ -15,6 +15,7 @@ exports[`render issues list 1`] = `
         "componentUuid": "",
         "creationDate": "",
         "flows": Array [],
+        "fromHotspot": false,
         "key": "foo",
         "message": "",
         "organization": "",
@@ -47,6 +48,7 @@ exports[`render issues list 1`] = `
         "componentUuid": "",
         "creationDate": "",
         "flows": Array [],
+        "fromHotspot": false,
         "key": "bar",
         "message": "",
         "organization": "",
index f12909c0f9a0fc311fcd5dd329502aba320ed4f5..10d0d20281b3a7d7650502b5efe6da4dd4cffc27 100644 (file)
@@ -89,6 +89,7 @@ export default class IssueActionsBar extends React.PureComponent {
     const canAssign = issue.actions.includes('assign');
     const canComment = issue.actions.includes('comment');
     const canSetSeverity = issue.actions.includes('set_severity');
+    const canSetType = issue.actions.includes('set_type');
     const canSetTags = issue.actions.includes('set_tags');
     const hasTransitions = issue.transitions && issue.transitions.length > 0;
 
@@ -97,8 +98,8 @@ export default class IssueActionsBar extends React.PureComponent {
         <ul className="issue-meta-list">
           <li className="issue-meta">
             <IssueType
-              canSetSeverity={canSetSeverity}
-              isOpen={this.props.currentPopup === 'set-type' && canSetSeverity}
+              canSetType={canSetType}
+              isOpen={this.props.currentPopup === 'set-type' && canSetType}
               issue={issue}
               setIssueProperty={this.setIssueProperty}
               togglePopup={this.props.togglePopup}
index 5b4e82d413b87077b497d761a61580ccf332bcec..6b20d3e9246717b708080ae8e4a6bd1e6a991e61 100644 (file)
@@ -27,7 +27,8 @@ import { Button } from '../../ui/buttons';
 
 export default class IssueMessage extends React.PureComponent {
   /*:: props: {
-    engine?: string;
+    engine?: string,
+    manualVulnerability: boolean,
     message: string,
     organization: string,
     rule: string
@@ -63,6 +64,13 @@ export default class IssueMessage extends React.PureComponent {
             </div>
           </Tooltip>
         )}
+        {this.props.manualVulnerability && (
+          <Tooltip overlay={translate('issue.manual_vulnerability.description')}>
+            <div className="outline-badge badge-tiny-height spacer-left vertical-text-top">
+              {translate('issue.manual_vulnerability')}
+            </div>
+          </Tooltip>
+        )}
       </div>
     );
   }
index 1781e6d637d4e37a41efda9d14f83bf7f9828b28..3e1bea1521033e6a3c5ea34862b4572e290d2881 100644 (file)
@@ -75,6 +75,7 @@ export default function IssueTitleBar(props /*: Props */) {
     <div className="issue-row">
       <IssueMessage
         engine={issue.externalRuleEngine}
+        manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'}
         message={issue.message}
         organization={issue.organization}
         rule={issue.rule}
index 380e8668a329f62f1dfbe2ca56504f35d45f6766..909a718cb97b2b47fe6927c93a2877de12c321ea 100644 (file)
@@ -30,7 +30,7 @@ import { translate } from '../../../helpers/l10n';
 
 /*::
 type Props = {
-  canSetSeverity: boolean,
+  canSetType: boolean,
   isOpen: boolean,
   issue: Issue,
   setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
@@ -55,12 +55,12 @@ export default class IssueType extends React.PureComponent {
 
   render() {
     const { issue } = this.props;
-    if (this.props.canSetSeverity) {
+    if (this.props.canSetType) {
       return (
         <div className="dropdown">
           <Toggler
             onRequestClose={this.handleClose}
-            open={this.props.isOpen && this.props.canSetSeverity}
+            open={this.props.isOpen && this.props.canSetType}
             overlay={<SetTypePopup issue={issue} onSelect={this.setType} />}>
             <Button
               className="button-link issue-action issue-action-with-options js-issue-set-type"
index 785bc2a21f1ff652211f38bd5999727acc14d969..0edabcb727fed13cae3c0ebcbdcf5ff9112d0a55 100644 (file)
@@ -24,6 +24,7 @@ import IssueMessage from '../IssueMessage';
 it('should render with the message and a link to open the rule', () => {
   const element = shallow(
     <IssueMessage
+      manualVulnerability={false}
       rule="javascript:S1067"
       message="Reduce the number of conditional operators (4) used in the expression"
       organization="myorg"
index 9134a0c1f79a64ca3952680ef0ce0c5ae2373c50..38e804a1cd573ffdd5314bc3265fd5ab0d820a48 100644 (file)
@@ -36,7 +36,8 @@ const issue = {
   rule: 'javascript:S1067',
   message: 'Reduce the number of conditional operators (4) used in the expression',
   flows: [],
-  secondaryLocations: []
+  secondaryLocations: [],
+  fromHotspot: false
 };
 
 const issueWithLocations = {
index 51132f338ca280f9043952df89aa889c7ac717a0..78bb502686befef4d05ce6af1dacb5374b27e3ea 100644 (file)
@@ -29,7 +29,7 @@ const issue = {
 it('should render without the action when the correct rights are missing', () => {
   const element = shallow(
     <IssueType
-      canSetSeverity={false}
+      canSetType={false}
       isOpen={false}
       issue={issue}
       setIssueProperty={jest.fn()}
@@ -42,7 +42,7 @@ it('should render without the action when the correct rights are missing', () =>
 it('should render with the action', () => {
   const element = shallow(
     <IssueType
-      canSetSeverity={true}
+      canSetType={true}
       isOpen={false}
       issue={issue}
       setIssueProperty={jest.fn()}
@@ -56,7 +56,7 @@ it('should open the popup when the button is clicked', () => {
   const toggle = jest.fn();
   const element = shallow(
     <IssueType
-      canSetSeverity={true}
+      canSetType={true}
       isOpen={false}
       issue={issue}
       setIssueProperty={jest.fn()}
index ac521d1dfc95dd20aa750be4934b8713d1fe68d0..b61379d35ab0e4017f6d49d7527e0b5e56c20cfd 100644 (file)
@@ -11,6 +11,7 @@ exports[`should render the titlebar correctly 1`] = `
   className="issue-row"
 >
   <IssueMessage
+    manualVulnerability={false}
     message="Reduce the number of conditional operators (4) used in the expression"
     organization="myorg"
     rule="javascript:S1067"
@@ -31,6 +32,7 @@ exports[`should render the titlebar correctly 1`] = `
             Object {
               "creationDate": "2017-03-01T09:36:01+0100",
               "flows": Array [],
+              "fromHotspot": false,
               "key": "AVsae-CQS-9G3txfbFN2",
               "line": 25,
               "message": "Reduce the number of conditional operators (4) used in the expression",
@@ -95,6 +97,7 @@ exports[`should render the titlebar with the filter 1`] = `
   className="issue-row"
 >
   <IssueMessage
+    manualVulnerability={false}
     message="Reduce the number of conditional operators (4) used in the expression"
     organization="myorg"
     rule="javascript:S1067"
@@ -115,6 +118,7 @@ exports[`should render the titlebar with the filter 1`] = `
             Object {
               "creationDate": "2017-03-01T09:36:01+0100",
               "flows": Array [],
+              "fromHotspot": false,
               "key": "AVsae-CQS-9G3txfbFN2",
               "line": 25,
               "message": "Reduce the number of conditional operators (4) used in the expression",
@@ -177,6 +181,7 @@ exports[`should render the titlebar with the filter 1`] = `
             Object {
               "creationDate": "2017-03-01T09:36:01+0100",
               "flows": Array [],
+              "fromHotspot": false,
               "key": "AVsae-CQS-9G3txfbFN2",
               "line": 25,
               "message": "Reduce the number of conditional operators (4) used in the expression",
index 2a5ce7ed3face3467fbe9f7833c4a6c3e7b44446..c436d91f1e34f7ed4f462dc53e260a00ed62a063 100644 (file)
@@ -68,6 +68,7 @@ export type Issue = {
   externalRuleEngine?: string,
   key: string,
   flows: Array<Array<FlowLocation>>,
+  fromHotspot: boolean,
   line?: number,
   message: string,
   organization: string,
index 125c9bd51e6d259be3f009f8feeed1b3da521394..b2cf1843f24e55c3f9480dda22209f6f03f453d1 100644 (file)
@@ -83,6 +83,7 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
   private Object locations = null;
 
   private boolean isFromExternalRuleEngine;
+
   // FUNCTIONAL DATES
   private Date creationDate;
   private Date updateDate;
index fb53c1bfbde19a285956136b004d76704cab6bac..3e0f4420b35ae925a3ae4374a8a38ce3c1d4d813 100644 (file)
@@ -566,6 +566,8 @@ issue.comment.formlink=Comment
 issue.comment.submit=Comment
 issue.comment.tell_why=Please tell why?
 issue.comment.delete_confirm_message=Do you want to delete this comment?
+issue.manual_vulnerability=Manual
+issue.manual_vulnerability.description=This Vulnerability was created from a Security Hotspot and has its own issue workflow.
 issue.rule_details=Rule Details
 issue.send_notifications=Send Notifications
 issue.transition=Transition
@@ -583,6 +585,20 @@ issue.transition.close=Close
 issue.transition.close.description=
 issue.transition.wontfix=Resolve as won't fix
 issue.transition.wontfix.description=This issue can be ignored because the rule is irrelevant in this context. Its effort won't be counted.
+issue.transition.detect=Detect
+issue.transition.detect.description=This security hotspot is actually a real vulnerability and must be fixed.
+issue.transition.dismiss=Dismiss
+issue.transition.dismiss.description=This vulnerability can't be fixed as is and needs more details from a security expert.
+issue.transition.reject=Reject
+issue.transition.reject.description=The fix has been reviewed by a security expert and the vulnerability is still there. Code must be fixed again.
+issue.transition.requestreview=Request review
+issue.transition.requestreview.description=The code has been fixed and a review by a security expert is required to confirm it.
+issue.transition.accept=Accept
+issue.transition.accept.description=The code has been fixed and the vulnerability has been removed. The issue can be closed.
+issue.transition.clear=Clear
+issue.transition.clear.description=There is no vulnerability in the code. The issue can be closed.
+issue.transition.reopenhotspot=Reopen
+issue.transition.reopenhotspot.description=This security hotspot should be analyzed again by a security expert.
 issue.set_severity=Change Severity
 issue.set_type=Change Type
 
index 7bf1ec4d02862e39a102c0a07a379e65dd386425..f34d80e00ad5b0de3bdd37b40e78d18e59a5c46f 100644 (file)
@@ -40,8 +40,19 @@ public interface DefaultTransitions {
    */
   String WONT_FIX = "wontfix";
 
+  /**
+   * @since 7.3
+   */
+  String DETECT = "detect";
+  String DISMISS = "dismiss";
+  String REJECT = "reject";
+  String REQUEST_REVIEW = "requestreview";
+  String ACCEPT = "accept";
+  String CLEAR = "clear";
+  String REOPEN_HOTSPOT = "reopenhotspot";
+
   /**
    * @since 4.4
    */
-  List<String> ALL = asList(CONFIRM, UNCONFIRM, REOPEN, RESOLVE, FALSE_POSITIVE, WONT_FIX, CLOSE);
+  List<String> ALL = asList(CONFIRM, UNCONFIRM, REOPEN, RESOLVE, FALSE_POSITIVE, WONT_FIX, CLOSE, DETECT, DISMISS, REJECT, REQUEST_REVIEW, ACCEPT, CLEAR, REOPEN_HOTSPOT);
 }
index e55bd9864db93c787b290bca92b0055e5ebc0e31..863c1345be2aaab853f446328f898af70c31e6dd 100644 (file)
@@ -159,6 +159,7 @@ message Issue {
   optional string pullRequest = 32;
 
   optional string externalRuleEngine = 33;
+  optional bool fromHotspot = 34;
 }
 
 message Transitions {