Repository: OpenAEV-Platform/openaev (java)

Issue:
Add cve collector
## Context
The implementation of a "CVE Collector" feature aims to leverage OpenCTI's existing framework around NIST to systematically gather and catalog common vulnerabilities and exposures (CVEs) directly within OpenBAS. This integration capitalizes on robust, authoritative vulnerability data, enabling users to proactively manage and assess security risks more effectively. By utilizing an established standard, we ensure that our users have access to comprehensive and up-to-date vulnerability information, improving their ability to anticipate and mitigate security threats.

## Use case
The use case involves organizations that need to maintain an updated inventory of CVEs affecting their systems. These organizations require a reliable and automated method to gather the latest vulnerabilities to assess their impact and prioritize remediation efforts. Without a dedicated CVE Collector feature, users may struggle to keep their vulnerability information current and consistent, hindering their overall security posture.

## Current Workaround
Currently, users may manually gather CVE information from various sources, including NIST databases and other security advisories. This manual process is inefficient and can lead to incomplete or outdated vulnerability inventories, as it relies heavily on continuous user intervention.

## Proposed Solution
The proposed solution involves integrating a CVE Collector within OpenBAS that automatically pulls and updates CVE information from OpenCTI's NIST-based framework. This feature would keep users' vulnerability inventories current by systematically incorporating new CVEs as they are published, providing a streamlined and dependable process for vulnerability management.

## Additional Information
Further details on technical integration with OpenCTI and how the data will be structured within OpenBAS can be provided as development progresses. Collaboration with OpenCTI development resources may be necessary to ensure seamless integration.

## If the feature request is approved, would you be willing to submit a PR?
Yes, help can be provided if assistance is needed in submitting a PR.


Discussion:

Patch:
diff --git a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerCollector.java b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerCollector.java
--- a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerCollector.java
+++ b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerCollector.java
@@ -2,7 +2,7 @@
 
 import io.openbas.collectors.expectations_expiration_manager.config.ExpectationsExpirationManagerConfig;
 import io.openbas.collectors.expectations_expiration_manager.service.ExpectationsExpirationManagerService;
-import io.openbas.integrations.CollectorService;
+import io.openbas.rest.collector.service.CollectorService;
 import jakarta.annotation.PostConstruct;
 import java.time.Duration;
 import lombok.RequiredArgsConstructor;
diff --git a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerJob.java b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerJob.java
--- a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerJob.java
+++ b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/ExpectationsExpirationManagerJob.java
@@ -2,7 +2,7 @@
 
 import io.openbas.collectors.expectations_expiration_manager.config.ExpectationsExpirationManagerConfig;
 import io.openbas.collectors.expectations_expiration_manager.service.ExpectationsExpirationManagerService;
-import io.openbas.integrations.CollectorService;
+import io.openbas.rest.collector.service.CollectorService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
diff --git a/openbas-api/src/main/java/io/openbas/collectors/expectations_vulnerability_manager/ExpectationsVulnerabilityManagerCollector.java b/openbas-api/src/main/java/io/openbas/collectors/expectations_vulnerability_manager/ExpectationsVulnerabilityManagerCollector.java
--- a/openbas-api/src/main/java/io/openbas/collectors/expectations_vulnerability_manager/ExpectationsVulnerabilityManagerCollector.java
+++ b/openbas-api/src/main/java/io/openbas/collectors/expectations_vulnerability_manager/ExpectationsVulnerabilityManagerCollector.java
@@ -1,6 +1,6 @@
 package io.openbas.collectors.expectations_vulnerability_manager;
 
