]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19340 Raise security hotspots events from CE
authorEric Giffon <eric.giffon@sonarsource.com>
Tue, 23 May 2023 12:48:53 +0000 (14:48 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 30 May 2023 20:02:52 +0000 (20:02 +0000)
34 files changed:
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/DefaultAssigneeIT.java
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/issue/ScmAccountToUserLoaderIT.java
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistPushEventsStepIT.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/DefaultAssignee.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueAssigner.java
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/Rule.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ScmAccountToUser.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ScmAccountToUserLoader.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/IssueEvent.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/PushEventFactory.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotClosed.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotRaised.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/TaintVulnerabilityClosed.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/TaintVulnerabilityRaised.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistPushEventsStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueAssignerTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/pushevent/PushEventFactoryTest.java
server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java

index 3d04097a7f668d534e95768697714f2de603bdc2..e7b36137021538a51374e40f0add15f9dcb1c659 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository;
 import org.sonar.ce.task.projectanalysis.component.TestSettingsRepository;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -43,7 +44,7 @@ public class DefaultAssigneeIT {
 
   @Test
   public void no_default_assignee() {
-    assertThat(underTest.loadDefaultAssigneeUuid()).isNull();
+    assertThat(underTest.loadDefaultAssigneeUserId()).isNull();
   }
 
   @Test
@@ -51,14 +52,17 @@ public class DefaultAssigneeIT {
     settings.setProperty(CoreProperties.DEFAULT_ISSUE_ASSIGNEE, "erik");
     UserDto userDto = db.users().insertUser("erik");
 
-    assertThat(underTest.loadDefaultAssigneeUuid()).isEqualTo(userDto.getUuid());
+    UserIdDto userId = underTest.loadDefaultAssigneeUserId();
+    assertThat(userId).isNotNull();
+    assertThat(userId.getUuid()).isEqualTo(userDto.getUuid());
+    assertThat(userId.getLogin()).isEqualTo(userDto.getLogin());
   }
 
   @Test
   public void configured_login_does_not_exist() {
     settings.setProperty(CoreProperties.DEFAULT_ISSUE_ASSIGNEE, "erik");
 
-    assertThat(underTest.loadDefaultAssigneeUuid()).isNull();
+    assertThat(underTest.loadDefaultAssigneeUserId()).isNull();
   }
 
   @Test
@@ -66,17 +70,23 @@ public class DefaultAssigneeIT {
     settings.setProperty(CoreProperties.DEFAULT_ISSUE_ASSIGNEE, "erik");
     db.users().insertUser(user -> user.setLogin("erik").setActive(false));
 
-    assertThat(underTest.loadDefaultAssigneeUuid()).isNull();
+    assertThat(underTest.loadDefaultAssigneeUserId()).isNull();
   }
 
   @Test
   public void default_assignee_is_cached() {
     settings.setProperty(CoreProperties.DEFAULT_ISSUE_ASSIGNEE, "erik");
     UserDto userDto = db.users().insertUser("erik");
-    assertThat(underTest.loadDefaultAssigneeUuid()).isEqualTo(userDto.getUuid());
+    UserIdDto userId = underTest.loadDefaultAssigneeUserId();
+    assertThat(userId).isNotNull();
+    assertThat(userId.getUuid()).isEqualTo(userDto.getUuid());
+    assertThat(userId.getLogin()).isEqualTo(userDto.getLogin());
 
     // The setting is updated but the assignee hasn't changed
     settings.setProperty(CoreProperties.DEFAULT_ISSUE_ASSIGNEE, "other");
-    assertThat(underTest.loadDefaultAssigneeUuid()).isEqualTo(userDto.getUuid());
+    userId = underTest.loadDefaultAssigneeUserId();
+    assertThat(userId).isNotNull();
+    assertThat(userId.getUuid()).isEqualTo(userDto.getUuid());
+    assertThat(userId.getLogin()).isEqualTo(userDto.getLogin());
   }
 }
index 9e91e0b66a9d0ce432ac5bc05fe2af633f7cece4..86d15f426d21b9297b9672986619e25c41b2db2f 100644 (file)
@@ -25,6 +25,7 @@ import org.slf4j.event.Level;
 import org.sonar.api.testfixtures.log.LogTester;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 import org.sonar.server.es.EsTester;
 
 import static java.util.Arrays.asList;
@@ -49,7 +50,10 @@ public class ScmAccountToUserLoaderIT {
     ScmAccountToUserLoader underTest = new ScmAccountToUserLoader(db.getDbClient());
 
     assertThat(underTest.load("missing")).isNull();
-    assertThat(underTest.load("jesuis@charlie.com")).isEqualTo(user.getUuid());
+    UserIdDto result = underTest.load("jesuis@charlie.com");
+    assertThat(result).isNotNull();
+    assertThat(result.getUuid()).isEqualTo(user.getUuid());
+    assertThat(result.getLogin()).isEqualTo(user.getLogin());
   }
 
   @Test
index 8ae97ad8528998d2d2e79507394b80aebfdec277..5d06b6ea3de5374044b8e5baeae99e54f9f25bc8 100644 (file)
@@ -74,7 +74,7 @@ public class PersistPushEventsStepIT {
 
   @Test
   public void description() {
-    assertThat(underTest.getDescription()).isEqualTo("Publishing taint vulnerabilities events");
+    assertThat(underTest.getDescription()).isEqualTo("Publishing taint vulnerabilities and security hotspots events");
   }
 
   @Test
index 65bd05b427b9f61ac206b84514f9e23f7dd8dcbe..2bf7cbc5cd5db8e10983955db5c1ab51910a19f8 100644 (file)
 package org.sonar.ce.task.projectanalysis.issue;
 
 import javax.annotation.CheckForNull;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
@@ -36,13 +37,13 @@ import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
  */
 public class DefaultAssignee {
 
-  private static final Logger LOG = Loggers.get(DefaultAssignee.class);
+  private static final Logger LOG = LoggerFactory.getLogger(DefaultAssignee.class);
 
   private final DbClient dbClient;
   private final ConfigurationRepository configRepository;
 
   private boolean loaded = false;
-  private String userUuid = null;
+  private UserIdDto userId = null;
 
   public DefaultAssignee(DbClient dbClient, ConfigurationRepository configRepository) {
     this.dbClient = dbClient;
@@ -50,26 +51,26 @@ public class DefaultAssignee {
   }
 
   @CheckForNull
-  public String loadDefaultAssigneeUuid() {
+  public UserIdDto loadDefaultAssigneeUserId() {
     if (loaded) {
-      return userUuid;
+      return userId;
     }
     String login = configRepository.getConfiguration().get(DEFAULT_ISSUE_ASSIGNEE).orElse(null);
     if (!isNullOrEmpty(login)) {
-      userUuid = findValidUserUuidFromLogin(login);
+      userId = findValidUserUuidFromLogin(login);
     }
     loaded = true;
-    return userUuid;
+    return userId;
   }
 
-  private String findValidUserUuidFromLogin(String login) {
+  private UserIdDto findValidUserUuidFromLogin(String login) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       UserDto user = dbClient.userDao().selectActiveUserByLogin(dbSession, login);
       if (user == null) {
         LOG.info("Property {} is set with an unknown login: {}", DEFAULT_ISSUE_ASSIGNEE, login);
         return null;
       }
-      return user.getUuid();
+      return new UserIdDto(user.getUuid(), user.getLogin());
     }
   }
 
index 3b440a0e40633d2f62bde0aad97b99f75209a2d1..7a59e1a4c24e3b1d33516a67bdcce6dacd3d998b 100644 (file)
@@ -23,8 +23,8 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.Optional;
 import org.apache.commons.lang.StringUtils;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.scm.Changeset;
@@ -33,6 +33,7 @@ import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.db.issue.IssueDto;
+import org.sonar.db.user.UserIdDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 
 import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
@@ -45,7 +46,7 @@ import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByScanBu
  */
 public class IssueAssigner extends IssueVisitor {
 
-  private static final Logger LOGGER = Loggers.get(IssueAssigner.class);
+  private static final Logger LOGGER = LoggerFactory.getLogger(IssueAssigner.class);
 
   private final ScmInfoRepository scmInfoRepository;
   private final DefaultAssignee defaultAssignee;
@@ -82,9 +83,8 @@ public class IssueAssigner extends IssueVisitor {
     }
 
     if (issue.assignee() == null) {
-      String assigneeUuid = scmAuthor.map(scmAccountToUser::getNullable).orElse(null);
-      assigneeUuid = defaultIfEmpty(assigneeUuid, defaultAssignee.loadDefaultAssigneeUuid());
-      issueUpdater.setNewAssignee(issue, assigneeUuid, changeContext);
+      UserIdDto userId = scmAuthor.map(scmAccountToUser::getNullable).orElse(defaultAssignee.loadDefaultAssigneeUserId());
+      issueUpdater.setNewAssignee(issue, userId, changeContext);
     }
   }
 
index d3c5317cbf9a89ce2c9b69b35cb8c11f298ce07d..9d4a6721dc545afa0b4e2a83d9cebad74ba84491 100644 (file)
@@ -216,6 +216,7 @@ public class IssueLifecycle {
     toIssue.setResolution(fromIssue.resolution());
     toIssue.setStatus(fromIssue.status());
     toIssue.setAssigneeUuid(fromIssue.assignee());
+    toIssue.setAssigneeLogin(fromIssue.assigneeLogin());
     toIssue.setAuthorLogin(fromIssue.authorLogin());
     toIssue.setTags(fromIssue.tags());
     toIssue.setEffort(debtCalculator.calculate(toIssue));
index 8db892aa480414058e96dd12967fd43b4370e8aa..4b6d801d5ca30f84d856176749f13fc904ef240c 100644 (file)
@@ -62,4 +62,6 @@ public interface Rule {
   String getDefaultRuleDescription();
 
   String getSeverity();
+
+  Set<String> getSecurityStandards();
 }
index d3f74921914351fc6f2cca1619e11751638286e9..aa46996a31530b47054afee399bb1f1350c3d026 100644 (file)
@@ -51,6 +51,7 @@ public class RuleImpl implements Rule {
   private final boolean isAdHoc;
   private final String defaultRuleDescription;
   private final String severity;
+  private final Set<String> securityStandards;
 
   public RuleImpl(RuleDto dto) {
     this.uuid = dto.getUuid();
@@ -66,6 +67,7 @@ public class RuleImpl implements Rule {
     this.isAdHoc = dto.isAdHoc();
     this.defaultRuleDescription = getNonNullDefaultRuleDescription(dto);
     this.severity = Optional.ofNullable(dto.getSeverityString()).orElse(dto.getAdHocSeverity());
+    this.securityStandards = dto.getSecurityStandards();
   }
 
   private static String getNonNullDefaultRuleDescription(RuleDto dto) {
@@ -130,6 +132,11 @@ public class RuleImpl implements Rule {
     return severity;
   }
 
+  @Override
+  public Set<String> getSecurityStandards() {
+    return securityStandards;
+  }
+
   @Override
   public boolean equals(@Nullable Object o) {
     if (this == o) {
index a41f4c0a774d311a0736bfff57bd179ecc329621..6f5dd4d2f54108c1434f6c620a2a3670632bbbd6 100644 (file)
@@ -221,5 +221,10 @@ public class RuleRepositoryImpl implements RuleRepository {
     public String getSeverity() {
       return addHocRule.getSeverity();
     }
+
+    @Override
+    public Set<String> getSecurityStandards() {
+      return Collections.emptySet();
+    }
   }
 }
index d1b50402c8558201282eb91ff3fa70d130458c35..b9319831aff8d914074daf1b6e07518b873d13aa 100644 (file)
 package org.sonar.ce.task.projectanalysis.issue;
 
 import org.sonar.ce.task.projectanalysis.util.cache.MemoryCache;
+import org.sonar.db.user.UserIdDto;
 
 /**
- * Cache of dictionary {SCM account -> SQ user uuid}. Kept in memory
+ * Cache of dictionary {SCM account -> SQ user uuid and login}. Kept in memory
  * during the execution of Compute Engine.
  */
-public class ScmAccountToUser extends MemoryCache<String, String> {
+public class ScmAccountToUser extends MemoryCache<String, UserIdDto> {
   public ScmAccountToUser(ScmAccountToUserLoader loader) {
     super(loader);
   }
index fac4f0799090720878cc9d267b29fe4ba944fd6c..979f687d9220f252a10da2234bfac27566f1d4c1 100644 (file)
@@ -23,19 +23,19 @@ import com.google.common.base.Joiner;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonar.ce.task.projectanalysis.util.cache.CacheLoader;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
-import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 
 /**
  * Loads the association between a SCM account and a SQ user
  */
-public class ScmAccountToUserLoader implements CacheLoader<String, String> {
+public class ScmAccountToUserLoader implements CacheLoader<String, UserIdDto> {
 
-  private static final Logger LOGGER = Loggers.get(ScmAccountToUserLoader.class);
+  private static final Logger LOGGER = LoggerFactory.getLogger(ScmAccountToUserLoader.class);
 
   private final DbClient dbClient;
 
@@ -44,26 +44,27 @@ public class ScmAccountToUserLoader implements CacheLoader<String, String> {
   }
 
   @Override
-  public String load(String scmAccount) {
+  public UserIdDto load(String scmAccount) {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      List<String> userUuids = dbClient.userDao().selectUserUuidByScmAccountOrLoginOrEmail(dbSession, scmAccount);
-      if (userUuids.size() == 1) {
-        return userUuids.iterator().next();
+      List<UserIdDto> users = dbClient.userDao().selectActiveUsersByScmAccountOrLoginOrEmail(dbSession, scmAccount);
+      if (users.size() == 1) {
+        return users.iterator().next();
       }
-      if (!userUuids.isEmpty()) {
-        List<UserDto> userDtos = dbClient.userDao().selectByUuids(dbSession, userUuids);
-        Collection<String> logins = userDtos.stream()
-          .map(UserDto::getLogin)
+      if (!users.isEmpty()) {
+        Collection<String> logins = users.stream()
+          .map(UserIdDto::getLogin)
           .sorted()
           .toList();
-        LOGGER.warn(String.format("Multiple users share the SCM account '%s': %s", scmAccount, Joiner.on(", ").join(logins)));
+        if (LOGGER.isWarnEnabled()) {
+          LOGGER.warn(String.format("Multiple users share the SCM account '%s': %s", scmAccount, Joiner.on(", ").join(logins)));
+        }
       }
       return null;
     }
   }
 
   @Override
-  public Map<String, String> loadAll(Collection<? extends String> scmAccounts) {
+  public Map<String, UserIdDto> loadAll(Collection<? extends String> scmAccounts) {
     throw new UnsupportedOperationException("Loading by multiple scm accounts is not supported yet");
   }
 }
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/IssueEvent.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/IssueEvent.java
new file mode 100644 (file)
index 0000000..65f4c49
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.pushevent;
+
+public abstract class IssueEvent {
+
+  private String key;
+  private String projectKey;
+
+  protected IssueEvent() {
+    // nothing to do
+  }
+
+  protected IssueEvent(String key, String projectKey) {
+    this.key = key;
+    this.projectKey = projectKey;
+  }
+
+  public abstract String getEventName();
+
+  public String getKey() {
+    return key;
+  }
+
+  public void setKey(String key) {
+    this.key = key;
+  }
+
+  public String getProjectKey() {
+    return projectKey;
+  }
+
+  public void setProjectKey(String projectKey) {
+    this.projectKey = projectKey;
+  }
+
+  @Override
+  public String toString() {
+    return "IssueEvent{" +
+      "name='" + getEventName() + '\'' +
+      ", key='" + key + '\'' +
+      ", projectKey='" + projectKey + '\'' +
+      '}';
+  }
+}
index 4e3ed70db3797599d2bcf33c7ff0b93c757dfdfd..8c3a5c7580ed404901acee8c6ab0613eec90025c 100644 (file)
@@ -21,13 +21,16 @@ package org.sonar.ce.task.projectanalysis.pushevent;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import org.jetbrains.annotations.NotNull;
 import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.rules.RuleType;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.issue.Rule;
+import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
 import org.sonar.ce.task.projectanalysis.locations.flow.FlowGenerator;
 import org.sonar.ce.task.projectanalysis.locations.flow.Location;
 import org.sonar.ce.task.projectanalysis.locations.flow.TextRange;
@@ -36,9 +39,11 @@ import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.db.pushevent.PushEventDto;
 import org.sonar.server.issue.TaintChecker;
+import org.sonar.server.security.SecurityStandards;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
+import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
 
 @ComputeEngineSide
 public class PushEventFactory {
@@ -48,47 +53,78 @@ public class PushEventFactory {
   private final AnalysisMetadataHolder analysisMetadataHolder;
   private final TaintChecker taintChecker;
   private final FlowGenerator flowGenerator;
+  private final RuleRepository ruleRepository;
 
-  public PushEventFactory(TreeRootHolder treeRootHolder,
-    AnalysisMetadataHolder analysisMetadataHolder, TaintChecker taintChecker, FlowGenerator flowGenerator) {
+  public PushEventFactory(TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder, TaintChecker taintChecker,
+    FlowGenerator flowGenerator, RuleRepository ruleRepository) {
     this.treeRootHolder = treeRootHolder;
     this.analysisMetadataHolder = analysisMetadataHolder;
     this.taintChecker = taintChecker;
     this.flowGenerator = flowGenerator;
+    this.ruleRepository = ruleRepository;
   }
 
   public Optional<PushEventDto> raiseEventOnIssue(String projectUuid, DefaultIssue currentIssue) {
     var currentIssueComponentUuid = currentIssue.componentUuid();
-    if (!taintChecker.isTaintVulnerability(currentIssue) || currentIssueComponentUuid == null) {
+    if (currentIssueComponentUuid == null) {
       return Optional.empty();
     }
 
-    var component = treeRootHolder.getComponentByUuid(Objects.requireNonNull(currentIssue.componentUuid()));
-    if (currentIssue.isNew() || currentIssue.isCopied() || isReopened(currentIssue)) {
-      return Optional.of(raiseTaintVulnerabilityRaisedEvent(projectUuid, component, currentIssue));
+    var component = treeRootHolder.getComponentByUuid(currentIssueComponentUuid);
+
+    if (isTaintVulnerability(currentIssue)) {
+      return raiseTaintVulnerabilityEvent(projectUuid, component, currentIssue);
+    }
+    if (isSecurityHotspot(currentIssue)) {
+      return raiseSecurityHotspotEvent(projectUuid, component, currentIssue);
+    }
+    return Optional.empty();
+  }
+
+  private boolean isTaintVulnerability(DefaultIssue issue) {
+    return taintChecker.isTaintVulnerability(issue);
+  }
+
+  private static boolean isSecurityHotspot(DefaultIssue issue) {
+    return RuleType.SECURITY_HOTSPOT.equals(issue.type());
+  }
+
+  private Optional<PushEventDto> raiseTaintVulnerabilityEvent(String projectUuid, Component component, DefaultIssue issue) {
+    if (shouldCreateRaisedEvent(issue)) {
+      return Optional.of(raiseTaintVulnerabilityRaisedEvent(projectUuid, component, issue));
+    }
+    if (issue.isBeingClosed()) {
+      return Optional.of(raiseTaintVulnerabilityClosedEvent(projectUuid, issue));
     }
-    if (currentIssue.isBeingClosed()) {
-      return Optional.of(raiseTaintVulnerabilityClosedEvent(projectUuid, currentIssue));
+    return Optional.empty();
+  }
+
+  private Optional<PushEventDto> raiseSecurityHotspotEvent(String projectUuid, Component component, DefaultIssue issue) {
+    if (shouldCreateRaisedEvent(issue)) {
+      return Optional.of(raiseSecurityHotspotRaisedEvent(projectUuid, component, issue));
+    }
+    if (issue.isBeingClosed()) {
+      return Optional.of(raiseSecurityHotspotClosedEvent(projectUuid, component, issue));
     }
     return Optional.empty();
   }
 
+  private static boolean shouldCreateRaisedEvent(DefaultIssue issue) {
+    return issue.isNew() || issue.isCopied() || isReopened(issue);
+  }
+
   private static boolean isReopened(DefaultIssue currentIssue) {
     var currentChange = currentIssue.currentChange();
     if (currentChange == null) {
       return false;
     }
     var status = currentChange.get("status");
-    return status != null && status.toString().equals("CLOSED|OPEN");
+    return status != null && Set.of("CLOSED|OPEN", "CLOSED|TO_REVIEW").contains(status.toString());
   }
 
   private PushEventDto raiseTaintVulnerabilityRaisedEvent(String projectUuid, Component component, DefaultIssue issue) {
     TaintVulnerabilityRaised event = prepareEvent(component, issue);
-    return new PushEventDto()
-      .setName("TaintVulnerabilityRaised")
-      .setProjectUuid(projectUuid)
-      .setLanguage(issue.language())
-      .setPayload(serializeEvent(event));
+    return createPushEventDto(projectUuid, issue, event);
   }
 
   private TaintVulnerabilityRaised prepareEvent(Component component, DefaultIssue issue) {
@@ -117,16 +153,52 @@ public class PushEventFactory {
     return mainLocation;
   }
 
-  private static PushEventDto raiseTaintVulnerabilityClosedEvent(String projectUuid, DefaultIssue issue) {
-    TaintVulnerabilityClosed event = new TaintVulnerabilityClosed(issue.key(), issue.projectKey());
+  private static PushEventDto createPushEventDto(String projectUuid, DefaultIssue issue, IssueEvent event) {
     return new PushEventDto()
-      .setName("TaintVulnerabilityClosed")
+      .setName(event.getEventName())
       .setProjectUuid(projectUuid)
       .setLanguage(issue.language())
       .setPayload(serializeEvent(event));
   }
 
-  private static byte[] serializeEvent(Object event) {
+  private static PushEventDto raiseTaintVulnerabilityClosedEvent(String projectUuid, DefaultIssue issue) {
+    TaintVulnerabilityClosed event = new TaintVulnerabilityClosed(issue.key(), issue.projectKey());
+    return createPushEventDto(projectUuid, issue, event);
+  }
+
+  private PushEventDto raiseSecurityHotspotRaisedEvent(String projectUuid, Component component, DefaultIssue issue) {
+    SecurityHotspotRaised event = new SecurityHotspotRaised();
+    event.setKey(issue.key());
+    event.setProjectKey(issue.projectKey());
+    event.setStatus(issue.getStatus());
+    event.setCreationDate(issue.creationDate().getTime());
+    event.setMainLocation(prepareMainLocation(component, issue));
+    event.setRuleKey(issue.getRuleKey().toString());
+    event.setVulnerabilityProbability(getVulnerabilityProbability(issue));
+    event.setBranch(analysisMetadataHolder.getBranch().getName());
+    event.setAssignee(issue.assigneeLogin());
+
+    return createPushEventDto(projectUuid, issue, event);
+  }
+
+  private String getVulnerabilityProbability(DefaultIssue issue) {
+    Rule rule = ruleRepository.getByKey(issue.getRuleKey());
+    SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory();
+    return sqCategory.getVulnerability().name();
+  }
+
+  private static PushEventDto raiseSecurityHotspotClosedEvent(String projectUuid, Component component, DefaultIssue issue) {
+    SecurityHotspotClosed event = new SecurityHotspotClosed();
+    event.setKey(issue.key());
+    event.setProjectKey(issue.projectKey());
+    event.setStatus(issue.getStatus());
+    event.setResolution(issue.resolution());
+    event.setFilePath(component.getName());
+
+    return createPushEventDto(projectUuid, issue, event);
+  }
+
+  private static byte[] serializeEvent(IssueEvent event) {
     return GSON.toJson(event).getBytes(UTF_8);
   }
 
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotClosed.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotClosed.java
new file mode 100644 (file)
index 0000000..3109c1a
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.pushevent;
+
+import com.google.common.annotations.VisibleForTesting;
+import javax.annotation.Nullable;
+
+public class SecurityHotspotClosed extends IssueEvent {
+
+  @VisibleForTesting
+  static final String EVENT_NAME = "SecurityHotspotClosed";
+
+  private String status;
+  private String resolution;
+  private String filePath;
+
+  public SecurityHotspotClosed() {
+    // nothing to do
+  }
+
+  @Override
+  public String getEventName() {
+    return EVENT_NAME;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getResolution() {
+    return resolution;
+  }
+
+  public void setResolution(@Nullable String resolution) {
+    this.resolution = resolution;
+  }
+
+  public String getFilePath() {
+    return filePath;
+  }
+
+  public void setFilePath(String filePath) {
+    this.filePath = filePath;
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotRaised.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/pushevent/SecurityHotspotRaised.java
new file mode 100644 (file)
index 0000000..fa08fa1
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.ce.task.projectanalysis.pushevent;
+
+import com.google.common.annotations.VisibleForTesting;
+import javax.annotation.Nullable;
+import org.sonar.ce.task.projectanalysis.locations.flow.Location;
+
+public class SecurityHotspotRaised extends IssueEvent {
+
+  @VisibleForTesting
+  static final String EVENT_NAME = "SecurityHotspotRaised";
+
+  private String status;
+  private String vulnerabilityProbability;
+  private long creationDate;
+  private Location mainLocation;
+  private String ruleKey;
+  private String branch;
+  private String assignee;
+
+  public SecurityHotspotRaised() {
+    // nothing to do
+  }
+
+  @Override
+  public String getEventName() {
+    return EVENT_NAME;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getVulnerabilityProbability() {
+    return vulnerabilityProbability;
+  }
+
+  public void setVulnerabilityProbability(String vulnerabilityProbability) {
+    this.vulnerabilityProbability = vulnerabilityProbability;
+  }
+
+  public long getCreationDate() {
+    return creationDate;
+  }
+
+  public void setCreationDate(long creationDate) {
+    this.creationDate = creationDate;
+  }
+
+  public Location getMainLocation() {
+    return mainLocation;
+  }
+
+  public void setMainLocation(Location mainLocation) {
+    this.mainLocation = mainLocation;
+  }
+
+  public String getRuleKey() {
+    return ruleKey;
+  }
+
+  public void setRuleKey(String ruleKey) {
+    this.ruleKey = ruleKey;
+  }
+
+  public String getBranch() {
+    return branch;
+  }
+
+  public void setBranch(String branch) {
+    this.branch = branch;
+  }
+
+  public String getAssignee() {
+    return assignee;
+  }
+
+  public void setAssignee(@Nullable String assignee) {
+    this.assignee = assignee;
+  }
+}
index 6d4857c1ad3410b0a299d35d9104e08562c4c8f4..34b77df014ebf46ed82efe187ee151a33f426c49 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.pushevent;
 
-public class TaintVulnerabilityClosed {
+public class TaintVulnerabilityClosed extends IssueEvent {
 
-  private String key;
-  private String projectKey;
+  private static final String EVENT_NAME = "TaintVulnerabilityClosed";
 
   public TaintVulnerabilityClosed() {
     // nothing to do
   }
 
   public TaintVulnerabilityClosed(String key, String projectKey) {
-    this.key = key;
-    this.projectKey = projectKey;
+    super(key, projectKey);
   }
 
-  public String getKey() {
-    return key;
+  @Override
+  public String getEventName() {
+    return EVENT_NAME;
   }
-
-  public void setKey(String key) {
-    this.key = key;
-  }
-
-  public String getProjectKey() {
-    return projectKey;
-  }
-
-  public void setProjectKey(String projectKey) {
-    this.projectKey = projectKey;
-  }
-
 }
index c988a3e0279934b8ce7c3d06cada9c7f071ee5c7..003718bdfc4354d04ea63e8ba6464189fa637a64 100644 (file)
@@ -24,10 +24,10 @@ import java.util.Optional;
 import org.sonar.ce.task.projectanalysis.locations.flow.Flow;
 import org.sonar.ce.task.projectanalysis.locations.flow.Location;
 
-public class TaintVulnerabilityRaised {
+public class TaintVulnerabilityRaised extends IssueEvent {
+
+  private static final String EVENT_NAME = "TaintVulnerabilityRaised";
 
-  private String key;
-  private String projectKey;
   private String branch;
   private long creationDate;
   private String ruleKey;
@@ -41,20 +41,9 @@ public class TaintVulnerabilityRaised {
     // nothing to do
   }
 
-  public String getKey() {
-    return key;
-  }
-
-  public void setKey(String key) {
-    this.key = key;
-  }
-
-  public String getProjectKey() {
-    return projectKey;
-  }
-
-  public void setProjectKey(String projectKey) {
-    this.projectKey = projectKey;
+  @Override
+  public String getEventName() {
+    return EVENT_NAME;
   }
 
   public String getBranch() {
index 38874cb47e00bd9c92be07931ae5e24263185e41..6b016fbe0010f1d27b7c17bca2e8d2a00b20a5d7 100644 (file)
@@ -100,7 +100,7 @@ public class PersistPushEventsStep implements ComputationStep {
 
   @Override
   public String getDescription() {
-    return "Publishing taint vulnerabilities events";
+    return "Publishing taint vulnerabilities and security hotspots events";
   }
 
 }
index a9334b82c2075b42fb2fc6f55905e9e1bfcf90a8..f0f251f95c34ed82117eef04a96c03fb6b8e0b66 100644 (file)
@@ -112,6 +112,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     defaultIssue.setStatus(next.getStatus());
     defaultIssue.setResolution(next.hasResolution() ? next.getResolution() : null);
     defaultIssue.setAssigneeUuid(next.hasAssigneeUuid() ? next.getAssigneeUuid() : null);
+    defaultIssue.setAssigneeLogin(next.hasAssigneeLogin() ? next.getAssigneeLogin() : null);
     defaultIssue.setChecksum(next.hasChecksum() ? next.getChecksum() : null);
     defaultIssue.setAuthorLogin(next.hasAuthorLogin() ? next.getAuthorLogin() : null);
     next.getCommentsList().forEach(c -> defaultIssue.addComment(toDefaultIssueComment(c)));
@@ -164,6 +165,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     builder.setStatus(defaultIssue.status());
     ofNullable(defaultIssue.resolution()).ifPresent(builder::setResolution);
     ofNullable(defaultIssue.assignee()).ifPresent(builder::setAssigneeUuid);
+    ofNullable(defaultIssue.assigneeLogin()).ifPresent(builder::setAssigneeLogin);
     ofNullable(defaultIssue.checksum()).ifPresent(builder::setChecksum);
     ofNullable(defaultIssue.authorLogin()).ifPresent(builder::setAuthorLogin);
     defaultIssue.defaultIssueComments().forEach(c -> builder.addComments(toProtoComment(c)));
index 8f6456d11ba0a6f35ce0c366d209b269d3741731..880424a347b894c902dca48225fb93874346c4fd 100644 (file)
@@ -82,6 +82,7 @@ message Issue {
   optional string ruleDescriptionContextKey = 44;
   optional sonarqube.db.issues.MessageFormattings messageFormattings = 45;
   optional string codeVariants = 46;
+  optional string assigneeLogin = 47;
 }
 
 message Comment {
index f6b48bf8b654e6dbcba73c48403fc26975678828..532ad64422ae5b0527d04a8267714999d5b44b4f 100644 (file)
@@ -42,6 +42,7 @@ public class DumbRule implements Rule {
   private String pluginKey;
   private boolean isExternal;
   private boolean isAdHoc;
+  private Set<String> securityStandards = new HashSet<>();
 
   public DumbRule(RuleKey key) {
     this.key = key;
@@ -105,6 +106,11 @@ public class DumbRule implements Rule {
     return null;
   }
 
+  @Override
+  public Set<String> getSecurityStandards() {
+    return securityStandards;
+  }
+
   @Override
   public boolean isExternal() {
     return isExternal;
index 286f9ce04b6fe0db048d6508e326856d883cef59..620d09d48488d134d389c715a845540555bf1ca9 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepositoryRule;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.user.UserIdDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 
 import static java.util.stream.Collectors.joining;
@@ -56,9 +57,9 @@ public class IssueAssignerTest {
   @Rule
   public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule().setAnalysisDate(123456789L);
 
-  private ScmAccountToUser scmAccountToUser = mock(ScmAccountToUser.class);
-  private DefaultAssignee defaultAssignee = mock(DefaultAssignee.class);
-  private IssueAssigner underTest = new IssueAssigner(analysisMetadataHolder, scmInfoRepository, scmAccountToUser, defaultAssignee, new IssueFieldsSetter());
+  private final ScmAccountToUser scmAccountToUser = mock(ScmAccountToUser.class);
+  private final DefaultAssignee defaultAssignee = mock(DefaultAssignee.class);
+  private final IssueAssigner underTest = new IssueAssigner(analysisMetadataHolder, scmInfoRepository, scmAccountToUser, defaultAssignee, new IssueFieldsSetter());
 
   @Before
   public void before() {
@@ -98,38 +99,41 @@ public class IssueAssignerTest {
   @Test
   public void assign_but_do_not_set_author_if_too_long() {
     String scmAuthor = range(0, 256).mapToObj(i -> "s").collect(joining());
-    addScmUser(scmAuthor, "John C");
+    addScmUser(scmAuthor, buildUserId("u123", "John C"));
     setSingleChangeset(scmAuthor, 123456789L, "rev-1");
     DefaultIssue issue = newIssueOnLines(1);
 
     underTest.onIssue(FILE, issue);
 
     assertThat(issue.authorLogin()).isNull();
-    assertThat(issue.assignee()).isEqualTo("John C");
+    assertThat(issue.assignee()).isEqualTo("u123");
+    assertThat(issue.assigneeLogin()).isEqualTo("John C");
 
     assertThat(logTester.logs(Level.DEBUG)).contains("SCM account '" + scmAuthor + "' is too long to be stored as issue author");
   }
 
   @Test
   public void assign_new_issue_to_author_of_change() {
-    addScmUser("john", "u123");
+    addScmUser("john", buildUserId("u123", "john"));
     setSingleChangeset("john", 123456789L, "rev-1");
     DefaultIssue issue = newIssueOnLines(1);
 
     underTest.onIssue(FILE, issue);
 
     assertThat(issue.assignee()).isEqualTo("u123");
+    assertThat(issue.assigneeLogin()).isEqualTo("john");
   }
 
   @Test
   public void assign_new_issue_to_default_assignee_if_author_not_found() {
     setSingleChangeset("john", 123456789L, "rev-1");
-    when(defaultAssignee.loadDefaultAssigneeUuid()).thenReturn("u1234");
+    when(defaultAssignee.loadDefaultAssigneeUserId()).thenReturn(new UserIdDto("u1234", "john"));
     DefaultIssue issue = newIssueOnLines(1);
 
     underTest.onIssue(FILE, issue);
 
     assertThat(issue.assignee()).isEqualTo("u1234");
+    assertThat(issue.assigneeLogin()).isEqualTo("john");
   }
 
   @Test
@@ -145,7 +149,7 @@ public class IssueAssignerTest {
 
   @Test
   public void do_not_assign_issue_if_unassigned_but_already_authored() {
-    addScmUser("john", "u1234");
+    addScmUser("john", buildUserId("u1234", "john"));
     setSingleChangeset("john", 123456789L, "rev-1");
     DefaultIssue issue = newIssueOnLines(1)
       .setAuthorLogin("john")
@@ -159,7 +163,7 @@ public class IssueAssignerTest {
 
   @Test
   public void assign_to_last_committer_of_file_if_issue_is_global_to_file() {
-    addScmUser("henry", "Henry V");
+    addScmUser("henry", buildUserId("u123", "Henry V"));
     Changeset changeset1 = Changeset.newChangesetBuilder()
       .setAuthor("john")
       .setDate(1_000L)
@@ -177,22 +181,24 @@ public class IssueAssignerTest {
 
     underTest.onIssue(FILE, issue);
 
-    assertThat(issue.assignee()).isEqualTo("Henry V");
+    assertThat(issue.assignee()).isEqualTo("u123");
+    assertThat(issue.assigneeLogin()).isEqualTo("Henry V");
   }
 
   @Test
   public void assign_to_default_assignee_if_no_author() {
     DefaultIssue issue = newIssueOnLines();
 
-    when(defaultAssignee.loadDefaultAssigneeUuid()).thenReturn("u123");
+    when(defaultAssignee.loadDefaultAssigneeUserId()).thenReturn(new UserIdDto("u123", "john"));
     underTest.onIssue(FILE, issue);
 
     assertThat(issue.assignee()).isEqualTo("u123");
+    assertThat(issue.assigneeLogin()).isEqualTo("john");
   }
 
   @Test
   public void assign_to_default_assignee_if_no_scm_on_issue_locations() {
-    addScmUser("john", "John C");
+    addScmUser("john", buildUserId("u123", "John C"));
     Changeset changeset = Changeset.newChangesetBuilder()
       .setAuthor("john")
       .setDate(123456789L)
@@ -203,13 +209,14 @@ public class IssueAssignerTest {
 
     underTest.onIssue(FILE, issue);
 
-    assertThat(issue.assignee()).isEqualTo("John C");
+    assertThat(issue.assignee()).isEqualTo("u123");
+    assertThat(issue.assigneeLogin()).isEqualTo("John C");
   }
 
   @Test
   public void assign_to_author_of_the_most_recent_change_in_all_issue_locations() {
-    addScmUser("john", "u1");
-    addScmUser("jane", "u2");
+    addScmUser("john", buildUserId("u1", "John"));
+    addScmUser("jane", buildUserId("u2", "Jane"));
     Changeset commit1 = Changeset.newChangesetBuilder()
       .setAuthor("john")
       .setDate(1_000L)
@@ -227,6 +234,7 @@ public class IssueAssignerTest {
 
     assertThat(issue.authorLogin()).isEqualTo("jane");
     assertThat(issue.assignee()).isEqualTo("u2");
+    assertThat(issue.assigneeLogin()).isEqualTo("Jane");
   }
 
   private void setSingleChangeset(@Nullable String author, Long date, String revision) {
@@ -238,8 +246,8 @@ public class IssueAssignerTest {
         .build());
   }
 
-  private void addScmUser(String scmAccount, String userUuid) {
-    when(scmAccountToUser.getNullable(scmAccount)).thenReturn(userUuid);
+  private void addScmUser(String scmAccount, UserIdDto userId) {
+    when(scmAccountToUser.getNullable(scmAccount)).thenReturn(userId);
   }
 
   private static DefaultIssue newIssueOnLines(int... lines) {
@@ -259,4 +267,7 @@ public class IssueAssignerTest {
       .setTextRange(DbCommons.TextRange.newBuilder().setStartLine(line).setEndLine(line).build()).build();
   }
 
+  private UserIdDto buildUserId(String uuid, String login) {
+    return new UserIdDto(uuid, login);
+  }
 }
index 924ccc81f6db8bf6929356ca48e4ffde635dfc0f..78a749c6d7c87fcc07215d9e29d30c82ac612243 100644 (file)
@@ -297,6 +297,7 @@ public class IssueLifecycleTest {
       .setStatus(STATUS_CLOSED)
       .setSeverity(BLOCKER)
       .setAssigneeUuid("base assignee uuid")
+      .setAssigneeLogin("base assignee login")
       .setAuthorLogin("base author")
       .setTags(newArrayList("base tag"))
       .setOnDisabledRule(true)
@@ -326,6 +327,7 @@ public class IssueLifecycleTest {
     assertThat(raw.resolution()).isEqualTo(RESOLUTION_FIXED);
     assertThat(raw.status()).isEqualTo(STATUS_CLOSED);
     assertThat(raw.assignee()).isEqualTo("base assignee uuid");
+    assertThat(raw.assigneeLogin()).isEqualTo("base assignee login");
     assertThat(raw.authorLogin()).isEqualTo("base author");
     assertThat(raw.tags()).containsOnly("base tag");
     assertThat(raw.effort()).isEqualTo(DEFAULT_DURATION);
@@ -383,6 +385,7 @@ public class IssueLifecycleTest {
       .setStatus(STATUS_RESOLVED)
       .setSeverity(BLOCKER)
       .setAssigneeUuid("base assignee uuid")
+      .setAssigneeLogin("base assignee login")
       .setAuthorLogin("base author")
       .setTags(newArrayList("base tag"))
       .setOnDisabledRule(true)
@@ -410,6 +413,7 @@ public class IssueLifecycleTest {
     assertThat(raw.resolution()).isEqualTo(RESOLUTION_FALSE_POSITIVE);
     assertThat(raw.status()).isEqualTo(STATUS_RESOLVED);
     assertThat(raw.assignee()).isEqualTo("base assignee uuid");
+    assertThat(raw.assigneeLogin()).isEqualTo("base assignee login");
     assertThat(raw.authorLogin()).isEqualTo("base author");
     assertThat(raw.tags()).containsOnly("base tag");
     assertThat(raw.codeVariants()).containsOnly("foo", "bar");
index 248bf1815aa88a769250e316d2016578706bfa6d..028a0c11843ebce0edb1b619b8ddd54bb010f090 100644 (file)
@@ -22,10 +22,11 @@ package org.sonar.ce.task.projectanalysis.pushevent;
 import com.google.gson.Gson;
 import java.nio.charset.StandardCharsets;
 import java.util.Date;
-import java.util.List;
+import java.util.Set;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.DateUtils;
@@ -34,11 +35,13 @@ import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
 import org.sonar.ce.task.projectanalysis.component.Component.Type;
 import org.sonar.ce.task.projectanalysis.component.MutableTreeRootHolderRule;
 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
 import org.sonar.ce.task.projectanalysis.locations.flow.FlowGenerator;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.FieldDiffs;
 import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
 import org.sonar.server.issue.TaintChecker;
 
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
@@ -59,24 +62,25 @@ public class PushEventFactoryTest {
   @Rule
   public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
     .setBranch(new TestBranch(BRANCH_NAME));
-
   private final FlowGenerator flowGenerator = new FlowGenerator(treeRootHolder);
-  private final PushEventFactory underTest = new PushEventFactory(treeRootHolder, analysisMetadataHolder, taintChecker, flowGenerator);
+  private final RuleRepository ruleRepository = mock(RuleRepository.class);
+  private final PushEventFactory underTest = new PushEventFactory(treeRootHolder, analysisMetadataHolder, taintChecker, flowGenerator,
+    ruleRepository);
 
   @Before
   public void setUp() {
-    when(taintChecker.getTaintRepositories()).thenReturn(List.of("roslyn.sonaranalyzer.security.cs",
-      "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity"));
-    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+    when(ruleRepository.getByKey(RuleKey.of("javasecurity", "S123"))).thenReturn(buildRule());
     buildComponentTree();
   }
 
   @Test
-  public void raise_event_to_repository_if_taint_vulnerability_is_new() {
+  public void raiseEventOnIssue_whenNewTaintVulnerability_shouldCreateRaisedEvent() {
     DefaultIssue defaultIssue = createDefaultIssue()
       .setNew(true)
       .setRuleDescriptionContextKey(randomAlphabetic(6));
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
       .isNotEmpty()
       .hasValueSatisfying(pushEventDto -> {
@@ -91,7 +95,8 @@ public class PushEventFactoryTest {
   private static void verifyPayload(byte[] payload, DefaultIssue defaultIssue) {
     assertThat(payload).isNotNull();
 
-    TaintVulnerabilityRaised taintVulnerabilityRaised = gson.fromJson(new String(payload, StandardCharsets.UTF_8), TaintVulnerabilityRaised.class);
+    TaintVulnerabilityRaised taintVulnerabilityRaised = gson.fromJson(new String(payload, StandardCharsets.UTF_8),
+      TaintVulnerabilityRaised.class);
     assertThat(taintVulnerabilityRaised.getProjectKey()).isEqualTo(defaultIssue.projectKey());
     assertThat(taintVulnerabilityRaised.getCreationDate()).isEqualTo(defaultIssue.creationDate().getTime());
     assertThat(taintVulnerabilityRaised.getKey()).isEqualTo(defaultIssue.key());
@@ -99,18 +104,21 @@ public class PushEventFactoryTest {
     assertThat(taintVulnerabilityRaised.getRuleKey()).isEqualTo(defaultIssue.ruleKey().toString());
     assertThat(taintVulnerabilityRaised.getType()).isEqualTo(defaultIssue.type().name());
     assertThat(taintVulnerabilityRaised.getBranch()).isEqualTo(BRANCH_NAME);
-    String ruleDescriptionContextKey = taintVulnerabilityRaised.getRuleDescriptionContextKey().orElseGet(() -> fail("No rule description context key"));
+    String ruleDescriptionContextKey = taintVulnerabilityRaised.getRuleDescriptionContextKey().orElseGet(() -> fail("No rule description " +
+      "context key"));
     assertThat(ruleDescriptionContextKey).isEqualTo(defaultIssue.getRuleDescriptionContextKey().orElse(null));
   }
 
   @Test
-  public void raise_event_to_repository_if_taint_vulnerability_is_reopened() {
+  public void raiseEventOnIssue_whenReopenedTaintVulnerability_shouldCreateRaisedEvent() {
     DefaultIssue defaultIssue = createDefaultIssue()
       .setChanged(true)
       .setNew(false)
       .setCopied(false)
       .setCurrentChange(new FieldDiffs().setDiff("status", "CLOSED", "OPEN"));
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
       .isNotEmpty()
       .hasValueSatisfying(pushEventDto -> {
@@ -120,21 +128,25 @@ public class PushEventFactoryTest {
   }
 
   @Test
-  public void skip_event_if_taint_vulnerability_status_change() {
+  public void raiseEventOnIssue_whenTaintVulnerabilityStatusChange_shouldSkipEvent() {
     DefaultIssue defaultIssue = createDefaultIssue()
       .setChanged(true)
       .setNew(false)
       .setCopied(false)
       .setCurrentChange(new FieldDiffs().setDiff("status", "OPEN", "FIXED"));
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
   }
 
   @Test
-  public void raise_event_to_repository_if_taint_vulnerability_is_copied() {
+  public void raiseEventOnIssue_whenCopiedTaintVulnerability_shouldCreateRaisedEvent() {
     DefaultIssue defaultIssue = createDefaultIssue()
       .setCopied(true);
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
       .isNotEmpty()
       .hasValueSatisfying(pushEventDto -> {
@@ -144,13 +156,14 @@ public class PushEventFactoryTest {
   }
 
   @Test
-  public void raise_event_to_repository_if_taint_vulnerability_is_closed() {
+  public void raiseEventOnIssue_whenClosedTaintVulnerability_shouldCreateClosedEvent() {
     DefaultIssue defaultIssue = createDefaultIssue()
-      .setComponentUuid("")
       .setNew(false)
       .setCopied(false)
       .setBeingClosed(true);
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
       .isNotEmpty()
       .hasValueSatisfying(pushEventDto -> {
@@ -160,7 +173,7 @@ public class PushEventFactoryTest {
   }
 
   @Test
-  public void skip_issue_if_issue_changed() {
+  public void raiseEventOnIssue_whenChangedTaintVulnerability_shouldSkipEvent() {
     DefaultIssue defaultIssue = new DefaultIssue()
       .setComponentUuid("issue-component-uuid")
       .setNew(false)
@@ -170,11 +183,13 @@ public class PushEventFactoryTest {
       .setCreationDate(DateUtils.parseDate("2022-01-01"))
       .setRuleKey(RuleKey.of("javasecurity", "S123"));
 
+    when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
+
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
   }
 
   @Test
-  public void skip_if_issue_not_from_taint_vulnerability_repository() {
+  public void raiseEventOnIssue_whenIssueNotFromTaintVulnerabilityRepository_shouldSkipEvent() {
     DefaultIssue defaultIssue = new DefaultIssue()
       .setComponentUuid("issue-component-uuid")
       .setChanged(true)
@@ -197,11 +212,11 @@ public class PushEventFactoryTest {
   }
 
   @Test
-  public void skip_if_issue_is_a_hotspot() {
+  public void raiseEventOnIssue_whenIssueDoesNotHaveLocations_shouldSkipEvent() {
     DefaultIssue defaultIssue = new DefaultIssue()
       .setComponentUuid("issue-component-uuid")
       .setChanged(true)
-      .setType(RuleType.SECURITY_HOTSPOT)
+      .setType(RuleType.VULNERABILITY)
       .setRuleKey(RuleKey.of("javasecurity", "S123"));
 
     when(taintChecker.isTaintVulnerability(any())).thenReturn(false);
@@ -210,14 +225,115 @@ public class PushEventFactoryTest {
   }
 
   @Test
-  public void skip_if_issue_does_not_have_locations() {
-    DefaultIssue defaultIssue = new DefaultIssue()
-      .setComponentUuid("issue-component-uuid")
+  public void raiseEventOnIssue_whenNewHotspot_shouldCreateRaisedEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setStatus(Issue.STATUS_TO_REVIEW)
+      .setNew(true)
+      .setRuleDescriptionContextKey(randomAlphabetic(6));
+
+    assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
+      .isNotEmpty()
+      .hasValueSatisfying(pushEventDto -> {
+        assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
+        verifyHotspotRaisedEventPayload(pushEventDto.getPayload(), defaultIssue);
+        assertThat(pushEventDto.getLanguage()).isEqualTo("java");
+        assertThat(pushEventDto.getProjectUuid()).isEqualTo("some-project-uuid");
+      });
+  }
+
+  private static void verifyHotspotRaisedEventPayload(byte[] payload, DefaultIssue defaultIssue) {
+    assertThat(payload).isNotNull();
+
+    SecurityHotspotRaised event = gson.fromJson(new String(payload, StandardCharsets.UTF_8), SecurityHotspotRaised.class);
+    assertThat(event.getProjectKey()).isEqualTo(defaultIssue.projectKey());
+    assertThat(event.getCreationDate()).isEqualTo(defaultIssue.creationDate().getTime());
+    assertThat(event.getKey()).isEqualTo(defaultIssue.key());
+    assertThat(event.getRuleKey()).isEqualTo(defaultIssue.ruleKey().toString());
+    assertThat(event.getStatus()).isEqualTo(Issue.STATUS_TO_REVIEW);
+    assertThat(event.getVulnerabilityProbability()).isEqualTo("LOW");
+    assertThat(event.getMainLocation()).isNotNull();
+    assertThat(event.getBranch()).isEqualTo(BRANCH_NAME);
+    assertThat(event.getAssignee()).isEqualTo("some-user-login");
+  }
+
+  @Test
+  public void raiseEventOnIssue_whenReopenedHotspot_shouldCreateRaisedEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setType(RuleType.SECURITY_HOTSPOT)
       .setChanged(true)
-      .setType(RuleType.VULNERABILITY)
-      .setRuleKey(RuleKey.of("javasecurity", "S123"));
+      .setNew(false)
+      .setCopied(false)
+      .setCurrentChange(new FieldDiffs().setDiff("status", "CLOSED", "TO_REVIEW"));
 
-    when(taintChecker.isTaintVulnerability(any())).thenReturn(false);
+    assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
+      .isNotEmpty()
+      .hasValueSatisfying(pushEventDto -> {
+        assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
+        assertThat(pushEventDto.getPayload()).isNotNull();
+      });
+  }
+
+  @Test
+  public void raiseEventOnIssue_whenCopiedHotspot_shouldCreateRaisedEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setCopied(true);
+
+    assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
+      .isNotEmpty()
+      .hasValueSatisfying(pushEventDto -> {
+        assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
+        assertThat(pushEventDto.getPayload()).isNotNull();
+      });
+  }
+
+  @Test
+  public void raiseEventOnIssue_whenClosedHotspot_shouldCreateClosedEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setNew(false)
+      .setCopied(false)
+      .setBeingClosed(true)
+      .setStatus(Issue.STATUS_CLOSED)
+      .setResolution(Issue.RESOLUTION_FIXED);
+
+    assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
+      .isNotEmpty()
+      .hasValueSatisfying(pushEventDto -> {
+        assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotClosed.EVENT_NAME);
+        verifyHotspotClosedEventPayload(pushEventDto.getPayload(), defaultIssue);
+        assertThat(pushEventDto.getLanguage()).isEqualTo("java");
+        assertThat(pushEventDto.getProjectUuid()).isEqualTo("some-project-uuid");
+      });
+  }
+
+  private static void verifyHotspotClosedEventPayload(byte[] payload, DefaultIssue defaultIssue) {
+    assertThat(payload).isNotNull();
+
+    SecurityHotspotClosed event = gson.fromJson(new String(payload, StandardCharsets.UTF_8), SecurityHotspotClosed.class);
+    assertThat(event.getProjectKey()).isEqualTo(defaultIssue.projectKey());
+    assertThat(event.getKey()).isEqualTo(defaultIssue.key());
+    assertThat(event.getStatus()).isEqualTo(Issue.STATUS_CLOSED);
+    assertThat(event.getResolution()).isEqualTo(Issue.RESOLUTION_FIXED);
+    assertThat(event.getFilePath()).isEqualTo("component-name");
+  }
+
+  @Test
+  public void raiseEventOnIssue_whenChangedHotspot_shouldSkipEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setType(RuleType.SECURITY_HOTSPOT)
+      .setChanged(true)
+      .setNew(false)
+      .setCopied(false);
+
+    assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
+  }
+
+  @Test
+  public void raiseEventOnIssue_whenComponentUuidNull_shouldSkipEvent() {
+    DefaultIssue defaultIssue = createDefaultIssue()
+      .setComponentUuid(null);
 
     assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
   }
@@ -226,6 +342,7 @@ public class PushEventFactoryTest {
     treeRootHolder.setRoot(ReportComponent.builder(Type.PROJECT, 1)
       .setUuid("uuid_1")
       .addChildren(ReportComponent.builder(Type.FILE, 2)
+        .setName("component-name")
         .setUuid("issue-component-uuid")
         .build())
       .addChildren(ReportComponent.builder(Type.FILE, 3)
@@ -236,7 +353,11 @@ public class PushEventFactoryTest {
 
   private DefaultIssue createDefaultIssue() {
     return new DefaultIssue()
+      .setKey("issue-key")
+      .setProjectKey("project-key")
       .setComponentUuid("issue-component-uuid")
+      .setAssigneeUuid("some-user-uuid")
+      .setAssigneeLogin("some-user-login")
       .setType(RuleType.VULNERABILITY)
       .setLanguage("java")
       .setCreationDate(new Date())
@@ -254,4 +375,10 @@ public class PushEventFactoryTest {
       .setRuleKey(RuleKey.of("javasecurity", "S123"));
   }
 
+  private org.sonar.ce.task.projectanalysis.issue.Rule buildRule() {
+    RuleDto ruleDto = new RuleDto();
+    ruleDto.setRuleKey(RuleKey.of("javasecurity", "S123"));
+    ruleDto.setSecurityStandards(Set.of("owasp-a1"));
+    return new org.sonar.ce.task.projectanalysis.issue.RuleImpl(ruleDto);
+  }
 }
index 1befd30a3c251e4e25a95869a4d44f9a4a0f9089..ab8afc81fded86f0c016eb0068fa2c5de9a19d57 100644 (file)
@@ -31,6 +31,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.IntStream;
+import org.assertj.core.groups.Tuple;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -91,7 +92,7 @@ public class UserDaoIT {
   }
 
   @Test
-  public void selectUserUuidByScmAccountOrLoginOrEmail_findsCorrectResults() {
+  public void selectActiveUsersByScmAccountOrLoginOrEmail_findsCorrectResults() {
     String user1 = db.users().insertUser(user -> user.setLogin("user1").setEmail("toto@tata.com")).getUuid();
     String user2 = db.users().insertUser(user -> user.setLogin("user2")).getUuid();
     String user3 = db.users().insertUser(user -> user.setLogin("user3").setScmAccounts(List.of("scmuser3", "scmuser3bis"))).getUuid();
@@ -99,11 +100,14 @@ public class UserDaoIT {
     db.users().insertUser(user -> user.setLogin("inactive_user1").setActive(false));
     db.users().insertUser(user -> user.setLogin("inactive_user2").setActive(false).setScmAccounts(List.of("inactive_user2")));
 
-    assertThat(underTest.selectUserUuidByScmAccountOrLoginOrEmail(session, "toto@tata.com")).containsExactly(user1);
-    assertThat(underTest.selectUserUuidByScmAccountOrLoginOrEmail(session, "user2")).containsExactly(user2);
-    assertThat(underTest.selectUserUuidByScmAccountOrLoginOrEmail(session, "scmuser3")).containsExactly(user3);
-    assertThat(underTest.selectUserUuidByScmAccountOrLoginOrEmail(session, "inactive_user1")).isEmpty();
-    assertThat(underTest.selectUserUuidByScmAccountOrLoginOrEmail(session, "inactive_user2")).isEmpty();
+    assertThat(underTest.selectActiveUsersByScmAccountOrLoginOrEmail(session, "toto@tata.com"))
+      .extracting(UserIdDto::getUuid, UserIdDto::getLogin).containsExactly(new Tuple(user1, "user1"));
+    assertThat(underTest.selectActiveUsersByScmAccountOrLoginOrEmail(session, "user2"))
+      .extracting(UserIdDto::getUuid, UserIdDto::getLogin).containsExactly(new Tuple(user2, "user2"));
+    assertThat(underTest.selectActiveUsersByScmAccountOrLoginOrEmail(session, "scmuser3"))
+      .extracting(UserIdDto::getUuid, UserIdDto::getLogin).containsExactly(new Tuple(user3, "user3"));
+    assertThat(underTest.selectActiveUsersByScmAccountOrLoginOrEmail(session, "inactive_user1")).isEmpty();
+    assertThat(underTest.selectActiveUsersByScmAccountOrLoginOrEmail(session, "inactive_user2")).isEmpty();
   }
 
   @Test
index 81104ebd1756ab905e2736cad2ba17acf300a640..41307b39273c2287f47c7d3d640d568c5625977e 100644 (file)
@@ -769,6 +769,7 @@ public final class IssueDto implements Serializable {
     issue.setChecksum(checksum);
     issue.setSeverity(severity);
     issue.setAssigneeUuid(assigneeUuid);
+    issue.setAssigneeLogin(assigneeLogin);
     issue.setComponentKey(componentKey);
     issue.setComponentUuid(componentUuid);
     issue.setProjectUuid(projectUuid);
index 84ab150041739581bd31af7c8261b8a11c227354..6466c28bcf9d504287fdd8706ac8abcc4964d638 100644 (file)
@@ -190,8 +190,8 @@ public class UserDao implements Dao {
   /**
    * This method is optimized for the first analysis: we tried to keep performance optimal (<10ms) for projects with large number of contributors
    */
-  public List<String> selectUserUuidByScmAccountOrLoginOrEmail(DbSession session, String scmAccountOrLoginOrEmail) {
-    return mapper(session).selectUserUuidByScmAccountOrLoginOrEmail(scmAccountOrLoginOrEmail);
+  public List<UserIdDto> selectActiveUsersByScmAccountOrLoginOrEmail(DbSession session, String scmAccountOrLoginOrEmail) {
+    return mapper(session).selectActiveUsersByScmAccountOrLoginOrEmail(scmAccountOrLoginOrEmail);
   }
 
   /**
index 6ff3eab31c2dbd61a30bd522ef9c44489ad72272..c2a525bc360bf4eaef9a9584d970b89714b015e1 100644 (file)
@@ -76,7 +76,7 @@ public class UserDto implements UserId {
     return uuid;
   }
 
-  UserDto setUuid(String uuid) {
+  public UserDto setUuid(String uuid) {
     this.uuid = uuid;
     return this;
   }
index 496bba6727cd4afc8bf80e87e56899d4a3acf6f5..b2c76e959150c2671de0e2e3d8f9e5cd7d9601c4 100644 (file)
@@ -40,7 +40,7 @@ public interface UserMapper {
   @CheckForNull
   List<UserDto> selectNullableByScmAccountOrLoginOrEmail(@Param("scmAccount") String scmAccountOrLoginOrEmail);
 
-  List<String> selectUserUuidByScmAccountOrLoginOrEmail(@Param("scmAccount") String scmAccountOrLoginOrEmail);
+  List<UserIdDto> selectActiveUsersByScmAccountOrLoginOrEmail(@Param("scmAccount") String scmAccountOrLoginOrEmail);
 
   /**
    * Select user by login. Note that disabled users are ignored.
index 4b9ff1521de690ec8f74080bf09c622965dc0fb5..fe46b5691903fd23df2ac4f9990a969bebbc335f 100644 (file)
       user_uuid = #{userUuid,jdbcType=VARCHAR}
     </delete>
 
-    <select id="selectUserUuidByScmAccountOrLoginOrEmail" parameterType="String" resultType="String">
-      select user_uuid as uuid from scm_accounts sa
+    <select id="selectActiveUsersByScmAccountOrLoginOrEmail" parameterType="String" resultType="org.sonar.db.user.UserIdDto">
+      select u.uuid, u.login
+      from scm_accounts sa
       left join users u on sa.user_uuid = u.uuid
       where u.active=${_true} and sa.scm_account = lower(#{scmAccount,jdbcType=VARCHAR})
       union
-      select uuid from
-      users u
+      select uuid, login
+      from users u
       where active=${_true} and (login=#{scmAccount,jdbcType=VARCHAR} or email=#{scmAccount,jdbcType=VARCHAR} )
     </select>
 
index 32b915be9a04fe2f843c25143c0bba82d6d80f71..b6d8b7443d973f8f658981fa7e0dc2e017df7ee9 100644 (file)
@@ -38,6 +38,7 @@ import org.sonar.core.issue.DefaultIssueComment;
 import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.base.Strings.isNullOrEmpty;
@@ -128,13 +129,14 @@ public class IssueFieldsSetter {
   /**
    * Used to set the assignee when it was null
    */
-  public boolean setNewAssignee(DefaultIssue issue, @Nullable String newAssigneeUuid, IssueChangeContext context) {
-    if (newAssigneeUuid == null) {
+  public boolean setNewAssignee(DefaultIssue issue, @Nullable UserIdDto userId, IssueChangeContext context) {
+    if (userId == null) {
       return false;
     }
     checkState(issue.assignee() == null, "It's not possible to update the assignee with this method, please use assign()");
-    issue.setFieldChange(context, ASSIGNEE, UNUSED, newAssigneeUuid);
-    issue.setAssigneeUuid(newAssigneeUuid);
+    issue.setFieldChange(context, ASSIGNEE, UNUSED, userId.getUuid());
+    issue.setAssigneeUuid(userId.getUuid());
+    issue.setAssigneeLogin(userId.getLogin());
     issue.setUpdateDate(context.date());
     issue.setChanged(true);
     issue.setSendNotifications(true);
index 29fb552eff342ce19d34943f8af6906a7461ee08..362c7844d6757ba4cc5d526a2a472a58bf744e25 100644 (file)
@@ -37,6 +37,7 @@ import org.sonar.db.protobuf.DbCommons;
 import org.sonar.db.protobuf.DbIssues;
 import org.sonar.db.protobuf.DbIssues.MessageFormattingType;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserIdDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -111,9 +112,10 @@ public class IssueFieldsSetterTest {
 
   @Test
   public void set_new_assignee() {
-    boolean updated = underTest.setNewAssignee(issue, "user_uuid", context);
+    boolean updated = underTest.setNewAssignee(issue, new UserIdDto("user_uuid", "user_login"), context);
     assertThat(updated).isTrue();
     assertThat(issue.assignee()).isEqualTo("user_uuid");
+    assertThat(issue.assigneeLogin()).isEqualTo("user_login");
     assertThat(issue.mustSendNotifications()).isTrue();
     FieldDiffs.Diff diff = issue.currentChange().get(ASSIGNEE);
     assertThat(diff.oldValue()).isEqualTo(UNUSED);
@@ -132,7 +134,8 @@ public class IssueFieldsSetterTest {
   public void fail_with_ISE_when_setting_new_assignee_on_already_assigned_issue() {
     issue.setAssigneeUuid("user_uuid");
 
-    assertThatThrownBy(() -> underTest.setNewAssignee(issue, "another_user_uuid", context))
+    UserIdDto userId = new UserIdDto("another_user_uuid", "another_user_login");
+    assertThatThrownBy(() -> underTest.setNewAssignee(issue, userId, context))
       .isInstanceOf(IllegalStateException.class)
       .hasMessage("It's not possible to update the assignee with this method, please use assign()");
   }
index b66bfe94f3107d742b6ac2215ced2f547c61d300..ffe5f42d7b92682b9f20590f9fc848dd9bf9323b 100644 (file)
@@ -73,6 +73,7 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
   private String status = null;
   private String resolution = null;
   private String assigneeUuid = null;
+  private String assigneeLogin = null;
   private String checksum = null;
   private String authorLogin = null;
   private List<DefaultIssueComment> comments = null;
@@ -348,6 +349,16 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
     return this;
   }
 
+  @CheckForNull
+  public String assigneeLogin() {
+    return assigneeLogin;
+  }
+
+  public DefaultIssue setAssigneeLogin(@Nullable String s) {
+    this.assigneeLogin = s;
+    return this;
+  }
+
   @Override
   public Date creationDate() {
     return creationDate;
index e51e8b13e3d514a331171cba3cf6cf1682f06685..92ae1d4cd7f89114086887c103756cfd95dc6b87 100644 (file)
  */
 package org.sonar.core.issue;
 
-import java.text.SimpleDateFormat;
 import java.util.List;
-import java.util.Set;
 import org.apache.commons.lang.StringUtils;
 import org.junit.Test;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.Duration;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class DefaultIssueTest {
 
-  private static final String TEST_CONTEXT_KEY = "test_context_key";
-  private DefaultIssue issue = new DefaultIssue();
-
-  @Test
-  public void test_setters_and_getters() throws Exception {
-    issue.setKey("ABCD")
-      .setComponentKey("org.sample.Sample")
-      .setProjectKey("Sample")
-      .setRuleKey(RuleKey.of("java", "S100"))
-      .setLanguage("xoo")
-      .setSeverity("MINOR")
-      .setManualSeverity(true)
-      .setMessage("a message")
-      .setLine(7)
-      .setGap(1.2d)
-      .setEffort(Duration.create(28800L))
-      .setStatus(Issue.STATUS_CLOSED)
-      .setResolution(Issue.RESOLUTION_FIXED)
-      .setAssigneeUuid("julien")
-      .setAuthorLogin("steph")
-      .setChecksum("c7b5db46591806455cf082bb348631e8")
-      .setLocations("loc")
-      .setLocationsChanged(true)
-      .setNew(true)
-      .setIsOnChangedLine(true)
-      .setIsNewCodeReferenceIssue(true)
-      .setIsNoLongerNewCodeReferenceIssue(true)
-      .setBeingClosed(true)
-      .setOnDisabledRule(true)
-      .setCopied(true)
-      .setChanged(true)
-      .setSendNotifications(true)
-      .setCreationDate(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"))
-      .setUpdateDate(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-20"))
-      .setCloseDate(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-21"))
-      .setSelectedAt(1400000000000L)
-      .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
-      .setType(RuleType.BUG)
-      .setTags(Set.of("tag1", "tag2"))
-      .setCodeVariants(Set.of("variant1", "variant2"));
-
-    assertThat((Object) issue.getLocations()).isEqualTo("loc");
-    assertThat(issue.locationsChanged()).isTrue();
-    assertThat(issue.key()).isEqualTo("ABCD");
-    assertThat(issue.componentKey()).isEqualTo("org.sample.Sample");
-    assertThat(issue.projectKey()).isEqualTo("Sample");
-    assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("java", "S100"));
-    assertThat(issue.language()).isEqualTo("xoo");
-    assertThat(issue.severity()).isEqualTo("MINOR");
-    assertThat(issue.manualSeverity()).isTrue();
-    assertThat(issue.message()).isEqualTo("a message");
-    assertThat(issue.line()).isEqualTo(7);
-    assertThat(issue.gap()).isEqualTo(1.2d);
-    assertThat(issue.effort()).isEqualTo(Duration.create(28800L));
-    assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED);
-    assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
-    assertThat(issue.assignee()).isEqualTo("julien");
-    assertThat(issue.authorLogin()).isEqualTo("steph");
-    assertThat(issue.checksum()).isEqualTo("c7b5db46591806455cf082bb348631e8");
-    assertThat(issue.isNew()).isTrue();
-    assertThat(issue.isOnChangedLine()).isTrue();
-    assertThat(issue.isNewCodeReferenceIssue()).isTrue();
-    assertThat(issue.isNoLongerNewCodeReferenceIssue()).isTrue();
-    assertThat(issue.isToBeMigratedAsNewCodeReferenceIssue()).isFalse();
-    assertThat(issue.isCopied()).isTrue();
-    assertThat(issue.isBeingClosed()).isTrue();
-    assertThat(issue.isOnDisabledRule()).isTrue();
-    assertThat(issue.isChanged()).isTrue();
-    assertThat(issue.mustSendNotifications()).isTrue();
-    assertThat(issue.creationDate()).isEqualTo(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"));
-    assertThat(issue.updateDate()).isEqualTo(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-20"));
-    assertThat(issue.closeDate()).isEqualTo(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-21"));
-    assertThat(issue.selectedAt()).isEqualTo(1400000000000L);
-    assertThat(issue.getRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
-    assertThat(issue.type()).isEqualTo(RuleType.BUG);
-    assertThat(issue.tags()).containsOnly("tag1", "tag2");
-    assertThat(issue.codeVariants()).containsOnly("variant1", "variant2");
-  }
+  private final DefaultIssue issue = new DefaultIssue();
 
   @Test
   public void set_empty_dates() {
@@ -187,9 +106,9 @@ public class DefaultIssueTest {
 
     List<DefaultIssueComment> comments = issue.defaultIssueComments();
     assertThat(comments).isEmpty();
-
+    DefaultIssueComment defaultIssueComment = new DefaultIssueComment();
     try {
-      comments.add(new DefaultIssueComment());
+      comments.add(defaultIssueComment);
       fail();
     } catch (UnsupportedOperationException e) {
       // ok
@@ -312,4 +231,55 @@ public class DefaultIssueTest {
     DefaultIssue defaultIssue = new DefaultIssue();
     assertThat(defaultIssue.characteristic()).isNull();
   }
+
+  @Test
+  public void setLine_whenLineIsNegative_shouldThrowException() {
+    int anyNegativeValue = Integer.MIN_VALUE;
+    assertThatThrownBy(() -> issue.setLine(anyNegativeValue))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage(String.format("Line must be null or greater than zero (got %s)", anyNegativeValue));
+  }
+
+  @Test
+  public void setLine_whenLineIsZero_shouldThrowException() {
+    assertThatThrownBy(() -> issue.setLine(0))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Line must be null or greater than zero (got 0)");
+  }
+
+  @Test
+  public void setGap_whenGapIsNegative_shouldThrowException() {
+    Double anyNegativeValue = -1.0;
+    assertThatThrownBy(() -> issue.setGap(anyNegativeValue))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage(String.format("Gap must be greater than or equal 0 (got %s)", anyNegativeValue));
+  }
+
+  @Test
+  public void setGap_whenGapIsZero_shouldWork() {
+    issue.setGap(0.0);
+    assertThat(issue.gap()).isEqualTo(0.0);
+  }
+
+  @Test
+  public void effortInMinutes_shouldConvertEffortToMinutes() {
+    issue.setEffort(Duration.create(60));
+    assertThat(issue.effortInMinutes()).isEqualTo(60L);
+  }
+
+  @Test
+  public void effortInMinutes_whenNull_shouldReturnNull() {
+    issue.setEffort(null);
+    assertThat(issue.effortInMinutes()).isNull();
+  }
+
+  @Test
+  public void tags_whenNull_shouldReturnEmptySet() {
+    assertThat(issue.tags()).isEmpty();
+  }
+
+  @Test
+  public void codeVariants_whenNull_shouldReturnEmptySet() {
+    assertThat(issue.codeVariants()).isEmpty();
+  }
 }