]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22914 Compute CVEs received from scanner report
authorantoine.vinot <antoine.vinot@sonarsource.com>
Fri, 6 Sep 2024 13:12:28 +0000 (15:12 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 12 Sep 2024 20:02:55 +0000 (20:02 +0000)
23 files changed:
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistCveStepIT.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepDependencyIT.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/batch/BatchReportReader.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/batch/BatchReportReaderImpl.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistCveStep.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
server/sonar-ce-task-projectanalysis/src/main/protobuf/project_dump.proto
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/batch/BatchReportReaderImplTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/batch/BatchReportReaderRule.java
server/sonar-db-dao/src/it/java/org/sonar/db/dependency/CveCweDaoIT.java
server/sonar-db-dao/src/it/java/org/sonar/db/dependency/CveDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/dependency/CveCweDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/dependency/CveCweMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/dependency/CveDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/dependency/CveMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/dependency/CveCweMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/dependency/CveMapper.xml
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java

diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistCveStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistCveStepIT.java
new file mode 100644 (file)
index 0000000..ad030ca
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.ce.task.projectanalysis.step;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.task.projectanalysis.batch.BatchReportReaderRule;
+import org.sonar.ce.task.step.TestComputationStepContext;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryImpl;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.dependency.CveCweDto;
+import org.sonar.db.dependency.CveDto;
+import org.sonar.scanner.protocol.output.ScannerReport.Cve;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+class PersistCveStepIT {
+
+  @RegisterExtension
+  private final DbTester db = DbTester.create(System2.INSTANCE);
+
+  @RegisterExtension
+  private final BatchReportReaderRule batchReportReader = new BatchReportReaderRule();
+
+  private final DbSession dbSession = db.getSession();
+  private final DbClient dbClient = db.getDbClient();
+  private final UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE;
+
+  private PersistCveStep persistCveStep;
+
+  @BeforeEach
+  void setUp() {
+    persistCveStep = new PersistCveStep(batchReportReader, dbClient, uuidFactory, System2.INSTANCE);
+  }
+
+  @Test
+  void getDescription_shouldReturnStepDescription() {
+    assertThat(persistCveStep.getDescription()).isEqualTo("Persist CVEs");
+  }
+
+  @Test
+  void execute_shouldInsertNewCVEs() {
+    Cve cve1 = buildCve("1").build();
+    Cve cve2 = buildCve("2").build();
+    batchReportReader.putCves(List.of(cve1, cve2));
+
+    persistCveStep.execute(new TestComputationStepContext());
+
+    assertCvePersistedInDatabase(cve1);
+    assertCvePersistedInDatabase(cve2);
+  }
+
+  private void assertCvePersistedInDatabase(Cve cve) {
+    CveDto cveDto = dbClient.cveDao().selectById(dbSession, cve.getCveId())
+      .orElseGet(() -> fail(String.format("CVE with id %s not found", cve.getCveId())));
+    assertThat(cveDto.id()).isEqualTo(cve.getCveId());
+    assertThat(cveDto.description()).isEqualTo(cve.getDescription());
+    assertThat(cveDto.cvssScore()).isEqualTo(cve.getCvssScore());
+    assertThat(cveDto.epssScore()).isEqualTo(cve.getEpssScore());
+    assertThat(cveDto.epssPercentile()).isEqualTo(cve.getEpssPercentile());
+    assertThat(cveDto.publishedAt()).isEqualTo(cve.getPublishedDate());
+    assertThat(cveDto.lastModifiedAt()).isEqualTo(cve.getLastModifiedDate());
+    assertThat(cveDto.uuid()).isNotBlank();
+    assertThat(cveDto.createdAt()).isNotNull();
+    assertThat(cveDto.updatedAt()).isNotNull();
+  }
+
+  @Test
+  void execute_shoudUpdateExistingCves() {
+    dbClient.cveDao().insert(dbSession, new CveDto("cve-uuid-1", "CVE-1", "Old description 1", 0.0F, 0.0F, 0.0F, 0L, 0L, 0L, 0L));
+    dbClient.cveDao().insert(dbSession, new CveDto("cve-uuid-2", "CVE-2", "Old description 2", 0.0F, 0.0F, 0.0F, 0L, 0L, 0L, 0L));
+    db.commit();
+    Cve cve1 = buildCve("1").build();
+    Cve cve2 = buildCve("2").build();
+    batchReportReader.putCves(List.of(cve1, cve2));
+
+    persistCveStep.execute(new TestComputationStepContext());
+
+    assertThat(db.countRowsOfTable(dbSession, "cves")).isEqualTo(2);
+    assertCvePersistedInDatabase(cve1);
+    assertCvePersistedInDatabase(cve2);
+  }
+
+  @Test
+  void execute_shouldInsertCwes_whenNewCVEs() {
+    Cve cve1 = buildCve("1").addCwe("CWE-11").addCwe("CWE-12").build();
+    Cve cve2 = buildCve("2").addCwe("CWE-11").build();
+    batchReportReader.putCves(List.of(cve1, cve2));
+
+    persistCveStep.execute(new TestComputationStepContext());
+
+    assertCveHasExactlyCwes(cve1, "CWE-11", "CWE-12");
+    assertCveHasExactlyCwes(cve2, "CWE-11");
+  }
+
+  @Test
+  void execute_shouldUpdateExistingCwesAndInsertNewOnes_whenUpdatingCVEs() {
+    dbClient.cveDao().insert(dbSession, new CveDto("cve-uuid-1", "CVE-1", "Old description 1", 0.0F, 0.0F, 0.0F, 0L, 0L, 0L, 0L));
+    dbClient.cveCweDao().insert(dbSession, new CveCweDto("cve-uuid-1", "CWE-1"));
+    dbClient.cveCweDao().insert(dbSession, new CveCweDto("cve-uuid-1", "CWE-2"));
+    db.commit();
+    Cve cve = buildCve("1").addCwe("CWE-2").addCwe("CWE-3").build();
+    batchReportReader.putCves(List.of(cve));
+
+    persistCveStep.execute(new TestComputationStepContext());
+
+    assertCveHasExactlyCwes(cve, "CWE-2", "CWE-3");
+  }
+
+  private void assertCveHasExactlyCwes(Cve cve, String... cwes) {
+    Set<String> cweInDb = dbClient.cveCweDao().selectByCveUuid(dbSession, getCveUuid(cve.getCveId()));
+    assertThat(cweInDb).containsExactlyInAnyOrder(cwes);
+  }
+
+  private String getCveUuid(String cveId) {
+    return dbClient.cveDao().selectById(dbSession, cveId)
+      .map(CveDto::uuid)
+      .orElseGet(() -> fail("CVE not found"));
+  }
+
+  private static Cve.Builder buildCve(String suffix) {
+    return Cve.newBuilder()
+      .setCveId("CVE-"+suffix)
+      .setCvssScore(7.5F)
+      .setEpssScore(0.1F)
+      .setEpssPercentile(0.4F)
+      .setDescription("Some CVE description "+suffix)
+      .setLastModifiedDate(5L)
+      .setPublishedDate(4L);
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepDependencyIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepDependencyIT.java
new file mode 100644 (file)
index 0000000..084a876
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.ce.task.projectanalysis.step;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.task.projectanalysis.batch.BatchReportReaderRule;
+import org.sonar.ce.task.projectanalysis.issue.AdHocRuleCreator;
+import org.sonar.ce.task.projectanalysis.issue.ProtoIssueCache;
+import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryImpl;
+import org.sonar.ce.task.projectanalysis.issue.UpdateConflictResolver;
+import org.sonar.ce.task.projectanalysis.period.Period;
+import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule;
+import org.sonar.ce.task.step.ComputationStep.Context;
+import org.sonar.ce.task.step.TestComputationStepContext;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.util.UuidFactoryImpl;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.dependency.CveDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.server.issue.IssueStorage;
+
+import static java.nio.file.Files.createTempFile;
+import static org.mockito.Mockito.mock;
+import static org.sonar.api.issue.IssueStatus.OPEN;
+import static org.sonar.api.rules.RuleType.VULNERABILITY;
+
+class PersistIssuesStepDependencyIT {
+
+  @RegisterExtension
+  private final DbTester db = DbTester.create();
+  @RegisterExtension
+  private final  BatchReportReaderRule reportReader = new BatchReportReaderRule();
+  @RegisterExtension
+  private final  PeriodHolderRule periodHolder = new PeriodHolderRule();
+  @TempDir
+  private Path tempDir;
+
+  private final DbSession dbSession = db.getSession();
+  private final DbClient dbClient = db.getDbClient();
+
+  private ProtoIssueCache protoIssueCache;
+
+  private PersistIssuesStep persistIssuesStep;
+
+  private ComponentDto project;
+  RuleDto scaRule;
+  private CveDto cve;
+
+  @BeforeEach
+  public void setUp() throws IOException {
+    periodHolder.setPeriod(new Period(NewCodePeriodType.NUMBER_OF_DAYS.name(), "10", 1000L));
+    protoIssueCache = new ProtoIssueCache(createTempFile(tempDir, null, null).toFile(), System2.INSTANCE);
+    reportReader.setMetadata(ScannerReport.Metadata.getDefaultInstance());
+
+    persistIssuesStep = new PersistIssuesStep(dbClient, System2.INSTANCE, new UpdateConflictResolver(), new RuleRepositoryImpl(mock(AdHocRuleCreator.class), dbClient),
+      periodHolder, protoIssueCache, new IssueStorage(), UuidFactoryImpl.INSTANCE);
+
+    project = db.components().insertPrivateProject().getMainBranchComponent();
+    scaRule = db.rules().insert(RuleTesting.newRule(RuleKey.of("external_sca", "use-of-vulnerable-dependency")));
+    cve = new CveDto("cve_uuid", "CVE-123", "Some CVE description", 1.0, 2.0, 3.0, 4L, 5L, 6L, 7L);
+    dbClient.cveDao().insert(dbSession, cve);
+    db.commit();
+  }
+
+  @Test
+  void execute_shouldInsertCveId_whenNewCveIssue() {
+    String issueKey = "ISSUE_KEY";
+    DefaultIssue defaultIssue = new DefaultIssue()
+      .setCveId(cve.id())
+      .setKey(issueKey)
+      .setType(VULNERABILITY)
+      .setComponentKey(project.getKey())
+      .setComponentUuid(project.uuid())
+      .setProjectUuid(project.uuid())
+      .setProjectKey(project.getKey())
+      .setRuleKey(scaRule.getKey())
+      .setStatus(OPEN.toString())
+      .setCreationDate(new Date());
+    protoIssueCache.newAppender().append(defaultIssue).close();
+
+    Context context = new TestComputationStepContext();
+    persistIssuesStep.execute(context);
+
+    List<Map<String, Object>> select = db.select(db.getSession(), "select * from issues_dependency");
+    Assertions.assertThat(select).hasSize(1);
+    Assertions.assertThat(select.get(0)).containsExactlyInAnyOrderEntriesOf(
+      Map.of(
+        "issue_uuid", issueKey,
+        "cve_uuid", cve.uuid())
+    );
+  }
+}
index f92f857574dc295be1c9a76c3e15b8669eadb151..de85e094a67374dd6e87c962ad1d654e523c2571 100644 (file)
@@ -70,4 +70,6 @@ public interface BatchReportReader {
   Optional<ScannerReport.ChangedLines> readComponentChangedLines(int fileRef);
 
   CloseableIterator<ScannerReport.AnalysisWarning> readAnalysisWarnings();
+
+  CloseableIterator<ScannerReport.Cve> readCves();
 }
index a7ce30c32fe0d820136161aadce8ffaf28013f58..df17a5ac89b680183c3e11e72bf166e3f901b16c 100644 (file)
@@ -227,4 +227,10 @@ public class BatchReportReaderImpl implements BatchReportReader {
     ensureInitialized();
     return delegate.readAnalysisWarnings();
   }
+
+  @Override
+  public CloseableIterator<ScannerReport.Cve> readCves() {
+    ensureInitialized();
+    return delegate.readCves();
+  }
 }
index 3100a1368b66e4aa02751a99bd6879a98ea40f27..93b6054564e8b8390fc3dcbe42c18da64c55603e 100644 (file)
@@ -270,6 +270,7 @@ public class TrackerRawInputFactory {
       }
       issue.setIsFromExternalRuleEngine(true);
       issue.setLocations(dbLocationsBuilder.build());
+      issue.setCveId(reportExternalIssue.getCveId());
 
       return issue;
     }
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistCveStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistCveStep.java
new file mode 100644 (file)
index 0000000..34bd2ad
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.ce.task.projectanalysis.step;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.dependency.CveCweDto;
+import org.sonar.db.dependency.CveDto;
+import org.sonar.scanner.protocol.output.ScannerReport;
+
+/**
+ * Step that persists CVEs and their CWEs in the database.
+ * CVEs are inserted or updated in the database based on the information from the scanner report.
+ * If CWEs need to be updated, we simply remove all CWEs from the CVE and insert what is sent by the scanner.
+ */
+public class PersistCveStep implements ComputationStep {
+
+  private static final Logger LOG = LoggerFactory.getLogger(PersistCveStep.class);
+
+  private final BatchReportReader batchReportReader;
+  private final DbClient dbClient;
+  private final UuidFactory uuidFactory;
+  private final System2 system2;
+
+  public PersistCveStep(BatchReportReader batchReportReader, DbClient dbClient, UuidFactory uuidFactory, System2 system2) {
+    this.batchReportReader = batchReportReader;
+    this.dbClient = dbClient;
+    this.uuidFactory = uuidFactory;
+    this.system2 = system2;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Persist CVEs";
+  }
+
+  @Override
+  public void execute(Context context) {
+    int count = 0;
+    try (DbSession dbSession = dbClient.openSession(false);
+         CloseableIterator<ScannerReport.Cve> batchCves = batchReportReader.readCves()) {
+      while (batchCves.hasNext()) {
+        updateOrInsertCve(dbSession, batchCves.next());
+        count++;
+      }
+      LOG.debug("{} CVEs were imported/updated", count);
+      dbSession.commit();
+    } catch (Exception exception) {
+      throw new IllegalStateException(String.format("CVEs import failed after processing %d CVEs successfully", count), exception);
+    }
+  }
+
+  private void updateOrInsertCve(DbSession dbSession, ScannerReport.Cve scannerCve) {
+    dbClient.cveDao().selectById(dbSession, scannerCve.getCveId())
+      .ifPresentOrElse(
+        cveDto -> updateCve(dbSession, cveDto, scannerCve),
+        () -> insertCve(dbSession, scannerCve));
+  }
+
+  private void updateCve(DbSession dbSession, CveDto cveInDb, ScannerReport.Cve scannerCve) {
+    CveDto dtoForUpdate = toDtoForUpdate(scannerCve, cveInDb);
+    dbClient.cveDao().update(dbSession, dtoForUpdate);
+    String cveUuid = cveInDb.uuid();
+    deleteThenInsertCwesIfUpdated(dbSession, scannerCve, cveUuid);
+  }
+
+  private CveDto toDtoForUpdate(ScannerReport.Cve cve, CveDto cveInDb) {
+    return new CveDto(
+      cveInDb.uuid(),
+      cve.getCveId(),
+      cve.getDescription(),
+      cve.getCvssScore(),
+      cve.getEpssScore(),
+      cve.getEpssPercentile(),
+      cve.getPublishedDate(),
+      cve.getLastModifiedDate(),
+      cveInDb.createdAt(),
+      system2.now()
+    );
+  }
+
+  private void deleteThenInsertCwesIfUpdated(DbSession dbSession, ScannerReport.Cve scannerCve, String cveUuid) {
+    Set<String> cweInDb = dbClient.cveCweDao().selectByCveUuid(dbSession, cveUuid);
+    Set<String> cweFromReport = new HashSet<>(scannerCve.getCweList());
+    if (!cweInDb.equals(cweFromReport)) {
+      dbClient.cveCweDao().deleteByCveUuid(dbSession, cveUuid);
+      cweFromReport.forEach(cwe -> dbClient.cveCweDao().insert(dbSession, new CveCweDto(cveUuid, cwe)));
+    }
+  }
+
+  private void insertCve(DbSession dbSession, ScannerReport.Cve scannerCve) {
+    CveDto dtoForInsert = toDtoForInsert(scannerCve);
+    dbClient.cveDao().insert(dbSession, dtoForInsert);
+    scannerCve.getCweList().forEach(cwe -> dbClient.cveCweDao().insert(dbSession, new CveCweDto(dtoForInsert.uuid(), cwe)));
+  }
+
+  private CveDto toDtoForInsert(ScannerReport.Cve cve) {
+    long now = system2.now();
+    return new CveDto(
+      uuidFactory.create(),
+      cve.getCveId(),
+      cve.getDescription(),
+      cve.getCvssScore(),
+      cve.getEpssScore(),
+      cve.getEpssPercentile(),
+      cve.getPublishedDate(),
+      cve.getLastModifiedDate(),
+      now,
+      now
+    );
+  }
+
+}
index a551254d0b472152dc36109688e3b022672f8b17..1f6aebe48b8d40335af9ebea331a3d0bfd9639e5 100644 (file)
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.sonar.api.utils.System2;
@@ -37,6 +38,8 @@ import org.sonar.core.util.UuidFactory;
 import org.sonar.db.BatchSession;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.dependency.CveDto;
+import org.sonar.db.dependency.IssuesDependencyDto;
 import org.sonar.db.issue.AnticipatedTransitionMapper;
 import org.sonar.db.issue.IssueChangeMapper;
 import org.sonar.db.issue.IssueDao;
@@ -46,6 +49,7 @@ import org.sonar.db.newcodeperiod.NewCodePeriodType;
 import org.sonar.server.issue.IssueStorage;
 
 import static org.sonar.core.util.FileUtils.humanReadableByteCountSI;
+import static org.sonar.db.issue.IssueDto.toDtoForComputationInsert;
 
 public class PersistIssuesStep implements ComputationStep {
   // holding up to 1000 DefaultIssue (max size of addedIssues and updatedIssues at any given time) in memory should not
@@ -134,7 +138,7 @@ public class PersistIssuesStep implements ComputationStep {
     List<IssueDto> issueDtos = new LinkedList<>();
     addedIssues.forEach(addedIssue -> {
       String ruleUuid = ruleRepository.getByKey(addedIssue.ruleKey()).getUuid();
-      IssueDto dto = IssueDto.toDtoForComputationInsert(addedIssue, ruleUuid, now);
+      IssueDto dto = toDtoForComputationInsert(addedIssue, ruleUuid, now);
       issueDao.insertWithoutImpacts(dbSession, dto);
       issueDtos.add(dto);
       if (isOnBranchUsingReferenceBranch() && addedIssue.isOnChangedLine()) {
@@ -145,7 +149,15 @@ public class PersistIssuesStep implements ComputationStep {
       addedIssue.getAnticipatedTransitionUuid().ifPresent(anticipatedTransitionMapper::delete);
     });
 
-    issueDtos.forEach(issueDto -> issueDao.insertIssueImpacts(dbSession, issueDto));
+    issueDtos.forEach(issueDto -> insertAdditionalIssueData(issueDao, dbSession, issueDto));
+  }
+
+  private void insertAdditionalIssueData(IssueDao issueDao, DbSession dbSession, IssueDto issueDto) {
+    issueDao.insertIssueImpacts(dbSession, issueDto);
+    if (issueDto.getCveId() != null) {
+      Consumer<CveDto> insertIssueDependency = cveDto -> dbClient.issuesDependencyDao().insert(dbSession, new IssuesDependencyDto(issueDto.getKey(), cveDto.uuid()));
+      dbClient.cveDao().selectById(dbSession, issueDto.getCveId()).ifPresent(insertIssueDependency);
+    }
   }
 
   private void persistUpdatedIssues(IssueStatistics statistics, List<DefaultIssue> updatedIssues, IssueDao issueDao,
index 9fbf27e0a356fe09b61da4a7d672bce384446083..34df5531d9da0702b3e13b7a206059e3309aede3 100644 (file)
@@ -109,6 +109,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
     PersistLiveMeasuresStep.class,
     PersistDuplicationDataStep.class,
     PersistAdHocRulesStep.class,
+    PersistCveStep.class,
     PersistIssuesStep.class,
     CleanIssueChangesStep.class,
     PersistProjectLinksStep.class,
index c3d9d13960951aef69e9acde29e2ca089bf9ca23..5b2760f17507798c6846ec2e20cb43cd30fe061b 100644 (file)
@@ -151,6 +151,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     for (IssueCache.FieldDiffs protoFieldDiffs : next.getChangesList()) {
       defaultIssue.addChange(toDefaultIssueChanges(protoFieldDiffs));
     }
+    defaultIssue.setCveId(next.hasCveId() ? next.getCveId() : null);
     return defaultIssue;
   }
 
@@ -213,7 +214,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
     for (FieldDiffs fieldDiffs : defaultIssue.changes()) {
       builder.addChanges(toProtoIssueChanges(fieldDiffs));
     }
-
+    ofNullable(defaultIssue.getCveId()).ifPresent(builder::setCveId);
     return builder.build();
   }
 
index acd3e4e62c39baa1bda0558957194d784c608cfe..8ef383a86876789eef0f06fefacf092a8996e1e0 100644 (file)
@@ -30,85 +30,86 @@ option optimize_for = SPEED;
 
 message Issue {
   optional string key = 1;
-  optional int32 ruleType = 2;
-  optional string componentUuid = 3;
-  optional string componentKey = 4;
-  reserved 5; //moduleUuid
-  reserved 6; // moduleUuidPath
-  optional string projectUuid = 7;
-  optional string projectKey = 8;
-  optional string ruleKey = 9;
+  optional int32 rule_type = 2;
+  optional string component_uuid = 3;
+  optional string component_key = 4;
+  reserved 5; //module_uuid
+  reserved 6; // module_uuid_path
+  optional string project_uuid = 7;
+  optional string project_key = 8;
+  optional string rule_key = 9;
   optional string language = 10;
   optional string severity = 11;
-  optional bool manualSeverity = 12;
+  optional bool manual_severity = 12;
   optional string message = 13;
   optional int32 line = 14;
   optional double gap = 15;
   optional int64 effort = 16;
   optional string status = 17;
   optional string resolution = 18;
-  optional string assigneeUuid = 19;
+  optional string assignee_uuid = 19;
   optional string checksum = 20;
-  optional string authorLogin = 22;
+  optional string author_login = 22;
   optional string tags = 23;
   optional sonarqube.db.issues.Locations locations = 24;
 
-  optional bool isFromExternalRuleEngine = 25;
+  optional bool is_from_external_rule_engine = 25;
 
   // FUNCTIONAL DATES
-  optional int64 creationDate = 26;
-  optional int64 updateDate = 27;
-  optional int64 closeDate = 28;
+  optional int64 creation_date = 26;
+  optional int64 update_date = 27;
+  optional int64 close_date = 28;
 
   repeated FieldDiffs changes = 29;
-  optional FieldDiffs currentChanges = 30;
+  optional FieldDiffs current_changes = 30;
 
-  optional bool isNew = 31;
-  optional bool isCopied = 32;
-  optional bool beingClosed = 33;
-  optional bool onDisabledRule = 34;
-  optional bool isChanged = 35;
-  optional bool sendNotifications = 36;
-  optional int64 selectedAt = 37;
+  optional bool is_new = 31;
+  optional bool is_copied = 32;
+  optional bool being_closed = 33;
+  optional bool on_disabled_rule = 34;
+  optional bool is_changed = 35;
+  optional bool send_notifications = 36;
+  optional int64 selected_at = 37;
 
   repeated Comment comments = 38;
 
-  optional bool quickFixAvailable = 39;
-
-  optional bool isOnReferencedBranch = 40;
-  optional bool isOnChangedLine = 41;
-  optional bool isNewCodeReferenceIssue = 42;
-  optional bool isNoLongerNewCodeReferenceIssue = 43;
-  optional string ruleDescriptionContextKey = 44;
-  optional sonarqube.db.issues.MessageFormattings messageFormattings = 45;
-  optional string codeVariants = 46;
-  optional string assigneeLogin = 47;
-  optional string anticipatedTransitionUuid = 48;
+  optional bool quick_fix_available = 39;
+
+  optional bool is_on_referenced_branch = 40;
+  optional bool is_on_changed_line = 41;
+  optional bool is_new_code_reference_issue = 42;
+  optional bool is_no_longer_new_code_reference_issue = 43;
+  optional string rule_description_context_key = 44;
+  optional sonarqube.db.issues.MessageFormattings message_formattings = 45;
+  optional string code_variants = 46;
+  optional string assignee_login = 47;
+  optional string anticipated_transition_uuid = 48;
   repeated Impact impacts = 49;
-  optional string cleanCodeAttribute = 50;
-  optional bool isPrioritizedRule = 51;
+  optional string clean_code_attribute = 50;
+  optional bool is_prioritized_rule = 51;
+  optional string cve_id = 52;
 }
 
 message Comment {
-  optional string issueKey = 1;
-  optional string userUuid = 2;
-  optional int64 createdAt = 3;
-  optional int64 updatedAt = 4;
+  optional string issue_key = 1;
+  optional string user_uuid = 2;
+  optional int64 created_at = 3;
+  optional int64 updated_at = 4;
   optional string key = 5;
-  optional string markdownText = 6;
-  optional bool isNew = 7;
+  optional string markdown_text = 6;
+  optional bool is_new = 7;
 }
 
 message FieldDiffs {
-  optional string userUuid = 1;
-  optional int64 creationDate = 2;
-  optional string issueKey = 3;
+  optional string user_uuid = 1;
+  optional int64 creation_date = 2;
+  optional string issue_key = 3;
   map<string, Diff> diffs = 4;
 }
 
 message Diff {
-  optional string oldValue = 1;
-  optional string newValue = 2;
+  optional string old_value = 1;
+  optional string new_value = 2;
 }
 
 message Impact {
index 1d99ac0ceba60f2f31fd306869d2e266a040a8a4..d1e3782b7f151de5694b90776b65a2d39688734b 100644 (file)
@@ -53,12 +53,12 @@ message Branch {
 message Analysis {
   int64 component_ref = 2;
   int64 date = 3;
-  string projectVersion = 4;
+  string project_version = 4;
   string period1_mode = 5;
   string period1_param = 6;
   int64 period1_date = 7;
   string uuid = 8;
-  string buildString = 9;
+  string build_string = 9;
 }
 
 message Metric {
@@ -123,10 +123,10 @@ message Issue {
   string project_uuid = 22;
   string rule_ref = 23;
 
-  optional string ruleDescriptionContextKey = 24;
+  optional string rule_description_context_key = 24;
 
-  repeated MessageFormatting messageFormattings = 25;
-  string codeVariants = 26;
+  repeated MessageFormatting message_formattings = 25;
+  string code_variants = 26;
 
   repeated Impact impacts = 27;
 }
@@ -177,7 +177,7 @@ message AdHocRule {
   string scope = 8;
   RuleMetadata metadata = 9;
   repeated Impact impacts = 11;
-  optional string cleanCodeAttribute = 12;
+  optional string clean_code_attribute = 12;
 }
 
 // stream of messages in settings.pb
@@ -253,4 +253,4 @@ enum Severity {
   LOW = 0;
   MEDIUM = 1;
   HIGH = 2;
-}
\ No newline at end of file
+}
index d181d85263f8a7943676142060162dccad795053..7019e756d2a8b0fb0cafe6932e74e7292adc6adb 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.impl.utils.JUnitTempFolder;
 import org.sonar.core.util.CloseableIterator;
 import org.sonar.scanner.protocol.output.FileStructure;
 import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReport.Cve;
 import org.sonar.scanner.protocol.output.ScannerReportWriter;
 
 import static com.google.common.collect.ImmutableList.of;
@@ -300,4 +301,28 @@ public class BatchReportReaderImplTest {
     assertThat(res).toIterable().containsExactlyElementsOf(warnings);
     res.close();
   }
+
+  @Test
+  public void readCves_shouldReturnCves() {
+    Cve cve1 = builCve("1").build();
+    writer.appendCve(cve1);
+    Cve cve2 = builCve("2").build();
+    writer.appendCve(cve2);
+
+    CloseableIterator<Cve> cveCloseableIterator = underTest.readCves();
+
+    assertThat(cveCloseableIterator).toIterable().containsExactlyInAnyOrder(cve1, cve2);
+  }
+
+  private Cve.Builder builCve(String suffix) {
+    return Cve.newBuilder()
+      .setCveId("CVE-" + suffix)
+      .addCwe("CWE-" + suffix)
+      .setDescription("Some CVE description " + suffix)
+      .setCvssScore(7.5F)
+      .setEpssScore(0.1F)
+      .setEpssPercentile(0.1F)
+      .setLastModifiedDate(1L)
+      .setPublishedDate(2L);
+  }
 }
index c6075de139ae91370dc208a3ef58e8f804283c1d..b960803b18a3d0c35d68098fb54469241efb05cd 100644 (file)
@@ -62,6 +62,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader, After
   private Map<Integer, ScannerReport.ChangedLines> changedLines = new HashMap<>();
   private List<ScannerReport.AnalysisWarning> analysisWarnings = Collections.emptyList();
   private byte[] analysisCache;
+  private List<ScannerReport.Cve> cves = new ArrayList<>();
 
   @Override
   public Statement apply(final Statement statement, Description description) {
@@ -326,4 +327,14 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader, After
   public void afterEach(ExtensionContext context) {
     clear();
   }
+
+  @Override
+  public CloseableIterator<ScannerReport.Cve> readCves() {
+    return CloseableIterator.from(cves.iterator());
+  }
+
+  public BatchReportReaderRule putCves(List<ScannerReport.Cve> cves) {
+    this.cves = cves;
+    return this;
+  }
 }
index 7b9587201f50a995fd279053c9f4cecaf1340246..ffffd54664a4c36903fb6f85d3811f16d010316f 100644 (file)
@@ -70,4 +70,23 @@ class CveCweDaoIT {
     assertThat(result).isEmpty();
   }
 
+  @Test
+  void deleteByCveUuid_shouldDeleteCwesAttachedToCve() {
+    String cveUuid = "CVE_UUID";
+    cveCweDao.insert(db.getSession(), new CveCweDto(cveUuid, "CWE-123"));
+    cveCweDao.insert(db.getSession(), new CveCweDto(cveUuid, "CWE-456"));
+    CveCweDto nonDeletedCwe = new CveCweDto("ANOTHER_CVE_UUID", "CWE-789");
+    cveCweDao.insert(db.getSession(), nonDeletedCwe);
+
+    cveCweDao.deleteByCveUuid(db.getSession(), cveUuid);
+
+    List<Map<String, Object>> result = db.select(db.getSession(), "select * from cve_cwe");
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0)).containsExactlyInAnyOrderEntriesOf(
+      Map.of(
+        "cve_uuid", nonDeletedCwe.cveUuid(),
+        "cwe", nonDeletedCwe.cwe())
+    );
+  }
+
 }
index 00c04e9df2176c7e492385fef5945ed631f582df..79bc9530ae00f6c8ba90f92690744a3ee58e8d8f 100644 (file)
@@ -89,4 +89,17 @@ class CveDaoIT {
     assertThat(result).usingRecursiveComparison().isEqualTo(cveDto);
   }
 
+  @Test
+  void update_shouldUpdateCveButCreationDate() {
+    CveDto insertedDto = new CveDto("uuid-1", "CVE-1", "Some CVE description 1", 1.0, 2.0, 3.0, 4L, 5L, 6L, 7L);
+    cveDao.insert(db.getSession(), insertedDto);
+    CveDto updatedDto = new CveDto("uuid-1", "CVE-2", "Some CVE description 2", 7.0, 1.0, 2.0, 3L, 4L, 5L, 6L);
+
+    cveDao.update(db.getSession(), updatedDto);
+
+    CveDto result = cveDao.selectById(db.getSession(), updatedDto.id()).orElseGet(() -> fail("CVE not found in database"));
+    assertThat(result).usingRecursiveComparison().ignoringFields("createdAt").isEqualTo(updatedDto);
+    assertThat(result.createdAt()).isEqualTo(insertedDto.createdAt());
+  }
+
 }
