aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorBelen Pruvost <belen.pruvost@sonarsource.com>2022-07-14 10:57:26 +0200
committersonartech <sonartech@sonarsource.com>2022-07-25 20:03:57 +0000
commit34e8dfb83433a7a2d016131c9f1be7373bffb7be (patch)
tree547fe118621e11536f32a7c6d74f68220844b7e5 /server
parentd66a7c69cbef6ee9647ac0233d915b2863c06338 (diff)
downloadsonarqube-34e8dfb83433a7a2d016131c9f1be7373bffb7be.tar.gz
sonarqube-34e8dfb83433a7a2d016131c9f1be7373bffb7be.zip
SONAR-16371 - Implement pull_taint endpoint
Diffstat (limited to 'server')
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java22
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java12
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java15
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java21
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml159
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java83
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java6
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java7
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java8
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java183
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java144
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java82
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java47
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java43
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java20
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java175
-rw-r--r--server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto34
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java53
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java491
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java2
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java118
22 files changed, 1509 insertions, 221 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
index 5d553b776a5..c95ee71c47c 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import javax.annotation.Nullable;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
import org.sonar.db.Pagination;
@@ -30,6 +31,7 @@ import org.sonar.db.RowNotFoundException;
import org.sonar.db.WildcardPosition;
import org.sonar.db.component.ComponentDto;
+import static java.util.Collections.emptyList;
import static org.sonar.db.DaoUtils.buildLikeValue;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;
@@ -59,11 +61,27 @@ public class IssueDao implements Dao {
}
public Set<String> selectIssueKeysByComponentUuid(DbSession session, String componentUuid) {
- return mapper(session).selectIssueKeysByComponentUuid(componentUuid);
+ return selectIssueKeysByComponentUuid(session, componentUuid, emptyList(), emptyList(), emptyList(),
+ null, false);
+ }
+
+ public Set<String> selectIssueKeysByComponentUuid(DbSession session, String componentUuid,
+ List<String> includingRepositories, List<String> excludingRepositories,
+ List<String> languages, @Nullable Boolean resolvedOnly, boolean openIssuesOnly) {
+ return mapper(session).selectIssueKeysByComponentUuid(componentUuid, includingRepositories, excludingRepositories,
+ languages, resolvedOnly, openIssuesOnly);
}
public Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(DbSession session, String componentUuid, long changedSince) {
- return mapper(session).selectIssueKeysByComponentUuidAndChangedSinceDate(componentUuid, changedSince);
+ return selectIssueKeysByComponentUuidAndChangedSinceDate(session, componentUuid, changedSince, emptyList(), emptyList(),
+ emptyList(), null);
+ }
+
+ public Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(DbSession session, String componentUuid, long changedSince,
+ List<String> includingRepositories, List<String> excludingRepositories,
+ List<String> languages, @Nullable Boolean resolvedOnly) {
+ return mapper(session).selectIssueKeysByComponentUuidAndChangedSinceDate(componentUuid, changedSince,
+ includingRepositories, excludingRepositories, languages, resolvedOnly);
}
public List<IssueDto> selectByComponentUuidPaginated(DbSession session, String componentUuid, int page) {
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
index 7611cff2f3c..1e063a82b15 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
@@ -87,6 +87,8 @@ public final class IssueDto implements Serializable {
*/
private Long selectedAt;
+ private Integer priority;
+
// joins
private String ruleKey;
private String ruleRepo;
@@ -581,6 +583,16 @@ public final class IssueDto implements Serializable {
return this;
}
+ @CheckForNull
+ public Integer getPriority() {
+ return priority;
+ }
+
+ public IssueDto setPriority(@Nullable Integer priority) {
+ this.priority = priority;
+ return this;
+ }
+
/**
* Should only be used to persist in E/S
* <p/>
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
index 3862db81734..3baf1db3549 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
@@ -22,6 +22,7 @@ package org.sonar.db.issue;
import java.util.Collection;
import java.util.List;
import java.util.Set;
+import javax.annotation.Nullable;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.ResultHandler;
import org.sonar.db.Pagination;
@@ -37,12 +38,20 @@ public interface IssueMapper {
List<IssueDto> selectByKeys(List<String> keys);
- Set<String> selectIssueKeysByComponentUuid(@Param("componentUuid") String componentUuid);
+ Set<String> selectIssueKeysByComponentUuid(@Param("componentUuid") String componentUuid,
+ @Param("includingRepositories") List<String> includingRepositories,
+ @Param("excludingRepositories") List<String> excludingRepositories,
+ @Param("languages") List<String> languages, @Param("resolvedOnly") @Nullable Boolean resolvedOnly,
+ @Param("openIssuesOnly") boolean openIssuesOnly);
- Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(@Param("componentUuid") String componentUuid, @Param("changedSince") long changedSince);
+ Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(@Param("componentUuid") String componentUuid,
+ @Param("changedSince") long changedSince,
+ @Param("includingRepositories") List<String> includingRepositories,
+ @Param("excludingRepositories") List<String> excludingRepositories,
+ @Param("languages") List<String> languages, @Param("resolvedOnly") @Nullable Boolean resolvedOnly);
List<IssueDto> selectByComponentUuidPaginated(@Param("componentUuid") String componentUuid,
- @Param("pagination") Pagination pagination);
+ @Param("pagination") Pagination pagination);
List<IssueDto> selectByKeysIfNotUpdatedAt(@Param("keys") List<String> keys, @Param("updatedAt") long updatedAt);
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java
index 0ce8683422b..1a5199244de 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java
@@ -19,23 +19,28 @@
*/
package org.sonar.db.issue;
+import java.util.ArrayList;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import static java.util.Objects.requireNonNullElse;
+
public class IssueQueryParams {
private final String branchUuid;
private final List<String> languages;
- private final List<String> ruleRepositories;
private final boolean resolvedOnly;
private final Long changedSince;
+ private final List<String> ruleRepositories;
+ private final List<String> excludingRuleRepositories;
- public IssueQueryParams(String branchUuid, @Nullable List<String> languages,
- @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
+ public IssueQueryParams(String branchUuid, @Nullable List<String> languages, @Nullable List<String> ruleRepositories,
+ @Nullable List<String> excludingRuleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
this.branchUuid = branchUuid;
- this.languages = languages;
- this.ruleRepositories = ruleRepositories;
+ this.languages = requireNonNullElse(languages, new ArrayList<>());
+ this.ruleRepositories = requireNonNullElse(ruleRepositories, new ArrayList<>());
+ this.excludingRuleRepositories = requireNonNullElse(excludingRuleRepositories, new ArrayList<>());
this.resolvedOnly = resolvedOnly;
this.changedSince = changedSince;
}
@@ -44,16 +49,18 @@ public class IssueQueryParams {
return branchUuid;
}
- @CheckForNull
public List<String> getLanguages() {
return languages;
}
- @CheckForNull
public List<String> getRuleRepositories() {
return ruleRepositories;
}
+ public List<String> getExcludingRuleRepositories() {
+ return excludingRuleRepositories;
+ }
+
public boolean isResolvedOnly() {
return resolvedOnly;
}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
index d78ed8ec0b1..2abc40a7e3f 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
@@ -473,17 +473,74 @@
select
i.kee
from issues i
- where
+ <if test="includingRepositories != null || excludingRepositories != null || languages != null">
+ inner join rules r on i.rule_uuid = r.uuid
+ </if>
+ where
i.project_uuid=#{componentUuid,jdbcType=VARCHAR}
+ <choose>
+ <when test="openIssuesOnly == true">
+ AND i.issue_type != 4
+ AND i.status != 'CLOSED'
+ </when>
+ <when test="resolvedOnly == true">
+ AND i.issue_type != 4
+ AND i.status = 'RESOLVED'
+ </when>
+ </choose>
+ <if test="includingRepositories != null and includingRepositories.size() > 0">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="includingRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="excludingRepositories != null and excludingRepositories.size() > 0">
+ AND r.plugin_name NOT IN
+ <foreach item="ruleRepository" index="index" collection="excludingRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="languages != null and languages.size() > 0">
+ AND r.language IN
+ <foreach item="language" index="index" collection="languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
</select>
<select id="selectIssueKeysByComponentUuidAndChangedSinceDate" parameterType="map" resultType="string">
select
i.kee
from issues i
+ <if test="includingRepositories != null || excludingRepositories != null || languages != null">
+ inner join rules r on i.rule_uuid = r.uuid
+ </if>
where
i.project_uuid=#{componentUuid,jdbcType=VARCHAR}
AND i.issue_update_date &gt;= #{changedSince,jdbcType=BIGINT}
+ AND i.status != 'CLOSED'
+ AND i.issue_type != 4
+ <if test="resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ <if test="includingRepositories != null and includingRepositories.size() > 0">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="includingRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="excludingRepositories != null and excludingRepositories.size() > 0">
+ AND r.plugin_name NOT IN
+ <foreach item="ruleRepository" index="index" collection="excludingRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="languages != null and languages.size() > 0">
+ AND r.language IN
+ <foreach item="language" index="index" collection="languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
</select>
<select id="selectByComponentUuidPaginated" parameterType="map" resultType="Issue">
@@ -551,6 +608,7 @@
result.ruleUuid as ruleUuid,
result.createdAt as createdAt,
result.status as status,
+ result.priority,
result.ruleType as ruleType,
result.ruleRepo as ruleRepo,
result.ruleKey as ruleKey,
@@ -560,7 +618,8 @@
result.type as type,
result.locations as locations,
result.component_uuid,
- c.path as filePath
+ c.path as filePath,
+ result.assigneeUuid
</sql>
<sql id="selectByBranchColumnsOuterQuery">
@@ -568,6 +627,7 @@
t.ruleUuid as ruleUuid,
t.createdAt as createdAt,
t.status as status,
+ t.priority,
t.ruleType as ruleType,
t.ruleRepo as ruleRepo,
t.ruleKey as ruleKey,
@@ -576,7 +636,8 @@
t.manualSeverity as manualSeverity,
t.type as type,
t.locations as locations,
- t.component_uuid
+ t.component_uuid,
+ t.assigneeUuid
</sql>
<sql id="selectByBranchColumns">
@@ -584,6 +645,7 @@
i.rule_uuid as ruleUuid,
i.created_at as createdAt,
i.status as status,
+ r.priority,
r.rule_type as ruleType,
r.plugin_name as ruleRepo,
r.plugin_rule_key as ruleKey,
@@ -592,7 +654,8 @@
i.manual_severity as manualSeverity,
i.issue_type as type,
i.locations as locations,
- i.component_uuid as component_uuid
+ i.component_uuid as component_uuid,
+ i.assignee as assigneeUuid
</sql>
<select id="selectByBranch" parameterType="map" resultType="Issue">
@@ -604,28 +667,28 @@
inner join components p on p.uuid=i.component_uuid
where
i.project_uuid = #{queryParams.branchUuid}
- <if test="queryParams.changedSince != null">
- AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
- </if>
- <if test="queryParams.resolvedOnly == true">
- AND i.status = 'RESOLVED'
- </if>
AND i.status &lt;&gt; 'CLOSED'
AND i.issue_type &lt;&gt; 4
- <if test="queryParams.ruleRepositories != null">
- AND r.plugin_name IN
- <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
- #{ruleRepository}
- </foreach>
- </if>
- <if test="queryParams.languages != null">
- AND r.language IN
- <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
- #{language}
- </foreach>
- </if>
- order by i.kee ASC
- limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER}
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ <if test="queryParams.ruleRepositories.size() > 0">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages.size() > 0">
+ AND r.language IN
+ <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
+ order by i.kee ASC
+ limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER}
</select>
<select id="selectByBranch" parameterType="map" resultType="Issue" databaseId="mssql">
@@ -642,21 +705,21 @@
inner join rules r on r.uuid = i.rule_uuid
where
i.project_uuid = #{queryParams.branchUuid}
- <if test="queryParams.changedSince != null">
- AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
- </if>
- <if test="queryParams.resolvedOnly == true">
- AND i.status = 'RESOLVED'
- </if>
AND i.status &lt;&gt; 'CLOSED'
- AND i.issue_type &lt;&gt; 4
- <if test="queryParams.ruleRepositories != null">
+ AND i.issue_type &lt;&gt; 4
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ <if test="queryParams.ruleRepositories.size() > 0">
AND r.plugin_name IN
<foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
#{ruleRepository}
</foreach>
</if>
- <if test="queryParams.languages != null">
+ <if test="queryParams.languages.size() > 0">
AND r.language IN
<foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
#{language}
@@ -679,21 +742,21 @@
inner join rules r on r.uuid = i.rule_uuid
where
i.project_uuid = #{queryParams.branchUuid}
- <if test="queryParams.changedSince != null">
- AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
- </if>
- <if test="queryParams.resolvedOnly == true">
- AND i.status = 'RESOLVED'
- </if>
AND i.status &lt;&gt; 'CLOSED'
- AND i.issue_type &lt;&gt; 4
- <if test="queryParams.ruleRepositories != null">
+ AND i.issue_type &lt;&gt; 4
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date &gt;= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ <if test="queryParams.ruleRepositories.size() > 0">
AND r.plugin_name IN
<foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
#{ruleRepository}
</foreach>
</if>
- <if test="queryParams.languages != null">
+ <if test="queryParams.languages.size() > 0">
AND r.language IN
<foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
#{language}
@@ -715,19 +778,25 @@
where
i.project_uuid = #{queryParams.branchUuid}
AND issue_update_date &gt;= #{queryParams.changedSince}
- <if test="queryParams.ruleRepositories != null">
+ AND i.status = 'CLOSED'
+ <if test="queryParams.ruleRepositories.size() > 0">
AND r.plugin_name IN
<foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
#{ruleRepository}
</foreach>
</if>
- <if test="queryParams.languages != null">
+ <if test="queryParams.excludingRuleRepositories.size() > 0">
+ AND r.plugin_name NOT IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.excludingRuleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages.size() > 0">
AND r.language IN
<foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
#{language}
</foreach>
</if>
- AND i.status = 'CLOSED'
</select>
</mapper>
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
index c74730f99a4..fa29f8053a2 100644
--- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
@@ -41,6 +41,7 @@ import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleTesting;
import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.math.RandomUtils.nextInt;
import static org.assertj.core.api.Assertions.assertThat;
@@ -152,6 +153,43 @@ public class IssueDaoTest {
}
@Test
+ public void selectIssueKeysByComponentUuidFiltersAccordingly() {
+ // contains I1 and I2
+ prepareTables();
+
+ // adds I3
+ underTest.insert(db.getSession(), newIssueDto("I3")
+ .setMessage("the message")
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setStatus("OPEN")
+ .setProjectUuid(PROJECT_UUID));
+
+ // Filter by including repositories
+ Set<String> issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, List.of("xoo"),
+ emptyList(), emptyList(), null, false);
+ // results are not ordered, so do not use "containsExactly"
+ assertThat(issues).containsOnly("I1", "I2", "I3");
+
+ // Filter by excluding repositories
+ issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), List.of("xoo"),
+ emptyList(), null, false);
+ assertThat(issues).isEmpty();
+
+ // Filter by language
+ issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), List.of("xoo"), null, false);
+ assertThat(issues).containsOnly("I1", "I2", "I3");
+
+ // Filter by resolved only
+ issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), emptyList(), true, false);
+ assertThat(issues).containsOnly("I1");
+
+ // Filter by non-closed issues only
+ issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), emptyList(), null, true);
+ assertThat(issues).containsOnly("I1", "I3");
+ }
+
+ @Test
public void selectIssueKeysByComponentUuidAndChangedSince() {
long t1 = 1_340_000_000_000L;
long t2 = 1_400_000_000_000L;
@@ -163,9 +201,41 @@ public class IssueDaoTest {
Set<String> issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2);
// results are not ordered, so do not use "containsExactly"
- assertThat(issues).containsOnly("I1", "I2");
+ assertThat(issues).containsOnly("I1");
+ }
+
+ @Test
+ public void selectIssueKeysByComponentUuidAndChangedSinceFiltersAccordingly() {
+ long t1 = 1_340_000_000_000L;
+ long t2 = 1_400_000_000_000L;
+ // contains I1 and I2
+ prepareTables();
+ // Insert I3, I4, where t1 < t2
+ IntStream.range(3, 5).forEach(i -> underTest.insert(db.getSession(), newIssueDto("I" + i).setUpdatedAt(t1)));
+
+ // Filter by including repositories
+ Set<String> issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, List.of("xoo"),
+ emptyList(), emptyList(), null);
+ // results are not ordered, so do not use "containsExactly"
+ assertThat(issues).containsOnly("I1");
+
+ // Filter by excluding repositories
+ issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2,
+ emptyList(), List.of("xoo"), emptyList(), null);
+ assertThat(issues).isEmpty();
+
+ // Filter by language
+ issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, emptyList(),
+ emptyList(), List.of("xoo"), null);
+ assertThat(issues).containsOnly("I1");
+
+ // Filter by resolved only
+ issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, emptyList(),
+ emptyList(), emptyList(), true);
+ assertThat(issues).containsOnly("I1");
}
+
@Test
public void selectByBranch() {
long updatedAt = 1_340_000_000_000L;
@@ -246,11 +316,11 @@ public class IssueDaoTest {
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), file.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), project.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), "does_not_exist")).isEmpty();
}
@@ -278,11 +348,11 @@ public class IssueDaoTest {
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), project))
.extracting(IssueDto::getKey)
.containsExactlyInAnyOrder(
- Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), module))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
ComponentDto notPersisted = ComponentTesting.newPrivateProjectDto();
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), notPersisted)).isEmpty();
@@ -787,6 +857,7 @@ public class IssueDaoTest {
underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
.setRuleUuid(RULE.getUuid())
.setComponentUuid(FILE_UUID)
+ .setStatus("CLOSED")
.setProjectUuid(PROJECT_UUID));
db.getSession().commit();
}
@@ -800,6 +871,6 @@ public class IssueDaoTest {
}
private static IssueQueryParams buildSelectByBranchQuery(ComponentDto branch, String language, boolean resolvedOnly, Long changedSince) {
- return new IssueQueryParams(branch.uuid(), List.of(language), List.of(language), resolvedOnly, changedSince);
+ return new IssueQueryParams(branch.uuid(), List.of(language), List.of(), List.of(), resolvedOnly, changedSince);
}
}
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java
index b23a2bbcc75..b68b4cd11e2 100644
--- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java
@@ -25,7 +25,6 @@ import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class IssueQueryParamsTest {
- private final List<String> languages = List.of("java");
private final List<String> ruleRepositories = List.of("js-security", "java");
@Test
@@ -34,11 +33,12 @@ public class IssueQueryParamsTest {
long changedSince = 1_000_000L;
String branchUuid = "master-branch-uuid";
- IssueQueryParams queryParameters = new IssueQueryParams(branchUuid, languages, ruleRepositories, resolvedOnly, changedSince);
+ IssueQueryParams queryParameters = new IssueQueryParams(branchUuid, null, ruleRepositories, null, resolvedOnly, changedSince);
assertThat(queryParameters.getBranchUuid()).isEqualTo(branchUuid);
- assertThat(queryParameters.getLanguages()).isEqualTo(languages);
+ assertThat(queryParameters.getLanguages()).isNotNull().isEmpty();
assertThat(queryParameters.getRuleRepositories()).isEqualTo(ruleRepositories);
+ assertThat(queryParameters.getExcludingRuleRepositories()).isNotNull().isEmpty();
assertThat(queryParameters.isResolvedOnly()).isFalse();
assertThat(queryParameters.getChangedSince()).isEqualTo(changedSince);
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
index 0abd66b3756..ed591a9127d 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java
@@ -22,7 +22,6 @@ package org.sonar.server.issue;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
@@ -30,7 +29,7 @@ import org.sonar.db.issue.IssueDto;
public class TaintChecker {
- private static final Set<String> TAINT_REPOSITORIES = Set.of("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity");
+ private static final List<String> TAINT_REPOSITORIES = List.of("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity");
private TaintChecker() {
throw new IllegalStateException("Utility class, cannot be instantiated.");
@@ -65,4 +64,8 @@ public class TaintChecker {
return issueDto -> !TAINT_REPOSITORIES.contains(issueDto.getRuleRepo());
}
+ public static List<String> getTaintRepositories() {
+ return TAINT_REPOSITORIES;
+ }
+
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
index e98b2ba45ba..7da4d7a26ee 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java
@@ -78,6 +78,14 @@ public class TaintCheckerTest {
assertThat(issuesByTaintStatus.get(false).get(2).getKey()).isEqualTo("standardIssue3");
}
+ @Test
+ public void test_getTaintRepositories() {
+ assertThat(TaintChecker.getTaintRepositories())
+ .hasSize(6)
+ .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity",
+ "tssecurity", "phpsecurity", "pythonsecurity");
+ }
+
private List<IssueDto> getIssues() {
List<IssueDto> issues = new ArrayList<>();
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java
new file mode 100644
index 00000000000..7c8b5ce2b35
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java
@@ -0,0 +1,183 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueQueryParams;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.ws.pull.ProtobufObjectGenerator;
+import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever;
+import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+import static java.util.Collections.emptyList;
+import static org.sonar.api.web.UserRole.USER;
+
+public abstract class BasePullAction implements IssuesWsAction {
+
+ protected static final String PROJECT_KEY_PARAM = "projectKey";
+ protected static final String BRANCH_NAME_PARAM = "branchName";
+ protected static final String LANGUAGES_PARAM = "languages";
+ protected static final String RULE_REPOSITORIES_PARAM = "ruleRepositories";
+ protected static final String RESOLVED_ONLY_PARAM = "resolvedOnly";
+ protected static final String CHANGED_SINCE_PARAM = "changedSince";
+ protected final String actionName;
+ protected final String issueType;
+ protected final String repositoryExample;
+ protected final String sinceVersion;
+ protected final String resourceExample;
+
+ private final ComponentFinder componentFinder;
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final PullActionResponseWriter pullActionResponseWriter;
+
+ protected BasePullAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession,
+ ProtobufObjectGenerator protobufObjectGenerator, String actionName, String issueType,
+ String repositoryExample, String sinceVersion, String resourceExample) {
+ this.componentFinder = componentFinder;
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.pullActionResponseWriter = new PullActionResponseWriter(system2, protobufObjectGenerator);
+ this.actionName = actionName;
+ this.issueType = issueType;
+ this.repositoryExample = repositoryExample;
+ this.sinceVersion = sinceVersion;
+ this.resourceExample = resourceExample;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ WebService.NewAction action = controller
+ .createAction(actionName)
+ .setHandler(this)
+ .setInternal(true)
+ .setResponseExample(getClass().getResource(resourceExample))
+ .setDescription(format("This endpoint fetches and returns all (unless filtered by optional params) the %s for a given branch. " +
+ "The %s returned are not paginated, so the response size can be big.", issueType, issueType))
+ .setSince(sinceVersion);
+
+ action.createParam(PROJECT_KEY_PARAM)
+ .setRequired(true)
+ .setDescription(format("Project key for which %s are fetched.", issueType))
+ .setExampleValue("sonarqube");
+
+ action.createParam(BRANCH_NAME_PARAM)
+ .setRequired(true)
+ .setDescription(format("Branch name for which %s are fetched.", issueType))
+ .setExampleValue("develop");
+
+ action.createParam(LANGUAGES_PARAM)
+ .setDescription(format("Comma separated list of languages. If not present all %s regardless of their language are returned.", issueType))
+ .setExampleValue("java,cobol");
+
+ action.createParam(CHANGED_SINCE_PARAM)
+ .setDescription(format("Timestamp. If present only %s modified after given timestamp are returned (both open and closed). " +
+ "If not present all non-closed %s are returned.", issueType, issueType))
+ .setExampleValue(1_654_032_306_000L);
+
+ if (issueType.equals("issues")) {
+ action.createParam(RULE_REPOSITORIES_PARAM)
+ .setDescription(format("Comma separated list of rule repositories. If not present all %s regardless of" +
+ " their rule repository are returned.", issueType))
+ .setExampleValue(repositoryExample);
+
+ action.createParam(RESOLVED_ONLY_PARAM)
+ .setDescription(format("If true only %s with resolved status are returned", issueType))
+ .setExampleValue("true");
+ }
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM);
+ String branchName = request.mandatoryParam(BRANCH_NAME_PARAM);
+ List<String> languages = request.paramAsStrings(LANGUAGES_PARAM);
+ String changedSince = request.param(CHANGED_SINCE_PARAM);
+ Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null;
+
+ if (issueType.equals("issues")) {
+ boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM));
+
+ List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM);
+ if (ruleRepositories != null && !ruleRepositories.isEmpty()) {
+ validateRuleRepositories(ruleRepositories);
+ }
+
+ streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output());
+ } else {
+ streamResponse(projectKey, branchName, languages, emptyList(), false, changedSinceTimestamp, response.stream().output());
+ }
+ }
+
+ private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream)
+ throws IOException {
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
+ userSession.checkProjectPermission(USER, projectDto);
+ BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchName, null);
+ pullActionResponseWriter.appendTimestampToResponse(outputStream);
+ IssueQueryParams issueQueryParams = initializeQueryParams(branchDto, languages, ruleRepositories, resolvedOnly, changedSince);
+ retrieveAndSendIssues(dbSession, issueQueryParams, outputStream);
+ }
+ }
+
+ private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream)
+ throws IOException {
+
+ var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+
+ Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams));
+ Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream);
+ issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer);
+
+ if (queryParams.getChangedSince() != null) {
+ // in the "incremental mode" we need to send SonarLint also recently closed issues keys
+ List<String> closedIssues = issuesRetriever.retrieveClosedIssues(dbSession);
+ pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream);
+ }
+ }
+
+ protected abstract void validateRuleRepositories(List<String> ruleRepositories);
+
+ protected abstract IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince);
+
+ protected abstract Set<String> getIssueKeysSnapshot(IssueQueryParams queryParams);
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
index d6aa030e55b..815edbec1b8 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
@@ -32,6 +32,7 @@ import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
+import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
public class IssueWsModule extends Module {
@@ -70,7 +71,9 @@ public class IssueWsModule extends Module {
BulkChangeAction.class,
QGChangeEventListenersImpl.class,
PullAction.class,
+ PullTaintAction.class,
PullActionResponseWriter.class,
- PullActionProtobufObjectGenerator.class);
+ PullActionProtobufObjectGenerator.class,
+ PullTaintActionProtobufObjectGenerator.class);
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java
index 24813f197e3..52aa0b61608 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java
@@ -19,145 +19,69 @@
*/
package org.sonar.server.issue.ws;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Consumer;
import javax.annotation.Nullable;
-import org.sonar.api.server.ws.Request;
-import org.sonar.api.server.ws.Response;
-import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
-import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueQueryParams;
-import org.sonar.db.project.ProjectDto;
import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever;
-import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
+import org.sonar.server.issue.TaintChecker;
+import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
import org.sonar.server.user.UserSession;
import static java.util.Optional.ofNullable;
-import static org.sonar.api.web.UserRole.USER;
+import static org.sonarqube.ws.WsUtils.checkArgument;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL;
-public class PullAction implements IssuesWsAction {
-
- private static final String PROJECT_KEY_PARAM = "projectKey";
- private static final String BRANCH_NAME_PARAM = "branchName";
- private static final String LANGUAGES_PARAM = "languages";
- private static final String RULE_REPOSITORIES_PARAM = "ruleRepositories";
- private static final String RESOLVED_ONLY_PARAM = "resolvedOnly";
- private static final String CHANGED_SINCE_PARAM = "changedSince";
+public class PullAction extends BasePullAction {
+ private static final String ISSUE_TYPE = "issues";
+ private static final String REPOSITORY_EXAMPLE = "java";
+ private static final String RESOURCE_EXAMPLE = "pull-example.proto";
+ private static final String SINCE_VERSION = "9.5";
private final DbClient dbClient;
- private final UserSession userSession;
- private final PullActionResponseWriter pullActionResponseWriter;
- private final ComponentFinder componentFinder;
- public PullAction(DbClient dbClient, UserSession userSession, PullActionResponseWriter pullActionResponseWriter, ComponentFinder componentFinder) {
+ public PullAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession,
+ PullActionProtobufObjectGenerator protobufObjectGenerator) {
+ super(system2, componentFinder, dbClient, userSession, protobufObjectGenerator, ACTION_PULL,
+ ISSUE_TYPE, REPOSITORY_EXAMPLE, SINCE_VERSION, RESOURCE_EXAMPLE);
this.dbClient = dbClient;
- this.userSession = userSession;
- this.pullActionResponseWriter = pullActionResponseWriter;
- this.componentFinder = componentFinder;
}
@Override
- public void define(WebService.NewController controller) {
- WebService.NewAction action = controller
- .createAction(ACTION_PULL)
- .setHandler(this)
- .setInternal(true)
- .setResponseExample(getClass().getResource("pull-example.proto"))
- .setDescription("This endpoint fetches and returns all (unless filtered by optional params) the issues for a given branch." +
- "The issues returned are not paginated, so the response size can be big.")
- .setSince("9.5");
-
- action.createParam(PROJECT_KEY_PARAM)
- .setRequired(true)
- .setDescription("Project key for which issues are fetched.")
- .setExampleValue("sonarqube");
-
- action.createParam(BRANCH_NAME_PARAM)
- .setRequired(true)
- .setDescription("Branch name for which issues are fetched.")
- .setExampleValue("develop");
-
- action.createParam(LANGUAGES_PARAM)
- .setDescription("Comma seperated list of languages. If not present all issues regardless of their language are returned.")
- .setExampleValue("java,cobol");
-
- action.createParam(RULE_REPOSITORIES_PARAM)
- .setDescription("Comma seperated list of rule repositories. If not present all issues regardless of" +
- " their rule repository are returned.")
- .setExampleValue("java");
-
- action.createParam(RESOLVED_ONLY_PARAM)
- .setDescription("If true only issues with resolved status are returned")
- .setExampleValue("true");
-
- action.createParam(CHANGED_SINCE_PARAM)
- .setDescription("Timestamp. If present only issues modified after given timestamp are returned (both open and closed). " +
- "If not present all non-closed issues are returned.")
- .setExampleValue(1_654_032_306_000L);
- }
-
- @Override
- public void handle(Request request, Response response) throws Exception {
- String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM);
- String branchName = request.mandatoryParam(BRANCH_NAME_PARAM);
- List<String> languages = request.paramAsStrings(LANGUAGES_PARAM);
- List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM);
- boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM));
- String changedSince = request.param(CHANGED_SINCE_PARAM);
- Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null;
-
- streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output());
- }
+ protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<Long> changedSinceDate = ofNullable(issueQueryParams.getChangedSince());
- private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages,
- @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream)
- throws IOException {
+ if (changedSinceDate.isPresent()) {
+ return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, issueQueryParams.getBranchUuid(),
+ changedSinceDate.get(), issueQueryParams.getRuleRepositories(), TaintChecker.getTaintRepositories(),
+ issueQueryParams.getLanguages(), issueQueryParams.isResolvedOnly());
+ }
- try (DbSession dbSession = dbClient.openSession(false)) {
- ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
- userSession.checkProjectPermission(USER, projectDto);
- BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchName, null);
- pullActionResponseWriter.appendTimestampToResponse(outputStream);
- IssueQueryParams pullActionQueryParams = new IssueQueryParams(branchDto.getUuid(), languages, ruleRepositories, resolvedOnly, changedSince);
- retrieveAndSendIssues(dbSession, pullActionQueryParams, outputStream);
+ return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(),
+ issueQueryParams.getRuleRepositories(), TaintChecker.getTaintRepositories(),
+ issueQueryParams.getLanguages(), issueQueryParams.isResolvedOnly(), true);
}
}
- private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream)
- throws IOException {
-
- var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
-
- Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams.getBranchUuid(), queryParams.getChangedSince()));
- Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream);
- issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer);
-
- if (queryParams.getChangedSince() != null) {
- // in the "incremental mode" we need to send SonarLint also recently closed issues keys
- List<String> closedIssues = issuesRetriever.retrieveClosedIssues(dbSession);
- pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream);
- }
+ @Override
+ protected IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
+ return new IssueQueryParams(branchDto.getUuid(), languages, ruleRepositories, TaintChecker.getTaintRepositories(), resolvedOnly, changedSince);
}
- private Set<String> getIssueKeysSnapshot(String componentUuid, @Nullable Long changedSince) {
- try (DbSession dbSession = dbClient.openSession(false)) {
- Optional<Long> changedSinceDate = ofNullable(changedSince);
- if (changedSinceDate.isPresent()) {
- return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, componentUuid, changedSinceDate.get());
- }
+ @Override
+ protected void validateRuleRepositories(List<String> ruleRepositories) {
+ checkArgument(ruleRepositories
+ .stream()
+ .filter(TaintChecker.getTaintRepositories()::contains)
+ .count() == 0, "Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities");
- return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, componentUuid);
- }
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java
new file mode 100644
index 00000000000..5c1e1f8faa7
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.issue.IssueQueryParams;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.TaintChecker;
+import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL_TAINT;
+
+public class PullTaintAction extends BasePullAction {
+ private static final String ISSUE_TYPE = "taint vulnerabilities";
+ private static final String RESOURCE_EXAMPLE = "pull-taint-example.proto";
+ private static final String SINCE_VERSION = "9.6";
+
+ private final DbClient dbClient;
+
+ public PullTaintAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession,
+ PullTaintActionProtobufObjectGenerator protobufObjectGenerator) {
+ super(system2, componentFinder, dbClient, userSession, protobufObjectGenerator, ACTION_PULL_TAINT,
+ ISSUE_TYPE, "", SINCE_VERSION, RESOURCE_EXAMPLE);
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams) {
+ Optional<Long> changedSinceDate = ofNullable(issueQueryParams.getChangedSince());
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ if (changedSinceDate.isPresent()) {
+ return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, issueQueryParams.getBranchUuid(),
+ changedSinceDate.get(), issueQueryParams.getRuleRepositories(), emptyList(),
+ issueQueryParams.getLanguages(), false);
+ }
+
+ return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(),
+ issueQueryParams.getRuleRepositories(),
+ emptyList(), issueQueryParams.getLanguages(),
+ issueQueryParams.isResolvedOnly(), true);
+
+ }
+ }
+
+ @Override
+ protected IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
+ return new IssueQueryParams(branchDto.getUuid(), languages, TaintChecker.getTaintRepositories(), emptyList(), resolvedOnly, changedSince);
+ }
+
+ @Override
+ protected void validateRuleRepositories(List<String> ruleRepositories) {
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java
new file mode 100644
index 00000000000..1286e5e4aa8
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws.pull;
+
+import com.google.protobuf.AbstractMessageLite;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonarqube.ws.Issues;
+
+public interface ProtobufObjectGenerator {
+ AbstractMessageLite generateTimestampMessage(long timestamp);
+
+ AbstractMessageLite generateIssueMessage(IssueDto issueDto);
+
+ AbstractMessageLite generateClosedIssueMessage(String uuid);
+
+ default Issues.TextRange buildTextRange(DbIssues.Locations mainLocation) {
+ int startLine = mainLocation.getTextRange().getStartLine();
+ int endLine = mainLocation.getTextRange().getEndLine();
+ int startOffset = mainLocation.getTextRange().getStartOffset();
+ int endOffset = mainLocation.getTextRange().getEndOffset();
+
+ return Issues.TextRange.newBuilder()
+ .setHash(mainLocation.getChecksum())
+ .setStartLine(startLine)
+ .setEndLine(endLine)
+ .setStartLineOffset(startOffset)
+ .setEndLineOffset(endOffset).build();
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java
index d4f066c6249..0a0df8afa24 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java
@@ -23,22 +23,28 @@ import org.sonar.api.server.ServerSide;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.protobuf.DbIssues;
import org.sonarqube.ws.Common;
-import org.sonarqube.ws.Issues;
+
+import static org.sonarqube.ws.Issues.IssueLite;
+import static org.sonarqube.ws.Issues.IssuesPullQueryTimestamp;
+import static org.sonarqube.ws.Issues.Location;
+import static org.sonarqube.ws.Issues.TextRange;
@ServerSide
-public class PullActionProtobufObjectGenerator {
+public class PullActionProtobufObjectGenerator implements ProtobufObjectGenerator {
- Issues.IssuesPullQueryTimestamp generateTimestampMessage(long timestamp) {
- Issues.IssuesPullQueryTimestamp.Builder responseBuilder = Issues.IssuesPullQueryTimestamp.newBuilder();
+ @Override
+ public IssuesPullQueryTimestamp generateTimestampMessage(long timestamp) {
+ IssuesPullQueryTimestamp.Builder responseBuilder = IssuesPullQueryTimestamp.newBuilder();
responseBuilder.setQueryTimestamp(timestamp);
return responseBuilder.build();
}
- Issues.IssueLite generateIssueMessage(IssueDto issueDto) {
- Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder();
+ @Override
+ public IssueLite generateIssueMessage(IssueDto issueDto) {
+ IssueLite.Builder issueBuilder = IssueLite.newBuilder();
DbIssues.Locations mainLocation = issueDto.parseLocations();
- Issues.Location.Builder locationBuilder = Issues.Location.newBuilder();
+ Location.Builder locationBuilder = Location.newBuilder();
if (issueDto.getMessage() != null) {
locationBuilder.setMessage(issueDto.getMessage());
}
@@ -46,10 +52,10 @@ public class PullActionProtobufObjectGenerator {
locationBuilder.setFilePath(issueDto.getFilePath());
}
if (mainLocation != null) {
- Issues.TextRange textRange = buildTextRange(mainLocation);
+ TextRange textRange = buildTextRange(mainLocation);
locationBuilder.setTextRange(textRange);
}
- Issues.Location location = locationBuilder.build();
+ Location location = locationBuilder.build();
issueBuilder.setKey(issueDto.getKey());
issueBuilder.setCreationDate(issueDto.getCreatedAt());
@@ -65,24 +71,11 @@ public class PullActionProtobufObjectGenerator {
return issueBuilder.build();
}
- Issues.IssueLite generateClosedIssueMessage(String uuid) {
- Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder();
+ @Override
+ public IssueLite generateClosedIssueMessage(String uuid) {
+ IssueLite.Builder issueBuilder = IssueLite.newBuilder();
issueBuilder.setKey(uuid);
issueBuilder.setClosed(true);
return issueBuilder.build();
}
-
- private static Issues.TextRange buildTextRange(DbIssues.Locations mainLocation) {
- int startLine = mainLocation.getTextRange().getStartLine();
- int endLine = mainLocation.getTextRange().getEndLine();
- int startOffset = mainLocation.getTextRange().getStartOffset();
- int endOffset = mainLocation.getTextRange().getEndOffset();
-
- return Issues.TextRange.newBuilder()
- .setHash(mainLocation.getChecksum())
- .setStartLine(startLine)
- .setEndLine(endLine)
- .setStartLineOffset(startOffset)
- .setEndLineOffset(endOffset).build();
- }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java
index 764afc5ccc2..0df9627f5f4 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java
@@ -19,35 +19,35 @@
*/
package org.sonar.server.issue.ws.pull;
+import com.google.protobuf.AbstractMessageLite;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
import org.sonar.db.issue.IssueDto;
-import org.sonarqube.ws.Issues;
@ServerSide
public class PullActionResponseWriter {
private final System2 system2;
- private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator;
+ private final ProtobufObjectGenerator protobufObjectGenerator;
- public PullActionResponseWriter(System2 system2, PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator) {
+ public PullActionResponseWriter(System2 system2, ProtobufObjectGenerator protobufObjectGenerator) {
this.system2 = system2;
- this.pullActionProtobufObjectGenerator = pullActionProtobufObjectGenerator;
+ this.protobufObjectGenerator = protobufObjectGenerator;
}
public void appendTimestampToResponse(OutputStream outputStream) throws IOException {
- Issues.IssuesPullQueryTimestamp issuesPullQueryTimestamp = pullActionProtobufObjectGenerator.generateTimestampMessage(system2.now());
- issuesPullQueryTimestamp.writeDelimitedTo(outputStream);
+ AbstractMessageLite messageLite = protobufObjectGenerator.generateTimestampMessage(system2.now());
+ messageLite.writeDelimitedTo(outputStream);
}
public void appendIssuesToResponse(List<IssueDto> issueDtos, OutputStream outputStream) {
try {
for (IssueDto issueDto : issueDtos) {
- Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateIssueMessage(issueDto);
- issueLite.writeDelimitedTo(outputStream);
+ AbstractMessageLite messageLite = protobufObjectGenerator.generateIssueMessage(issueDto);
+ messageLite.writeDelimitedTo(outputStream);
}
outputStream.flush();
} catch (IOException e) {
@@ -58,8 +58,8 @@ public class PullActionResponseWriter {
public void appendClosedIssuesUuidsToResponse(List<String> closedIssuesUuids,
OutputStream outputStream) throws IOException {
for (String uuid : closedIssuesUuids) {
- Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateClosedIssueMessage(uuid);
- issueLite.writeDelimitedTo(outputStream);
+ AbstractMessageLite messageLite = protobufObjectGenerator.generateClosedIssueMessage(uuid);
+ messageLite.writeDelimitedTo(outputStream);
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java
new file mode 100644
index 00000000000..bef5de03c82
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java
@@ -0,0 +1,175 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws.pull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.SeverityUtil;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Issues;
+
+import static org.sonar.db.protobuf.DbIssues.Locations;
+import static org.sonarqube.ws.Issues.TaintVulnerabilityLite;
+import static org.sonarqube.ws.Issues.TaintVulnerabilityPullQueryTimestamp;
+
+@ServerSide
+public class PullTaintActionProtobufObjectGenerator implements ProtobufObjectGenerator {
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private Map<String, ComponentDto> componentsMap;
+
+ public PullTaintActionProtobufObjectGenerator(DbClient dbClient, UserSession userSession) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public TaintVulnerabilityPullQueryTimestamp generateTimestampMessage(long timestamp) {
+ refreshComponents();
+ TaintVulnerabilityPullQueryTimestamp.Builder responseBuilder = TaintVulnerabilityPullQueryTimestamp.newBuilder();
+ responseBuilder.setQueryTimestamp(timestamp);
+ return responseBuilder.build();
+ }
+
+ @Override
+ public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto) {
+ TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder();
+ Locations locations = issueDto.parseLocations();
+
+ if (componentsMap == null) {
+ refreshComponents();
+ }
+
+ Issues.Location.Builder locationBuilder = Issues.Location.newBuilder();
+ if (issueDto.getMessage() != null) {
+ locationBuilder.setMessage(issueDto.getMessage());
+ }
+ if (issueDto.getFilePath() != null) {
+ locationBuilder.setFilePath(issueDto.getFilePath());
+ }
+ if (locations != null) {
+ Issues.TextRange textRange = buildTextRange(locations);
+ locationBuilder.setTextRange(textRange);
+ getFlows(taintBuilder, locations, issueDto);
+ }
+
+ taintBuilder.setAssignedToSubscribedUser(issueDto.getAssigneeUuid() != null &&
+ issueDto.getAssigneeUuid().equals(userSession.getUuid()));
+
+ taintBuilder.setKey(issueDto.getKey());
+ taintBuilder.setCreationDate(issueDto.getCreatedAt());
+ taintBuilder.setResolved(issueDto.getStatus().equals(org.sonar.api.issue.Issue.STATUS_RESOLVED));
+ taintBuilder.setRuleKey(issueDto.getRuleKey().toString());
+ if (issueDto.getPriority() != null) {
+ taintBuilder.setSeverity(SeverityUtil.getSeverityFromOrdinal(issueDto.getPriority()));
+ }
+ taintBuilder.setType(Common.RuleType.forNumber(issueDto.getType()).name());
+ taintBuilder.setClosed(false);
+ taintBuilder.setMainLocation(locationBuilder.build());
+
+ return taintBuilder.build();
+ }
+
+ @Override
+ public TaintVulnerabilityLite generateClosedIssueMessage(String uuid) {
+ TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder();
+ taintBuilder.setKey(uuid);
+ taintBuilder.setClosed(true);
+ return taintBuilder.build();
+ }
+
+ private void getFlows(TaintVulnerabilityLite.Builder taintBuilder, Locations locations, IssueDto issueDto) {
+ List<Issues.Flow> flows = new ArrayList<>();
+
+ for (DbIssues.Flow f : locations.getFlowList()) {
+ Set<String> componentUuids = new HashSet<>();
+
+ Issues.Flow.Builder builder = Issues.Flow.newBuilder();
+ List<Issues.Location> flowLocations = new ArrayList<>();
+ getComponentUuids(f, componentUuids);
+
+ for (DbIssues.Location l : f.getLocationList()) {
+ Issues.Location.Builder flowLocationBuilder = Issues.Location
+ .newBuilder()
+ .setMessage(l.getMsg())
+ .setTextRange(buildTextRange(l));
+ if (l.hasComponentId() && componentsMap.containsKey(l.getComponentId())) {
+ flowLocationBuilder.setFilePath(componentsMap.get(l.getComponentId()).path());
+ } else {
+ flowLocationBuilder.setFilePath(issueDto.getFilePath());
+ }
+ flowLocations.add(flowLocationBuilder.build());
+ }
+ builder.addAllLocations(flowLocations);
+ flows.add(builder.build());
+
+ taintBuilder.addAllFlows(flows);
+ }
+ }
+
+ private void getComponentUuids(DbIssues.Flow f, Set<String> componentUuids) {
+ for (DbIssues.Location l : f.getLocationList()) {
+ if (l.hasComponentId() && !componentsMap.containsKey(l.getComponentId())) {
+ componentUuids.add(l.getComponentId());
+ }
+ }
+
+ if (!componentUuids.isEmpty()) {
+ componentsMap.putAll(getLocationComponents(componentUuids));
+ }
+ }
+
+ private static Issues.TextRange buildTextRange(DbIssues.Location location) {
+ int startLine = location.getTextRange().getStartLine();
+ int endLine = location.getTextRange().getEndLine();
+ int startOffset = location.getTextRange().getStartOffset();
+ int endOffset = location.getTextRange().getEndOffset();
+
+ return Issues.TextRange.newBuilder()
+ .setHash(location.getChecksum())
+ .setStartLine(startLine)
+ .setEndLine(endLine)
+ .setStartLineOffset(startOffset)
+ .setEndLineOffset(endOffset).build();
+ }
+
+ private void refreshComponents() {
+ componentsMap = new HashMap<>();
+ }
+
+ private Map<String, ComponentDto> getLocationComponents(Set<String> components) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return dbClient.componentDao().selectByUuids(dbSession, components)
+ .stream().collect(Collectors.toMap(ComponentDto::uuid, c -> c));
+ }
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto
new file mode 100644
index 00000000000..fc9d872c01a
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto
@@ -0,0 +1,34 @@
+# The response contains a single protocol buffer message: TaintVulnerabilityPullQueryTimestamp followed by 0..n number of TaintLite protocol buffer messages.
+message TaintVulnerabilityPullQueryTimestamp {
+ required int64 queryTimestamp = 1;
+}
+
+message TaintLite {
+ required string key = 1;
+ optional int64 creationDate = 2;
+ optional bool resolved = 3;
+ optional string ruleKey = 4;
+ optional string severity = 5;
+ optional string type = 6;
+ optional Location mainLocation = 7;
+ optional bool closed = 8;
+ optional Flow flows = 9;
+}
+
+message Location {
+ optional string filePath = 1;
+ optional string message = 2;
+ optional TextRange textRange = 3;
+}
+
+message Flow {
+ repeated Location locations = 1;
+}
+
+message TextRange {
+ optional int32 startLine = 1;
+ optional int32 startLineOffset = 2;
+ optional int32 endLine = 3;
+ optional int32 endLineOffset = 4;
+ optional string hash = 5;
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java
index 0f12633ce0d..bb19b9ca648 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java
@@ -43,7 +43,6 @@ import org.sonar.server.component.ComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
-import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
@@ -78,14 +77,13 @@ public class PullActionTest {
private final System2 system2 = mock(System2.class);
private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator = new PullActionProtobufObjectGenerator();
- private final PullActionResponseWriter pullActionResponseWriter = new PullActionResponseWriter(system2, pullActionProtobufObjectGenerator);
private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes);
private final IssueDbTester issueDbTester = new IssueDbTester(db);
private final ComponentDbTester componentDbTester = new ComponentDbTester(db);
- private final PullAction underTest = new PullAction(db.getDbClient(), userSession, pullActionResponseWriter, componentFinder);
+ private final PullAction underTest = new PullAction(system2, componentFinder, db.getDbClient(), userSession, pullActionProtobufObjectGenerator);
private final WsActionTester tester = new WsActionTester(underTest);
private RuleDto correctRule, incorrectRule;
@@ -159,7 +157,7 @@ public class PullActionTest {
loginWithBrowsePermission(issueDto);
TestRequest request = tester.newRequest()
- .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("projectKey", issueDto.getProjectKey())
.setParam("branchName", "non-existent-branch");
assertThatThrownBy(request::execute)
@@ -168,6 +166,18 @@ public class PullActionTest {
}
@Test
+ public void givenTaintRuleRepository_throwException() {
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", "project-key")
+ .setParam("branchName", "branch-name")
+ .setParam("ruleRepositories", "javasecurity");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities");
+ }
+
+ @Test
public void givenValidProjectKeyAndOneIssueOnBranch_returnOneIssue() throws IOException {
DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
.setStartLine(1)
@@ -179,7 +189,6 @@ public class PullActionTest {
.setChecksum("hash")
.setTextRange(textRange);
-
RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000"));
IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR")
.setManualSeverity(true)
@@ -196,10 +205,10 @@ public class PullActionTest {
TestResponse response = request.execute();
List<Issues.IssueLite> issues = readAllIssues(response);
- Issues.IssueLite issueLite = issues.get(0);
assertThat(issues).hasSize(1);
+ Issues.IssueLite issueLite = issues.get(0);
assertThat(issueLite.getKey()).isEqualTo(issueDto.getKey());
assertThat(issueLite.getUserSeverity()).isEqualTo("MINOR");
assertThat(issueLite.getCreationDate()).isEqualTo(NOW);
@@ -219,6 +228,38 @@ public class PullActionTest {
}
@Test
+ public void givenValidProjectKeyAndOneTaintVulnerabilityOnBranch_returnNoIssues() throws IOException {
+ DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+ .setStartLine(1)
+ .setEndLine(2)
+ .setStartOffset(3)
+ .setEndOffset(4)
+ .build();
+ DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+ .setChecksum("hash")
+ .setTextRange(textRange);
+
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("javasecurity").setRuleKey("S1000"));
+ IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("message")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_RESOLVED)
+ .setLocations(mainLocation.build())
+ .setType(Common.RuleType.BUG.getNumber()));
+ loginWithBrowsePermission(issueDto);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).isEmpty();
+ }
+
+ @Test
public void givenIssueOnAnotherBranch_returnOneIssue() throws IOException {
ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop");
ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch));
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java
new file mode 100644
index 00000000000..647e7a41acd
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java
@@ -0,0 +1,491 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.issue.IssueDbTester;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleRepositoryDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Issues;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+public class PullTaintActionTest {
+ private static final long NOW = 10_000_000_000L;
+ private static final long PAST = 1_000_000_000L;
+
+ private static final String DEFAULT_BRANCH = "master";
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private final System2 system2 = mock(System2.class);
+ private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT);
+ private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes);
+ private final IssueDbTester issueDbTester = new IssueDbTester(db);
+ private final ComponentDbTester componentDbTester = new ComponentDbTester(db);
+
+ private PullTaintActionProtobufObjectGenerator objectGenerator = new PullTaintActionProtobufObjectGenerator(db.getDbClient(), userSession);
+ private PullTaintAction underTest = new PullTaintAction(system2, componentFinder, db.getDbClient(), userSession, objectGenerator);
+ private WsActionTester tester = new WsActionTester(underTest);
+
+ private RuleDto correctRule, incorrectRule;
+ private ComponentDto correctProject, incorrectProject;
+ private ComponentDto correctFile, incorrectFile;
+
+ @Before
+ public void setUp() {
+ when(system2.now()).thenReturn(NOW);
+ RuleRepositoryDto repository = new RuleRepositoryDto("javasecurity", "java", "Security SonarAnalyzer");
+ db.getDbClient().ruleRepositoryDao().insert(db.getSession(), List.of(repository));
+ correctRule = db.rules().insertIssueRule(r -> r.setRepositoryKey("javasecurity").setRuleKey("S1000").setSeverity(3));
+
+ correctProject = db.components().insertPrivateProject();
+ correctFile = db.components().insertComponent(newFileDto(correctProject));
+
+ incorrectRule = db.rules().insertIssueRule();
+ incorrectProject = db.components().insertPrivateProject();
+ incorrectFile = db.components().insertComponent(newFileDto(incorrectProject));
+
+
+ }
+
+ @Test
+ public void givenMissingParams_expectIllegalArgumentException() {
+ TestRequest request = tester.newRequest();
+
+ assertThatThrownBy(() -> request.executeProtobuf(Issues.IssuesPullQueryTimestamp.class))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void givenNotExistingProjectKey_throwException() {
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", "projectKey")
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("Project 'projectKey' not found");
+ }
+
+ @Test
+ public void givenValidProjectKeyWithoutPermissionsTo_throwException() {
+ userSession.logIn();
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(ForbiddenException.class)
+ .hasMessage("Insufficient privileges");
+ }
+
+ @Test
+ public void givenNotExistingBranchKey_throwException() {
+ DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+ .setStartLine(1)
+ .setEndLine(2)
+ .setStartOffset(3)
+ .setEndOffset(4)
+ .build();
+ DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+ .setChecksum("hash")
+ .setTextRange(textRange);
+
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000"));
+ IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("message")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_RESOLVED)
+ .setLocations(mainLocation.build())
+ .setType(Common.RuleType.BUG.getNumber()));
+ loginWithBrowsePermission(issueDto);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("branchName", "non-existent-branch");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage(format("Branch 'non-existent-branch' in project '%s' not found", issueDto.getProjectKey()));
+ }
+
+ @Test
+ public void givenValidProjectKeyAndOneNormalIssueOnBranch_returnNoTaintVulnerabilities() throws IOException {
+ DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+ .setStartLine(1)
+ .setEndLine(2)
+ .setStartOffset(3)
+ .setEndOffset(4)
+ .build();
+ DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+ .setChecksum("hash")
+ .setTextRange(textRange);
+
+ RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000"));
+ IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("message")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_RESOLVED)
+ .setLocations(mainLocation.build())
+ .setType(Common.RuleType.BUG.getNumber()));
+ loginWithBrowsePermission(issueDto);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).isEmpty();
+ }
+
+ @Test
+ public void givenValidProjectKeyAndOneTaintOnBranch_returnOneTaint_WithMetadataSeverity() throws IOException {
+ loginWithBrowsePermission(correctProject.projectUuid(), correctFile.uuid());
+ DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+ .setStartLine(1)
+ .setEndLine(2)
+ .setStartOffset(3)
+ .setEndOffset(4)
+ .build();
+ DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+ .setChecksum("hash")
+ .setTextRange(textRange);
+
+ IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setRule(correctRule)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setAssigneeUuid(userSession.getUuid())
+ .setManualSeverity(true)
+ .setMessage("message")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_OPEN)
+ .setLocations(mainLocation.build())
+ .setType(Common.RuleType.VULNERABILITY.getNumber()));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+ Issues.TaintVulnerabilityLite taintLite = taints.get(0);
+
+ assertThat(taints).hasSize(1);
+
+ assertThat(taintLite.getKey()).isEqualTo(issueDto.getKey());
+ assertThat(taintLite.getSeverity()).isEqualTo("CRITICAL");
+ assertThat(taintLite.getCreationDate()).isEqualTo(NOW);
+ assertThat(taintLite.getResolved()).isFalse();
+ assertThat(taintLite.getRuleKey()).isEqualTo("javasecurity:S1000");
+ assertThat(taintLite.getType()).isEqualTo(Common.RuleType.forNumber(issueDto.getType()).name());
+ assertThat(taintLite.getAssignedToSubscribedUser()).isTrue();
+
+ Issues.Location location = taintLite.getMainLocation();
+ assertThat(location.getMessage()).isEqualTo(issueDto.getMessage());
+
+ Issues.TextRange locationTextRange = location.getTextRange();
+ assertThat(locationTextRange.getStartLine()).isEqualTo(1);
+ assertThat(locationTextRange.getEndLine()).isEqualTo(2);
+ assertThat(locationTextRange.getStartLineOffset()).isEqualTo(3);
+ assertThat(locationTextRange.getEndLineOffset()).isEqualTo(4);
+ assertThat(locationTextRange.getHash()).isEqualTo("hash");
+ }
+
+ @Test
+ public void givenTaintOnAnotherBranch_returnOneTaint() throws IOException {
+ ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop");
+ ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch));
+ generateTaints(correctRule, developBranch, developFile, 1);
+ loginWithBrowsePermission(developBranch.uuid(), developFile.uuid());
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", developBranch.getKey())
+ .setParam("branchName", "develop");
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).hasSize(1);
+ }
+
+ @Test
+ public void given15TaintsInTheTable_returnOnly10ThatBelongToProject() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ generateTaints(correctRule, correctProject, correctFile, 10);
+ generateTaints(incorrectRule, incorrectProject, incorrectFile, 5);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).hasSize(10);
+ }
+
+ @Test
+ public void givenNoTaintsBelongToTheProject_return0Taints() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ generateTaints(incorrectRule, incorrectProject, incorrectFile, 5);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).isEmpty();
+ }
+
+ @Test
+ public void testLanguagesParam_return1Taint() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javaRule = db.rules().insert(r -> r.setLanguage("java").setRepositoryKey("javasecurity"));
+ RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("javascript").setRepositoryKey("javasecurity"));
+
+ IssueDto javaIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaRule)
+ .setRuleUuid(javaRule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setLanguage("java")
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(Common.RuleType.VULNERABILITY.getNumber()));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javascriptRule)
+ .setRuleUuid(javascriptRule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setLanguage("java")
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(Common.RuleType.VULNERABILITY.getNumber()));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("languages", "java");
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).hasSize(1);
+ assertThat(taints.get(0).getKey()).isEqualTo(javaIssue.getKey());
+ }
+
+ @Test
+ public void testLanguagesParam_givenWrongLanguage_return0Taints() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("jssecurity"));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javascriptRule)
+ .setRuleUuid(javascriptRule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(2));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("languages", "java");
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).isEmpty();
+ }
+
+ @Test
+ public void given1TaintAnd1NormalIssue_return1Taint() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javaRule = db.rules().insert(r -> r.setRepositoryKey("javasecurity"));
+ RuleDto javaScriptRule = db.rules().insert(r -> r.setRepositoryKey("javascript"));
+
+ IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaRule)
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(2));
+
+ //this one should not be returned - it is a normal issue, no taint
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaScriptRule)
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(Common.RuleType.VULNERABILITY.getNumber()));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).hasSize(1);
+ assertThat(taints.get(0).getKey()).isEqualTo(issueDto.getKey());
+ }
+
+ @Test
+ public void inIncrementalModeReturnClosedIssues() throws IOException {
+ IssueDto openIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setRule(correctRule)
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_OPEN)
+ .setType(Common.RuleType.BUG.getNumber()));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setRule(correctRule)
+ .setMessage("closedIssue")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_CLOSED)
+ .setType(Common.RuleType.BUG.getNumber())
+ .setComponentUuid(openIssue.getComponentUuid())
+ .setProjectUuid(openIssue.getProjectUuid())
+ .setIssueUpdateTime(PAST)
+ .setIssueCreationTime(PAST));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setRule(incorrectRule)
+ .setMessage("closedIssue")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_CLOSED)
+ .setType(Common.RuleType.BUG.getNumber())
+ .setComponentUuid(openIssue.getComponentUuid())
+ .setProjectUuid(openIssue.getProjectUuid())
+ .setIssueUpdateTime(PAST)
+ .setIssueCreationTime(PAST));
+
+ loginWithBrowsePermission(openIssue);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", openIssue.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("changedSince", PAST + "");
+
+ TestResponse response = request.execute();
+ List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response);
+
+ assertThat(taints).hasSize(2);
+ }
+
+ private void generateTaints(RuleDto rule, ComponentDto project, ComponentDto file, int numberOfIssues) {
+ for (int j = 0; j < numberOfIssues; j++) {
+ issueDbTester.insert(i -> i.setProject(project)
+ .setRule(rule)
+ .setComponent(file)
+ .setStatus(Issue.STATUS_OPEN)
+ .setType(3));
+ }
+ }
+
+ private List<Issues.TaintVulnerabilityLite> readAllTaint(TestResponse response) throws IOException {
+ List<Issues.TaintVulnerabilityLite> taints = new ArrayList<>();
+ InputStream inputStream = response.getInputStream();
+ Issues.TaintVulnerabilityPullQueryTimestamp.parseDelimitedFrom(inputStream);
+
+ while (inputStream.available() > 0) {
+ taints.add(Issues.TaintVulnerabilityLite.parseDelimitedFrom(inputStream));
+ }
+
+ return taints;
+ }
+
+ private void loginWithBrowsePermission(IssueDto issueDto) {
+ loginWithBrowsePermission(issueDto.getProjectUuid(), issueDto.getComponentUuid());
+ }
+
+ private void loginWithBrowsePermission(String projectUuid, String componentUuid) {
+ UserDto user = dbTester.users().insertUser("john");
+ userSession.logIn(user)
+ .addProjectPermission(USER,
+ db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), projectUuid).get(),
+ db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), componentUuid).get());
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java
index 30233c93ec6..76046a4c40b 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java
@@ -46,7 +46,7 @@ public class PullActionIssuesRetrieverTest {
private final List<String> ruleRepositories = List.of("js-security", "java");
private final Long defaultChangedSince = 1_000_000L;
- private final IssueQueryParams queryParams = new IssueQueryParams(branchUuid, languages, ruleRepositories, false, defaultChangedSince);
+ private final IssueQueryParams queryParams = new IssueQueryParams(branchUuid, languages, ruleRepositories, null, false, defaultChangedSince);
private final IssueDao issueDao = mock(IssueDao.class);
@Before
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java
new file mode 100644
index 00000000000..1d84e1267ef
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.ws.pull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.tester.UserSessionRule;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PullTaintActionResponseWriterTest {
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ private final System2 system2 = mock(System2.class);
+ private final PullTaintActionProtobufObjectGenerator protobufObjectGenerator = new PullTaintActionProtobufObjectGenerator(db.getDbClient(),
+ userSession);
+
+ private final PullActionResponseWriter underTest = new PullActionResponseWriter(system2, protobufObjectGenerator);
+
+ @Before
+ public void before() {
+ when(system2.now()).thenReturn(1_000_000L);
+ }
+
+ @Test
+ public void appendIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+ IssueDto issueDto = new IssueDto();
+ issueDto.setFilePath("filePath");
+ issueDto.setKee("key");
+ issueDto.setStatus("OPEN");
+ issueDto.setRuleKey("repo", "rule");
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .setTextRange(range(2, 3))
+ .addFlow(newFlow(newLocation(4, 5)))
+ .addFlow(newFlow(newLocation(6, 7, "another-component")))
+ .build();
+
+ issueDto.setLocations(locations);
+
+ underTest.appendIssuesToResponse(List.of(issueDto), outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+
+ @Test
+ public void appendClosedIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+
+ underTest.appendClosedIssuesUuidsToResponse(List.of("uuid", "uuid2"), outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+
+ @Test
+ public void appendTimestampToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+
+ underTest.appendTimestampToResponse(outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+
+ private static DbIssues.Location newLocation(int startLine, int endLine) {
+ return DbIssues.Location.newBuilder().setTextRange(range(startLine, endLine)).build();
+ }
+
+ private static DbIssues.Location newLocation(int startLine, int endLine, String componentUuid) {
+ return DbIssues.Location.newBuilder().setTextRange(range(startLine, endLine)).setComponentId(componentUuid).build();
+ }
+
+
+ private static org.sonar.db.protobuf.DbCommons.TextRange range(int startLine, int endLine) {
+ return DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build();
+ }
+
+ private static DbIssues.Flow newFlow(DbIssues.Location... locations) {
+ DbIssues.Flow.Builder builder = DbIssues.Flow.newBuilder();
+ Arrays.stream(locations).forEach(builder::addLocation);
+ return builder.build();
+ }
+}