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;
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()) {
@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);
}
}
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;
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()
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;
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
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 {
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
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;
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
import javax.annotation.Nullable;
import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
import org.sonar.db.user.UserDto;
interface Function {
Context setCloseDate(boolean b);
Context setLine(@Nullable Integer line);
+
+ Context setType(@Nullable RuleType type);
}
void execute(Context context);
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;
updater.setLine(issue, line);
return this;
}
+
+ @Override
+ public Function.Context setType(RuleType type) {
+ updater.setType(issue, type, changeContext);
+ return this;
+ }
}
}
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
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;
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())
// 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());
}
// 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());
--- /dev/null
+/*
+ * 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);
+ }
+}
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;
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);
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);
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);
}
}
- public void addActions(String issueKey, List<String> actions) {
+ public void addActions(String issueKey, Iterable<String> actions) {
actionsByIssueKey.putAll(issueKey, actions);
}
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);
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;
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;
}
}
- 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;
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());
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;
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);
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;
.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();
@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");
}
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;
}
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;
.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));
fromExternalRule?: boolean;
key: string;
flows: FlowLocation[][];
+ fromHotspot: boolean;
line?: number;
message: string;
organization: string;
status: '',
type: '',
secondaryLocations: [],
- flows: []
+ flows: [],
+ fromHotspot: false
};
it('should render', () => {
"componentUuid": "",
"creationDate": "",
"flows": Array [],
+ "fromHotspot": false,
"key": "",
"message": "",
"organization": "",
creationDate: '',
key: '',
flows: [],
+ fromHotspot: false,
message: '',
organization: '',
project: '',
creationDate: '',
key: '',
flows: [],
+ fromHotspot: false,
message: '',
organization: '',
project: '',
creationDate: '',
key: '',
flows: [],
+ fromHotspot: false,
message: '',
organization: '',
project: '',
"componentUuid": "",
"creationDate": "",
"flows": Array [],
+ "fromHotspot": false,
"key": "issue-1",
"message": "",
"organization": "",
"componentUuid": "",
"creationDate": "",
"flows": Array [],
+ "fromHotspot": false,
"key": "issue-2",
"message": "",
"organization": "",
"componentUuid": "",
"creationDate": "",
"flows": Array [],
+ "fromHotspot": false,
"key": "foo",
"message": "",
"organization": "",
"componentUuid": "",
"creationDate": "",
"flows": Array [],
+ "fromHotspot": false,
"key": "bar",
"message": "",
"organization": "",
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;
<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}
export default class IssueMessage extends React.PureComponent {
/*:: props: {
- engine?: string;
+ engine?: string,
+ manualVulnerability: boolean,
message: string,
organization: string,
rule: string
</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>
);
}
<div className="issue-row">
<IssueMessage
engine={issue.externalRuleEngine}
+ manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'}
message={issue.message}
organization={issue.organization}
rule={issue.rule}
/*::
type Props = {
- canSetSeverity: boolean,
+ canSetType: boolean,
isOpen: boolean,
issue: Issue,
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
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"
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"
rule: 'javascript:S1067',
message: 'Reduce the number of conditional operators (4) used in the expression',
flows: [],
- secondaryLocations: []
+ secondaryLocations: [],
+ fromHotspot: false
};
const issueWithLocations = {
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()}
it('should render with the action', () => {
const element = shallow(
<IssueType
- canSetSeverity={true}
+ canSetType={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
const toggle = jest.fn();
const element = shallow(
<IssueType
- canSetSeverity={true}
+ canSetType={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
className="issue-row"
>
<IssueMessage
+ manualVulnerability={false}
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
rule="javascript:S1067"
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",
className="issue-row"
>
<IssueMessage
+ manualVulnerability={false}
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
rule="javascript:S1067"
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",
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",
externalRuleEngine?: string,
key: string,
flows: Array<Array<FlowLocation>>,
+ fromHotspot: boolean,
line?: number,
message: string,
organization: string,
private Object locations = null;
private boolean isFromExternalRuleEngine;
+
// FUNCTIONAL DATES
private Date creationDate;
private Date updateDate;
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
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
*/
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);
}
optional string pullRequest = 32;
optional string externalRuleEngine = 33;
+ optional bool fromHotspot = 34;
}
message Transitions {