index c974ae08e9f4e78469070706ea15b4bfc40f1f7d..df6b5e5fed403ee878e4b5255527ed7a8e12aa94 100644 (file)
@@ -36,4 +36,9 @@ public class CveCweDao implements Dao {
   public Set<String> selectByCveUuid(DbSession dbSession, String cveUuid) {
     return mapper(dbSession).selectByCveUuid(cveUuid);
   }
+
+  public void deleteByCveUuid(DbSession dbSession, String cveUuid) {
+    mapper(dbSession).deleteByCveUuid(cveUuid);
+  }
+
 }
index 612eb7ba67f6583810d90e5380ce0446b7d24cc4..886a690cd68bf908335343df80840cb5772ad1df 100644 (file)
@@ -25,4 +25,6 @@ public interface CveCweMapper {
   void insert(CveCweDto cveCweDto);
 
   Set<String> selectByCveUuid(String cveUuid);
+
+  void deleteByCveUuid(String cveUuid);
 }
index 640ed6eb17379df783f05c7eed0146d26d8b4cce..d7201f8ee27057ce51609aa04d24129dc3077822 100644 (file)
@@ -33,6 +33,10 @@ public class CveDao implements Dao {
     return Optional.ofNullable(mapper(dbSession).selectById(id));
   }
 
