3 * Copyright (C) 2009-2023 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 com.google.gson.GsonBuilder;
24 import java.util.EnumMap;
25 import java.util.List;
27 import java.util.Optional;
29 import org.jetbrains.annotations.NotNull;
30 import org.sonar.api.ce.ComputeEngineSide;
31 import org.sonar.api.issue.impact.Severity;
32 import org.sonar.api.issue.impact.SoftwareQuality;
33 import org.sonar.api.rules.RuleType;
34 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
35 import org.sonar.ce.task.projectanalysis.component.Component;
36 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
37 import org.sonar.ce.task.projectanalysis.issue.Rule;
38 import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
39 import org.sonar.ce.task.projectanalysis.locations.flow.FlowGenerator;
40 import org.sonar.ce.task.projectanalysis.locations.flow.Location;
41 import org.sonar.ce.task.projectanalysis.locations.flow.TextRange;
42 import org.sonar.core.issue.DefaultIssue;
43 import org.sonar.db.protobuf.DbCommons;
44 import org.sonar.db.protobuf.DbIssues;
45 import org.sonar.db.pushevent.PushEventDto;
46 import org.sonar.server.issue.TaintChecker;
47 import org.sonar.server.security.SecurityStandards;
49 import static java.nio.charset.StandardCharsets.UTF_8;
50 import static java.util.Objects.requireNonNull;
51 import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
54 public class PushEventFactory {
55 private static final Gson GSON = new GsonBuilder().create();
57 private final TreeRootHolder treeRootHolder;
58 private final AnalysisMetadataHolder analysisMetadataHolder;
59 private final TaintChecker taintChecker;
60 private final FlowGenerator flowGenerator;
61 private final RuleRepository ruleRepository;
63 public PushEventFactory(TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder, TaintChecker taintChecker,
64 FlowGenerator flowGenerator, RuleRepository ruleRepository) {
65 this.treeRootHolder = treeRootHolder;
66 this.analysisMetadataHolder = analysisMetadataHolder;
67 this.taintChecker = taintChecker;
68 this.flowGenerator = flowGenerator;
69 this.ruleRepository = ruleRepository;
72 public Optional<PushEventDto> raiseEventOnIssue(String projectUuid, DefaultIssue currentIssue) {
73 var currentIssueComponentUuid = currentIssue.componentUuid();
74 if (currentIssueComponentUuid == null) {
75 return Optional.empty();
78 var component = treeRootHolder.getComponentByUuid(currentIssueComponentUuid);
80 if (isTaintVulnerability(currentIssue)) {
81 return raiseTaintVulnerabilityEvent(projectUuid, component, currentIssue);
83 if (isSecurityHotspot(currentIssue)) {
84 return raiseSecurityHotspotEvent(projectUuid, component, currentIssue);
86 return Optional.empty();
89 private boolean isTaintVulnerability(DefaultIssue issue) {
90 return taintChecker.isTaintVulnerability(issue);
93 private static boolean isSecurityHotspot(DefaultIssue issue) {
94 return RuleType.SECURITY_HOTSPOT.equals(issue.type());
97 private Optional<PushEventDto> raiseTaintVulnerabilityEvent(String projectUuid, Component component, DefaultIssue issue) {
98 if (shouldCreateRaisedEvent(issue)) {
99 return Optional.of(raiseTaintVulnerabilityRaisedEvent(projectUuid, component, issue));
101 if (issue.isBeingClosed()) {
102 return Optional.of(raiseTaintVulnerabilityClosedEvent(projectUuid, issue));
104 return Optional.empty();
107 private Optional<PushEventDto> raiseSecurityHotspotEvent(String projectUuid, Component component, DefaultIssue issue) {
108 if (shouldCreateRaisedEvent(issue)) {
109 return Optional.of(raiseSecurityHotspotRaisedEvent(projectUuid, component, issue));
111 if (issue.isBeingClosed()) {
112 return Optional.of(raiseSecurityHotspotClosedEvent(projectUuid, component, issue));
114 return Optional.empty();
117 private static boolean shouldCreateRaisedEvent(DefaultIssue issue) {
118 return issue.isNew() || issue.isCopied() || isReopened(issue);
121 private static boolean isReopened(DefaultIssue currentIssue) {
122 var currentChange = currentIssue.currentChange();
123 if (currentChange == null) {
126 var status = currentChange.get("status");
127 return status != null && Set.of("CLOSED|OPEN", "CLOSED|TO_REVIEW").contains(status.toString());
130 private PushEventDto raiseTaintVulnerabilityRaisedEvent(String projectUuid, Component component, DefaultIssue issue) {
131 TaintVulnerabilityRaised event = prepareEvent(component, issue);
132 return createPushEventDto(projectUuid, issue, event);
135 private TaintVulnerabilityRaised prepareEvent(Component component, DefaultIssue issue) {
136 TaintVulnerabilityRaised event = new TaintVulnerabilityRaised();
137 event.setProjectKey(issue.projectKey());
138 event.setCreationDate(issue.creationDate().getTime());
139 event.setKey(issue.key());
140 event.setSeverity(issue.severity());
141 event.setRuleKey(issue.getRuleKey().toString());
142 event.setType(issue.type().name());
143 event.setBranch(analysisMetadataHolder.getBranch().getName());
144 event.setMainLocation(prepareMainLocation(component, issue));
145 event.setFlows(flowGenerator.convertFlows(component.getName(), requireNonNull(issue.getLocations())));
146 issue.getRuleDescriptionContextKey().ifPresent(event::setRuleDescriptionContextKey);
148 Rule rule = ruleRepository.getByKey(issue.getRuleKey());
149 event.setCleanCodeAttribute(rule.cleanCodeAttribute().name());
150 event.setCleanCodeAttributeCategory(rule.cleanCodeAttribute().getAttributeCategory().name());
151 event.setImpacts(computeEffectiveImpacts(rule.getDefaultImpacts(), issue.impacts()));
155 private static List<TaintVulnerabilityRaised.Impact> computeEffectiveImpacts(Map<SoftwareQuality, Severity> defaultImpacts, Map<SoftwareQuality, Severity> impacts) {
156 Map<SoftwareQuality, Severity> impactMap = new EnumMap<>(defaultImpacts);
157 impacts.forEach((softwareQuality, severity) -> impactMap.computeIfPresent(softwareQuality, (existingSoftwareQuality, existingSeverity) -> severity));
158 return impactMap.entrySet().stream()
160 TaintVulnerabilityRaised.Impact impact = new TaintVulnerabilityRaised.Impact();
161 impact.setSoftwareQuality(e.getKey().name());
162 impact.setSeverity(e.getValue().name());
167 private static Location prepareMainLocation(Component component, DefaultIssue issue) {
168 DbIssues.Locations issueLocations = requireNonNull(issue.getLocations());
169 TextRange mainLocationTextRange = getTextRange(issueLocations.getTextRange(), issueLocations.getChecksum());
171 Location mainLocation = new Location();
172 Optional.ofNullable(issue.getMessage()).ifPresent(mainLocation::setMessage);
173 mainLocation.setFilePath(component.getName());
174 mainLocation.setTextRange(mainLocationTextRange);
178 private static PushEventDto createPushEventDto(String projectUuid, DefaultIssue issue, IssueEvent event) {
179 return new PushEventDto()
180 .setName(event.getEventName())
181 .setProjectUuid(projectUuid)
182 .setLanguage(issue.language())
183 .setPayload(serializeEvent(event));
186 private static PushEventDto raiseTaintVulnerabilityClosedEvent(String projectUuid, DefaultIssue issue) {
187 TaintVulnerabilityClosed event = new TaintVulnerabilityClosed(issue.key(), issue.projectKey());
188 return createPushEventDto(projectUuid, issue, event);
191 private PushEventDto raiseSecurityHotspotRaisedEvent(String projectUuid, Component component, DefaultIssue issue) {
192 SecurityHotspotRaised event = new SecurityHotspotRaised();
193 event.setKey(issue.key());
194 event.setProjectKey(issue.projectKey());
195 event.setStatus(issue.getStatus());
196 event.setCreationDate(issue.creationDate().getTime());
197 event.setMainLocation(prepareMainLocation(component, issue));
198 event.setRuleKey(issue.getRuleKey().toString());
199 event.setVulnerabilityProbability(getVulnerabilityProbability(issue));
200 event.setBranch(analysisMetadataHolder.getBranch().getName());
201 event.setAssignee(issue.assigneeLogin());
203 return createPushEventDto(projectUuid, issue, event);
206 private String getVulnerabilityProbability(DefaultIssue issue) {
207 Rule rule = ruleRepository.getByKey(issue.getRuleKey());
208 SecurityStandards.SQCategory sqCategory = fromSecurityStandards(rule.getSecurityStandards()).getSqCategory();
209 return sqCategory.getVulnerability().name();
212 private static PushEventDto raiseSecurityHotspotClosedEvent(String projectUuid, Component component, DefaultIssue issue) {
213 SecurityHotspotClosed event = new SecurityHotspotClosed();
214 event.setKey(issue.key());
215 event.setProjectKey(issue.projectKey());
216 event.setStatus(issue.getStatus());
217 event.setResolution(issue.resolution());
218 event.setFilePath(component.getName());
220 return createPushEventDto(projectUuid, issue, event);
223 private static byte[] serializeEvent(IssueEvent event) {
224 return GSON.toJson(event).getBytes(UTF_8);
228 private static TextRange getTextRange(DbCommons.TextRange source, String checksum) {
229 TextRange textRange = new TextRange();
230 textRange.setStartLine(source.getStartLine());
231 textRange.setStartLineOffset(source.getStartOffset());
232 textRange.setEndLine(source.getEndLine());
233 textRange.setEndLineOffset(source.getEndOffset());
234 textRange.setHash(checksum);