3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.pushevent;
22 import com.google.gson.Gson;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Date;
26 import org.assertj.core.groups.Tuple;
27 import org.junit.Before;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.sonar.api.issue.Issue;
31 import org.sonar.api.issue.impact.Severity;
32 import org.sonar.api.issue.impact.SoftwareQuality;
33 import org.sonar.api.rule.RuleKey;
34 import org.sonar.api.rules.CleanCodeAttribute;
35 import org.sonar.api.rules.RuleType;
36 import org.sonar.api.utils.DateUtils;
37 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
38 import org.sonar.ce.task.projectanalysis.analysis.TestBranch;
39 import org.sonar.ce.task.projectanalysis.component.Component.Type;
40 import org.sonar.ce.task.projectanalysis.component.MutableTreeRootHolderRule;
41 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
42 import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
43 import org.sonar.ce.task.projectanalysis.locations.flow.FlowGenerator;
44 import org.sonar.core.issue.DefaultIssue;
45 import org.sonar.core.issue.FieldDiffs;
46 import org.sonar.db.issue.ImpactDto;
47 import org.sonar.db.protobuf.DbCommons;
48 import org.sonar.db.protobuf.DbIssues;
49 import org.sonar.db.rule.RuleDto;
50 import org.sonar.server.issue.TaintChecker;
52 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
53 import static org.assertj.core.api.Assertions.assertThat;
54 import static org.assertj.core.api.Assertions.fail;
55 import static org.assertj.core.api.Assertions.tuple;
56 import static org.mockito.ArgumentMatchers.any;
57 import static org.mockito.Mockito.mock;
58 import static org.mockito.Mockito.when;
60 public class PushEventFactoryTest {
62 private static final Gson gson = new Gson();
63 private static final String BRANCH_NAME = "develop";
65 private final TaintChecker taintChecker = mock(TaintChecker.class);
67 public MutableTreeRootHolderRule treeRootHolder = new MutableTreeRootHolderRule();
69 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
70 .setBranch(new TestBranch(BRANCH_NAME));
71 private final FlowGenerator flowGenerator = new FlowGenerator(treeRootHolder);
72 private final RuleRepository ruleRepository = mock(RuleRepository.class);
73 private final PushEventFactory underTest = new PushEventFactory(treeRootHolder, analysisMetadataHolder, taintChecker, flowGenerator,
78 when(ruleRepository.getByKey(RuleKey.of("javasecurity", "S123"))).thenReturn(buildRule());
83 public void raiseEventOnIssue_whenNewTaintVulnerability_shouldCreateRaisedEvent() {
84 DefaultIssue defaultIssue = createDefaultIssue()
86 .setRuleDescriptionContextKey(randomAlphabetic(6));
88 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
90 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
92 .hasValueSatisfying(pushEventDto -> {
93 assertThat(pushEventDto.getName()).isEqualTo("TaintVulnerabilityRaised");
94 verifyPayload(pushEventDto.getPayload(), defaultIssue);
95 assertThat(pushEventDto.getLanguage()).isEqualTo("java");
96 assertThat(pushEventDto.getProjectUuid()).isEqualTo("some-project-uuid");
101 private static void verifyPayload(byte[] payload, DefaultIssue defaultIssue) {
102 assertThat(payload).isNotNull();
104 TaintVulnerabilityRaised taintVulnerabilityRaised = gson.fromJson(new String(payload, StandardCharsets.UTF_8),
105 TaintVulnerabilityRaised.class);
106 assertThat(taintVulnerabilityRaised.getProjectKey()).isEqualTo(defaultIssue.projectKey());
107 assertThat(taintVulnerabilityRaised.getCreationDate()).isEqualTo(defaultIssue.creationDate().getTime());
108 assertThat(taintVulnerabilityRaised.getKey()).isEqualTo(defaultIssue.key());
109 assertThat(taintVulnerabilityRaised.getSeverity()).isEqualTo(defaultIssue.severity());
110 assertThat(taintVulnerabilityRaised.getRuleKey()).isEqualTo(defaultIssue.ruleKey().toString());
111 assertThat(taintVulnerabilityRaised.getType()).isEqualTo(defaultIssue.type().name());
112 assertThat(taintVulnerabilityRaised.getBranch()).isEqualTo(BRANCH_NAME);
113 assertThat(taintVulnerabilityRaised.getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.CONVENTIONAL.name());
114 assertThat(taintVulnerabilityRaised.getCleanCodeAttributeCategory()).isEqualTo(CleanCodeAttribute.CONVENTIONAL.getAttributeCategory().name());
115 assertThat(taintVulnerabilityRaised.getImpacts()).extracting(TaintVulnerabilityRaised.Impact::getSoftwareQuality, TaintVulnerabilityRaised.Impact::getSeverity)
116 .containsExactlyInAnyOrder(Tuple.tuple(SoftwareQuality.MAINTAINABILITY.name(), Severity.MEDIUM.name()),
117 Tuple.tuple(SoftwareQuality.RELIABILITY.name(), Severity.HIGH.name()));
119 String ruleDescriptionContextKey = taintVulnerabilityRaised.getRuleDescriptionContextKey().orElseGet(() -> fail("No rule description " +
121 assertThat(ruleDescriptionContextKey).isEqualTo(defaultIssue.getRuleDescriptionContextKey().orElse(null));
125 public void raiseEventOnIssue_whenNewTaintVulnerabilityWithImpactAtRuleAndIssueLevel_shouldMergeImpacts() {
126 DefaultIssue defaultIssue = createDefaultIssue()
128 .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)
129 .setRuleDescriptionContextKey(randomAlphabetic(6));
131 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
133 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
135 .hasValueSatisfying(pushEventDto -> {
136 TaintVulnerabilityRaised taintVulnerabilityRaised = gson.fromJson(new String(pushEventDto.getPayload(), StandardCharsets.UTF_8),
137 TaintVulnerabilityRaised.class);
138 assertThat(taintVulnerabilityRaised.getImpacts()).extracting(TaintVulnerabilityRaised.Impact::getSoftwareQuality, TaintVulnerabilityRaised.Impact::getSeverity)
139 .containsExactlyInAnyOrder(tuple(SoftwareQuality.MAINTAINABILITY.name(), Severity.HIGH.name()), tuple(SoftwareQuality.RELIABILITY.name(), Severity.HIGH.name()));
144 public void raiseEventOnIssue_whenReopenedTaintVulnerability_shouldCreateRaisedEvent() {
145 DefaultIssue defaultIssue = createDefaultIssue()
149 .setCurrentChange(new FieldDiffs().setDiff("status", "CLOSED", "OPEN"));
151 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
153 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
155 .hasValueSatisfying(pushEventDto -> {
156 assertThat(pushEventDto.getName()).isEqualTo("TaintVulnerabilityRaised");
157 assertThat(pushEventDto.getPayload()).isNotNull();
162 public void raiseEventOnIssue_whenTaintVulnerabilityStatusChange_shouldSkipEvent() {
163 DefaultIssue defaultIssue = createDefaultIssue()
167 .setCurrentChange(new FieldDiffs().setDiff("status", "OPEN", "FIXED"));
169 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
171 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
175 public void raiseEventOnIssue_whenCopiedTaintVulnerability_shouldCreateRaisedEvent() {
176 DefaultIssue defaultIssue = createDefaultIssue()
179 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
181 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
183 .hasValueSatisfying(pushEventDto -> {
184 assertThat(pushEventDto.getName()).isEqualTo("TaintVulnerabilityRaised");
185 assertThat(pushEventDto.getPayload()).isNotNull();
190 public void raiseEventOnIssue_whenClosedTaintVulnerability_shouldCreateClosedEvent() {
191 DefaultIssue defaultIssue = createDefaultIssue()
194 .setBeingClosed(true);
196 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
198 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
200 .hasValueSatisfying(pushEventDto -> {
201 assertThat(pushEventDto.getName()).isEqualTo("TaintVulnerabilityClosed");
202 assertThat(pushEventDto.getPayload()).isNotNull();
207 public void raiseEventOnIssue_whenChangedTaintVulnerability_shouldSkipEvent() {
208 DefaultIssue defaultIssue = new DefaultIssue()
209 .setComponentUuid("issue-component-uuid")
213 .setType(RuleType.VULNERABILITY)
214 .setCreationDate(DateUtils.parseDate("2022-01-01"))
215 .setRuleKey(RuleKey.of("javasecurity", "S123"));
217 when(taintChecker.isTaintVulnerability(any())).thenReturn(true);
219 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
223 public void raiseEventOnIssue_whenIssueNotFromTaintVulnerabilityRepository_shouldSkipEvent() {
224 DefaultIssue defaultIssue = new DefaultIssue()
225 .setComponentUuid("issue-component-uuid")
227 .setType(RuleType.VULNERABILITY)
228 .setRuleKey(RuleKey.of("weirdrepo", "S123"));
230 when(taintChecker.isTaintVulnerability(any())).thenReturn(false);
232 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
234 defaultIssue = new DefaultIssue()
235 .setComponentUuid("issue-component-uuid")
238 .setBeingClosed(true)
239 .setType(RuleType.VULNERABILITY)
240 .setRuleKey(RuleKey.of("weirdrepo", "S123"));
242 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
246 public void raiseEventOnIssue_whenIssueDoesNotHaveLocations_shouldSkipEvent() {
247 DefaultIssue defaultIssue = new DefaultIssue()
248 .setComponentUuid("issue-component-uuid")
250 .setType(RuleType.VULNERABILITY)
251 .setRuleKey(RuleKey.of("javasecurity", "S123"));
253 when(taintChecker.isTaintVulnerability(any())).thenReturn(false);
255 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
259 public void raiseEventOnIssue_whenNewHotspot_shouldCreateRaisedEvent() {
260 DefaultIssue defaultIssue = createDefaultIssue()
261 .setType(RuleType.SECURITY_HOTSPOT)
262 .setStatus(Issue.STATUS_TO_REVIEW)
264 .setRuleDescriptionContextKey(randomAlphabetic(6));
266 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
268 .hasValueSatisfying(pushEventDto -> {
269 assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
270 verifyHotspotRaisedEventPayload(pushEventDto.getPayload(), defaultIssue);
271 assertThat(pushEventDto.getLanguage()).isEqualTo("java");
272 assertThat(pushEventDto.getProjectUuid()).isEqualTo("some-project-uuid");
276 private static void verifyHotspotRaisedEventPayload(byte[] payload, DefaultIssue defaultIssue) {
277 assertThat(payload).isNotNull();
279 SecurityHotspotRaised event = gson.fromJson(new String(payload, StandardCharsets.UTF_8), SecurityHotspotRaised.class);
280 assertThat(event.getProjectKey()).isEqualTo(defaultIssue.projectKey());
281 assertThat(event.getCreationDate()).isEqualTo(defaultIssue.creationDate().getTime());
282 assertThat(event.getKey()).isEqualTo(defaultIssue.key());
283 assertThat(event.getRuleKey()).isEqualTo(defaultIssue.ruleKey().toString());
284 assertThat(event.getStatus()).isEqualTo(Issue.STATUS_TO_REVIEW);
285 assertThat(event.getVulnerabilityProbability()).isEqualTo("LOW");
286 assertThat(event.getMainLocation()).isNotNull();
287 assertThat(event.getBranch()).isEqualTo(BRANCH_NAME);
288 assertThat(event.getAssignee()).isEqualTo("some-user-login");
292 public void raiseEventOnIssue_whenReopenedHotspot_shouldCreateRaisedEvent() {
293 DefaultIssue defaultIssue = createDefaultIssue()
294 .setType(RuleType.SECURITY_HOTSPOT)
298 .setCurrentChange(new FieldDiffs().setDiff("status", "CLOSED", "TO_REVIEW"));
300 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
302 .hasValueSatisfying(pushEventDto -> {
303 assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
304 assertThat(pushEventDto.getPayload()).isNotNull();
309 public void raiseEventOnIssue_whenCopiedHotspot_shouldCreateRaisedEvent() {
310 DefaultIssue defaultIssue = createDefaultIssue()
311 .setType(RuleType.SECURITY_HOTSPOT)
314 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
316 .hasValueSatisfying(pushEventDto -> {
317 assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotRaised.EVENT_NAME);
318 assertThat(pushEventDto.getPayload()).isNotNull();
323 public void raiseEventOnIssue_whenClosedHotspot_shouldCreateClosedEvent() {
324 DefaultIssue defaultIssue = createDefaultIssue()
325 .setType(RuleType.SECURITY_HOTSPOT)
328 .setBeingClosed(true)
329 .setStatus(Issue.STATUS_CLOSED)
330 .setResolution(Issue.RESOLUTION_FIXED);
332 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue))
334 .hasValueSatisfying(pushEventDto -> {
335 assertThat(pushEventDto.getName()).isEqualTo(SecurityHotspotClosed.EVENT_NAME);
336 verifyHotspotClosedEventPayload(pushEventDto.getPayload(), defaultIssue);
337 assertThat(pushEventDto.getLanguage()).isEqualTo("java");
338 assertThat(pushEventDto.getProjectUuid()).isEqualTo("some-project-uuid");
342 private static void verifyHotspotClosedEventPayload(byte[] payload, DefaultIssue defaultIssue) {
343 assertThat(payload).isNotNull();
345 SecurityHotspotClosed event = gson.fromJson(new String(payload, StandardCharsets.UTF_8), SecurityHotspotClosed.class);
346 assertThat(event.getProjectKey()).isEqualTo(defaultIssue.projectKey());
347 assertThat(event.getKey()).isEqualTo(defaultIssue.key());
348 assertThat(event.getStatus()).isEqualTo(Issue.STATUS_CLOSED);
349 assertThat(event.getResolution()).isEqualTo(Issue.RESOLUTION_FIXED);
350 assertThat(event.getFilePath()).isEqualTo("component-name");
354 public void raiseEventOnIssue_whenChangedHotspot_shouldSkipEvent() {
355 DefaultIssue defaultIssue = createDefaultIssue()
356 .setType(RuleType.SECURITY_HOTSPOT)
361 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
365 public void raiseEventOnIssue_whenComponentUuidNull_shouldSkipEvent() {
366 DefaultIssue defaultIssue = createDefaultIssue()
367 .setComponentUuid(null);
369 assertThat(underTest.raiseEventOnIssue("some-project-uuid", defaultIssue)).isEmpty();
372 private void buildComponentTree() {
373 treeRootHolder.setRoot(ReportComponent.builder(Type.PROJECT, 1)
375 .addChildren(ReportComponent.builder(Type.FILE, 2)
376 .setName("component-name")
377 .setUuid("issue-component-uuid")
379 .addChildren(ReportComponent.builder(Type.FILE, 3)
380 .setUuid("location-component-uuid")
385 private DefaultIssue createDefaultIssue() {
386 return new DefaultIssue()
388 .setProjectKey("project-key")
389 .setComponentUuid("issue-component-uuid")
390 .setAssigneeUuid("some-user-uuid")
391 .setAssigneeLogin("some-user-login")
392 .setType(RuleType.VULNERABILITY)
394 .setCreationDate(new Date())
395 .setLocations(DbIssues.Locations.newBuilder()
396 .addFlow(DbIssues.Flow.newBuilder()
397 .addLocation(DbIssues.Location.newBuilder()
398 .setChecksum("checksum")
399 .setComponentId("location-component-uuid")
402 .setTextRange(DbCommons.TextRange.newBuilder()
406 .setRuleKey(RuleKey.of("javasecurity", "S123"));
409 private org.sonar.ce.task.projectanalysis.issue.Rule buildRule() {
410 RuleDto ruleDto = new RuleDto();
411 ruleDto.setRuleKey(RuleKey.of("javasecurity", "S123"));
412 ruleDto.setSecurityStandards(Set.of("owasp-a1"));
413 ruleDto.setCleanCodeAttribute(CleanCodeAttribute.CONVENTIONAL);
414 ruleDto.addDefaultImpact(new ImpactDto().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY).setSeverity(Severity.MEDIUM));
415 ruleDto.addDefaultImpact(new ImpactDto().setSoftwareQuality(SoftwareQuality.RELIABILITY).setSeverity(Severity.HIGH));
416 return new org.sonar.ce.task.projectanalysis.issue.RuleImpl(ruleDto);