+  public void update(DbSession dbSession, CveDto cveDto) {
+    mapper(dbSession).update(cveDto);
+  }
+
   private static CveMapper mapper(DbSession dbSession) {
     return dbSession.getMapper(CveMapper.class);
   }
index 1cf344709ce5196f3da2b034e4b9ccfa168b4ce2..f4ea3c5325bdb2868ab01d47aef90f3544fd68a2 100644 (file)
@@ -23,4 +23,6 @@ public interface CveMapper {
   void insert(CveDto cveDto);
 
   CveDto selectById(String id);
+
+  void update(CveDto cveDto);
 }
index c60fdb3075d6a46b787ef8ea67d217e5625d0561..0107b8c399c95e14e322f1b0dcab2f4c1d69ec4b 100644 (file)
@@ -166,7 +166,8 @@ public final class IssueDto implements Serializable {
       .setCleanCodeAttribute(issue.getCleanCodeAttribute())
       // technical dates
       .setCreatedAt(now)
-      .setUpdatedAt(now);
+      .setUpdatedAt(now)
+      .setCveId(issue.getCveId());
   }
 
   @NotNull
@@ -880,8 +881,9 @@ public final class IssueDto implements Serializable {
     return cveId;
   }
 
-  public void setCveId(String cveId) {
+  public IssueDto setCveId(@Nullable String cveId) {
     this.cveId = cveId;
+    return this;
   }
 
   @Override
index 5002efd6e28135aba24fcc75823d0da3f0f3c34d..b335eac573100387d40f55bd20a26c7347bdbeb0 100644 (file)
@@ -18,4 +18,9 @@
     where cve_uuid = #{cveUuid,jdbcType=VARCHAR}
   </select>
 
+  <delete id="deleteByCveUuid" parameterType="string">
+    delete from cve_cwe
+    where cve_uuid = #{cveUuid,jdbcType=VARCHAR}
+  </delete>
+
 </mapper>
index 22833f96b4f2c6eb19742ed1d9290232d370946c..83d45d500c8ef1e46113395479026ea6a33d8c82 100644 (file)
@@ -15,7 +15,7 @@
       c.updated_at as updatedAt
     </sql>
 
-   <insert id="insert" parameterType="org.sonar.db.dependency.CveDto" useGeneratedKeys="false">
+   <insert id="insert" parameterType="Cve" useGeneratedKeys="false">
     insert into cves (
       uuid,
       id,
@@ -41,7 +41,7 @@
     )
    </insert>
 
-  <select id="selectById" parameterType="string" resultType="org.sonar.db.dependency.CveDto">
+  <select id="selectById" parameterType="string" resultType="Cve">
     select <include refid="cveColumns"/>
     from
       cves c
       c.id = #{id, jdbcType=VARCHAR}
   </select>
 
+  <update id="update" parameterType="Cve" useGeneratedKeys="false">
+    update cves
+    set
+      id = #{id, jdbcType=VARCHAR},
+      description = #{description, jdbcType=VARCHAR},
+      cvss_score = #{cvssScore, jdbcType=DOUBLE},
+      epss_score = #{epssScore, jdbcType=DOUBLE},
+      epss_percentile = #{epssPercentile, jdbcType=DOUBLE},
+      published_at = #{publishedAt, jdbcType=BIGINT},
+      last_modified_at = #{lastModifiedAt, jdbcType=BIGINT},
+      updated_at = #{updatedAt, jdbcType=BIGINT}
+    where
+      uuid = #{uuid, jdbcType=BIGINT}
+  </update>
+
 </mapper>
index 83427a1a193eac899eb75c2971824e4a171e8202..7480488b75e8f5c2762c65d7a74c7303b8902f26 100644 (file)
@@ -140,6 +140,8 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
   private Map<SoftwareQuality, org.sonar.api.issue.impact.Severity> impacts = new EnumMap<>(SoftwareQuality.class);
   private CleanCodeAttribute cleanCodeAttribute = null;
 
+  private String cveId = null;
+
   @Override
   public String key() {
     return key;
@@ -771,4 +773,13 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
   public Date getUpdateDate() {
     return updateDate;
   }
+
+  public String getCveId() {
+    return cveId;
+  }
+
+  public DefaultIssue setCveId(@Nullable String cveId) {
+    this.cveId = cveId;
+    return this;
+  }
 }