-import io.openbas.integrations.CollectorService;
+import io.openbas.rest.collector.service.CollectorService;
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
diff --git a/openbas-api/src/main/java/io/openbas/migration/V4_14__Add_collector_state.java b/openbas-api/src/main/java/io/openbas/migration/V4_14__Add_collector_state.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/migration/V4_14__Add_collector_state.java
@@ -0,0 +1,22 @@
+package io.openbas.migration;
+
+import java.sql.Statement;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+import org.springframework.stereotype.Component;
+
+@Component
+public class V4_14__Add_collector_state extends BaseJavaMigration {
+
+  @Override
+  public void migrate(Context context) throws Exception {
+    Statement select = context.getConnection().createStatement();
+    select.execute(
+        """
+                  ALTER TABLE collectors
+                    ADD COLUMN collector_state JSONB DEFAULT '{}' ;
+                  """);
+
+    select.execute("ALTER TABLE cves RENAME COLUMN cve_cvss TO cve_cvss_v31;");
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/rest/collector/CollectorApi.java b/openbas-api/src/main/java/io/openbas/rest/collector/CollectorApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/collector/CollectorApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/collector/CollectorApi.java
@@ -2,54 +2,33 @@
 
 import static io.openbas.database.model.User.ROLE_ADMIN;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.openbas.config.OpenBASConfig;
 import io.openbas.database.model.Collector;
 import io.openbas.database.repository.CollectorRepository;
 import io.openbas.database.repository.SecurityPlatformRepository;
 import io.openbas.rest.collector.form.CollectorCreateInput;
 import io.openbas.rest.collector.form.CollectorUpdateInput;
-import io.openbas.rest.exception.ElementNotFoundException;
+import io.openbas.rest.collector.service.CollectorService;
 import io.openbas.rest.helper.RestBehavior;
 import io.openbas.service.FileService;
-import jakarta.annotation.Resource;
 import jakarta.transaction.Transactional;
 import jakarta.validation.Valid;
 import java.time.Instant;
 import java.util.Optional;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
 import org.springframework.http.MediaType;
 import org.springframework.security.access.annotation.Secured;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 @RestController
+@RequiredArgsConstructor
 public class CollectorApi extends RestBehavior {
 
-  @Resource private OpenBASConfig openBASConfig;
+  private final CollectorService collectorService;
+  private final CollectorRepository collectorRepository;
+  private final SecurityPlatformRepository securityPlatformRepository;
 
-  private CollectorRepository collectorRepository;
-
-  private FileService fileService;
-
-  private SecurityPlatformRepository securityPlatformRepository;
-
-  @Resource protected ObjectMapper mapper;
-
-  @Autowired
-  public void setFileService(FileService fileService) {
-    this.fileService = fileService;
-  }
-
-  @Autowired
-  public void setCollectorRepository(CollectorRepository collectorRepository) {
-    this.collectorRepository = collectorRepository;
-  }
-
-  @Autowired
-  public void setSecurityPlatformRepository(SecurityPlatformRepository securityPlatformRepository) {
-    this.securityPlatformRepository = securityPlatformRepository;
-  }
+  private final FileService fileService;
 
   @GetMapping("/api/collectors")
   public Iterable<Collector> collectors() {
@@ -76,13 +55,18 @@ private Collector updateCollector(
     return collectorRepository.save(collector);
   }
 
+  @GetMapping("/api/collectors/{collectorId}")
+  @Secured(ROLE_ADMIN)
+  public Collector getCollector(@PathVariable String collectorId) {
+    return collectorService.collector(collectorId);
+  }
+
   @Secured(ROLE_ADMIN)
   @PutMapping("/api/collectors/{collectorId}")
   @Transactional(rollbackOn = Exception.class)
   public Collector updateCollector(
       @PathVariable String collectorId, @Valid @RequestBody CollectorUpdateInput input) {
-    Collector collector =
-        collectorRepository.findById(collectorId).orElseThrow(ElementNotFoundException::new);
+    Collector collector = collectorService.collector(collectorId);
     return updateCollector(
         collector,
         collector.getType(),
diff --git a/openbas-framework/src/main/java/io/openbas/integrations/CollectorService.java b/openbas-api/src/main/java/io/openbas/rest/collector/service/CollectorService.java
similarity index 63%
rename from openbas-framework/src/main/java/io/openbas/integrations/CollectorService.java
rename to openbas-api/src/main/java/io/openbas/rest/collector/service/CollectorService.java
--- a/openbas-framework/src/main/java/io/openbas/integrations/CollectorService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/collector/service/CollectorService.java
@@ -1,37 +1,50 @@
-package io.openbas.integrations;
+package io.openbas.rest.collector.service;
 
 import static io.openbas.service.FileService.COLLECTORS_IMAGES_BASE_PATH;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import io.openbas.database.model.Collector;
 import io.openbas.database.repository.CollectorRepository;
+import io.openbas.rest.exception.ElementNotFoundException;
 import io.openbas.service.FileService;
 import jakarta.annotation.Resource;
 import jakarta.transaction.Transactional;
-import jakarta.validation.constraints.NotBlank;
 import java.io.InputStream;
-import org.springframework.beans.factory.annotation.Autowired;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+@Slf4j
 @Service
+@RequiredArgsConstructor
 public class CollectorService {
 
   @Resource protected ObjectMapper mapper;
 
-  private FileService fileService;
+  private final CollectorRepository collectorRepository;
+  private final FileService fileService;
 
-  private CollectorRepository collectorRepository;
+  // -- CRUD --
 
-  @Resource
-  public void setFileService(FileService fileService) {
-    this.fileService = fileService;
+  public Collector collector(String id) {
+    return collectorRepository
+        .findById(id)
+        .orElseThrow(() -> new ElementNotFoundException("Collector not found with id: " + id));
   }
 
-  @Autowired
-  public void setCollectorRepository(CollectorRepository collectorRepository) {
-    this.collectorRepository = collectorRepository;
+  public Collector updateCollectorState(Collector collectorToUpdate, ObjectNode newState) {
+    ObjectNode state =
+        Optional.ofNullable(collectorToUpdate.getState()).orElse(mapper.createObjectNode());
+    newState
+        .fieldNames()
+        .forEachRemaining(fieldName -> state.set(fieldName, newState.get(fieldName)));
+    return collectorRepository.save(collectorToUpdate);
   }
 
+  // -- ACTION --
+
   @Transactional
   public void register(String id, String type, String name, InputStream iconData) throws Exception {
     if (iconData != null) {
@@ -61,12 +74,4 @@ public void register(String id, String type, String name, InputStream iconData)
       collectorRepository.save(newCollector);
     }
   }
-
-  // -- CRUD --
-
-  public Collector collector(@NotBlank final String collectorId) {
-    return collectorRepository
-        .findById(collectorId)
-        .orElseThrow(() -> new RuntimeException("Collector not found"));
-  }
 }
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java b/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
@@ -4,10 +4,7 @@
 import static io.openbas.database.model.User.ROLE_USER;
 
 import io.openbas.aop.LogExecutionTime;
-import io.openbas.rest.cve.form.CveCreateInput;
-import io.openbas.rest.cve.form.CveOutput;
-import io.openbas.rest.cve.form.CveSimple;
-import io.openbas.rest.cve.form.CveUpdateInput;
+import io.openbas.rest.cve.form.*;
 import io.openbas.rest.cve.service.CveService;
 import io.openbas.rest.helper.RestBehavior;
 import io.openbas.utils.mapper.CveMapper;
@@ -16,6 +13,7 @@
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.transaction.Transactional;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.security.access.annotation.Secured;
@@ -61,6 +59,14 @@ public CveSimple createCve(@Valid @RequestBody CveCreateInput input) {
     return cveMapper.toCveSimple(cveService.createCve(input));
   }
 
+  @Secured(ROLE_ADMIN)
+  @Operation(summary = "Bulk insert CVEs")
+  @LogExecutionTime
+  @PostMapping(CVE_API + "/bulk")
+  public void bulkInsertCVEsForCollector(@Valid @RequestBody @NotNull CVEBulkInsertInput input) {
+    this.cveService.bulkUpsertCVEs(input);
+  }
+
   @Secured(ROLE_ADMIN)
   @Operation(summary = "Update an existing CVE")
   @PutMapping(CVE_API + "/{cveId}")
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/form/CVEBulkInsertInput.java b/openbas-api/src/main/java/io/openbas/rest/cve/form/CVEBulkInsertInput.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/form/CVEBulkInsertInput.java
@@ -0,0 +1,29 @@
+package io.openbas.rest.cve.form;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.openbas.config.AppConfig;
+import jakarta.validation.constraints.NotNull;
+import java.time.Instant;
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class CVEBulkInsertInput {
+
+  @JsonProperty("cves")
+  @NotNull(message = AppConfig.MANDATORY_MESSAGE)
+  private List<CveCreateInput> cves;
+
+  @JsonProperty("last_modified_date_fetched")
+  private Instant lastModifiedDateFetched;
+
+  @JsonProperty("last_index")
+  private Integer lastIndex;
+
+  @JsonProperty("initial_dataset_completed")
+  private Boolean initialDatasetCompleted;
+
+  @JsonProperty("source_identifier")
+  @NotNull(message = AppConfig.MANDATORY_MESSAGE)
+  private String sourceIdentifier;
+}
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/form/CveCreateInput.java b/openbas-api/src/main/java/io/openbas/rest/cve/form/CveCreateInput.java
--- a/openbas-api/src/main/java/io/openbas/rest/cve/form/CveCreateInput.java
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/form/CveCreateInput.java
@@ -23,7 +23,7 @@ public class CveCreateInput extends CveInput {
   @NotNull
   @DecimalMin("0.0")
   @DecimalMax("10.0")
-  @JsonProperty("cve_cvss")
+  @JsonProperty("cve_cvss_v31")
   @Schema(description = "CVSS score", example = "7.5", minimum = "0.0", maximum = "10.0")
-  private BigDecimal cvss;
+  private BigDecimal cvssV31;
 }
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/form/CveSimple.java b/openbas-api/src/main/java/io/openbas/rest/cve/form/CveSimple.java
--- a/openbas-api/src/main/java/io/openbas/rest/cve/form/CveSimple.java
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/form/CveSimple.java
@@ -27,9 +27,9 @@ public class CveSimple {
   private String externalId;
 
   @NotNull
-  @JsonProperty("cve_cvss")
+  @JsonProperty("cve_cvss_v31")
   @Schema(description = "CVSS score", example = "7.8")
-  private BigDecimal cvss;
+  private BigDecimal cvssV31;
 
   @JsonProperty("cve_published")
   @Schema(description = "CVE published date")
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/service/CveService.java b/openbas-api/src/main/java/io/openbas/rest/cve/service/CveService.java
--- a/openbas-api/src/main/java/io/openbas/rest/cve/service/CveService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/service/CveService.java
@@ -1,21 +1,29 @@
 package io.openbas.rest.cve.service;
 
+import static io.openbas.helper.StreamHelper.fromIterable;
 import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import io.openbas.config.cache.LicenseCacheManager;
+import io.openbas.database.model.Collector;
 import io.openbas.database.model.Cve;
 import io.openbas.database.model.Cwe;
 import io.openbas.database.repository.CveRepository;
 import io.openbas.database.repository.CweRepository;
 import io.openbas.ee.Ee;
-import io.openbas.rest.cve.form.CveCreateInput;
-import io.openbas.rest.cve.form.CveUpdateInput;
-import io.openbas.rest.cve.form.CweInput;
+import io.openbas.rest.collector.service.CollectorService;
+import io.openbas.rest.cve.form.*;
 import io.openbas.rest.exception.ElementNotFoundException;
 import io.openbas.utils.pagination.SearchPaginationInput;
+import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -24,6 +32,7 @@
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 @Slf4j
 @Service
@@ -32,11 +41,15 @@ public class CveService {
 
   private static final String CVE_NOT_FOUND_MSG = "CVE not found with id: ";
 
+  private final CollectorService collectorService;
   private final Ee eeService;
+
   private final CveRepository cveRepository;
   private final CweRepository cweRepository;
   private final LicenseCacheManager licenseCacheManager;
 
+  @Resource protected ObjectMapper mapper;
+
   public Cve createCve(final @Valid CveCreateInput input) {
     final Cve cve = new Cve();
     if (eeService.isEnterpriseLicenseInactive(licenseCacheManager.getEnterpriseEditionInfo())) {
@@ -47,6 +60,54 @@ public Cve createCve(final @Valid CveCreateInput input) {
     return cveRepository.save(cve);
   }
 
+  private List<Cve> batchUpsertCves(List<CveCreateInput> cveInputs) {
+    // Extract external IDs
+    Set<String> externalIds =
+        cveInputs.stream().map(CveCreateInput::getExternalId).collect(Collectors.toSet());
+
+    // Batch fetch existing CVEs
+    Map<String, Cve> existingCvesMap =
+        cveRepository.findAllByExternalIdIn(externalIds.stream().toList()).stream()
+            .collect(Collectors.toMap(Cve::getExternalId, Function.identity()));
+
+    // Process with pre-fetched data
+    List<Cve> cves =
+        cveInputs.stream()
+            .map(
+                cveInput -> {
+                  Cve cve = existingCvesMap.getOrDefault(cveInput.getExternalId(), new Cve());
+                  cve.setUpdateAttributes(cveInput);
+                  updateCweAssociations(cve, cveInput.getCwes());
+                  return cve;
+                })
+            .toList();
+
+    return fromIterable(cveRepository.saveAll(cves));
+  }
+
+  private void updateCollectorStateFromCVEBulkInsertInput(
+      Collector collector, @NotNull CVEBulkInsertInput inputs) {
+    ObjectNode collectorNewState = mapper.createObjectNode();
+    collectorNewState.put(
+        "last_modified_date_fetched", inputs.getLastModifiedDateFetched().toString());
+    collectorNewState.put("last_index", inputs.getLastIndex().toString());
+    collectorNewState.put("initial_dataset_completed", inputs.getInitialDatasetCompleted());
+    this.collectorService.updateCollectorState(collector, collectorNewState);
+  }
+
+  @Transactional(rollbackFor = Exception.class)
+  public void bulkUpsertCVEs(@NotNull CVEBulkInsertInput inputs) {
+    Collector collector = this.collectorService.collector(inputs.getSourceIdentifier());
+
+    List<Cve> cves = this.batchUpsertCves(inputs.getCves());
+    this.updateCollectorStateFromCVEBulkInsertInput(collector, inputs);
+
+    log.info(
+        "Bulk upsert {} CVEs with last modified date fetched: {}",
+        cves.size(),
+        inputs.getLastModifiedDateFetched());
+  }
+
   public Page<Cve> searchCves(final @Valid SearchPaginationInput input) {
     return buildPaginationJPA(
         (Specification<Cve> spec, Pageable pageable) -> cveRepository.findAll(spec, pageable),
diff --git a/openbas-api/src/main/java/io/openbas/rest/payload/service/PayloadUpsertService.java b/openbas-api/src/main/java/io/openbas/rest/payload/service/PayloadUpsertService.java
--- a/openbas-api/src/main/java/io/openbas/rest/payload/service/PayloadUpsertService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/payload/service/PayloadUpsertService.java
@@ -7,7 +7,7 @@
 import io.openbas.database.repository.AttackPatternRepository;
 import io.openbas.database.repository.PayloadRepository;
 import io.openbas.ee.Ee;
-import io.openbas.integrations.CollectorService;
+import io.openbas.rest.collector.service.CollectorService;
 import io.openbas.rest.document.DocumentService;
 import io.openbas.rest.payload.PayloadUtils;
 import io.openbas.rest.payload.form.PayloadUpsertInput;
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
@@ -30,7 +30,7 @@ public CveSimple toCveSimple(final Cve cve) {
     return CveSimple.builder()
         .id(cve.getId())
         .externalId(cve.getExternalId())
-        .cvss(cve.getCvss())
+        .cvssV31(cve.getCvssV31())
         .published(cve.getPublished())
         .build();
   }
@@ -42,7 +42,7 @@ public CveOutput toCveOutput(final Cve cve) {
     return CveOutput.builder()
         .id(cve.getId())
         .externalId(cve.getExternalId())
-        .cvss(cve.getCvss())
+        .cvssV31(cve.getCvssV31())
         .published(cve.getPublished())
         .sourceIdentifier(cve.getSourceIdentifier())
         .description(cve.getDescription())
diff --git a/openbas-model/src/main/java/io/openbas/database/model/Collector.java b/openbas-model/src/main/java/io/openbas/database/model/Collector.java
--- a/openbas-model/src/main/java/io/openbas/database/model/Collector.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Collector.java
@@ -4,6 +4,8 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.hypersistence.utils.hibernate.type.json.JsonType;
 import io.openbas.database.audit.ModelBaseListener;
 import jakarta.persistence.*;
 import jakarta.validation.constraints.NotBlank;
@@ -12,6 +14,7 @@
 import java.util.Objects;
 import lombok.Getter;
 import lombok.Setter;
+import org.hibernate.annotations.Type;
 
 @Getter
 @Setter
@@ -63,6 +66,11 @@ public class Collector implements Base {
   @JsonProperty("collector_security_platform")
   private SecurityPlatform securityPlatform;
 
+  @JsonProperty("collector_state")
+  @Column(name = "collector_state")
+  @Type(JsonType.class)
+  private ObjectNode state;
+
   @JsonIgnore
   @Override
   public boolean isUserHasAccess(User user) {
diff --git a/openbas-model/src/main/java/io/openbas/database/model/Cve.java b/openbas-model/src/main/java/io/openbas/database/model/Cve.java
--- a/openbas-model/src/main/java/io/openbas/database/model/Cve.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Cve.java
@@ -95,10 +95,10 @@ public enum VulnerabilityStatus {
   private List<String> referenceUrls = new ArrayList<>();
 
   @NotNull
-  @Column(name = "cve_cvss", precision = 3, scale = 1)
-  @JsonProperty("cve_cvss")
+  @Column(name = "cve_cvss_v31", precision = 3, scale = 1)
+  @JsonProperty("cve_cvss_v31")
   @Queryable(searchable = true, filterable = true, sortable = true)
-  private BigDecimal cvss;
+  private BigDecimal cvssV31;
 
   @ArraySchema(schema = @Schema(type = "string"))
   @ManyToMany
diff --git a/openbas-model/src/main/java/io/openbas/database/model/Cwe.java b/openbas-model/src/main/java/io/openbas/database/model/Cwe.java
--- a/openbas-model/src/main/java/io/openbas/database/model/Cwe.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Cwe.java
@@ -12,14 +12,10 @@
 import org.hibernate.annotations.UpdateTimestamp;
 import org.hibernate.annotations.UuidGenerator;
 
+@Data
 @Entity
 @Table(name = "cwes")
 @EntityListeners(ModelBaseListener.class)
-@Getter
-@Setter
-@NoArgsConstructor
-@AllArgsConstructor
-@EqualsAndHashCode(onlyExplicitlyIncluded = true)
 public class Cwe implements Base {
 
   @Id
diff --git a/openbas-model/src/main/java/io/openbas/database/repository/CveRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/CveRepository.java
--- a/openbas-model/src/main/java/io/openbas/database/repository/CveRepository.java
+++ b/openbas-model/src/main/java/io/openbas/database/repository/CveRepository.java
@@ -1,6 +1,7 @@
 package io.openbas.database.repository;
 
 import io.openbas.database.model.Cve;
+import java.util.List;
 import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.repository.CrudRepository;
@@ -10,4 +11,6 @@
 public interface CveRepository extends CrudRepository<Cve, String>, JpaSpecificationExecutor<Cve> {
 
   Optional<Cve> findByExternalId(String externalId);
+
+  List<Cve> findAllByExternalIdIn(List<String> externalIds);
 }


Test patch:
diff --git a/openbas-api/src/test/java/io/openbas/rest/CveApiTest.java b/openbas-api/src/test/java/io/openbas/rest/CveApiTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/CveApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/CveApiTest.java
@@ -1,6 +1,10 @@
 package io.openbas.rest;
 
+import static io.openbas.rest.cve.CveApi.CVE_API;
 import static io.openbas.utils.JsonUtils.asJsonString;
+import static io.openbas.utils.fixtures.CveInputFixture.CVE_EXTERNAL_ID;
+import static io.openbas.utils.fixtures.CveInputFixture.createDefaultCveCreateInput;
+import static java.time.Instant.now;
 import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
 import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
@@ -8,16 +12,22 @@
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.openbas.IntegrationTest;
+import io.openbas.database.model.Collector;
 import io.openbas.database.model.Cve;
 import io.openbas.database.repository.CveRepository;
+import io.openbas.rest.collector.service.CollectorService;
+import io.openbas.rest.cve.form.CVEBulkInsertInput;
 import io.openbas.rest.cve.form.CveCreateInput;
 import io.openbas.rest.cve.form.CveUpdateInput;
+import io.openbas.utils.fixtures.CollectorFixture;
+import io.openbas.utils.fixtures.composers.CollectorComposer;
 import io.openbas.utils.fixtures.composers.CveComposer;
 import io.openbas.utils.mockUser.WithMockAdminUser;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.annotation.Resource;
 import jakarta.transaction.Transactional;
 import java.math.BigDecimal;
+import java.util.List;
 import org.junit.jupiter.api.*;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
@@ -29,14 +39,24 @@
 @DisplayName("CVE API Integration Tests")
 class CveApiTest extends IntegrationTest {
 
-  private static final String CVE_URI = "/api/cves";
-
   @Resource protected ObjectMapper mapper;
   @Autowired private MockMvc mvc;
+  private Collector collector;
 
   @Autowired private CveComposer cveComposer;
+  @Autowired private CollectorComposer collectorComposer;
+  @Autowired private CollectorService collectorService;
   @Autowired private CveRepository cveRepository;
 
+  @BeforeAll
+  void init() {
+    collector =
+        collectorComposer
+            .forCollector(CollectorFixture.createDefaultCollector("CS"))
+            .persist()
+            .get();
+  }
+
   @BeforeEach
   void setUp() {
     cveComposer.reset();
@@ -52,12 +72,12 @@ class WhenWorkingWithCves {
     void shouldCreateNewCve() throws Exception {
       CveCreateInput input = new CveCreateInput();
       input.setExternalId("CVE-2025-1234");
-      input.setCvss(new BigDecimal("5.2"));
+      input.setCvssV31(new BigDecimal("5.2"));
       input.setDescription("Test summary for CVE creation");
 
       String response =
           mvc.perform(
-                  post(CVE_URI)
+                  post(CVE_API)
                       .contentType(MediaType.APPLICATION_JSON)
                       .content(asJsonString(input)))
               .andExpect(status().isOk())
@@ -73,13 +93,13 @@ void shouldCreateNewCve() throws Exception {
     void shouldFetchCveById() throws Exception {
       Cve cve = new Cve();
       cve.setExternalId("CVE-2025-5678");
-      cve.setCvss(new BigDecimal("8.9"));
+      cve.setCvssV31(new BigDecimal("8.9"));
       cve.setDescription("Test CVE");
 
       cveComposer.forCve(cve).persist();
 
       String response =
-          mvc.perform(get(CVE_URI + "/" + cve.getId()))
+          mvc.perform(get(CVE_API + "/" + cve.getId()))
               .andExpect(status().isOk())
               .andReturn()
               .getResponse()
@@ -93,7 +113,7 @@ void shouldFetchCveById() throws Exception {
     void shouldUpdateCve() throws Exception {
       Cve cve = new Cve();
       cve.setExternalId("CVE-2025-5679");
-      cve.setCvss(new BigDecimal("4.5"));
+      cve.setCvssV31(new BigDecimal("4.5"));
       cve.setDescription("Old description");
       cveComposer.forCve(cve).persist();
 
@@ -101,7 +121,7 @@ void shouldUpdateCve() throws Exception {
       updateInput.setDescription("Updated Summary");
 
       mvc.perform(
-              put(CVE_URI + "/" + cve.getId())
+              put(CVE_API + "/" + cve.getId())
                   .contentType(MediaType.APPLICATION_JSON)
                   .content(asJsonString(updateInput)))
           .andExpect(status().isOk())
@@ -116,16 +136,44 @@ void shouldUpdateCve() throws Exception {
                   cveRepository.findById(cve.getId()).map(cve1 -> cve1.getDescription()).get()));
     }
 
+    @Test
+    @DisplayName("Should bulk insert multiple CVEs")
+    void shouldBulkInsertCVEs() throws Exception {
+      // -- PREPARE -
+      CveCreateInput input = createDefaultCveCreateInput();
+      CVEBulkInsertInput inputs = new CVEBulkInsertInput();
+      inputs.setSourceIdentifier(collector.getId());
+      inputs.setLastModifiedDateFetched(now());
+      inputs.setLastIndex(1234);
+      inputs.setInitialDatasetCompleted(false);
+      inputs.setCves(List.of(input));
+
+      // -- EXECUTE --
+
+      mvc.perform(
+              post(CVE_API + "/bulk")
+                  .content(asJsonString(inputs))
+                  .contentType(MediaType.APPLICATION_JSON)
+                  .accept(MediaType.APPLICATION_JSON))
+          .andExpect(status().isOk())
+          .andReturn()
+          .getResponse()
+          .getContentAsString();
+
+      // -- ASSERT --
+      Assertions.assertTrue(cveRepository.findByExternalId(CVE_EXTERNAL_ID).isPresent());
+    }
+
     @Test
     @DisplayName("Should delete a CVE")
     void shouldDeleteCve() throws Exception {
       Cve cve = new Cve();
       cve.setExternalId("CVE-2025-5679");
-      cve.setCvss(new BigDecimal("7.5"));
+      cve.setCvssV31(new BigDecimal("7.5"));
       cve.setDescription("To be deleted");
       cveComposer.forCve(cve).persist();
 
-      mvc.perform(delete(CVE_URI + "/" + cve.getExternalId())).andExpect(status().isOk());
+      mvc.perform(delete(CVE_API + "/" + cve.getExternalId())).andExpect(status().isOk());
 
       Assertions.assertFalse(cveRepository.findById(cve.getExternalId()).isPresent());
     }
@@ -135,13 +183,13 @@ void shouldDeleteCve() throws Exception {
     void shouldReturnCvesOnSearch() throws Exception {
       Cve cve = new Cve();
       cve.setExternalId("CVE-2024-5679");
-      cve.setCvss(new BigDecimal("4.5"));
+      cve.setCvssV31(new BigDecimal("4.5"));
       cve.setDescription("Cve 1");
       cveComposer.forCve(cve).persist();
 
       Cve cve1 = new Cve();
       cve1.setExternalId("CVE-2025-5671");
-      cve1.setCvss(new BigDecimal("1.8"));
+      cve1.setCvssV31(new BigDecimal("1.8"));
       cve1.setDescription("Cve 2");
       cveComposer.forCve(cve1).persist();
 
@@ -151,7 +199,7 @@ void shouldReturnCvesOnSearch() throws Exception {
 
       String response =
           mvc.perform(
-                  post(CVE_URI + "/search")
+                  post(CVE_API + "/search")
                       .content(asJsonString(input))
                       .contentType(MediaType.APPLICATION_JSON)
                       .accept(MediaType.APPLICATION_JSON))
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/CveInputFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/CveInputFixture.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/CveInputFixture.java
@@ -0,0 +1,19 @@
+package io.openbas.utils.fixtures;
+
+import io.openbas.rest.cve.form.CveCreateInput;
+import java.math.BigDecimal;
+
+public class CveInputFixture {
+
+  public static final String CVE_EXTERNAL_ID = "CVE-2025-5679";
+  public static final BigDecimal CVE_CVSS_V31 = new BigDecimal("4.5");
+  public static final String CVE_DESCRIPTION = "Description";
+
+  public static CveCreateInput createDefaultCveCreateInput() {
+    CveCreateInput input = new CveCreateInput();
+    input.setExternalId(CVE_EXTERNAL_ID);
+    input.setCvssV31(CVE_CVSS_V31);
+    input.setDescription(CVE_DESCRIPTION);
+    return input;
+  }
+}