You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RuleCreator.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.server.common.rule;
  21. import com.google.common.base.Splitter;
  22. import com.google.common.base.Strings;
  23. import java.util.ArrayList;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Optional;
  27. import java.util.Set;
  28. import java.util.function.Function;
  29. import java.util.stream.Collectors;
  30. import javax.annotation.Nullable;
  31. import org.apache.commons.lang3.StringUtils;
  32. import org.sonar.api.issue.impact.SoftwareQuality;
  33. import org.sonar.api.rule.RuleKey;
  34. import org.sonar.api.rule.RuleStatus;
  35. import org.sonar.api.rule.Severity;
  36. import org.sonar.api.rules.CleanCodeAttribute;
  37. import org.sonar.api.rules.RuleType;
  38. import org.sonar.api.server.ServerSide;
  39. import org.sonar.api.server.rule.RuleParamType;
  40. import org.sonar.api.server.rule.internal.ImpactMapper;
  41. import org.sonar.api.utils.System2;
  42. import org.sonar.core.util.UuidFactory;
  43. import org.sonar.db.DbClient;
  44. import org.sonar.db.DbSession;
  45. import org.sonar.db.issue.ImpactDto;
  46. import org.sonar.db.rule.RuleDescriptionSectionDto;
  47. import org.sonar.db.rule.RuleDto;
  48. import org.sonar.db.rule.RuleDto.Format;
  49. import org.sonar.db.rule.RuleParamDto;
  50. import org.sonar.server.common.rule.service.NewCustomRule;
  51. import org.sonar.server.exceptions.BadRequestException;
  52. import org.sonar.server.rule.index.RuleIndexer;
  53. import org.sonar.server.util.TypeValidations;
  54. import org.springframework.util.CollectionUtils;
  55. import static com.google.common.base.Preconditions.checkArgument;
  56. import static com.google.common.collect.Lists.newArrayList;
  57. import static java.lang.String.format;
  58. import static java.util.Objects.requireNonNull;
  59. import static java.util.Optional.ofNullable;
  60. import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
  61. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  62. @ServerSide
  63. public class RuleCreator {
  64. private static final String TEMPLATE_KEY_NOT_EXIST_FORMAT = "The template key doesn't exist: %s";
  65. private final System2 system2;
  66. private final RuleIndexer ruleIndexer;
  67. private final DbClient dbClient;
  68. private final TypeValidations typeValidations;
  69. private final UuidFactory uuidFactory;
  70. public RuleCreator(System2 system2, RuleIndexer ruleIndexer, DbClient dbClient, TypeValidations typeValidations, UuidFactory uuidFactory) {
  71. this.system2 = system2;
  72. this.ruleIndexer = ruleIndexer;
  73. this.dbClient = dbClient;
  74. this.typeValidations = typeValidations;
  75. this.uuidFactory = uuidFactory;
  76. }
  77. public RuleDto create(DbSession dbSession, NewCustomRule newRule) {
  78. RuleKey templateKey = newRule.templateKey();
  79. RuleDto templateRule = dbClient.ruleDao().selectByKey(dbSession, templateKey)
  80. .orElseThrow(() -> new IllegalArgumentException(format(TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey)));
  81. checkArgument(templateRule.isTemplate(), "This rule is not a template rule: %s", templateKey.toString());
  82. checkArgument(templateRule.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, templateKey.toString());
  83. validateCustomRule(newRule, dbSession, templateKey);
  84. Optional<RuleDto> definition = loadRule(dbSession, newRule.ruleKey());
  85. RuleDto ruleDto = definition.map(dto -> updateExistingRule(dto, newRule, dbSession))
  86. .orElseGet(() -> createCustomRule(newRule, templateRule, dbSession));
  87. ruleIndexer.commitAndIndex(dbSession, ruleDto.getUuid());
  88. return ruleDto;
  89. }
  90. public List<RuleDto> create(DbSession dbSession, List<NewCustomRule> newRules) {
  91. Set<RuleKey> templateKeys = newRules.stream().map(NewCustomRule::templateKey).collect(Collectors.toSet());
  92. Map<RuleKey, RuleDto> templateRules = dbClient.ruleDao().selectByKeys(dbSession, templateKeys)
  93. .stream()
  94. .collect(Collectors.toMap(
  95. RuleDto::getKey,
  96. Function.identity()));
  97. if (!templateRules.isEmpty()) {
  98. final Set keys = new java.util.HashSet(templateKeys);
  99. keys.removeAll(templateRules.keySet());
  100. checkArgument(keys.isEmpty(), "Rule template keys(" + keys + ") should exists for each custom rule!");
  101. }
  102. templateRules.values().forEach(ruleDto -> {
  103. checkArgument(ruleDto.isTemplate(), "This rule is not a template rule: %s", ruleDto.getKey().toString());
  104. checkArgument(ruleDto.getStatus() != RuleStatus.REMOVED, TEMPLATE_KEY_NOT_EXIST_FORMAT, ruleDto.getKey().toString());
  105. });
  106. List<RuleDto> customRules = newRules.stream()
  107. .map(newCustomRule -> {
  108. RuleDto templateRule = templateRules.get(newCustomRule.templateKey());
  109. validateCustomRule(newCustomRule, dbSession, templateRule.getKey());
  110. return createCustomRule(newCustomRule, templateRule, dbSession);
  111. })
  112. .toList();
  113. ruleIndexer.commitAndIndex(dbSession, customRules.stream().map(RuleDto::getUuid).toList());
  114. return customRules;
  115. }
  116. private void validateCustomRule(NewCustomRule newRule, DbSession dbSession, RuleKey templateKey) {
  117. List<String> errors = new ArrayList<>();
  118. validateRuleKey(errors, newRule.ruleKey(), templateKey);
  119. validateName(errors, newRule);
  120. validateDescription(errors, newRule);
  121. if (newRule.status() == RuleStatus.REMOVED) {
  122. errors.add(format("Rule status '%s' is not allowed", RuleStatus.REMOVED));
  123. }
  124. String severity = newRule.severity();
  125. if (severity != null && !Severity.ALL.contains(severity)) {
  126. errors.add(format("Severity \"%s\" is invalid", severity));
  127. }
  128. if (!CollectionUtils.isEmpty(newRule.getImpacts()) && (StringUtils.isNotBlank(newRule.severity()) || newRule.type() != null)) {
  129. errors.add("The rule cannot have both impacts and type/severity specified");
  130. }
  131. for (RuleParamDto ruleParam : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateKey)) {
  132. try {
  133. validateParam(ruleParam, newRule.parameter(ruleParam.getName()));
  134. } catch (BadRequestException validationError) {
  135. errors.addAll(validationError.errors());
  136. }
  137. }
  138. checkRequest(errors.isEmpty(), errors);
  139. }
  140. private void validateParam(RuleParamDto ruleParam, @Nullable String value) {
  141. if (value != null) {
  142. RuleParamType ruleParamType = RuleParamType.parse(ruleParam.getType());
  143. if (ruleParamType.multiple()) {
  144. List<String> values = newArrayList(Splitter.on(",").split(value));
  145. typeValidations.validate(values, ruleParamType.type(), ruleParamType.values());
  146. } else {
  147. typeValidations.validate(value, ruleParamType.type(), ruleParamType.values());
  148. }
  149. }
  150. }
  151. private static void validateName(List<String> errors, NewCustomRule newRule) {
  152. if (Strings.isNullOrEmpty(newRule.name())) {
  153. errors.add("The name is missing");
  154. }
  155. }
  156. private static void validateDescription(List<String> errors, NewCustomRule newRule) {
  157. if (Strings.isNullOrEmpty(newRule.markdownDescription())) {
  158. errors.add("The description is missing");
  159. }
  160. }
  161. private static void validateRuleKey(List<String> errors, RuleKey ruleKey, RuleKey templateKey) {
  162. if (!ruleKey.repository().equals(templateKey.repository())) {
  163. errors.add("Custom and template keys must be in the same repository");
  164. }
  165. }
  166. private Optional<RuleDto> loadRule(DbSession dbSession, RuleKey ruleKey) {
  167. return dbClient.ruleDao().selectByKey(dbSession, ruleKey);
  168. }
  169. private RuleDto createCustomRule(NewCustomRule newRule, RuleDto templateRuleDto, DbSession dbSession) {
  170. RuleDescriptionSectionDto ruleDescriptionSectionDto = createDefaultRuleDescriptionSection(uuidFactory.create(), requireNonNull(newRule.markdownDescription()));
  171. RuleDto ruleDto = new RuleDto()
  172. .setUuid(uuidFactory.create())
  173. .setRuleKey(newRule.ruleKey())
  174. .setPluginKey(templateRuleDto.getPluginKey())
  175. .setTemplateUuid(templateRuleDto.getUuid())
  176. .setConfigKey(templateRuleDto.getConfigKey())
  177. .setName(newRule.name())
  178. .setStatus(ofNullable(newRule.status()).orElse(RuleStatus.READY))
  179. .setLanguage(templateRuleDto.getLanguage())
  180. .setDefRemediationFunction(templateRuleDto.getDefRemediationFunction())
  181. .setDefRemediationGapMultiplier(templateRuleDto.getDefRemediationGapMultiplier())
  182. .setDefRemediationBaseEffort(templateRuleDto.getDefRemediationBaseEffort())
  183. .setGapDescription(templateRuleDto.getGapDescription())
  184. .setScope(templateRuleDto.getScope())
  185. .setSystemTags(templateRuleDto.getSystemTags())
  186. .setSecurityStandards(templateRuleDto.getSecurityStandards())
  187. .setIsExternal(false)
  188. .setIsAdHoc(false)
  189. .setCreatedAt(system2.now())
  190. .setUpdatedAt(system2.now())
  191. .setDescriptionFormat(Format.MARKDOWN)
  192. .addRuleDescriptionSectionDto(ruleDescriptionSectionDto);
  193. setCleanCodeAttributeAndImpacts(newRule, ruleDto, templateRuleDto);
  194. Set<String> tags = templateRuleDto.getTags();
  195. if (!tags.isEmpty()) {
  196. ruleDto.setTags(tags);
  197. }
  198. dbClient.ruleDao().insert(dbSession, ruleDto);
  199. for (RuleParamDto templateRuleParamDto : dbClient.ruleDao().selectRuleParamsByRuleKey(dbSession, templateRuleDto.getKey())) {
  200. String customRuleParamValue = Strings.emptyToNull(newRule.parameter(templateRuleParamDto.getName()));
  201. createCustomRuleParams(customRuleParamValue, ruleDto, templateRuleParamDto, dbSession);
  202. }
  203. return ruleDto;
  204. }
  205. private static void setCleanCodeAttributeAndImpacts(NewCustomRule newRule, RuleDto ruleDto, RuleDto templateRuleDto) {
  206. RuleType ruleType = newRule.type();
  207. int type = ruleType == null ? templateRuleDto.getType() : ruleType.getDbConstant();
  208. String severity = ofNullable(newRule.severity()).orElse(Severity.MAJOR);
  209. if (type == RuleType.SECURITY_HOTSPOT.getDbConstant()) {
  210. ruleDto.setType(type).setSeverity(severity);
  211. } else {
  212. ruleDto.setCleanCodeAttribute(ofNullable(newRule.getCleanCodeAttribute()).orElse(CleanCodeAttribute.CONVENTIONAL));
  213. if (!CollectionUtils.isEmpty(newRule.getImpacts())) {
  214. newRule.getImpacts().stream()
  215. .map(impact -> new ImpactDto(impact.softwareQuality(), impact.severity()))
  216. .forEach(ruleDto::addDefaultImpact);
  217. // Back-map old type and severity from the impact
  218. Map.Entry<SoftwareQuality, org.sonar.api.issue.impact.Severity> impact = ImpactMapper.getBestImpactForBackmapping(
  219. newRule.getImpacts().stream().collect(Collectors.toMap(NewCustomRule.Impact::softwareQuality, NewCustomRule.Impact::severity)));
  220. ruleDto.setType(ImpactMapper.convertToRuleType(impact.getKey()).getDbConstant());
  221. ruleDto.setSeverity(ImpactMapper.convertToDeprecatedSeverity(impact.getValue()));
  222. } else {
  223. // Map old type and severity to impact
  224. SoftwareQuality softwareQuality = ImpactMapper.convertToSoftwareQuality(RuleType.valueOf(type));
  225. org.sonar.api.issue.impact.Severity impactSeverity = ImpactMapper.convertToImpactSeverity(severity);
  226. ruleDto.addDefaultImpact(new ImpactDto()
  227. .setSoftwareQuality(softwareQuality)
  228. .setSeverity(impactSeverity))
  229. .setType(type)
  230. .setSeverity(severity);
  231. }
  232. }
  233. }
  234. private void createCustomRuleParams(@Nullable String paramValue, RuleDto ruleDto, RuleParamDto templateRuleParam, DbSession dbSession) {
  235. RuleParamDto ruleParamDto = RuleParamDto.createFor(ruleDto)
  236. .setName(templateRuleParam.getName())
  237. .setType(templateRuleParam.getType())
  238. .setDescription(templateRuleParam.getDescription())
  239. .setDefaultValue(paramValue);
  240. dbClient.ruleDao().insertRuleParam(dbSession, ruleDto, ruleParamDto);
  241. }
  242. private RuleDto updateExistingRule(RuleDto ruleDto, NewCustomRule newRule, DbSession dbSession) {
  243. if (ruleDto.getStatus().equals(RuleStatus.REMOVED)) {
  244. if (newRule.isPreventReactivation()) {
  245. throw new ReactivationException(format("A removed rule with the key '%s' already exists", ruleDto.getKey().rule()), ruleDto.getKey());
  246. } else {
  247. ruleDto.setStatus(RuleStatus.READY)
  248. .setUpdatedAt(system2.now());
  249. dbClient.ruleDao().update(dbSession, ruleDto);
  250. }
  251. } else {
  252. throw new IllegalArgumentException(format("A rule with the key '%s' already exists", ruleDto.getKey().rule()));
  253. }
  254. return ruleDto;
  255. }
  256. }