} else if (value instanceof Integer) {
// To be consistent, all INTEGER types are mapped as Long
value = ((Integer) value).longValue();
+ } else if (value instanceof Byte) {
+ Byte byteValue = (Byte) value;
+ value = byteValue.intValue();
} else if (value instanceof Timestamp) {
value = new Date(((Timestamp) value).getTime());
}
.add(2701, "Add index to org_qprofile.parent_uuid", AddIndexToOrgQProfileParentUuid.class)
.add(2702, "Add column webhooks.secret", AddWebhooksSecret.class)
.add(2703, "Add security fields to Elasticsearch indices", AddSecurityFieldsToElasticsearchIndices.class)
- .add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class);
+ .add(2704, "Add InternalComponentProperties table", CreateInternalComponentPropertiesTable.class)
+ .add(2707, "Update statuses of Security Hotspots", UpdateSecurityHotspotsStatuses.class);
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.platform.db.migration.version.v78;
+
+import com.google.common.collect.Maps;
+import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.es.MigrationEsClient;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+@SupportsBlueGreen
+public class UpdateSecurityHotspotsStatuses extends DataChange {
+
+ private static final String RESOLUTION_FIXED = "FIXED";
+ private static final String RESOLUTION_WONT_FIX = "WONTFIX";
+
+ private static final String STATUS_OPEN = "OPEN";
+ private static final String STATUS_REOPENED = "REOPENED";
+ private static final String STATUS_RESOLVED = "RESOLVED";
+
+ private static final String STATUS_TO_REVIEW = "TOREVIEW";
+ private static final String STATUS_IN_REVIEW = "INREVIEW";
+ private static final String STATUS_REVIEWED = "REVIEWED";
+
+ private static final int RULE_TYPE_SECURITY_HOTSPOT = 4;
+
+ private final Configuration configuration;
+ private final System2 system2;
+ private final MigrationEsClient esClient;
+ private final UuidFactory uuidFactory;
+
+ public UpdateSecurityHotspotsStatuses(Database db, Configuration configuration, System2 system2, MigrationEsClient esClient, UuidFactory uuidFactory) {
+ super(db);
+ this.configuration = configuration;
+ this.system2 = system2;
+ this.esClient = esClient;
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ if (configuration.getBoolean("sonar.sonarcloud.enabled").orElse(false)) {
+ return;
+ }
+ long now = system2.now();
+ MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("security hotspots");
+ massUpdate.select("select i.kee, i.status, i.resolution, i.issue_type from issues i " +
+ "inner join rules r on r.id = i.rule_id and r.rule_type = ? " +
+ "where (i.resolution is null or i.resolution in (?, ?)) and i.issue_type=? " +
+ // Add status check for the re-entrance, in order to not reload already migrated issues
+ "and i.status not in (?, ?, ?)")
+ .setInt(1, RULE_TYPE_SECURITY_HOTSPOT)
+ .setString(2, RESOLUTION_FIXED)
+ .setString(3, RESOLUTION_WONT_FIX)
+ .setInt(4, RULE_TYPE_SECURITY_HOTSPOT)
+ .setString(5, STATUS_TO_REVIEW)
+ .setString(6, STATUS_IN_REVIEW)
+ .setString(7, STATUS_REVIEWED);
+ massUpdate.update("update issues set status=?, resolution=?, updated_at=? where kee=? ");
+ massUpdate.update("insert into issue_changes (kee, issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date) values (?, ?, 'diff', ?, ?, ?, ?)");
+ massUpdate.execute((row, update, updateIndex) -> {
+ String issueKey = row.getString(1);
+ String status = row.getString(2);
+ String resolution = row.getNullableString(3);
+
+ IssueUpdate issueUpdate = new IssueUpdate(status, resolution);
+ FieldDiffs fieldDiffs = issueUpdate.process();
+ if (!issueUpdate.isUpdated()) {
+ return false;
+ }
+ if (updateIndex == 0) {
+ update.setString(1, issueUpdate.getNewStatus());
+ update.setString(2, issueUpdate.getNewResolution());
+ update.setLong(3, now);
+ update.setString(4, issueKey);
+ return true;
+ } else {
+ // No changelog on OPEN issue as there was no previous state
+ if (!status.equals(STATUS_OPEN)) {
+ update.setString(1, uuidFactory.create());
+ update.setString(2, issueKey);
+ update.setString(3, fieldDiffs.toEncodedString());
+ update.setLong(4, now);
+ update.setLong(5, now);
+ update.setLong(6, now);
+ return true;
+ }
+ return false;
+ }
+ });
+ esClient.deleteIndexes("issues");
+ }
+
+ private static class IssueUpdate {
+
+ private static final String RESOLUTION_FIELD = "resolution";
+ private static final String STATUS_FIELD = "status";
+
+ private final String status;
+ private final String resolution;
+
+ private String newStatus;
+ private String newResolution;
+ private boolean updated;
+
+ IssueUpdate(String status, @CheckForNull String resolution) {
+ this.status = status;
+ this.resolution = resolution;
+ }
+
+ FieldDiffs process() {
+ if ((status.equals(STATUS_OPEN) || (status.equals(STATUS_REOPENED))) && resolution == null) {
+ newStatus = STATUS_TO_REVIEW;
+ newResolution = null;
+ updated = true;
+ } else if (status.equals(STATUS_RESOLVED) && resolution != null) {
+ if (resolution.equals(RESOLUTION_FIXED)) {
+ newStatus = STATUS_IN_REVIEW;
+ newResolution = null;
+ updated = true;
+ } else if (resolution.equals(RESOLUTION_WONT_FIX)) {
+ newStatus = STATUS_REVIEWED;
+ newResolution = RESOLUTION_FIXED;
+ updated = true;
+ }
+ }
+ FieldDiffs fieldDiffs = new FieldDiffs();
+ fieldDiffs.setDiff(STATUS_FIELD, status, newStatus);
+ fieldDiffs.setDiff(RESOLUTION_FIELD, resolution, newResolution);
+ return fieldDiffs;
+ }
+
+ String getNewStatus() {
+ return newStatus;
+ }
+
+ String getNewResolution() {
+ return newResolution;
+ }
+
+ boolean isUpdated() {
+ return updated;
+ }
+ }
+
+ /**
+ * Inspired and simplified from {@link org.sonar.core.issue.FieldDiffs}
+ */
+ static class FieldDiffs implements Serializable {
+
+ private final Map<String, Diff> diffs = Maps.newLinkedHashMap();
+
+ void setDiff(String field, @Nullable String oldValue, @Nullable String newValue) {
+ diffs.put(field, new Diff(oldValue, newValue));
+ }
+
+ String toEncodedString() {
+ StringBuilder sb = new StringBuilder();
+ boolean notFirst = false;
+ for (Map.Entry<String, FieldDiffs.Diff> entry : diffs.entrySet()) {
+ if (notFirst) {
+ sb.append(',');
+ } else {
+ notFirst = true;
+ }
+ sb.append(entry.getKey());
+ sb.append('=');
+ sb.append(entry.getValue().toEncodedString());
+ }
+ return sb.toString();
+ }
+
+ static class Diff implements Serializable {
+ private String oldValue;
+ private String newValue;
+
+ Diff(@Nullable String oldValue, @Nullable String newValue) {
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ }
+
+ private String toEncodedString() {
+ StringBuilder sb = new StringBuilder();
+ if (oldValue != null) {
+ sb.append(oldValue);
+ sb.append('|');
+ }
+ if (newValue != null) {
+ sb.append(newValue);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FieldDiffs.Diff diff = (FieldDiffs.Diff) o;
+ return Objects.equals(oldValue, diff.oldValue) &&
+ Objects.equals(newValue, diff.newValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oldValue, newValue);
+ }
+ }
+
+ }
+
+}
@Test
public void verify_migration_count() {
- verifyMigrationCount(underTest, 5);
+ verifyMigrationCount(underTest, 6);
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.platform.db.migration.version.v78;
+
+import java.sql.SQLException;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.math.RandomUtils;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.es.MigrationEsClient;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class UpdateSecurityHotspotsStatusesTest {
+
+ private static final long PAST = 5_000_000_000L;
+ private static final long NOW = 10_000_000_000L;
+
+ @Rule
+ public CoreDbTester db = CoreDbTester.createForSchema(UpdateSecurityHotspotsStatusesTest.class, "schema.sql");
+
+ private MapSettings settings = new MapSettings();
+ private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+ private MigrationEsClient esClient = mock(MigrationEsClient.class);
+
+ private DataChange underTest = new UpdateSecurityHotspotsStatuses(db.database(), settings.asConfig(), system2, esClient, UuidFactoryFast.getInstance());
+
+ @Test
+ public void migrate_open_and_reopen_hotspots() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("OPEN", null, 4, rule);
+ String issue2 = insertIssue("REOPENED", null, 4, rule);
+ // Other type of issues should not be updated
+ String issue3 = insertIssue("OPEN", null, 1, rule);
+ String issue4 = insertIssue("REOPENED", null, 2, rule);
+ String issue5 = insertIssue("OPEN", null, 3, rule);
+
+ underTest.execute();
+
+ assertIssues(
+ tuple(issue1, "TOREVIEW", null, 4, NOW),
+ tuple(issue2, "TOREVIEW", null, 4, NOW),
+ // Not updated
+ tuple(issue3, "OPEN", null, 1, PAST),
+ tuple(issue4, "REOPENED", null, 2, PAST),
+ tuple(issue5, "OPEN", null, 3, PAST));
+ }
+
+ @Test
+ public void migrate_resolved_as_fixed_and_wont_fix_hotspots() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("RESOLVED", "FIXED", 4, rule);
+ String issue2 = insertIssue("RESOLVED", "WONTFIX", 4, rule);
+ // Other type of issues should not be updated
+ String issue3 = insertIssue("RESOLVED", "FIXED", 1, rule);
+ String issue4 = insertIssue("RESOLVED", "WONTFIX", 2, rule);
+ String issue5 = insertIssue("RESOLVED", "WONTFIX", 3, rule);
+
+ underTest.execute();
+
+ assertIssues(
+ tuple(issue1, "INREVIEW", null, 4, NOW),
+ tuple(issue2, "REVIEWED", "FIXED", 4, NOW),
+ // Not updated
+ tuple(issue3, "RESOLVED", "FIXED", 1, PAST),
+ tuple(issue4, "RESOLVED", "WONTFIX", 2, PAST),
+ tuple(issue5, "RESOLVED", "WONTFIX", 3, PAST));
+ }
+
+ @Test
+ public void insert_issue_changes() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("REOPENED", null, 4, rule);
+ // No changelog on OPEN issue as there was no previous state
+ String issue2 = insertIssue("OPEN", null, 4, rule);
+ String issue3 = insertIssue("RESOLVED", "FIXED", 4, rule);
+ String issue4 = insertIssue("RESOLVED", "WONTFIX", 4, rule);
+
+ underTest.execute();
+
+ assertIssueChanges(
+ tuple(issue1, "diff", "status=REOPENED|TOREVIEW,resolution=", NOW, NOW, NOW),
+ tuple(issue3, "diff", "status=RESOLVED|INREVIEW,resolution=FIXED|", NOW, NOW, NOW),
+ tuple(issue4, "diff", "status=RESOLVED|REVIEWED,resolution=WONTFIX|FIXED", NOW, NOW, NOW));
+ }
+
+ @Test
+ public void do_not_update_vulnerabilities_coming_from_hotspot() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("OPEN", null, 3, rule);
+
+ underTest.execute();
+
+ assertIssues(tuple(issue1, "OPEN", null, 3, PAST));
+ assertNoIssueChanges();
+ }
+
+ @Test
+ public void do_not_update_closed_hotspots() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("CLOSED", "FIXED", 4, rule);
+ String issue2 = insertIssue("CLOSED", "REMOVED", 4, rule);
+
+ underTest.execute();
+
+ assertIssues(
+ tuple(issue1, "CLOSED", "FIXED", 4, PAST),
+ tuple(issue2, "CLOSED", "REMOVED", 4, PAST));
+ assertNoIssueChanges();
+ }
+
+ @Test
+ public void do_nothing_on_sonarcloud() throws SQLException {
+ settings.setProperty("sonar.sonarcloud.enabled", "true");
+ int rule = insertRule(4);
+ String issue1 = insertIssue("OPEN", null, 4, rule);
+
+ underTest.execute();
+
+ assertIssues(tuple(issue1, "OPEN", null, 4, PAST));
+ assertNoIssueChanges();
+ }
+
+ @Test
+ public void migration_is_reentrant() throws SQLException {
+ int rule = insertRule(4);
+ String issue1 = insertIssue("OPEN", null, 4, rule);
+ String issue2 = insertIssue("REOPENED", null, 4, rule);
+
+ underTest.execute();
+ assertIssues(
+ tuple(issue1, "TOREVIEW", null, 4, NOW),
+ tuple(issue2, "TOREVIEW", null, 4, NOW));
+
+ // Set a new date for NOW in order to check that issues has not been updated again
+ system2.setNow(NOW + 1_000_000_000L);
+ underTest.execute();
+ assertIssues(
+ tuple(issue1, "TOREVIEW", null, 4, NOW),
+ tuple(issue2, "TOREVIEW", null, 4, NOW));
+ }
+
+ @Test
+ public void issues_index_is_removed() throws SQLException {
+ underTest.execute();
+
+ verify(esClient).deleteIndexes("issues");
+ }
+
+ private void assertIssues(Tuple... expectedTuples) {
+ assertThat(db.select("SELECT kee, status, resolution, issue_type, updated_at FROM issues")
+ .stream()
+ .map(map -> new Tuple(map.get("KEE"), map.get("STATUS"), map.get("RESOLUTION"), map.get("ISSUE_TYPE"), map.get("UPDATED_AT")))
+ .collect(toList()))
+ .containsExactlyInAnyOrder(expectedTuples);
+ }
+
+ private void assertNoIssueChanges() {
+ assertThat(db.countRowsOfTable("issue_changes")).isZero();
+ }
+
+ private void assertIssueChanges(Tuple... expectedTuples) {
+ assertThat(db.select("SELECT issue_key, change_type, change_data, created_at, updated_at, issue_change_creation_date FROM issue_changes")
+ .stream()
+ .map(map -> new Tuple(map.get("ISSUE_KEY"), map.get("CHANGE_TYPE"), map.get("CHANGE_DATA"), map.get("CREATED_AT"), map.get("UPDATED_AT"),
+ map.get("ISSUE_CHANGE_CREATION_DATE")))
+ .collect(toList()))
+ .containsExactlyInAnyOrder(expectedTuples);
+ }
+
+ private String insertIssue(String status, @Nullable String resolution, int issueType, int ruleId) {
+ String issueKey = randomAlphabetic(3);
+ db.executeInsert(
+ "ISSUES",
+ "KEE", issueKey,
+ "STATUS", status,
+ "RESOLUTION", resolution,
+ "RULE_ID", ruleId,
+ "ISSUE_TYPE", issueType,
+ "COMPONENT_UUID", randomAlphanumeric(10),
+ "PROJECT_UUID", randomAlphanumeric(10),
+ "MANUAL_SEVERITY", false,
+ "CREATED_AT", PAST,
+ "UPDATED_AT", PAST);
+ return issueKey;
+ }
+
+ private int insertRule(int ruleType) {
+ int id = RandomUtils.nextInt();
+ db.executeInsert("RULES",
+ "ID", id,
+ "RULE_TYPE", ruleType,
+ "IS_EXTERNAL", false,
+ "PLUGIN_RULE_KEY", randomAlphanumeric(3),
+ "PLUGIN_NAME", randomAlphanumeric(3),
+ "SCOPE", "MAIN",
+ "IS_AD_HOC", false,
+ "CREATED_AT", PAST,
+ "UPDATED_AT", PAST);
+ return id;
+ }
+
+}
--- /dev/null
+CREATE TABLE "ISSUES" (
+ "ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ "KEE" VARCHAR(50) UNIQUE NOT NULL,
+ "COMPONENT_UUID" VARCHAR(50),
+ "PROJECT_UUID" VARCHAR(50),
+ "RULE_ID" INTEGER,
+ "SEVERITY" VARCHAR(10),
+ "MANUAL_SEVERITY" BOOLEAN NOT NULL,
+ "MESSAGE" VARCHAR(4000),
+ "LINE" INTEGER,
+ "GAP" DOUBLE,
+ "EFFORT" INTEGER,
+ "STATUS" VARCHAR(20),
+ "RESOLUTION" VARCHAR(20),
+ "CHECKSUM" VARCHAR(1000),
+ "REPORTER" VARCHAR(255),
+ "ASSIGNEE" VARCHAR(255),
+ "AUTHOR_LOGIN" VARCHAR(255),
+ "ACTION_PLAN_KEY" VARCHAR(50) NULL,
+ "ISSUE_ATTRIBUTES" VARCHAR(4000),
+ "TAGS" VARCHAR(4000),
+ "ISSUE_CREATION_DATE" BIGINT,
+ "ISSUE_CLOSE_DATE" BIGINT,
+ "ISSUE_UPDATE_DATE" BIGINT,
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT,
+ "LOCATIONS" BLOB,
+ "ISSUE_TYPE" TINYINT,
+ "FROM_HOTSPOT" BOOLEAN NULL
+);
+CREATE UNIQUE INDEX "ISSUES_KEE" ON "ISSUES" ("KEE");
+CREATE INDEX "ISSUES_COMPONENT_UUID" ON "ISSUES" ("COMPONENT_UUID");
+CREATE INDEX "ISSUES_PROJECT_UUID" ON "ISSUES" ("PROJECT_UUID");
+CREATE INDEX "ISSUES_RULE_ID" ON "ISSUES" ("RULE_ID");
+CREATE INDEX "ISSUES_RESOLUTION" ON "ISSUES" ("RESOLUTION");
+CREATE INDEX "ISSUES_ASSIGNEE" ON "ISSUES" ("ASSIGNEE");
+CREATE INDEX "ISSUES_CREATION_DATE" ON "ISSUES" ("ISSUE_CREATION_DATE");
+CREATE INDEX "ISSUES_UPDATED_AT" ON "ISSUES" ("UPDATED_AT");
+
+CREATE TABLE "ISSUE_CHANGES" (
+ "ID" BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ "KEE" VARCHAR(50),
+ "ISSUE_KEY" VARCHAR(50) NOT NULL,
+ "USER_LOGIN" VARCHAR(255),
+ "CHANGE_TYPE" VARCHAR(40),
+ "CHANGE_DATA" VARCHAR(16777215),
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT,
+ "ISSUE_CHANGE_CREATION_DATE" BIGINT
+);
+CREATE INDEX "ISSUE_CHANGES_KEE" ON "ISSUE_CHANGES" ("KEE");
+CREATE INDEX "ISSUE_CHANGES_ISSUE_KEY" ON "ISSUE_CHANGES" ("ISSUE_KEY");
+
+CREATE TABLE "RULES" (
+ "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+ "PLUGIN_KEY" VARCHAR(200),
+ "PLUGIN_RULE_KEY" VARCHAR(200) NOT NULL,
+ "PLUGIN_NAME" VARCHAR(255) NOT NULL,
+ "DESCRIPTION" VARCHAR(16777215),
+ "DESCRIPTION_FORMAT" VARCHAR(20),
+ "PRIORITY" INTEGER,
+ "IS_TEMPLATE" BOOLEAN DEFAULT FALSE,
+ "IS_EXTERNAL" BOOLEAN NOT NULL,
+ "IS_AD_HOC" BOOLEAN NOT NULL,
+ "TEMPLATE_ID" INTEGER,
+ "PLUGIN_CONFIG_KEY" VARCHAR(200),
+ "NAME" VARCHAR(200),
+ "STATUS" VARCHAR(40),
+ "LANGUAGE" VARCHAR(20),
+ "SCOPE" VARCHAR(20) NOT NULL,
+ "DEF_REMEDIATION_FUNCTION" VARCHAR(20),
+ "DEF_REMEDIATION_GAP_MULT" VARCHAR(20),
+ "DEF_REMEDIATION_BASE_EFFORT" VARCHAR(20),
+ "GAP_DESCRIPTION" VARCHAR(4000),
+ "SYSTEM_TAGS" VARCHAR(4000),
+ "SECURITY_STANDARDS" VARCHAR(4000),
+ "RULE_TYPE" TINYINT,
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT
+);
+CREATE UNIQUE INDEX "RULES_REPO_KEY" ON "RULES" ("PLUGIN_NAME", "PLUGIN_RULE_KEY");
.containsEntry("COMPONENT_UUID", issue.componentUuid())
.containsEntry("EFFORT", updated.effortInMinutes())
.containsEntry("ISSUE_ATTRIBUTES", "fox=bax")
- .containsEntry("ISSUE_TYPE", (byte) 3)
+ .containsEntry("ISSUE_TYPE", 3)
.containsEntry("KEE", issue.key())
.containsEntry("LINE", (long) updated.line())
.containsEntry("PROJECT_UUID", updated.projectUuid())