diff --git a/openaev-api/src/main/java/io/openaev/migration/V4_44__Rename_cves_table_to_vulnerabilities.java b/openaev-api/src/main/java/io/openaev/migration/V4_44__Rename_cves_table_to_vulnerabilities.java new file mode 100644 index 0000000000..259fb8a743 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/migration/V4_44__Rename_cves_table_to_vulnerabilities.java @@ -0,0 +1,209 @@ +package io.openaev.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_44__Rename_cves_table_to_vulnerabilities extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + try (Statement stmt = context.getConnection().createStatement()) { + + // --- Rename main table --- + stmt.execute( + """ + ALTER TABLE cves RENAME TO vulnerabilities; + """); + // + // --- Rename columns in vulnerabilities table --- + stmt.execute( + """ + ALTER TABLE vulnerabilities RENAME COLUMN cve_id TO vulnerability_id; + ALTER TABLE vulnerabilities RENAME COLUMN cve_external_id TO vulnerability_external_id; + ALTER TABLE vulnerabilities RENAME COLUMN cve_source_identifier TO vulnerability_source_identifier; + ALTER TABLE vulnerabilities RENAME COLUMN cve_published TO vulnerability_published; + ALTER TABLE vulnerabilities RENAME COLUMN cve_description TO vulnerability_description; + ALTER TABLE vulnerabilities RENAME COLUMN cve_vuln_status TO vulnerability_vuln_status; + ALTER TABLE vulnerabilities RENAME COLUMN cve_cvss_v31 TO vulnerability_cvss_v31; + ALTER TABLE vulnerabilities RENAME COLUMN cve_cisa_exploit_add TO vulnerability_cisa_exploit_add; + ALTER TABLE vulnerabilities RENAME COLUMN cve_cisa_action_due TO vulnerability_cisa_action_due; + ALTER TABLE vulnerabilities RENAME COLUMN cve_cisa_required_action TO vulnerability_cisa_required_action; + ALTER TABLE vulnerabilities RENAME COLUMN cve_cisa_vulnerability_name TO vulnerability_cisa_vulnerability_name; + ALTER TABLE vulnerabilities RENAME COLUMN cve_remediation TO vulnerability_remediation; + ALTER TABLE vulnerabilities RENAME COLUMN cve_created_at TO vulnerability_created_at; + ALTER TABLE vulnerabilities RENAME COLUMN cve_updated_at TO vulnerability_updated_at; + """); + + // --- Rename indexes of the vulnerabilities table --- + stmt.execute( + """ + ALTER INDEX idx_cves_cvss RENAME TO idx_vulnerabilities_cvss; + ALTER INDEX idx_cves_published RENAME TO idx_vulnerabilities_published; + """); + + // --- Rename join table CVEs ↔ CWEs --- + stmt.execute( + """ + ALTER TABLE cves_cwes RENAME TO vulnerabilities_cwes; + ALTER TABLE vulnerabilities_cwes RENAME COLUMN cve_id TO vulnerability_id; + """); + + // --- Rename indexes of the join table --- + stmt.execute( + """ + ALTER INDEX idx_cves_cwes_cve_id RENAME TO idx_vulnerabilities_cwes_vulnerability_id; + ALTER INDEX idx_cves_cwes_cwe_id RENAME TO idx_vulnerabilities_cwes_cwe_id; + """); + + // --- Rename reference URL table --- + stmt.execute( + """ + ALTER TABLE cve_reference_urls RENAME TO vulnerability_reference_urls; + ALTER TABLE vulnerability_reference_urls RENAME COLUMN cve_id TO vulnerability_id; + ALTER TABLE vulnerability_reference_urls RENAME COLUMN cve_reference_url TO vulnerability_reference_url; + """); + + // --- Rename index of the reference URL table --- + stmt.execute( + """ + ALTER INDEX idx_cve_reference_urls_cve_id RENAME TO idx_vulnerability_reference_urls_vulnerability_id; + """); + } + } +} + +// ROLLBACK SCRIPT +// BEGIN; +// +// -- ============================================================================ +// -- 1. Rollback of the table vulnerability_reference_urls to cve_reference_urls +// -- ============================================================================ +// +// -- Rename the index of the reference URL table +// ALTER INDEX IF EXISTS idx_vulnerability_reference_urls_vulnerability_id +// RENAME TO idx_cve_reference_urls_cve_id; +// +// -- Rename the columns of the reference URL table +// ALTER TABLE IF EXISTS vulnerability_reference_urls +// RENAME COLUMN vulnerability_reference_url TO cve_reference_url; +// +// ALTER TABLE IF EXISTS vulnerability_reference_urls +// RENAME COLUMN vulnerability_id TO cve_id; +// +// -- Rename the reference URL table +// ALTER TABLE IF EXISTS vulnerability_reference_urls +// RENAME TO cve_reference_urls; +// +// -- ============================================================================ +// -- 2. Rollback of the join table vulnerabilities_cwes to cves_cwes +// -- ============================================================================ +// +// -- Rename the indexes of the join table +// ALTER INDEX IF EXISTS idx_vulnerabilities_cwes_cwe_id +// RENAME TO idx_cves_cwes_cwe_id; +// +// ALTER INDEX IF EXISTS idx_vulnerabilities_cwes_vulnerability_id +// RENAME TO idx_cves_cwes_cve_id; +// +// -- Rename the column in the join table +// ALTER TABLE IF EXISTS vulnerabilities_cwes +// RENAME COLUMN vulnerability_id TO cve_id; +// +// -- Rename the join table +// ALTER TABLE IF EXISTS vulnerabilities_cwes +// RENAME TO cves_cwes; +// +// -- ============================================================================ +// -- 3. Rollback of the main table vulnerabilities to cves +// -- ============================================================================ +// +// -- Rename the indexes of the main table +// ALTER INDEX IF EXISTS idx_vulnerabilities_published +// RENAME TO idx_cves_published; +// +// ALTER INDEX IF EXISTS idx_vulnerabilities_cvss +// RENAME TO idx_cves_cvss; +// +// -- Rename all columns in the vulnerabilities table back to their original names +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_updated_at TO cve_updated_at; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_created_at TO cve_created_at; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_remediation TO cve_remediation; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_cisa_vulnerability_name TO cve_cisa_vulnerability_name; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_cisa_required_action TO cve_cisa_required_action; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_cisa_action_due TO cve_cisa_action_due; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_cisa_exploit_add TO cve_cisa_exploit_add; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_cvss_v31 TO cve_cvss_v31; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_vuln_status TO cve_vuln_status; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_description TO cve_description; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_published TO cve_published; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_source_identifier TO cve_source_identifier; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_external_id TO cve_external_id; +// +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME COLUMN vulnerability_id TO cve_id; +// +// -- Rename the main table +// ALTER TABLE IF EXISTS vulnerabilities +// RENAME TO cves; +// +// -- ============================================================================ +// -- Validation and commit +// -- ============================================================================ +// +// -- Verify that the tables have been correctly renamed +// DO $$ +// BEGIN +// -- Verify the existence of the table cves +// IF NOT EXISTS (SELECT 1 FROM information_schema.tables +// WHERE table_schema = 'public' +// AND table_name = 'cves') THEN +// RAISE EXCEPTION 'Error: The table cves has not been correctly created'; +// END IF; +// +// -- Verify the existence of the table cves_cwes +// IF NOT EXISTS (SELECT 1 FROM information_schema.tables +// WHERE table_schema = 'public' +// AND table_name = 'cves_cwes') THEN +// RAISE EXCEPTION 'Error: The table cves_cwes has not been correctly created'; +// END IF; +// +// -- Verify the existence of the table cve_reference_urls +// IF NOT EXISTS (SELECT 1 FROM information_schema.tables +// WHERE table_schema = 'public' +// AND table_name = 'cve_reference_urls') THEN +// RAISE EXCEPTION 'Error: The table cve_reference_urls has not been correctly created'; +// END IF; +// +// RAISE NOTICE 'Rollback successfully performed. All tables have been restored.'; +// END $$; +// +// -- If everything is OK, commit the transaction +// COMMIT; diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/CveApi.java b/openaev-api/src/main/java/io/openaev/rest/cve/CveApi.java index ddd4a1039c..b54be08e66 100644 --- a/openaev-api/src/main/java/io/openaev/rest/cve/CveApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/cve/CveApi.java @@ -5,9 +5,11 @@ import io.openaev.database.model.Action; import io.openaev.database.model.ResourceType; import io.openaev.rest.cve.form.*; -import io.openaev.rest.cve.service.CveService; import io.openaev.rest.helper.RestBehavior; +import io.openaev.rest.vulnerability.form.*; +import io.openaev.rest.vulnerability.service.VulnerabilityService; import io.openaev.utils.mapper.CveMapper; +import io.openaev.utils.mapper.VulnerabilityMapper; import io.openaev.utils.pagination.SearchPaginationInput; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,69 +20,88 @@ import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; +/** + * @deprecated (since = "1.19.0", forRemoval = true) in favor of @See vulnerabilityApi + */ +@Deprecated(since = "1.19", forRemoval = true) @RestController @RequiredArgsConstructor -@Tag(name = "CVE API", description = "Operations related to CVEs") +@Tag(name = "Cve API", description = "Operations related to CVEs") public class CveApi extends RestBehavior { public static final String CVE_API = "/api/cves"; - private final CveService cveService; + private final VulnerabilityService vulnerabilityService; + private final VulnerabilityMapper vulnerabilityMapper; private final CveMapper cveMapper; @LogExecutionTime @Operation(summary = "Search CVEs") @PostMapping(CVE_API + "/search") - @RBAC(actionPerformed = Action.SEARCH, resourceType = ResourceType.CVE) + @RBAC(actionPerformed = Action.SEARCH, resourceType = ResourceType.VULNERABILITY) public Page searchCves(@Valid @RequestBody SearchPaginationInput input) { - return cveService.searchCves(input).map(cveMapper::toCveSimple); + return vulnerabilityService.searchVulnerabilities(input).map(cveMapper::toCveSimple); } @Operation(summary = "Get a CVE by ID", description = "Fetches detailed CVE info by ID") @GetMapping(CVE_API + "/{cveId}") - @RBAC(resourceId = "#cveId", actionPerformed = Action.READ, resourceType = ResourceType.CVE) + @RBAC( + resourceId = "#cveId", + actionPerformed = Action.READ, + resourceType = ResourceType.VULNERABILITY) public CveOutput getCve(@PathVariable String cveId) { - return cveMapper.toCveOutput(cveService.findById(cveId)); + return cveMapper.toCveOutput(vulnerabilityService.findById(cveId)); } @Operation( summary = "Get a CVE by external ID", description = "Fetches detailed CVE info by external CVE ID") @GetMapping(CVE_API + "/external-id/{externalId}") - @RBAC(resourceId = "#externalId", actionPerformed = Action.READ, resourceType = ResourceType.CVE) + @RBAC( + resourceId = "#externalId", + actionPerformed = Action.READ, + resourceType = ResourceType.VULNERABILITY) public CveOutput getCvebyExternalId(@PathVariable String externalId) { - return cveMapper.toCveOutput(cveService.findByExternalId(externalId)); + return cveMapper.toCveOutput(vulnerabilityService.findByExternalId(externalId)); } @Operation(summary = "Create a new CVE") @PostMapping(CVE_API) - @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.CVE) + @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.VULNERABILITY) @Transactional(rollbackOn = Exception.class) - public CveSimple createCve(@Valid @RequestBody CveCreateInput input) { - return cveMapper.toCveSimple(cveService.createCve(input)); + public CveSimple createCve(@Valid @RequestBody VulnerabilityCreateInput input) { + return cveMapper.toCveSimple(vulnerabilityService.createVulnerability(input)); } @Operation(summary = "Bulk insert CVEs") @LogExecutionTime @PostMapping(CVE_API + "/bulk") - @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.CVE) + @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.VULNERABILITY) public void bulkInsertCVEsForCollector(@Valid @RequestBody @NotNull CVEBulkInsertInput input) { - this.cveService.bulkUpsertCVEs(input); + this.vulnerabilityService.bulkUpsertVulnerabilities( + vulnerabilityMapper.fromCVEBulkInsertInput(input)); } @Operation(summary = "Update an existing CVE") @PutMapping(CVE_API + "/{cveId}") - @RBAC(resourceId = "#cveId", actionPerformed = Action.WRITE, resourceType = ResourceType.CVE) + @RBAC( + resourceId = "#cveId", + actionPerformed = Action.WRITE, + resourceType = ResourceType.VULNERABILITY) @Transactional(rollbackOn = Exception.class) - public CveSimple updateCve(@PathVariable String cveId, @Valid @RequestBody CveUpdateInput input) { - return cveMapper.toCveSimple(cveService.updateCve(cveId, input)); + public CveSimple updateCve( + @PathVariable String cveId, @Valid @RequestBody VulnerabilityUpdateInput input) { + return cveMapper.toCveSimple(vulnerabilityService.updateVulnerability(cveId, input)); } @Operation(summary = "Delete a CVE") @DeleteMapping(CVE_API + "/{cveId}") - @RBAC(resourceId = "#cveId", actionPerformed = Action.DELETE, resourceType = ResourceType.CVE) + @RBAC( + resourceId = "#cveId", + actionPerformed = Action.DELETE, + resourceType = ResourceType.VULNERABILITY) @Transactional(rollbackOn = Exception.class) public void deleteCve(@PathVariable String cveId) { - cveService.deleteById(cveId); + vulnerabilityService.deleteById(cveId); } } diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/form/CveInput.java b/openaev-api/src/main/java/io/openaev/rest/cve/form/CveInput.java index efbc6b5d6e..59f732755f 100644 --- a/openaev-api/src/main/java/io/openaev/rest/cve/form/CveInput.java +++ b/openaev-api/src/main/java/io/openaev/rest/cve/form/CveInput.java @@ -1,7 +1,8 @@ package io.openaev.rest.cve.form; import com.fasterxml.jackson.annotation.JsonProperty; -import io.openaev.database.model.Cve; +import io.openaev.database.model.Vulnerability; +import io.openaev.rest.vulnerability.form.CweInput; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -30,7 +31,7 @@ public class CveInput { @Enumerated(EnumType.STRING) @JsonProperty("cve_vuln_status") @Schema(description = "Vulnerability status", example = "ANALYZED") - private Cve.VulnerabilityStatus vulnStatus; + private Vulnerability.VulnerabilityStatus vulnStatus; @JsonProperty("cve_cisa_exploit_add") @Schema(description = "Date when CISA added the CVE to the exploited list") diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/form/CveOutput.java b/openaev-api/src/main/java/io/openaev/rest/cve/form/CveOutput.java index 3e0d1c22c9..8bd132e50d 100644 --- a/openaev-api/src/main/java/io/openaev/rest/cve/form/CveOutput.java +++ b/openaev-api/src/main/java/io/openaev/rest/cve/form/CveOutput.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.openaev.database.model.Cve; +import io.openaev.rest.vulnerability.form.CweOutput; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/service/CveService.java b/openaev-api/src/main/java/io/openaev/rest/cve/service/CveService.java deleted file mode 100644 index 69fc90f858..0000000000 --- a/openaev-api/src/main/java/io/openaev/rest/cve/service/CveService.java +++ /dev/null @@ -1,206 +0,0 @@ -package io.openaev.rest.cve.service; - -import static io.openaev.helper.StreamHelper.fromIterable; -import static io.openaev.utils.pagination.PaginationUtils.buildPaginationJPA; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.openaev.config.cache.LicenseCacheManager; -import io.openaev.database.model.*; -import io.openaev.database.repository.CveRepository; -import io.openaev.database.repository.CweRepository; -import io.openaev.ee.Ee; -import io.openaev.rest.collector.service.CollectorService; -import io.openaev.rest.cve.form.*; -import io.openaev.rest.exception.ElementNotFoundException; -import io.openaev.utils.pagination.SearchPaginationInput; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeanUtils; -import org.springframework.data.domain.Page; -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 -@RequiredArgsConstructor -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())) { - input.setRemediation(null); - } - cve.setUpdateAttributes(input); - updateCweAssociations(cve, input.getCwes()); - return cveRepository.save(cve); - } - - private List batchUpsertCves(List cveInputs) { - // Extract external IDs - Set externalIds = - cveInputs.stream().map(CveCreateInput::getExternalId).collect(Collectors.toSet()); - - // Batch fetch existing CVEs - Map existingCvesMap = - getVulnerabilitiesByExternalIds(externalIds).stream() - .collect(Collectors.toMap(Cve::getExternalId, Function.identity())); - - // Process with pre-fetched data - List 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 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 searchCves(final @Valid SearchPaginationInput input) { - return buildPaginationJPA( - (Specification spec, Pageable pageable) -> cveRepository.findAll(spec, pageable), - input, - Cve.class); - } - - public Cve updateCve(final String cveId, final @Valid CveUpdateInput input) { - final Cve existingCve = findById(cveId); - if (eeService.isEnterpriseLicenseInactive(licenseCacheManager.getEnterpriseEditionInfo())) { - input.setRemediation(null); - BeanUtils.copyProperties(input, existingCve, "remediation"); - } else { - existingCve.setUpdateAttributes(input); - } - updateCweAssociations(existingCve, input.getCwes()); - return cveRepository.save(existingCve); - } - - public Cve findById(final String cveId) { - return cveRepository - .findById(cveId) - .orElseThrow(() -> new ElementNotFoundException(CVE_NOT_FOUND_MSG + cveId)); - } - - public Set findAllByIdsOrThrowIfMissing(final Set vulnIds) { - Set vulns = this.cveRepository.getAllByIdInIgnoreCase(vulnIds); - throwIfMissing(vulnIds, vulns, Cve::getId); - return vulns; - } - - public Cve findByExternalId(String externalId) { - return cveRepository - .findByExternalId(externalId) - .orElseThrow(() -> new ElementNotFoundException(CVE_NOT_FOUND_MSG + externalId)); - } - - public Set findAllByExternalIdsOrThrowIfMissing(final Set vulnIds) { - Set vulns = getVulnerabilitiesByExternalIds(vulnIds); - throwIfMissing(vulnIds, vulns, Cve::getExternalId); - return vulns; - } - - private void throwIfMissing( - Set requiredIds, - Set fetchedVulnerabilities, - Function getId) { - - List fetchedIdLower = - fetchedVulnerabilities.stream().map(vuln -> getId.apply(vuln).toLowerCase()).toList(); - - List missingIds = - requiredIds.stream().filter(id -> !fetchedIdLower.contains(id.toLowerCase())).toList(); - - if (!missingIds.isEmpty()) { - throw new ElementNotFoundException( - String.format("Missing vulnerabilities: %s", String.join(", ", missingIds))); - } - } - - public void deleteById(final String cveId) { - cveRepository.deleteById(cveId); - } - - private void updateCweAssociations(Cve cve, List cweInputs) { - if (cweInputs == null || cweInputs.isEmpty()) { - cve.setCwes(Collections.emptyList()); - return; - } - - List cweEntities = - cweInputs.stream() - .map( - input -> - cweRepository - .findByExternalId(input.getExternalId()) - .orElseGet( - () -> { - Cwe newCwe = new Cwe(); - newCwe.setExternalId(input.getExternalId()); - newCwe.setSource(input.getSource()); - return cweRepository.save(newCwe); - })) - .collect(Collectors.toList()); - - cve.setCwes(cweEntities); - } - - /** - * Resolves external Vulnerability Refs from a set of vulnerability {@link Cve} entities. - * - * @param externalIds set vulnerability Refs - * @return set of resolved vulnerability entities - */ - public Set getVulnerabilitiesByExternalIds(Set externalIds) { - if (externalIds.isEmpty()) { - return Collections.emptySet(); - } - return this.cveRepository.getAllByExternalIdInIgnoreCase(externalIds); - } -} diff --git a/openaev-api/src/main/java/io/openaev/rest/inject/service/InjectAssistantService.java b/openaev-api/src/main/java/io/openaev/rest/inject/service/InjectAssistantService.java index cf2c58d051..a17f75fadc 100644 --- a/openaev-api/src/main/java/io/openaev/rest/inject/service/InjectAssistantService.java +++ b/openaev-api/src/main/java/io/openaev/rest/inject/service/InjectAssistantService.java @@ -265,7 +265,7 @@ private List buildInjectsForAnyPlatformAndArchitecture( */ public List generateInjectsWithTargetsByVulnerabilities( Scenario scenario, - Set vulnerabilities, + Set vulnerabilities, Map> assetGroupListMap, int injectsPerVulnerability, InjectorContract contractForPlaceholder) { @@ -288,10 +288,10 @@ public List generateInjectsWithTargetsByVulnerabilities( } private Map> computeMapVulnerabilityInjectorContracts( - Set vulnerabilities, int injectsPerVulnerability) { + Set vulnerabilities, int injectsPerVulnerability) { Set vulnerabilityExternalIds = vulnerabilities.stream() - .map(Cve::getExternalId) + .map(Vulnerability::getExternalId) .map(String::toLowerCase) .collect(Collectors.toSet()); @@ -318,14 +318,14 @@ private Map> computeMapVulnerabilityInjectorContra /** * Builds a set of {@link Inject} objects for a given vulnerability * - * @param vulnerability the {@link Cve} vulnerability to generate injects for + * @param vulnerability the {@link Vulnerability} vulnerability to generate injects for * @param injectorContracts related to this vulnerability * @param assetGroupListMap assets and associated endpoints involved in the automatic assignment * @param contractForPlaceholder contract to use for placeholder injects * @return a set of generated {@link Inject} objects, never {@code null} */ private Set buildInjectsWithTargetsByVulnerability( - Cve vulnerability, + Vulnerability vulnerability, Set injectorContracts, Map> assetGroupListMap, InjectorContract contractForPlaceholder) { @@ -334,7 +334,6 @@ private Set buildInjectsWithTargetsByVulnerability( return Set.of( buildManualInject(contractForPlaceholder, vulnerability.getExternalId(), null, null)); } - Set injects = new HashSet<>(); for (InjectorContract ic : injectorContracts) { Inject inject = diff --git a/openaev-api/src/main/java/io/openaev/rest/injector_contract/InjectorContractService.java b/openaev-api/src/main/java/io/openaev/rest/injector_contract/InjectorContractService.java index 02773e306a..96e9f08938 100644 --- a/openaev-api/src/main/java/io/openaev/rest/injector_contract/InjectorContractService.java +++ b/openaev-api/src/main/java/io/openaev/rest/injector_contract/InjectorContractService.java @@ -18,7 +18,6 @@ import io.openaev.injectors.email.EmailContract; import io.openaev.injectors.ovh.OvhSmsContract; import io.openaev.rest.attack_pattern.service.AttackPatternService; -import io.openaev.rest.cve.service.CveService; import io.openaev.rest.exception.ElementNotFoundException; import io.openaev.rest.injector_contract.form.InjectorContractAddInput; import io.openaev.rest.injector_contract.form.InjectorContractInput; @@ -26,6 +25,7 @@ import io.openaev.rest.injector_contract.form.InjectorContractUpdateMappingInput; import io.openaev.rest.injector_contract.output.InjectorContractBaseOutput; import io.openaev.rest.injector_contract.output.InjectorContractFullOutput; +import io.openaev.rest.vulnerability.service.VulnerabilityService; import io.openaev.service.UserService; import io.openaev.utils.TargetType; import jakarta.persistence.EntityManager; @@ -58,7 +58,7 @@ public class InjectorContractService { private final InjectorContractRepository injectorContractRepository; private final AttackPatternService attackPatternService; - private final CveService cveService; + private final VulnerabilityService vulnerabilityService; private final InjectorRepository injectorRepository; private final UserService userService; private final AttackPatternRepository attackPatternRepository; @@ -244,11 +244,11 @@ public InjectorContract updateInjectorContract( private void setVulnerabilitiesFromExternalOrInternalIds( List externalIds, List internalIds, InjectorContract injectorContract) { - Set vulns = new HashSet<>(); + Set vulns = new HashSet<>(); if (!externalIds.isEmpty()) { - vulns = cveService.findAllByExternalIdsOrThrowIfMissing(new HashSet<>(externalIds)); + vulns = vulnerabilityService.findAllByExternalIdsOrThrowIfMissing(new HashSet<>(externalIds)); } else if (!internalIds.isEmpty()) { - vulns = cveService.findAllByIdsOrThrowIfMissing(new HashSet<>(internalIds)); + vulns = vulnerabilityService.findAllByIdsOrThrowIfMissing(new HashSet<>(internalIds)); } injectorContract.setVulnerabilities(vulns); } @@ -263,7 +263,8 @@ public InjectorContract updateAttackPatternMappings( attackPatternService.findAllByInternalIdsThrowIfMissing( new HashSet<>(input.getAttackPatternsIds()))); injectorContract.setVulnerabilities( - cveService.findAllByIdsOrThrowIfMissing(new HashSet<>(input.getVulnerabilityIds()))); + vulnerabilityService.findAllByIdsOrThrowIfMissing( + new HashSet<>(input.getVulnerabilityIds()))); injectorContract.setUpdatedAt(Instant.now()); return injectorContractRepository.save(injectorContract); } diff --git a/openaev-api/src/main/java/io/openaev/rest/stream/StreamApi.java b/openaev-api/src/main/java/io/openaev/rest/stream/StreamApi.java index 8d6ccf43e3..fee16f3096 100644 --- a/openaev-api/src/main/java/io/openaev/rest/stream/StreamApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/stream/StreamApi.java @@ -67,7 +67,7 @@ private void sendStreamEvent(FluxSink flux, BaseEvent event) { } private static final EnumSet RESOURCES_STREAM_BLACKLIST = - EnumSet.of(ResourceType.CVE, ResourceType.PAYLOAD); + EnumSet.of(ResourceType.VULNERABILITY, ResourceType.PAYLOAD); @Async @Transactional diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/VulnerabilityApi.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/VulnerabilityApi.java new file mode 100644 index 0000000000..e1a2b7cea5 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/VulnerabilityApi.java @@ -0,0 +1,112 @@ +package io.openaev.rest.vulnerability; + +import io.openaev.aop.LogExecutionTime; +import io.openaev.aop.RBAC; +import io.openaev.database.model.Action; +import io.openaev.database.model.ResourceType; +import io.openaev.rest.helper.RestBehavior; +import io.openaev.rest.vulnerability.form.*; +import io.openaev.rest.vulnerability.service.VulnerabilityService; +import io.openaev.utils.mapper.VulnerabilityMapper; +import io.openaev.utils.pagination.SearchPaginationInput; +import io.swagger.v3.oas.annotations.Operation; +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.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@Tag( + name = "Vulnerability API", + description = "Operations related to Vulnerabilities (CVE and more)") +public class VulnerabilityApi extends RestBehavior { + + public static final String VULNERABILITY_API = "/api/vulnerabilities"; + + private final VulnerabilityService vulnerabilityService; + private final VulnerabilityMapper vulnerabilityMapper; + + @LogExecutionTime + @Operation(summary = "Search Vulnerabilities") + @PostMapping(VULNERABILITY_API + "/search") + @RBAC(actionPerformed = Action.SEARCH, resourceType = ResourceType.VULNERABILITY) + public Page searchVulnerabilities( + @Valid @RequestBody SearchPaginationInput input) { + return vulnerabilityService + .searchVulnerabilities(input) + .map(vulnerabilityMapper::toVulnerabilitySimple); + } + + @Operation( + summary = "Get a Vulnerability by ID", + description = "Fetches detailed Vulnerability info by ID") + @GetMapping(VULNERABILITY_API + "/{vulnerabilityId}") + @RBAC( + resourceId = "#vulnerabilityId", + actionPerformed = Action.READ, + resourceType = ResourceType.VULNERABILITY) + public VulnerabilityOutput getVulnerability(@PathVariable String vulnerabilityId) { + return vulnerabilityMapper.toVulnerabilityOutput( + vulnerabilityService.findById(vulnerabilityId)); + } + + @Operation( + summary = "Get a Vulnerability by external ID", + description = "Fetches detailed Vulnerability info by external Vulnerability ID") + @GetMapping(VULNERABILITY_API + "/external-id/{externalId}") + @RBAC( + resourceId = "#externalId", + actionPerformed = Action.READ, + resourceType = ResourceType.VULNERABILITY) + public VulnerabilityOutput getVulnerabilityByExternalId(@PathVariable String externalId) { + return vulnerabilityMapper.toVulnerabilityOutput( + vulnerabilityService.findByExternalId(externalId)); + } + + @Operation(summary = "Create a new Vulnerability") + @PostMapping(VULNERABILITY_API) + @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.VULNERABILITY) + @Transactional(rollbackOn = Exception.class) + public VulnerabilitySimple createVulnerability( + @Valid @RequestBody VulnerabilityCreateInput input) { + return vulnerabilityMapper.toVulnerabilitySimple( + vulnerabilityService.createVulnerability(input)); + } + + @Operation(summary = "Bulk insert Vulnerabilities") + @LogExecutionTime + @PostMapping(VULNERABILITY_API + "/bulk") + @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.VULNERABILITY) + public void bulkInsertVulnerabilitiesForCollector( + @Valid @RequestBody @NotNull VulnerabilityBulkInsertInput input) { + this.vulnerabilityService.bulkUpsertVulnerabilities(input); + } + + @Operation(summary = "Update an existing Vulnerability") + @PutMapping(VULNERABILITY_API + "/{vulnerabilityId}") + @RBAC( + resourceId = "#vulnerabilityId", + actionPerformed = Action.WRITE, + resourceType = ResourceType.VULNERABILITY) + @Transactional(rollbackOn = Exception.class) + public VulnerabilitySimple updateVulnerability( + @PathVariable String vulnerabilityId, @Valid @RequestBody VulnerabilityUpdateInput input) { + return vulnerabilityMapper.toVulnerabilitySimple( + vulnerabilityService.updateVulnerability(vulnerabilityId, input)); + } + + @Operation(summary = "Delete a Vulnerability") + @DeleteMapping(VULNERABILITY_API + "/{vulnerabilityId}") + @RBAC( + resourceId = "#vulnerabilityId", + actionPerformed = Action.DELETE, + resourceType = ResourceType.VULNERABILITY) + @Transactional(rollbackOn = Exception.class) + public void deleteVulnerability(@PathVariable String vulnerabilityId) { + vulnerabilityService.deleteById(vulnerabilityId); + } +} diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/form/CweInput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweInput.java similarity index 81% rename from openaev-api/src/main/java/io/openaev/rest/cve/form/CweInput.java rename to openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweInput.java index 114787d87d..a45613cd73 100644 --- a/openaev-api/src/main/java/io/openaev/rest/cve/form/CweInput.java +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweInput.java @@ -1,4 +1,4 @@ -package io.openaev.rest.cve.form; +package io.openaev.rest.vulnerability.form; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,7 +8,7 @@ @Getter @Setter -@Schema(description = "CWE input used in CVE creation/update") +@Schema(description = "CWE input used in vulnerability creation/update") public class CweInput { @NotBlank diff --git a/openaev-api/src/main/java/io/openaev/rest/cve/form/CweOutput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweOutput.java similarity index 92% rename from openaev-api/src/main/java/io/openaev/rest/cve/form/CweOutput.java rename to openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweOutput.java index f1005b15e6..2c1ade042a 100644 --- a/openaev-api/src/main/java/io/openaev/rest/cve/form/CweOutput.java +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/CweOutput.java @@ -1,4 +1,4 @@ -package io.openaev.rest.cve.form; +package io.openaev.rest.vulnerability.form; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityBulkInsertInput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityBulkInsertInput.java new file mode 100644 index 0000000000..d6ac8a15ac --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityBulkInsertInput.java @@ -0,0 +1,29 @@ +package io.openaev.rest.vulnerability.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.config.AppConfig; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; +import lombok.Data; + +@Data +public class VulnerabilityBulkInsertInput { + + @JsonProperty("vulnerabilities") + @NotNull(message = AppConfig.MANDATORY_MESSAGE) + private List vulnerabilities; + + @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/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityCreateInput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityCreateInput.java new file mode 100644 index 0000000000..49bab9e101 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityCreateInput.java @@ -0,0 +1,29 @@ +package io.openaev.rest.vulnerability.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "Payload to create a Vulnerabilty") +public class VulnerabilityCreateInput extends VulnerabilityInput { + + @NotBlank + @JsonProperty("vulnerability_external_id") + @Schema(description = "External Unique Vulnerabilty Identifier", example = "CVE-2024-0001") + private String externalId; + + @NotNull + @DecimalMin("0.0") + @DecimalMax("10.0") + @JsonProperty("vulnerability_cvss_v31") + @Schema(description = "CVSS score", example = "7.5", minimum = "0.0", maximum = "10.0") + private BigDecimal cvssV31; +} diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityInput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityInput.java new file mode 100644 index 0000000000..d036d485f4 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityInput.java @@ -0,0 +1,62 @@ +package io.openaev.rest.vulnerability.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.database.model.Vulnerability; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import java.time.Instant; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "Common base input for vulnerability creation and update") +public class VulnerabilityInput { + + @JsonProperty("vulnerability_source_identifier") + @Schema(description = "Identifier of the vulnerability source", example = "MITRE") + private String sourceIdentifier; + + @JsonProperty("vulnerability_published") + @Schema(description = "Publication date of the vulnerability") + private Instant published; + + @JsonProperty("vulnerability_description") + @Schema(description = "Description of the vulnerability") + private String description; + + @Enumerated(EnumType.STRING) + @JsonProperty("vulnerability_vuln_status") + @Schema(description = "Vulnerability status", example = "ANALYZED") + private Vulnerability.VulnerabilityStatus vulnStatus; + + @JsonProperty("vulnerability_cisa_exploit_add") + @Schema(description = "Date when CISA added the vulnerability to the exploited list") + private Instant cisaExploitAdd; + + @JsonProperty("vulnerability_cisa_action_due") + @Schema(description = "Date when action is due by CISA") + private Instant cisaActionDue; + + @JsonProperty("vulnerability_cisa_required_action") + @Schema(description = "Action required by CISA") + private String cisaRequiredAction; + + @JsonProperty("vulnerability_cisa_vulnerability_name") + @Schema(description = "Vulnerability name used by CISA") + private String cisaVulnerabilityName; + + @JsonProperty("vulnerability_remediation") + @Schema(description = "Suggested remediation") + private String remediation; + + @JsonProperty("vulnerability_reference_urls") + @Schema(description = "List of reference URLs") + private List referenceUrls; + + @JsonProperty("vulnerability_cwes") + @Schema(description = "List of linked CWEs") + private List cwes; +} diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityOutput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityOutput.java new file mode 100644 index 0000000000..cff272f726 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityOutput.java @@ -0,0 +1,60 @@ +package io.openaev.rest.vulnerability.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.database.model.Vulnerability; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import java.time.Instant; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Setter +@Getter +@SuperBuilder +@Schema(description = "Full vulnerability output including references and CWEs") +public class VulnerabilityOutput extends VulnerabilitySimple { + + @JsonProperty("vulnerability_source_identifier") + @Schema(description = "Source identifier") + private String sourceIdentifier; + + @JsonProperty("vulnerability_description") + @Schema(description = "Detailed vulnerability description") + private String description; + + @Enumerated(EnumType.STRING) + @JsonProperty("vulnerability_vuln_status") + @Schema(description = "Status of the vulnerability") + private Vulnerability.VulnerabilityStatus vulnStatus; + + @JsonProperty("vulnerability_cisa_exploit_add") + @Schema(description = "CISA exploit addition date") + private Instant cisaExploitAdd; + + @JsonProperty("vulnerability_cisa_action_due") + @Schema(description = "CISA required action due date") + private Instant cisaActionDue; + + @JsonProperty("vulnerability_cisa_required_action") + @Schema(description = "Action required by CISA") + private String cisaRequiredAction; + + @JsonProperty("vulnerability_cisa_vulnerability_name") + @Schema(description = "Name used by CISA for the vulnerability") + private String cisaVulnerabilityName; + + @JsonProperty("vulnerability_remediation") + @Schema(description = "Remediation suggestions") + private String remediation; + + @JsonProperty("vulnerability_reference_urls") + @Schema(description = "External references") + private List referenceUrls; + + @JsonProperty("vulnerability_cwes") + @Schema(description = "List of CWE outputs") + private List cwes; +} diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilitySimple.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilitySimple.java new file mode 100644 index 0000000000..e7069e643f --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilitySimple.java @@ -0,0 +1,37 @@ +package io.openaev.rest.vulnerability.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.Instant; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@SuperBuilder +@Schema(description = "Simplified Vulnerability representation") +public class VulnerabilitySimple { + + @NotBlank + @JsonProperty("vulnerability_id") + @Schema(description = "Id") + private String id; + + @NotBlank + @JsonProperty("vulnerability_external_id") + @Schema(description = "External Vulnerability identifier", example = "CVE-2024-0001") + private String externalId; + + @NotNull + @JsonProperty("vulnerability_cvss_v31") + @Schema(description = "CVSS score", example = "7.8") + private BigDecimal cvssV31; + + @JsonProperty("vulnerability_published") + @Schema(description = "Vulnerability published date") + private Instant published; +} diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityUpdateInput.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityUpdateInput.java new file mode 100644 index 0000000000..90ba4f78e3 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/form/VulnerabilityUpdateInput.java @@ -0,0 +1,12 @@ +package io.openaev.rest.vulnerability.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "Payload to update a vulnerability") +public class VulnerabilityUpdateInput extends VulnerabilityInput { + // Inherits all fields from CveInput +} diff --git a/openaev-api/src/main/java/io/openaev/rest/vulnerability/service/VulnerabilityService.java b/openaev-api/src/main/java/io/openaev/rest/vulnerability/service/VulnerabilityService.java new file mode 100644 index 0000000000..589956347e --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/rest/vulnerability/service/VulnerabilityService.java @@ -0,0 +1,216 @@ +package io.openaev.rest.vulnerability.service; + +import static io.openaev.helper.StreamHelper.fromIterable; +import static io.openaev.utils.pagination.PaginationUtils.buildPaginationJPA; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.openaev.config.cache.LicenseCacheManager; +import io.openaev.database.model.*; +import io.openaev.database.repository.CweRepository; +import io.openaev.database.repository.VulnerabilityRepository; +import io.openaev.ee.Ee; +import io.openaev.rest.collector.service.CollectorService; +import io.openaev.rest.exception.ElementNotFoundException; +import io.openaev.rest.vulnerability.form.*; +import io.openaev.utils.pagination.SearchPaginationInput; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; +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 +@RequiredArgsConstructor +public class VulnerabilityService { + + private static final String VULNERABILITY_NOT_FOUND_MSG = "Vulnerability not found with id: "; + + private final CollectorService collectorService; + private final Ee eeService; + + private final VulnerabilityRepository vulnerabilityRepository; + private final CweRepository cweRepository; + private final LicenseCacheManager licenseCacheManager; + + @Resource protected ObjectMapper mapper; + + public Vulnerability createVulnerability(final @Valid VulnerabilityCreateInput input) { + final Vulnerability vulnerability = new Vulnerability(); + if (eeService.isEnterpriseLicenseInactive(licenseCacheManager.getEnterpriseEditionInfo())) { + input.setRemediation(null); + } + vulnerability.setUpdateAttributes(input); + updateCweAssociations(vulnerability, input.getCwes()); + return vulnerabilityRepository.save(vulnerability); + } + + private List batchUpsertVulnerabilities( + List vulnerabilityInput) { + // Extract external IDs + Set externalIds = + vulnerabilityInput.stream() + .map(VulnerabilityCreateInput::getExternalId) + .collect(Collectors.toSet()); + + // Batch fetch existing vulnerabilities + Map existingVulnerabilitiesMap = + getVulnerabilitiesByExternalIds(externalIds).stream() + .collect(Collectors.toMap(Vulnerability::getExternalId, Function.identity())); + + // Process with pre-fetched data + List vulnerabilities = + vulnerabilityInput.stream() + .map( + cveInput -> { + Vulnerability vulnerability = + existingVulnerabilitiesMap.getOrDefault( + cveInput.getExternalId(), new Vulnerability()); + vulnerability.setUpdateAttributes(cveInput); + updateCweAssociations(vulnerability, cveInput.getCwes()); + return vulnerability; + }) + .toList(); + + return fromIterable(vulnerabilityRepository.saveAll(vulnerabilities)); + } + + private void updateCollectorStateFromVulnerabilityBulkInsertInput( + Collector collector, @NotNull VulnerabilityBulkInsertInput 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 bulkUpsertVulnerabilities(@NotNull VulnerabilityBulkInsertInput inputs) { + Collector collector = this.collectorService.collector(inputs.getSourceIdentifier()); + + List vulnerabilities = + this.batchUpsertVulnerabilities(inputs.getVulnerabilities()); + this.updateCollectorStateFromVulnerabilityBulkInsertInput(collector, inputs); + + log.info( + "Bulk upsert {} vulnerabilities with last modified date fetched: {}", + vulnerabilities.size(), + inputs.getLastModifiedDateFetched()); + } + + public Page searchVulnerabilities(final @Valid SearchPaginationInput input) { + return buildPaginationJPA( + (Specification spec, Pageable pageable) -> + vulnerabilityRepository.findAll(spec, pageable), + input, + Vulnerability.class); + } + + public Vulnerability updateVulnerability( + final String vulnerabilityId, final @Valid VulnerabilityUpdateInput input) { + final Vulnerability existingVulnerability = findById(vulnerabilityId); + if (eeService.isEnterpriseLicenseInactive(licenseCacheManager.getEnterpriseEditionInfo())) { + input.setRemediation(null); + BeanUtils.copyProperties(input, existingVulnerability, "remediation"); + } else { + existingVulnerability.setUpdateAttributes(input); + } + updateCweAssociations(existingVulnerability, input.getCwes()); + return vulnerabilityRepository.save(existingVulnerability); + } + + public Vulnerability findById(final String vulnerabilityId) { + return vulnerabilityRepository + .findById(vulnerabilityId) + .orElseThrow( + () -> new ElementNotFoundException(VULNERABILITY_NOT_FOUND_MSG + vulnerabilityId)); + } + + public Set findAllByIdsOrThrowIfMissing(final Set vulnIds) { + Set vulns = this.vulnerabilityRepository.getAllByIdInIgnoreCase(vulnIds); + throwIfMissing(vulnIds, vulns, Vulnerability::getId); + return vulns; + } + + public Vulnerability findByExternalId(String externalId) { + return vulnerabilityRepository + .findByExternalId(externalId) + .orElseThrow(() -> new ElementNotFoundException(VULNERABILITY_NOT_FOUND_MSG + externalId)); + } + + public Set findAllByExternalIdsOrThrowIfMissing(final Set vulnIds) { + Set vulns = getVulnerabilitiesByExternalIds(vulnIds); + throwIfMissing(vulnIds, vulns, Vulnerability::getExternalId); + return vulns; + } + + private void throwIfMissing( + Set requiredIds, + Set fetchedVulnerabilities, + Function getId) { + + List fetchedIdLower = + fetchedVulnerabilities.stream().map(vuln -> getId.apply(vuln).toLowerCase()).toList(); + + List missingIds = + requiredIds.stream().filter(id -> !fetchedIdLower.contains(id.toLowerCase())).toList(); + + if (!missingIds.isEmpty()) { + throw new ElementNotFoundException( + String.format("Missing vulnerabilities: %s", String.join(", ", missingIds))); + } + } + + public void deleteById(final String vulnerabilityId) { + vulnerabilityRepository.deleteById(vulnerabilityId); + } + + private void updateCweAssociations(Vulnerability vulnerability, List cweInputs) { + if (cweInputs == null || cweInputs.isEmpty()) { + vulnerability.setCwes(Collections.emptyList()); + return; + } + + List cweEntities = + cweInputs.stream() + .map( + input -> + cweRepository + .findByExternalId(input.getExternalId()) + .orElseGet( + () -> { + Cwe newCwe = new Cwe(); + newCwe.setExternalId(input.getExternalId()); + newCwe.setSource(input.getSource()); + return cweRepository.save(newCwe); + })) + .collect(Collectors.toList()); + + vulnerability.setCwes(cweEntities); + } + + /** + * Resolves external Vulnerability Refs from a set of vulnerability {@link Vulnerability} + * entities. + * + * @param externalIds set vulnerability Refs + * @return set of resolved vulnerability entities + */ + public Set getVulnerabilitiesByExternalIds(Set externalIds) { + if (externalIds.isEmpty()) { + return Collections.emptySet(); + } + return this.vulnerabilityRepository.getAllByExternalIdInIgnoreCase(externalIds); + } +} diff --git a/openaev-api/src/main/java/io/openaev/service/PermissionService.java b/openaev-api/src/main/java/io/openaev/service/PermissionService.java index a9a7b0a4bc..9e57061fb8 100644 --- a/openaev-api/src/main/java/io/openaev/service/PermissionService.java +++ b/openaev-api/src/main/java/io/openaev/service/PermissionService.java @@ -21,7 +21,7 @@ public class PermissionService { ResourceType.PLAYER, ResourceType.TEAM, ResourceType.PLATFORM_SETTING, - ResourceType.CVE, + ResourceType.VULNERABILITY, ResourceType.TAG, ResourceType.ATTACK_PATTERN, ResourceType.KILL_CHAIN_PHASE, diff --git a/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageInjectService.java b/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageInjectService.java index 3c5ee8222c..dd408d03fc 100644 --- a/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageInjectService.java +++ b/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageInjectService.java @@ -7,10 +7,10 @@ import io.openaev.database.repository.InjectRepository; import io.openaev.injectors.manual.ManualContract; import io.openaev.rest.attack_pattern.service.AttackPatternService; -import io.openaev.rest.cve.service.CveService; import io.openaev.rest.inject.service.InjectAssistantService; import io.openaev.rest.inject.service.InjectService; import io.openaev.rest.injector_contract.InjectorContractService; +import io.openaev.rest.vulnerability.service.VulnerabilityService; import io.openaev.service.AssetGroupService; import java.util.*; import java.util.stream.Collectors; @@ -32,7 +32,7 @@ public class SecurityCoverageInjectService { private final InjectService injectService; private final InjectAssistantService injectAssistantService; private final AttackPatternService attackPatternService; - private final CveService vulnerabilityService; + private final VulnerabilityService vulnerabilityService; private final AssetGroupService assetGroupService; private final InjectorContractService injectorContractService; @@ -108,11 +108,11 @@ private void createInjectsByVulnerabilities( InjectorContract contractForPlaceholder) { // 1. Fetch internal Ids for Vulnerabilities - Set requiredVulnerabilities = + Set requiredVulnerabilities = vulnerabilityService.getVulnerabilitiesByExternalIds(getExternalIds(vulnerabilityRefs)); // 2. Fetch covered vulnerabilities and endpoints - Map> currentlyCoveredCveInjectsMap = + Map> currentlyCoveredCveInjectsMap = buildCoveredCveInjectsMap(scenario.getInjects()); // 3. remove obsolete injects @@ -120,8 +120,8 @@ private void createInjectsByVulnerabilities( findObsoleteInjects(currentlyCoveredCveInjectsMap, requiredVulnerabilities)); // 4. Identify missing injects - Set missingVulns = new HashSet<>(); - for (Cve key : requiredVulnerabilities) { + Set missingVulns = new HashSet<>(); + for (Vulnerability key : requiredVulnerabilities) { if (!currentlyCoveredCveInjectsMap.containsKey(key)) { missingVulns.add(key); } @@ -139,7 +139,7 @@ private void createInjectsByVulnerabilities( } } - private Map> buildCoveredCveInjectsMap(List coveredInjects) { + private Map> buildCoveredCveInjectsMap(List coveredInjects) { return coveredInjects.stream() // Keep only injects that have a contract and vulnerabilities .filter( @@ -156,11 +156,12 @@ private Map> buildCoveredCveInjectsMap(List coveredInje } private List findObsoleteInjects( - Map> coveredCveEndpointsMap, Set requiredVulnerabilities) { + Map> coveredCveEndpointsMap, + Set requiredVulnerabilities) { List injectsToRemove = new ArrayList<>(); - for (Map.Entry> entry : coveredCveEndpointsMap.entrySet()) { - Cve coveredVuln = entry.getKey(); + for (Map.Entry> entry : coveredCveEndpointsMap.entrySet()) { + Vulnerability coveredVuln = entry.getKey(); Set injects = entry.getValue(); for (Inject inject : injects) { diff --git a/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageService.java b/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageService.java index 3af83a16a2..24aac1be81 100644 --- a/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageService.java +++ b/openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageService.java @@ -17,10 +17,10 @@ import io.openaev.database.repository.ScenarioRepository; import io.openaev.database.repository.SecurityCoverageRepository; import io.openaev.rest.attack_pattern.service.AttackPatternService; -import io.openaev.rest.cve.service.CveService; import io.openaev.rest.exercise.service.ExerciseService; import io.openaev.rest.settings.PreviewFeature; import io.openaev.rest.tag.TagService; +import io.openaev.rest.vulnerability.service.VulnerabilityService; import io.openaev.service.AssetService; import io.openaev.service.PreviewFeatureService; import io.openaev.service.ScenarioService; @@ -71,7 +71,7 @@ public class SecurityCoverageService { private final Parser stixParser; private final ObjectMapper objectMapper; - private final CveService cveService; + private final VulnerabilityService vulnerabilityService; private final PreviewFeatureService previewFeatureService; @@ -363,9 +363,9 @@ private BaseType getVulnerabilityCoverage(String externalRef, Exercise exerci return getCoverage( externalRef, exercise, - id -> cveService.getVulnerabilitiesByExternalIds(Set.of(id)), + id -> vulnerabilityService.getVulnerabilitiesByExternalIds(Set.of(id)), InjectorContract::getVulnerabilities, - Cve::getId); + Vulnerability::getId); } private BaseType getAttackPatternCoverage(String externalRef, Exercise exercise) { diff --git a/openaev-api/src/main/java/io/openaev/utils/mapper/CveMapper.java b/openaev-api/src/main/java/io/openaev/utils/mapper/CveMapper.java index a29cfde2e7..381e489c11 100644 --- a/openaev-api/src/main/java/io/openaev/utils/mapper/CveMapper.java +++ b/openaev-api/src/main/java/io/openaev/utils/mapper/CveMapper.java @@ -3,10 +3,11 @@ import io.openaev.config.cache.LicenseCacheManager; import io.openaev.database.model.Cve; import io.openaev.database.model.Cwe; +import io.openaev.database.model.Vulnerability; import io.openaev.ee.Ee; import io.openaev.rest.cve.form.CveOutput; import io.openaev.rest.cve.form.CveSimple; -import io.openaev.rest.cve.form.CweOutput; +import io.openaev.rest.vulnerability.form.CweOutput; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,37 +24,44 @@ public class CveMapper { private final Ee eeService; private final LicenseCacheManager licenseCacheManager; - public CveSimple toCveSimple(final Cve cve) { - if (cve == null) { + private Cve.VulnerabilityStatus mapVulnerabilityStatus(Vulnerability.VulnerabilityStatus status) { + if (status == null) { + return null; + } + return Cve.VulnerabilityStatus.valueOf(status.name()); + } + + public CveSimple toCveSimple(final Vulnerability vulnerability) { + if (vulnerability == null) { return null; } return CveSimple.builder() - .id(cve.getId()) - .externalId(cve.getExternalId()) - .cvssV31(cve.getCvssV31()) - .published(cve.getPublished()) + .id(vulnerability.getId()) + .externalId(vulnerability.getExternalId()) + .cvssV31(vulnerability.getCvssV31()) + .published(vulnerability.getPublished()) .build(); } - public CveOutput toCveOutput(final Cve cve) { - if (cve == null) { + public CveOutput toCveOutput(final Vulnerability vulnerability) { + if (vulnerability == null) { return null; } return CveOutput.builder() - .id(cve.getId()) - .externalId(cve.getExternalId()) - .cvssV31(cve.getCvssV31()) - .published(cve.getPublished()) - .sourceIdentifier(cve.getSourceIdentifier()) - .description(cve.getDescription()) - .vulnStatus(cve.getVulnStatus()) - .cisaActionDue(cve.getCisaActionDue()) - .cisaExploitAdd(cve.getCisaExploitAdd()) - .cisaRequiredAction(cve.getCisaRequiredAction()) - .cisaVulnerabilityName(cve.getCisaVulnerabilityName()) - .remediation(getRemediationIfLicensed(cve)) - .referenceUrls(new ArrayList<>(cve.getReferenceUrls())) - .cwes(toCweOutputs(cve.getCwes())) + .id(vulnerability.getId()) + .externalId(vulnerability.getExternalId()) + .cvssV31(vulnerability.getCvssV31()) + .published(vulnerability.getPublished()) + .sourceIdentifier(vulnerability.getSourceIdentifier()) + .description(vulnerability.getDescription()) + .vulnStatus(mapVulnerabilityStatus(vulnerability.getVulnStatus())) + .cisaActionDue(vulnerability.getCisaActionDue()) + .cisaExploitAdd(vulnerability.getCisaExploitAdd()) + .cisaRequiredAction(vulnerability.getCisaRequiredAction()) + .cisaVulnerabilityName(vulnerability.getCisaVulnerabilityName()) + .remediation(getRemediationIfLicensed(vulnerability)) + .referenceUrls(new ArrayList<>(vulnerability.getReferenceUrls())) + .cwes(toCweOutputs(vulnerability.getCwes())) .build(); } @@ -71,9 +79,9 @@ public CweOutput toCweOutput(final Cwe cwe) { return CweOutput.builder().externalId(cwe.getExternalId()).source(cwe.getSource()).build(); } - private String getRemediationIfLicensed(final Cve cve) { + private String getRemediationIfLicensed(final Vulnerability vulnerability) { if (eeService.isLicenseActive(licenseCacheManager.getEnterpriseEditionInfo())) { - return cve.getRemediation(); + return vulnerability.getRemediation(); } else { log.debug("Enterprise Edition license inactive - omitting remediation field"); return null; diff --git a/openaev-api/src/main/java/io/openaev/utils/mapper/VulnerabilityMapper.java b/openaev-api/src/main/java/io/openaev/utils/mapper/VulnerabilityMapper.java new file mode 100644 index 0000000000..33359f340f --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/utils/mapper/VulnerabilityMapper.java @@ -0,0 +1,126 @@ +package io.openaev.utils.mapper; + +import io.openaev.config.cache.LicenseCacheManager; +import io.openaev.database.model.Cwe; +import io.openaev.database.model.Vulnerability; +import io.openaev.ee.Ee; +import io.openaev.rest.cve.form.CVEBulkInsertInput; +import io.openaev.rest.cve.form.CveCreateInput; +import io.openaev.rest.vulnerability.form.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Slf4j +public class VulnerabilityMapper { + + private final Ee eeService; + private final LicenseCacheManager licenseCacheManager; + + public VulnerabilitySimple toVulnerabilitySimple(final Vulnerability vulnerability) { + if (vulnerability == null) { + return null; + } + return VulnerabilitySimple.builder() + .id(vulnerability.getId()) + .externalId(vulnerability.getExternalId()) + .cvssV31(vulnerability.getCvssV31()) + .published(vulnerability.getPublished()) + .build(); + } + + public VulnerabilityOutput toVulnerabilityOutput(final Vulnerability vulnerability) { + if (vulnerability == null) { + return null; + } + return VulnerabilityOutput.builder() + .id(vulnerability.getId()) + .externalId(vulnerability.getExternalId()) + .cvssV31(vulnerability.getCvssV31()) + .published(vulnerability.getPublished()) + .sourceIdentifier(vulnerability.getSourceIdentifier()) + .description(vulnerability.getDescription()) + .vulnStatus(vulnerability.getVulnStatus()) + .cisaActionDue(vulnerability.getCisaActionDue()) + .cisaExploitAdd(vulnerability.getCisaExploitAdd()) + .cisaRequiredAction(vulnerability.getCisaRequiredAction()) + .cisaVulnerabilityName(vulnerability.getCisaVulnerabilityName()) + .remediation(getRemediationIfLicensed(vulnerability)) + .referenceUrls(new ArrayList<>(vulnerability.getReferenceUrls())) + .cwes(toCweOutputs(vulnerability.getCwes())) + .build(); + } + + public VulnerabilityBulkInsertInput fromCVEBulkInsertInput(final CVEBulkInsertInput input) { + if (input == null) { + return null; + } + + VulnerabilityBulkInsertInput result = new VulnerabilityBulkInsertInput(); + result.setVulnerabilities(toVulnerabilityCreateInputs(input.getCves())); + result.setLastModifiedDateFetched(input.getLastModifiedDateFetched()); + result.setLastIndex(input.getLastIndex()); + result.setInitialDatasetCompleted(input.getInitialDatasetCompleted()); + result.setSourceIdentifier(input.getSourceIdentifier()); + return result; + } + + private List toVulnerabilityCreateInputs( + final List cves) { + if (cves == null) { + return List.of(); + } + return cves.stream().map(this::toVulnerabilityCreateInput).collect(Collectors.toList()); + } + + private VulnerabilityCreateInput toVulnerabilityCreateInput(final CveCreateInput cve) { + if (cve == null) { + return null; + } + + VulnerabilityCreateInput input = new VulnerabilityCreateInput(); + input.setExternalId(cve.getExternalId()); + input.setSourceIdentifier(cve.getSourceIdentifier()); + input.setCvssV31(cve.getCvssV31()); + input.setPublished(cve.getPublished()); + input.setDescription(cve.getDescription()); + input.setVulnStatus(cve.getVulnStatus()); + input.setCisaExploitAdd(cve.getCisaExploitAdd()); + input.setCisaActionDue(cve.getCisaActionDue()); + input.setCisaRequiredAction(cve.getCisaRequiredAction()); + input.setCisaVulnerabilityName(cve.getCisaVulnerabilityName()); + input.setRemediation(cve.getRemediation()); + input.setReferenceUrls(cve.getReferenceUrls()); + input.setCwes(cve.getCwes()); + return input; + } + + private List toCweOutputs(final List cwes) { + if (cwes == null || cwes.isEmpty()) { + return Collections.emptyList(); + } + return cwes.stream().map(this::toCweOutput).collect(Collectors.toList()); + } + + public CweOutput toCweOutput(final Cwe cwe) { + if (cwe == null) { + return null; + } + return CweOutput.builder().externalId(cwe.getExternalId()).source(cwe.getSource()).build(); + } + + private String getRemediationIfLicensed(final Vulnerability vulnerability) { + if (eeService.isLicenseActive(licenseCacheManager.getEnterpriseEditionInfo())) { + return vulnerability.getRemediation(); + } else { + log.debug("Enterprise Edition license inactive - omitting remediation field"); + return null; + } + } +} diff --git a/openaev-api/src/test/java/io/openaev/api/stix_process/StixApiTest.java b/openaev-api/src/test/java/io/openaev/api/stix_process/StixApiTest.java index c7d7b40931..4cab124a82 100644 --- a/openaev-api/src/test/java/io/openaev/api/stix_process/StixApiTest.java +++ b/openaev-api/src/test/java/io/openaev/api/stix_process/StixApiTest.java @@ -5,7 +5,7 @@ import static io.openaev.injector_contract.InjectorContractContentUtilsTest.createContentWithFieldAssetGroup; import static io.openaev.rest.scenario.ScenarioApi.SCENARIO_URI; import static io.openaev.service.TagRuleService.OPENCTI_TAG_NAME; -import static io.openaev.utils.fixtures.CveFixture.CVE_2023_48788; +import static io.openaev.utils.fixtures.VulnerabilityFixture.CVE_2023_48788; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @@ -56,7 +56,7 @@ class StixApiTest extends IntegrationTest { @Autowired private SecurityCoverageRepository securityCoverageRepository; @Autowired private AttackPatternComposer attackPatternComposer; - @Autowired private CveComposer vulnerabilityComposer; + @Autowired private VulnerabilityComposer vulnerabilityComposer; @Autowired private TagRuleComposer tagRuleComposer; @Autowired private AssetGroupComposer assetGroupComposer; @Autowired private EndpointComposer endpointComposer; @@ -139,7 +139,8 @@ void setUp() throws Exception { InjectorContractFixture.createInjectorContract(createContentWithFieldAsset())) .withInjector(injectorFixture.getWellKnownOaevImplantInjector()) .withVulnerability( - vulnerabilityComposer.forCve(CveFixture.createDefaultCve("CVE-2025-56785"))) + vulnerabilityComposer.forVulnerability( + VulnerabilityFixture.createVulnerabilityInput("CVE-2025-56785"))) .persist(); injectorContractComposer @@ -147,7 +148,8 @@ void setUp() throws Exception { InjectorContractFixture.createInjectorContract(createContentWithFieldAssetGroup())) .withInjector(injectorFixture.getWellKnownOaevImplantInjector()) .withVulnerability( - vulnerabilityComposer.forCve(CveFixture.createDefaultCve("CVE-2025-56786"))) + vulnerabilityComposer.forVulnerability( + VulnerabilityFixture.createVulnerabilityInput("CVE-2025-56786"))) .persist(); tagRuleComposer diff --git a/openaev-api/src/test/java/io/openaev/rest/CveApiTest.java b/openaev-api/src/test/java/io/openaev/rest/CveApiTest.java index 290b76a2b5..6dcfb66c06 100644 --- a/openaev-api/src/test/java/io/openaev/rest/CveApiTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/CveApiTest.java @@ -2,9 +2,8 @@ import static io.openaev.rest.cve.CveApi.CVE_API; import static io.openaev.utils.JsonUtils.asJsonString; -import static io.openaev.utils.fixtures.CveFixture.CVE_2025_5678; -import static io.openaev.utils.fixtures.CveInputFixture.CVE_EXTERNAL_ID; -import static io.openaev.utils.fixtures.CveInputFixture.createDefaultCveCreateInput; +import static io.openaev.utils.fixtures.VulnerabilityFixture.CVE_2025_5678; +import static io.openaev.utils.fixtures.VulnerabilityFixture.VULNERABILITY_EXTERNAL_ID; import static java.time.Instant.now; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @@ -14,14 +13,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.openaev.IntegrationTest; import io.openaev.database.model.Collector; -import io.openaev.database.model.Cve; -import io.openaev.database.repository.CveRepository; +import io.openaev.database.model.Vulnerability; +import io.openaev.database.repository.VulnerabilityRepository; import io.openaev.rest.cve.form.CVEBulkInsertInput; import io.openaev.rest.cve.form.CveCreateInput; -import io.openaev.rest.cve.form.CveUpdateInput; +import io.openaev.rest.vulnerability.form.VulnerabilityCreateInput; +import io.openaev.rest.vulnerability.form.VulnerabilityUpdateInput; import io.openaev.utils.fixtures.CollectorFixture; import io.openaev.utils.fixtures.composers.CollectorComposer; -import io.openaev.utils.fixtures.composers.CveComposer; +import io.openaev.utils.fixtures.composers.VulnerabilityComposer; import io.openaev.utils.mockUser.WithMockUser; import io.openaev.utils.pagination.SearchPaginationInput; import jakarta.annotation.Resource; @@ -43,9 +43,9 @@ class CveApiTest extends IntegrationTest { @Autowired private MockMvc mvc; private Collector collector; - @Autowired private CveComposer cveComposer; + @Autowired private VulnerabilityComposer vulnerabilityComposer; @Autowired private CollectorComposer collectorComposer; - @Autowired private CveRepository cveRepository; + @Autowired private VulnerabilityRepository vulnerabilityRepository; @BeforeAll void init() { @@ -58,7 +58,7 @@ void init() { @BeforeEach void setUp() { - cveComposer.reset(); + collectorComposer.reset(); } @Nested @@ -69,7 +69,7 @@ class WhenWorkingWithCves { @Test @DisplayName("Should create a new CVE successfully") void shouldCreateNewCve() throws Exception { - CveCreateInput input = new CveCreateInput(); + VulnerabilityCreateInput input = new VulnerabilityCreateInput(); input.setExternalId("CVE-2025-1234"); input.setCvssV31(new BigDecimal("5.2")); input.setDescription("Test summary for CVE creation"); @@ -90,12 +90,12 @@ void shouldCreateNewCve() throws Exception { @Test @DisplayName("Should fetch a CVE by ID") void shouldFetchCveById() throws Exception { - Cve cve = new Cve(); + Vulnerability cve = new Vulnerability(); cve.setExternalId(CVE_2025_5678); cve.setCvssV31(new BigDecimal("8.9")); cve.setDescription("Test CVE"); - cveComposer.forCve(cve).persist(); + vulnerabilityComposer.forVulnerability(cve).persist(); String response = mvc.perform(get(CVE_API + "/" + cve.getId())) @@ -110,13 +110,13 @@ void shouldFetchCveById() throws Exception { @Test @DisplayName("Should update an existing CVE") void shouldUpdateCve() throws Exception { - Cve cve = new Cve(); + Vulnerability cve = new Vulnerability(); cve.setExternalId("CVE-2025-5679"); cve.setCvssV31(new BigDecimal("4.5")); cve.setDescription("Old description"); - cveComposer.forCve(cve).persist(); + vulnerabilityComposer.forVulnerability(cve).persist(); - CveUpdateInput updateInput = new CveUpdateInput(); + VulnerabilityUpdateInput updateInput = new VulnerabilityUpdateInput(); updateInput.setDescription("Updated Summary"); mvc.perform( @@ -132,26 +132,34 @@ void shouldUpdateCve() throws Exception { updateInput .getDescription() .equals( - cveRepository.findById(cve.getId()).map(cve1 -> cve1.getDescription()).get())); + vulnerabilityRepository + .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 -- + CveCreateInput cveInput = new CveCreateInput(); + cveInput.setExternalId(VULNERABILITY_EXTERNAL_ID); + cveInput.setSourceIdentifier(collector.getId()); + cveInput.setCvssV31(BigDecimal.valueOf(7.8)); + cveInput.setPublished(now()); + cveInput.setDescription("Sample CVE for testing"); + cveInput.setReferenceUrls(List.of("https://example.com/cve")); + + CVEBulkInsertInput input = new CVEBulkInsertInput(); + input.setSourceIdentifier(collector.getId()); + input.setLastModifiedDateFetched(now()); + input.setLastIndex(1234); + input.setInitialDatasetCompleted(false); + input.setCves(List.of(cveInput)); mvc.perform( post(CVE_API + "/bulk") - .content(asJsonString(inputs)) + .content(asJsonString(input)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -159,38 +167,38 @@ void shouldBulkInsertCVEs() throws Exception { .getResponse() .getContentAsString(); - // -- ASSERT -- - Assertions.assertTrue(cveRepository.findByExternalId(CVE_EXTERNAL_ID).isPresent()); + Assertions.assertTrue( + vulnerabilityRepository.findByExternalId(VULNERABILITY_EXTERNAL_ID).isPresent()); } @Test @DisplayName("Should delete a CVE") void shouldDeleteCve() throws Exception { - Cve cve = new Cve(); + Vulnerability cve = new Vulnerability(); cve.setExternalId("CVE-2025-5679"); cve.setCvssV31(new BigDecimal("7.5")); cve.setDescription("To be deleted"); - cveComposer.forCve(cve).persist(); + vulnerabilityComposer.forVulnerability(cve).persist(); mvc.perform(delete(CVE_API + "/" + cve.getExternalId())).andExpect(status().isOk()); - Assertions.assertFalse(cveRepository.findById(cve.getExternalId()).isPresent()); + Assertions.assertFalse(vulnerabilityRepository.findById(cve.getExternalId()).isPresent()); } @Test @DisplayName("Should return CVEs on search") void shouldReturnCvesOnSearch() throws Exception { - Cve cve = new Cve(); + Vulnerability cve = new Vulnerability(); cve.setExternalId("CVE-2024-5679"); cve.setCvssV31(new BigDecimal("4.5")); cve.setDescription("Cve 1"); - cveComposer.forCve(cve).persist(); + vulnerabilityComposer.forVulnerability(cve).persist(); - Cve cve1 = new Cve(); + Vulnerability cve1 = new Vulnerability(); cve1.setExternalId("CVE-2025-5671"); cve1.setCvssV31(new BigDecimal("1.8")); cve1.setDescription("Cve 2"); - cveComposer.forCve(cve1).persist(); + vulnerabilityComposer.forVulnerability(cve1).persist(); SearchPaginationInput input = new SearchPaginationInput(); input.setSize(10); diff --git a/openaev-api/src/test/java/io/openaev/rest/VulnerabilityApiTest.java b/openaev-api/src/test/java/io/openaev/rest/VulnerabilityApiTest.java new file mode 100644 index 0000000000..0426c04ecf --- /dev/null +++ b/openaev-api/src/test/java/io/openaev/rest/VulnerabilityApiTest.java @@ -0,0 +1,222 @@ +package io.openaev.rest; + +import static io.openaev.rest.vulnerability.VulnerabilityApi.VULNERABILITY_API; +import static io.openaev.utils.JsonUtils.asJsonString; +import static io.openaev.utils.fixtures.VulnerabilityFixture.*; +import static io.openaev.utils.fixtures.VulnerabilityFixture.CVE_2025_5678; +import static io.openaev.utils.fixtures.VulnerabilityInputFixture.createDefaultVulnerabilityInput; +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.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.openaev.IntegrationTest; +import io.openaev.database.model.Collector; +import io.openaev.database.model.Vulnerability; +import io.openaev.database.repository.VulnerabilityRepository; +import io.openaev.rest.vulnerability.form.VulnerabilityBulkInsertInput; +import io.openaev.rest.vulnerability.form.VulnerabilityCreateInput; +import io.openaev.rest.vulnerability.form.VulnerabilityUpdateInput; +import io.openaev.utils.fixtures.CollectorFixture; +import io.openaev.utils.fixtures.composers.CollectorComposer; +import io.openaev.utils.fixtures.composers.VulnerabilityComposer; +import io.openaev.utils.mockUser.WithMockUser; +import io.openaev.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; +import org.springframework.test.web.servlet.MockMvc; + +@TestInstance(PER_CLASS) +@Transactional +@WithMockUser(isAdmin = true) +@DisplayName("CVE API Integration Tests") +class VulnerabilityApiTest extends IntegrationTest { + + @Resource protected ObjectMapper mapper; + @Autowired private MockMvc mvc; + private Collector collector; + + @Autowired private VulnerabilityComposer vulnerabilityComposer; + @Autowired private CollectorComposer collectorComposer; + @Autowired private VulnerabilityRepository vulnerabilityRepository; + + @BeforeAll + void init() { + collector = + collectorComposer + .forCollector(CollectorFixture.createDefaultCollector("CS")) + .persist() + .get(); + } + + @BeforeEach + void setUp() { + vulnerabilityComposer.reset(); + } + + @Nested + @DisplayName("When working with vulnerabilities") + @WithMockUser(isAdmin = true) + class WhenWorkingWithVulnerabilities { + + @Test + @DisplayName("Should create a new vulnerability successfully") + void shouldCreateNewVulnerability() throws Exception { + VulnerabilityCreateInput input = new VulnerabilityCreateInput(); + input.setExternalId("CVE-2025-1234"); + input.setCvssV31(new BigDecimal("5.2")); + input.setDescription("Test summary for vulnerability creation"); + + String response = + mvc.perform( + post(VULNERABILITY_API) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThatJson(response).node("vulnerability_external_id").isEqualTo("CVE-2025-1234"); + } + + @Test + @DisplayName("Should fetch a Vulnerability by ID") + void shouldFetchCveById() throws Exception { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setExternalId(CVE_2025_5678); + vulnerability.setCvssV31(new BigDecimal("8.9")); + vulnerability.setDescription("Test Vulnerability"); + + vulnerabilityComposer.forVulnerability(vulnerability).persist(); + + String response = + mvc.perform(get(VULNERABILITY_API + "/" + vulnerability.getId())) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThatJson(response).node("vulnerability_external_id").isEqualTo(CVE_2025_5678); + } + + @Test + @DisplayName("Should update an existing Vulnerability") + void shouldUpdateVulnerability() throws Exception { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setExternalId("CVE-2025-5679"); + vulnerability.setCvssV31(new BigDecimal("4.5")); + vulnerability.setDescription("Old description"); + vulnerabilityComposer.forVulnerability(vulnerability).persist(); + + VulnerabilityUpdateInput updateInput = new VulnerabilityUpdateInput(); + updateInput.setDescription("Updated Summary"); + + mvc.perform( + put(VULNERABILITY_API + "/" + vulnerability.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(updateInput))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assertions.assertTrue( + updateInput + .getDescription() + .equals( + vulnerabilityRepository + .findById(vulnerability.getId()) + .map(cve1 -> cve1.getDescription()) + .get())); + } + + @Test + @DisplayName("Should bulk insert multiple Vulnerabilities") + void shouldBulkInsertVulnerabilities() throws Exception { + // -- PREPARE - + VulnerabilityCreateInput input = createDefaultVulnerabilityInput(); + VulnerabilityBulkInsertInput inputs = new VulnerabilityBulkInsertInput(); + inputs.setSourceIdentifier(collector.getId()); + inputs.setLastModifiedDateFetched(now()); + inputs.setLastIndex(1234); + inputs.setInitialDatasetCompleted(false); + inputs.setVulnerabilities(List.of(input)); + + // -- EXECUTE -- + + mvc.perform( + post(VULNERABILITY_API + "/bulk") + .content(asJsonString(inputs)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // -- ASSERT -- + Assertions.assertTrue( + vulnerabilityRepository.findByExternalId(VULNERABILITY_EXTERNAL_ID).isPresent()); + } + + @Test + @DisplayName("Should delete a Vulnerability") + void shouldDeleteVulnerability() throws Exception { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setExternalId("CVE-2025-5679"); + vulnerability.setCvssV31(new BigDecimal("7.5")); + vulnerability.setDescription("To be deleted"); + vulnerabilityComposer.forVulnerability(vulnerability).persist(); + + mvc.perform(delete(VULNERABILITY_API + "/" + vulnerability.getExternalId())) + .andExpect(status().isOk()); + + Assertions.assertFalse( + vulnerabilityRepository.findById(vulnerability.getExternalId()).isPresent()); + } + + @Test + @DisplayName("Should return Vulnerabilities on search") + void shouldReturnVulnerabilitiesOnSearch() throws Exception { + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setExternalId("CVE-2024-5679"); + vulnerability.setCvssV31(new BigDecimal("4.5")); + vulnerability.setDescription("Vulnerability 1"); + vulnerabilityComposer.forVulnerability(vulnerability).persist(); + + Vulnerability cve1 = new Vulnerability(); + cve1.setExternalId("CVE-2025-5671"); + cve1.setCvssV31(new BigDecimal("1.8")); + cve1.setDescription("Vulnerability 2"); + vulnerabilityComposer.forVulnerability(cve1).persist(); + + SearchPaginationInput input = new SearchPaginationInput(); + input.setSize(10); + input.setPage(0); + + String response = + mvc.perform( + post(VULNERABILITY_API + "/search") + .content(asJsonString(input)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThatJson(response) + .inPath("content[*].vulnerability_external_id") + .isArray() + .contains("CVE-2024-5679", "CVE-2025-5671"); + } + } +} diff --git a/openaev-api/src/test/java/io/openaev/rest/inject/StructuredOutputUtilsTest.java b/openaev-api/src/test/java/io/openaev/rest/inject/StructuredOutputUtilsTest.java index 1993567094..584e4f94ee 100644 --- a/openaev-api/src/test/java/io/openaev/rest/inject/StructuredOutputUtilsTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/inject/StructuredOutputUtilsTest.java @@ -126,7 +126,7 @@ void given_raw_output_tasklist_should_return_names() { } @Test - @DisplayName("Should get cve from raw output command") + @DisplayName("Should get vulnerability from raw output command") void given_raw_output_tasklist_should_return_cve() { // username:RID:LM_Hash:NTLM_Hash::: String rawOutput = @@ -134,8 +134,8 @@ void given_raw_output_tasklist_should_return_cve() { + " \"severity\": \"critical\",\n" + " \"host\": \"192.168.56.23\",\n" + " \"classification\": {\n" - + " \"cve-id\": [\n" - + " \"cve-2023-35078\"\n" + + " \"vulnerability-id\": [\n" + + " \"vulnerability-2023-35078\"\n" + " ]\n" + " }\n" + "}"; @@ -157,7 +157,7 @@ void given_raw_output_tasklist_should_return_cve() { String regex = "\"severity\"\\s*:\\s*\"([^\"]+)\"[\\s\\S]*?" + "\"host\"\\s*:\\s*\"([^\"]+)\"[\\s\\S]*?" - + "\"cve-id\"\\s*:\\s*\\[\\s*((?:\"[^\"]+\"\\s*,?\\s*)+)"; + + "\"vulnerability-id\"\\s*:\\s*\\[\\s*((?:\"[^\"]+\"\\s*,?\\s*)+)"; this.testRegexExtraction( rawOutput, @@ -165,7 +165,7 @@ void given_raw_output_tasklist_should_return_cve() { ContractOutputType.CVE, "CVE", regex, - "[{\"asset_id\":null,\"id\":\"\\\"cve-2023-35078\\\"\",\"host\":\"192.168.56.23\",\"severity\":\"critical\"}]"); + "[{\"asset_id\":null,\"id\":\"\\\"vulnerability-2023-35078\\\"\",\"host\":\"192.168.56.23\",\"severity\":\"critical\"}]"); } @Test diff --git a/openaev-api/src/test/java/io/openaev/rest/injector_contract/InjectorContractApiTest.java b/openaev-api/src/test/java/io/openaev/rest/injector_contract/InjectorContractApiTest.java index 39fdbd59f7..57f539b764 100644 --- a/openaev-api/src/test/java/io/openaev/rest/injector_contract/InjectorContractApiTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/injector_contract/InjectorContractApiTest.java @@ -3,7 +3,6 @@ import static io.openaev.rest.injector_contract.InjectorContractApi.INJECTOR_CONTRACT_URL; import static io.openaev.service.UserService.buildAuthenticationToken; import static io.openaev.utils.JsonUtils.asJsonString; -import static io.openaev.utils.fixtures.CveFixture.getRandomExternalVulnerabilityId; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.hamcrest.Matchers.equalTo; @@ -28,14 +27,14 @@ import io.openaev.rest.injector_contract.output.InjectorContractBaseOutput; import io.openaev.rest.injector_contract.output.InjectorContractFullOutput; import io.openaev.utils.fixtures.*; -import io.openaev.utils.fixtures.CveFixture; import io.openaev.utils.fixtures.InjectorContractFixture; import io.openaev.utils.fixtures.InjectorFixture; import io.openaev.utils.fixtures.PaginationFixture; +import io.openaev.utils.fixtures.VulnerabilityFixture; import io.openaev.utils.fixtures.composers.*; import io.openaev.utils.fixtures.composers.AttackPatternComposer; -import io.openaev.utils.fixtures.composers.CveComposer; import io.openaev.utils.fixtures.composers.InjectorContractComposer; +import io.openaev.utils.fixtures.composers.VulnerabilityComposer; import io.openaev.utils.fixtures.files.AttackPatternFixture; import io.openaev.utils.mockUser.WithMockUser; import io.openaev.utils.pagination.SearchPaginationInput; @@ -71,7 +70,7 @@ public class InjectorContractApiTest extends IntegrationTest { @Autowired private InjectorFixture injectorFixture; @Autowired private InjectorContractComposer injectorContractComposer; @Autowired private AttackPatternComposer attackPatternComposer; - @Autowired private CveComposer cveComposer; + @Autowired private VulnerabilityComposer vulnerabilityComposer; @Autowired private InjectorContractRepository injectorContractRepository; @Autowired private PayloadComposer payloadComposer; @@ -85,7 +84,7 @@ public void setup() { injectorContractComposer.reset(); attackPatternComposer.reset(); payloadComposer.reset(); - cveComposer.reset(); + vulnerabilityComposer.reset(); userComposer.reset(); groupComposer.reset(); roleComposer.reset(); @@ -168,15 +167,18 @@ void updatingAttackPatternMappingsWithNonExistingAttackPatternsFailWithNotFound( @DisplayName("Updating vulnerability mappings succeeds") void updatingVulnerabilitiesMappingsSucceeds() throws Exception { for (int i = 0; i < 3; ++i) { - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); } em.flush(); em.clear(); InjectorContractUpdateMappingInput input = new InjectorContractUpdateMappingInput(); - input.setVulnerabilityIds(cveComposer.generatedItems.stream().map(Cve::getId).toList()); + input.setVulnerabilityIds( + vulnerabilityComposer.generatedItems.stream().map(Vulnerability::getId).toList()); mvc.perform( put(INJECTOR_CONTRACT_URL @@ -279,9 +281,11 @@ void deleteCustomContractSucceeds() throws Exception { @Test @DisplayName("Updating contract succeeds") void updateContractSucceeds() throws Exception { - CveComposer.Composer vulnWrapper = - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + VulnerabilityComposer.Composer vulnWrapper = + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); AttackPatternComposer.Composer attackPatternWrapper = attackPatternComposer @@ -317,13 +321,17 @@ void updateContractSucceeds() throws Exception { @Test @DisplayName("Updating contract succeeds with external vuln IDs") void updateContractWithExtVulnIdsSucceeds() throws Exception { - CveComposer.Composer vulnWrapper = - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + VulnerabilityComposer.Composer vulnWrapper = + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); - CveComposer.Composer otherVulnWrapper = - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + VulnerabilityComposer.Composer otherVulnWrapper = + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); AttackPatternComposer.Composer attackPatternWrapper = attackPatternComposer @@ -556,8 +564,10 @@ void withExistingAttackPatternsByExternalIdCreateContractSucceeds() throws Excep @DisplayName("With existing vulnerabilities, creating contract succeeds") void withExistingVulnerabilitiesCreateContractSucceeds() throws Exception { for (int i = 0; i < 3; ++i) { - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); } em.flush(); @@ -565,7 +575,8 @@ void withExistingVulnerabilitiesCreateContractSucceeds() throws Exception { InjectorContractAddInput input = new InjectorContractAddInput(); input.setId(injectorContractInternalId); - input.setVulnerabilityIds(cveComposer.generatedItems.stream().map(Cve::getId).toList()); + input.setVulnerabilityIds( + vulnerabilityComposer.generatedItems.stream().map(Vulnerability::getId).toList()); input.setInjectorId(injectorFixture.getWellKnownOaevImplantInjector().getId()); input.setContent("{\"fields\":[]}"); @@ -603,7 +614,7 @@ void withExistingVulnerabilitiesCreateContractSucceeds() throws Exception { injectorContractInternalId, String.join( ",", - cveComposer.generatedItems.stream() + vulnerabilityComposer.generatedItems.stream() .map(vuln -> String.format("\"" + vuln.getId() + "\"")) .toList()))); } @@ -612,8 +623,10 @@ void withExistingVulnerabilitiesCreateContractSucceeds() throws Exception { @DisplayName("With existing vulnerabilities by external ID, creating contract succeeds") void withExistingVulnerabilitiesByExternalIdCreateContractSucceeds() throws Exception { for (int i = 0; i < 3; ++i) { - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); } em.flush(); @@ -623,7 +636,7 @@ void withExistingVulnerabilitiesByExternalIdCreateContractSucceeds() throws Exce input.setId(injectorContractInternalId); input.setVulnerabilityExternalIds( // force converting the ids to lower case; it must work in case-insensitive mode - cveComposer.generatedItems.stream() + vulnerabilityComposer.generatedItems.stream() .map(vuln -> vuln.getExternalId().toLowerCase()) .toList()); input.setInjectorId(injectorFixture.getWellKnownOaevImplantInjector().getId()); @@ -663,7 +676,7 @@ void withExistingVulnerabilitiesByExternalIdCreateContractSucceeds() throws Exce injectorContractInternalId, String.join( ",", - cveComposer.generatedItems.stream() + vulnerabilityComposer.generatedItems.stream() .map(vuln -> String.format("\"" + vuln.getId() + "\"")) .toList()))); } @@ -807,15 +820,18 @@ void updatingAttackPatternMappingsWithNonExistingAttackPatternsFailWithNotFound( @DisplayName("Updating vulnerability mappings succeeds") void updatingVulnerabilitiesMappingsSucceeds() throws Exception { for (int i = 0; i < 3; ++i) { - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); } em.flush(); em.clear(); InjectorContractUpdateMappingInput input = new InjectorContractUpdateMappingInput(); - input.setVulnerabilityIds(cveComposer.generatedItems.stream().map(Cve::getId).toList()); + input.setVulnerabilityIds( + vulnerabilityComposer.generatedItems.stream().map(Vulnerability::getId).toList()); mvc.perform( put(INJECTOR_CONTRACT_URL + "/" + externalId + "/mapping") @@ -904,9 +920,11 @@ void deleteCustomContractSucceeds() throws Exception { @Test @DisplayName("Updating contract succeeds") void updateContractSucceeds() throws Exception { - CveComposer.Composer vulnWrapper = - cveComposer - .forCve(CveFixture.createDefaultCve(getRandomExternalVulnerabilityId())) + VulnerabilityComposer.Composer vulnWrapper = + vulnerabilityComposer + .forVulnerability( + VulnerabilityFixture.createVulnerabilityInput( + VulnerabilityFixture.getRandomExternalVulnerabilityId())) .persist(); AttackPatternComposer.Composer attackPatternWrapper = attackPatternComposer diff --git a/openaev-api/src/test/java/io/openaev/rest/stream/StreamApiTest.java b/openaev-api/src/test/java/io/openaev/rest/stream/StreamApiTest.java index 8b04f10f9f..f986b57da8 100644 --- a/openaev-api/src/test/java/io/openaev/rest/stream/StreamApiTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/stream/StreamApiTest.java @@ -126,8 +126,8 @@ public void test_listenDatabaseUpdate_WHEN_user_has_not_permission() { @Test public void test_given_databaseEvent_when_eventIsCVE_then_doNothing() { - Cve cve = new Cve(); - BaseEvent event = new BaseEvent(DATA_UPDATE, cve, mock(ObjectMapper.class)); + Vulnerability vulnerability = new Vulnerability(); + BaseEvent event = new BaseEvent(DATA_UPDATE, vulnerability, mock(ObjectMapper.class)); streamApi.listenDatabaseUpdate(event); diff --git a/openaev-api/src/test/java/io/openaev/service/stix/SecurityCoverageServiceTest.java b/openaev-api/src/test/java/io/openaev/service/stix/SecurityCoverageServiceTest.java index eaee4aff2f..e18ab7eddc 100644 --- a/openaev-api/src/test/java/io/openaev/service/stix/SecurityCoverageServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/service/stix/SecurityCoverageServiceTest.java @@ -46,7 +46,7 @@ public class SecurityCoverageServiceTest extends IntegrationTest { @Autowired private SecurityCoverageSendJobComposer securityCoverageSendJobComposer; @Autowired private InjectorFixture injectorFixture; @Autowired private AttackPatternComposer attackPatternComposer; - @Autowired private CveComposer vulnerabilityComposer; + @Autowired private VulnerabilityComposer vulnerabilityComposer; @Autowired private SecurityPlatformComposer securityPlatformComposer; @Autowired private EntityManager entityManager; @Autowired private SecurityCoverageSendJobService securityCoverageSendJobService; @@ -78,17 +78,17 @@ public void setup() { */ private ExerciseComposer.Composer createExerciseWrapperWithInjectsForDomainObjects( Map attackPatternWrappers, - Map vulnWrappers) { + Map vulnWrappers) { // ensure attack patterns have IDs attackPatternWrappers.keySet().forEach(AttackPatternComposer.Composer::persist); // ensure vulns have IDs - vulnWrappers.keySet().forEach(CveComposer.Composer::persist); + vulnWrappers.keySet().forEach(VulnerabilityComposer.Composer::persist); List attackPatternList = attackPatternWrappers.keySet().stream().map(AttackPatternComposer.Composer::get).toList(); - List vulnerabilities = - vulnWrappers.keySet().stream().map(CveComposer.Composer::get).toList(); + List vulnerabilities = + vulnWrappers.keySet().stream().map(VulnerabilityComposer.Composer::get).toList(); ExerciseComposer.Composer exerciseWrapper = exerciseComposer @@ -131,7 +131,8 @@ private ExerciseComposer.Composer createExerciseWrapperWithInjectsForDomainObjec } } - for (Map.Entry vulnw : vulnWrappers.entrySet()) { + for (Map.Entry vulnw : + vulnWrappers.entrySet()) { if (vulnw.getValue()) { // this vuln should be covered exerciseWrapper.withInject( injectComposer @@ -377,8 +378,9 @@ public class withEnabledPreviewFeature { "When all vulnerabilities are covered and all expectations are successful, bundle is correct") public void whenAllVulnerabilitiesAreCoveredAndAllExpectationsAreSuccessful_bundleIsCorrect() throws ParsingException, JsonProcessingException { - CveComposer.Composer vuln1 = - vulnerabilityComposer.forCve(CveFixture.createDefaultCve("CVE-1234-5678")); + VulnerabilityComposer.Composer vuln1 = + vulnerabilityComposer.forVulnerability( + VulnerabilityFixture.createVulnerabilityInput("CVE-1234-5678")); // create exercise cover all TTPs ExerciseComposer.Composer exerciseWrapper = createExerciseWrapperWithInjectsForDomainObjects(Map.of(), Map.of(vuln1, true)); @@ -445,8 +447,9 @@ public class WithoutPreviewFeature { "When all vulnerabilities are covered and all expectations are successful, bundle is correct") public void whenAllVulnerabilitiesAreCoveredAndAllExpectationsAreSuccessful_bundleIsCorrect() throws ParsingException, JsonProcessingException { - CveComposer.Composer vuln1 = - vulnerabilityComposer.forCve(CveFixture.createDefaultCve("CVE-1234-5678")); + VulnerabilityComposer.Composer vuln1 = + vulnerabilityComposer.forVulnerability( + VulnerabilityFixture.createVulnerabilityInput("CVE-1234-5678")); // create exercise cover all TTPs ExerciseComposer.Composer exerciseWrapper = createExerciseWrapperWithInjectsForDomainObjects(Map.of(), Map.of(vuln1, true)); diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/CveFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/CveFixture.java deleted file mode 100644 index f5ac65bf25..0000000000 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/CveFixture.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.openaev.utils.fixtures; - -import io.openaev.database.model.Cve; -import java.math.BigDecimal; -import java.util.UUID; - -public class CveFixture { - - public static final String CVE_2023_48788 = "CVE-2023-48788"; - public static final String CVE_2025_5678 = "CVE-2025-5678"; - - public static Cve createDefaultCve(String externalId) { - Cve cve = new Cve(); - cve.setCvssV31(new BigDecimal("10.0")); - cve.setExternalId(externalId); - return cve; - } - - public static String getRandomExternalVulnerabilityId() { - return "CVE-%s".formatted(UUID.randomUUID()); - } -} diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/FindingFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/FindingFixture.java index ecaf92f609..57f42ade8b 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/FindingFixture.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/FindingFixture.java @@ -32,7 +32,7 @@ public static Finding createDefaultCveFindingWithRandomTitle() { .formatted(Math.round(Math.random() * 1000), Math.round(Math.random() * 1000000)); Finding finding = new Finding(); finding.setType(ContractOutputType.CVE); - finding.setName("cve"); + finding.setName("vulnerability"); finding.setField(TEXT_FIELD); finding.setValue(cveName); finding.setLabels(new String[] {"reconnaissance phase"}); diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/SecurityCoverageFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/SecurityCoverageFixture.java index d6ab4c3d50..bc826d8cc7 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/SecurityCoverageFixture.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/SecurityCoverageFixture.java @@ -2,9 +2,9 @@ import io.openaev.cron.ScheduleFrequency; import io.openaev.database.model.AttackPattern; -import io.openaev.database.model.Cve; import io.openaev.database.model.SecurityCoverage; import io.openaev.database.model.StixRefToExternalRef; +import io.openaev.database.model.Vulnerability; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,7 +26,7 @@ public static SecurityCoverage createDefaultSecurityCoverage() { } public static SecurityCoverage createSecurityCoverageWithDomainObjects( - List attackPatterns, List vulnerabilities) { + List attackPatterns, List vulnerabilities) { Set attackPatternRefs = attackPatterns.stream() .map( diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityFixture.java new file mode 100644 index 0000000000..e0616c2b24 --- /dev/null +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityFixture.java @@ -0,0 +1,24 @@ +package io.openaev.utils.fixtures; + +import io.openaev.database.model.Vulnerability; +import java.math.BigDecimal; +import java.util.UUID; + +public class VulnerabilityFixture { + + public static final String CVE_2023_48788 = "CVE-2023-48788"; + public static final String CVE_2025_5678 = "CVE-2025-5678"; + public static final String VULNERABILITY_EXTERNAL_ID = "CVE-2025-5679"; + public static final BigDecimal VULNERABILITY_CVSS_V31 = new BigDecimal("4.5"); + + public static Vulnerability createVulnerabilityInput(String externalId) { + Vulnerability input = new Vulnerability(); + input.setExternalId(externalId); + input.setCvssV31(VULNERABILITY_CVSS_V31); + return input; + } + + public static String getRandomExternalVulnerabilityId() { + return "CVE-%s".formatted(UUID.randomUUID()); + } +} diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/CveInputFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityInputFixture.java similarity index 61% rename from openaev-api/src/test/java/io/openaev/utils/fixtures/CveInputFixture.java rename to openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityInputFixture.java index 388bf0881d..d1148b3635 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/CveInputFixture.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/VulnerabilityInputFixture.java @@ -1,16 +1,15 @@ package io.openaev.utils.fixtures; -import io.openaev.rest.cve.form.CveCreateInput; +import io.openaev.rest.vulnerability.form.VulnerabilityCreateInput; import java.math.BigDecimal; -public class CveInputFixture { - +public class VulnerabilityInputFixture { 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(); + public static VulnerabilityCreateInput createDefaultVulnerabilityInput() { + VulnerabilityCreateInput input = new VulnerabilityCreateInput(); input.setExternalId(CVE_EXTERNAL_ID); input.setCvssV31(CVE_CVSS_V31); input.setDescription(CVE_DESCRIPTION); diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/InjectorContractComposer.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/InjectorContractComposer.java index aee21d6418..797793ed3c 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/InjectorContractComposer.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/InjectorContractComposer.java @@ -8,9 +8,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.openaev.database.model.AttackPattern; -import io.openaev.database.model.Cve; import io.openaev.database.model.Injector; import io.openaev.database.model.InjectorContract; +import io.openaev.database.model.Vulnerability; import io.openaev.database.repository.InjectorContractRepository; import io.openaev.database.repository.InjectorRepository; import io.openaev.injectors.challenge.model.ChallengeContent; @@ -36,7 +36,7 @@ public class Composer extends InnerComposerBase { private final InjectorContract injectorContract; private final List attackPatternComposer = new ArrayList<>(); - private final List vulnerabilityComposer = new ArrayList<>(); + private final List vulnerabilityComposer = new ArrayList<>(); private Optional payloadComposer = Optional.empty(); private final List challengeComposers = new ArrayList<>(); private final List articleComposers = new ArrayList<>(); @@ -62,9 +62,9 @@ public Composer withAttackPattern(AttackPatternComposer.Composer attackPatternCo return this; } - public Composer withVulnerability(CveComposer.Composer cveComposer) { + public Composer withVulnerability(VulnerabilityComposer.Composer cveComposer) { this.vulnerabilityComposer.add(cveComposer); - Set tempVulnerability = this.injectorContract.getVulnerabilities(); + Set tempVulnerability = this.injectorContract.getVulnerabilities(); tempVulnerability.add(cveComposer.get()); this.injectorContract.setVulnerabilities(tempVulnerability); return this; @@ -141,7 +141,7 @@ public Composer persist() { challengeComposers.forEach(ChallengeComposer.Composer::persist); articleComposers.forEach(ArticleComposer.Composer::persist); attackPatternComposer.forEach(AttackPatternComposer.Composer::persist); - vulnerabilityComposer.forEach(CveComposer.Composer::persist); + vulnerabilityComposer.forEach(VulnerabilityComposer.Composer::persist); if (!WELL_KNOWN_CONTRACT_IDS.contains(injectorContract.getId())) { entityManager.persist(injectorContract.getInjector()); injectorRepository.save(injectorContract.getInjector()); @@ -158,7 +158,7 @@ public Composer delete() { challengeComposers.forEach(ChallengeComposer.Composer::delete); articleComposers.forEach(ArticleComposer.Composer::delete); attackPatternComposer.forEach(AttackPatternComposer.Composer::delete); - vulnerabilityComposer.forEach(CveComposer.Composer::delete); + vulnerabilityComposer.forEach(VulnerabilityComposer.Composer::delete); if (!WELL_KNOWN_CONTRACT_IDS.contains(injectorContract.getId())) { injectorContractRepository.delete(injectorContract); } diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/VulnerabilityComposer.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/VulnerabilityComposer.java new file mode 100644 index 0000000000..207e0289ce --- /dev/null +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/composers/VulnerabilityComposer.java @@ -0,0 +1,43 @@ +package io.openaev.utils.fixtures.composers; + +import io.openaev.database.model.Vulnerability; +import io.openaev.database.repository.VulnerabilityRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class VulnerabilityComposer extends ComposerBase { + + @Autowired private VulnerabilityRepository vulnerabilityRepository; + + public class Composer extends InnerComposerBase { + + private final Vulnerability vulnerability; + + public Composer(Vulnerability vulnerability) { + this.vulnerability = vulnerability; + } + + @Override + public VulnerabilityComposer.Composer persist() { + vulnerabilityRepository.save(this.vulnerability); + return this; + } + + @Override + public VulnerabilityComposer.Composer delete() { + vulnerabilityRepository.delete(this.vulnerability); + return this; + } + + @Override + public Vulnerability get() { + return this.vulnerability; + } + } + + public VulnerabilityComposer.Composer forVulnerability(Vulnerability vulnerability) { + generatedItems.add(vulnerability); + return new VulnerabilityComposer.Composer(vulnerability); + } +} diff --git a/openaev-front/src/actions/cve-actions.ts b/openaev-front/src/actions/cve-actions.ts deleted file mode 100644 index 6a3aa58391..0000000000 --- a/openaev-front/src/actions/cve-actions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { simpleCall, simpleDelCall, simplePostCall, simplePutCall } from '../utils/Action'; -import type { CveCreateInput, CveSimple, CveUpdateInput, SearchPaginationInput } from '../utils/api-types'; - -const CVE_URI = '/api/cves'; - -export const searchCves = (searchPaginationInput: SearchPaginationInput) => { - const data = searchPaginationInput; - const uri = `${CVE_URI}/search`; - return simplePostCall(uri, data); -}; - -export const fetchCve = (cveId: string) => { - const uri = `${CVE_URI}/${cveId}`; - return simpleCall(uri); -}; - -export const fetchCveByExternalId = (cveId: string) => { - const uri = `${CVE_URI}/external-id/${cveId}`; - return simpleCall(uri); -}; - -export const addCve = (data: CveCreateInput) => { - return simplePostCall(`${CVE_URI}`, data); -}; - -export const updateCve = (cveId: CveSimple['cve_id'], data: CveUpdateInput) => { - const uri = `${CVE_URI}/${cveId}`; - return simplePutCall(uri, data); -}; - -export const deleteCve = (cveId: CveSimple['cve_id']) => { - const uri = `${CVE_URI}/${cveId}`; - return simpleDelCall(uri); -}; diff --git a/openaev-front/src/actions/vulnerability-actions.ts b/openaev-front/src/actions/vulnerability-actions.ts new file mode 100644 index 0000000000..dcba7c9a75 --- /dev/null +++ b/openaev-front/src/actions/vulnerability-actions.ts @@ -0,0 +1,34 @@ +import { simpleCall, simpleDelCall, simplePostCall, simplePutCall } from '../utils/Action'; +import type { SearchPaginationInput, VulnerabilityCreateInput, VulnerabilitySimple, VulnerabilityUpdateInput } from '../utils/api-types'; + +const VULNERABILITY_URI = '/api/vulnerabilities'; + +export const searchVulnerabilities = (searchPaginationInput: SearchPaginationInput) => { + const data = searchPaginationInput; + const uri = `${VULNERABILITY_URI}/search`; + return simplePostCall(uri, data); +}; + +export const fetchVulnerability = (vulnerabilityId: string) => { + const uri = `${VULNERABILITY_URI}/${vulnerabilityId}`; + return simpleCall(uri); +}; + +export const fetchVulnerabilityByExternalId = (vulnerabilityId: string) => { + const uri = `${VULNERABILITY_URI}/external-id/${vulnerabilityId}`; + return simpleCall(uri); +}; + +export const addVulnerability = (data: VulnerabilityCreateInput) => { + return simplePostCall(`${VULNERABILITY_URI}`, data); +}; + +export const updateVulnerability = (vulnerabilityId: VulnerabilitySimple['vulnerability_id'], data: VulnerabilityUpdateInput) => { + const uri = `${VULNERABILITY_URI}/${vulnerabilityId}`; + return simplePutCall(uri, data); +}; + +export const deleteVulnerability = (vulnerabilityId: VulnerabilitySimple['vulnerability_id']) => { + const uri = `${VULNERABILITY_URI}/${vulnerabilityId}`; + return simpleDelCall(uri); +}; diff --git a/openaev-front/src/admin/components/findings/Finding.ts b/openaev-front/src/admin/components/findings/Finding.ts index 89c580dd2e..d21dec057d 100644 --- a/openaev-front/src/admin/components/findings/Finding.ts +++ b/openaev-front/src/admin/components/findings/Finding.ts @@ -6,7 +6,7 @@ enum ContractOutputElementType { ipv4 = 'IPv4', ipv6 = 'IPv6', credentials = 'Credentials', - cve = 'CVE', + vulnerability = 'CVE', }; export default ContractOutputElementType; diff --git a/openaev-front/src/admin/components/findings/FindingDetail.tsx b/openaev-front/src/admin/components/findings/FindingDetail.tsx index d00adb752a..4102343084 100644 --- a/openaev-front/src/admin/components/findings/FindingDetail.tsx +++ b/openaev-front/src/admin/components/findings/FindingDetail.tsx @@ -1,19 +1,19 @@ import { useEffect, useState } from 'react'; -import { fetchCveByExternalId } from '../../../actions/cve-actions'; +import { fetchVulnerabilityByExternalId } from '../../../actions/vulnerability-actions'; import type { Page } from '../../../components/common/queryable/Page'; import { type Header } from '../../../components/common/SortHeadersList'; import Tabs, { type TabsEntry } from '../../../components/common/tabs/Tabs'; import useTabs from '../../../components/common/tabs/useTabs'; import { useFormatter } from '../../../components/i18n'; -import { type AggregatedFindingOutput, type CveOutput, type RelatedFindingOutput, type SearchPaginationInput } from '../../../utils/api-types'; +import { type AggregatedFindingOutput, type RelatedFindingOutput, type SearchPaginationInput, type VulnerabilityOutput } from '../../../utils/api-types'; import useEnterpriseEdition from '../../../utils/hooks/useEnterpriseEdition'; -import { type CveStatus } from '../settings/cves/CveDetail'; -import CveTabPanel from '../settings/cves/CveTabPanel'; -import GeneralVulnerabilityInfoTab from '../settings/cves/GeneralVulnerabilityInfoTab'; -import RelatedInjectsTab from '../settings/cves/RelatedInjectsTab'; -import RemediationInfoTab from '../settings/cves/RemediationInfoTab'; -import TabLabelWithEE from '../settings/cves/TabLabelWithEE'; +import GeneralVulnerabilityInfoTab from '../settings/vulnerabilities/GeneralVulnerabilityInfoTab'; +import RelatedInjectsTab from '../settings/vulnerabilities/RelatedInjectsTab'; +import RemediationInfoTab from '../settings/vulnerabilities/RemediationInfoTab'; +import TabLabelWithEE from '../settings/vulnerabilities/TabLabelWithEE'; +import { type VulnerabilityStatus } from '../settings/vulnerabilities/VulnerabilityDetail'; +import VulnerabilityTabPanel from '../settings/vulnerabilities/VulnerabilityTabPanel'; interface Props { searchFindings: (input: SearchPaginationInput) => Promise<{ data: Page }>; @@ -42,24 +42,24 @@ const FindingDetail = ({ const isCVE = selectedFinding.finding_type === 'cve'; - const [cve, setCve] = useState(null); - const [cveStatus, setCveStatus] = useState('loading'); + const [vulnerability, setVulnerability] = useState(null); + const [vulnerabilityStatus, setVulnerabilityStatus] = useState('loading'); useEffect(() => { if (!isCVE || !selectedFinding.finding_value) return; - setCveStatus('loading'); + setVulnerabilityStatus('loading'); - fetchCveByExternalId(selectedFinding.finding_value) + fetchVulnerabilityByExternalId(selectedFinding.finding_value) .then((res) => { - setCve(res.data); - if (res.data?.cve_cvss_v31 && onCvssScore) { - onCvssScore(res.data.cve_cvss_v31); + setVulnerability(res.data); + if (res.data?.vulnerability_cvss_v31 && onCvssScore) { + onCvssScore(res.data.vulnerability_cvss_v31); } - setCveStatus(res.data ? 'loaded' : 'notAvailable'); + setVulnerabilityStatus(res.data ? 'loaded' : 'notAvailable'); }) - .catch(() => setCveStatus('notAvailable')); + .catch(() => setVulnerabilityStatus('notAvailable')); }, [selectedFinding, isCVE]); const tabEntries: TabsEntry[] = isCVE @@ -83,9 +83,9 @@ const FindingDetail = ({ switch (currentTab) { case 'General': return ( - - - + + + ); case 'Related Injects': return ( @@ -100,9 +100,9 @@ const FindingDetail = ({ case 'Remediation': return isEE ? ( - - - + + + ) : null; default: diff --git a/openaev-front/src/admin/components/settings/Index.tsx b/openaev-front/src/admin/components/settings/Index.tsx index 36b3d208be..83c14b19fc 100644 --- a/openaev-front/src/admin/components/settings/Index.tsx +++ b/openaev-front/src/admin/components/settings/Index.tsx @@ -4,7 +4,6 @@ import { errorWrapper } from '../../../components/Error'; import NotFound from '../../../components/NotFound'; import Organizations from '../teams/Organizations'; import AttackPatterns from './attack_patterns/AttackPatterns'; -import Cves from './cves/Cves'; import XlsMappers from './data_ingestion/XlsMappers'; import Experience from './experience/Experience'; import Groups from './groups/Groups'; @@ -15,6 +14,7 @@ import Roles from './roles/Roles'; import TagRules from './tag_rules/TagRules'; import Tags from './tags/Tags'; import Users from './users/Users'; +import Vulnerabilities from './vulnerabilities/Vulnerabilities'; const Index = () => { return ( @@ -31,7 +31,7 @@ const Index = () => { - + } /> diff --git a/openaev-front/src/admin/components/settings/TaxonomiesMenu.tsx b/openaev-front/src/admin/components/settings/TaxonomiesMenu.tsx index 606db22d83..b753e42861 100644 --- a/openaev-front/src/admin/components/settings/TaxonomiesMenu.tsx +++ b/openaev-front/src/admin/components/settings/TaxonomiesMenu.tsx @@ -22,7 +22,7 @@ const TaxonomiesMenuComponent: FunctionComponent = () => { label: 'Kill chain phases', }, { - path: '/admin/settings/taxonomies/cves', + path: '/admin/settings/taxonomies/vulnerabilities', icon: () => (), label: 'Vulnerabilities', }, diff --git a/openaev-front/src/admin/components/settings/cves/CreateCve.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/CreateVulnerability.tsx similarity index 67% rename from openaev-front/src/admin/components/settings/cves/CreateCve.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/CreateVulnerability.tsx index 1d1b8d58a1..cb0720dfad 100644 --- a/openaev-front/src/admin/components/settings/cves/CreateCve.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/CreateVulnerability.tsx @@ -3,11 +3,11 @@ import { Fab } from '@mui/material'; import { type FunctionComponent, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; -import { addCve } from '../../../../actions/cve-actions'; +import { addVulnerability } from '../../../../actions/vulnerability-actions'; import Drawer from '../../../../components/common/Drawer'; import { useFormatter } from '../../../../components/i18n'; -import { type CveCreateInput, type CveSimple } from '../../../../utils/api-types'; -import CveForm from './CveForm'; +import { type VulnerabilityCreateInput, type VulnerabilitySimple } from '../../../../utils/api-types'; +import VulnerabilityForm from './VulnerabilityForm'; const useStyles = makeStyles()({ createButton: { @@ -17,16 +17,16 @@ const useStyles = makeStyles()({ }, }); -interface Props { onCreate?: (result: CveSimple) => void } +interface Props { onCreate?: (result: VulnerabilitySimple) => void } -const CreateCve: FunctionComponent = ({ onCreate }) => { +const CreateVulnerability: FunctionComponent = ({ onCreate }) => { const [open, setOpen] = useState(false); const { t } = useFormatter(); const { classes } = useStyles(); - const onSubmit = (data: CveCreateInput) => { - addCve(data).then( - (result: { data: CveSimple }) => { + const onSubmit = (data: VulnerabilityCreateInput) => { + addVulnerability(data).then( + (result: { data: VulnerabilitySimple }) => { if (result) { if (onCreate) { onCreate(result.data); @@ -57,7 +57,7 @@ const CreateCve: FunctionComponent = ({ onCreate }) => { handleClose={() => setOpen(false)} title={t('Add a new vulnerability')} > - @@ -66,4 +66,4 @@ const CreateCve: FunctionComponent = ({ onCreate }) => { ); }; -export default CreateCve; +export default CreateVulnerability; diff --git a/openaev-front/src/admin/components/settings/cves/GeneralVulnerabilityInfoTab.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/GeneralVulnerabilityInfoTab.tsx similarity index 73% rename from openaev-front/src/admin/components/settings/cves/GeneralVulnerabilityInfoTab.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/GeneralVulnerabilityInfoTab.tsx index 8d211786d2..a034488aad 100644 --- a/openaev-front/src/admin/components/settings/cves/GeneralVulnerabilityInfoTab.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/GeneralVulnerabilityInfoTab.tsx @@ -3,20 +3,20 @@ import { useTheme } from '@mui/material/styles'; import CVSSBadge from '../../../../components/CvssBadge'; import { useFormatter } from '../../../../components/i18n'; -import type { CveOutput, CweOutput } from '../../../../utils/api-types'; +import type { CweOutput, VulnerabilityOutput } from '../../../../utils/api-types'; import { emptyFilled } from '../../../../utils/String'; -interface Props { cve: CveOutput } +interface Props { vulnerability: VulnerabilityOutput } -const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { +const GeneralVulnerabilityInfoTab = ({ vulnerability }: Props) => { const { fldt, t } = useFormatter(); const theme = useTheme(); return (
- {/* CVE Id */} + {/* VULNERABILITY ID */} - {emptyFilled(cve?.cve_external_id)} + {emptyFilled(vulnerability?.vulnerability_external_id)} {/* Description */} @@ -24,7 +24,7 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {t('Description')} - {emptyFilled(cve?.cve_description)} + {emptyFilled(vulnerability?.vulnerability_description)} @@ -35,11 +35,11 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {`${t('Published')}: `} - {fldt(cve?.cve_published)} + {fldt(vulnerability?.vulnerability_published)} {`${t('Source')}: `} - {emptyFilled(cve?.cve_source_identifier)} + {emptyFilled(vulnerability?.vulnerability_source_identifier)} @@ -48,7 +48,7 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {t('Metrics Version 3.1')} - {cve?.cve_cvss_v31 && } + {vulnerability?.vulnerability_cvss_v31 && } {/* References */} @@ -56,8 +56,8 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {t('References to Advisories, Solutions, and Tools')} - {cve?.cve_reference_urls?.length ? ( - cve.cve_reference_urls.map((url, index) => ( + {vulnerability?.vulnerability_reference_urls?.length ? ( + vulnerability.vulnerability_reference_urls.map((url, index) => ( {url} @@ -77,19 +77,19 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {`${t('Vulnerability Name')}: `} - {emptyFilled(cve?.cve_cisa_vulnerability_name)} + {emptyFilled(vulnerability?.vulnerability_cisa_vulnerability_name)} {`${t('Date Added')}: `} - {fldt(cve?.cve_cisa_exploit_add)} + {fldt(vulnerability?.vulnerability_cisa_exploit_add)} {`${t('Due Date')}: `} - {fldt(cve?.cve_cisa_action_due)} + {fldt(vulnerability?.vulnerability_cisa_action_due)} {`${t('Required Action')}: `} - {emptyFilled(cve?.cve_cisa_required_action)} + {emptyFilled(vulnerability?.vulnerability_cisa_required_action)} @@ -99,8 +99,8 @@ const GeneralVulnerabilityInfoTab = ({ cve }: Props) => { {t('Weakness Enumeration')} - {cve?.cve_cwes?.length ? ( - cve.cve_cwes.map((cwe: CweOutput) => ( + {vulnerability?.vulnerability_cwes?.length ? ( + vulnerability.vulnerability_cwes.map((cwe: CweOutput) => ( {cwe.cwe_external_id} diff --git a/openaev-front/src/admin/components/settings/cves/RelatedInjectsTab.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/RelatedInjectsTab.tsx similarity index 100% rename from openaev-front/src/admin/components/settings/cves/RelatedInjectsTab.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/RelatedInjectsTab.tsx diff --git a/openaev-front/src/admin/components/settings/cves/RemediationInfoTab.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/RemediationInfoTab.tsx similarity index 62% rename from openaev-front/src/admin/components/settings/cves/RemediationInfoTab.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/RemediationInfoTab.tsx index 44505a4998..b02fe11997 100644 --- a/openaev-front/src/admin/components/settings/cves/RemediationInfoTab.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/RemediationInfoTab.tsx @@ -2,11 +2,11 @@ import { Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { useFormatter } from '../../../../components/i18n'; -import type { CveOutput } from '../../../../utils/api-types'; +import type { VulnerabilityOutput } from '../../../../utils/api-types'; -interface Props { cve: CveOutput } +interface Props { vulnerability: VulnerabilityOutput } -const RemediationInfoTab = ({ cve }: Props) => { +const RemediationInfoTab = ({ vulnerability }: Props) => { const { t } = useFormatter(); const theme = useTheme(); @@ -17,7 +17,7 @@ const RemediationInfoTab = ({ cve }: Props) => {
         
-          {cve?.cve_remediation ?? t('There is no information yet on a vulnerability remediation for this vulnerability.')}
+          {vulnerability?.vulnerability_remediation ?? t('There is no information yet on a vulnerability remediation for this vulnerability.')}
         
       
diff --git a/openaev-front/src/admin/components/settings/cves/TabLabelWithEE.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/TabLabelWithEE.tsx similarity index 100% rename from openaev-front/src/admin/components/settings/cves/TabLabelWithEE.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/TabLabelWithEE.tsx diff --git a/openaev-front/src/admin/components/settings/cves/Cves.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/Vulnerabilities.tsx similarity index 61% rename from openaev-front/src/admin/components/settings/cves/Cves.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/Vulnerabilities.tsx index 3c143ec029..914585c265 100644 --- a/openaev-front/src/admin/components/settings/cves/Cves.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/Vulnerabilities.tsx @@ -4,7 +4,7 @@ import { type CSSProperties, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router'; import { makeStyles } from 'tss-react/mui'; -import { searchCves } from '../../../../actions/cve-actions'; +import { searchVulnerabilities } from '../../../../actions/vulnerability-actions'; import Breadcrumbs from '../../../../components/Breadcrumbs'; import Drawer from '../../../../components/common/Drawer'; import { initSorting } from '../../../../components/common/queryable/Page'; @@ -17,13 +17,13 @@ import { type Header } from '../../../../components/common/SortHeadersList'; import CVSSBadge from '../../../../components/CvssBadge'; import { useFormatter } from '../../../../components/i18n'; import PaginatedListLoader from '../../../../components/PaginatedListLoader'; -import { type CveSimple, type SearchPaginationInput } from '../../../../utils/api-types'; +import { type SearchPaginationInput, type VulnerabilitySimple } from '../../../../utils/api-types'; import { Can } from '../../../../utils/permissions/PermissionsProvider'; import { ACTIONS, SUBJECTS } from '../../../../utils/permissions/types'; import TaxonomiesMenu from '../TaxonomiesMenu'; -import CreateCve from './CreateCve'; -import CveDetail from './CveDetail'; -import CvePopover from './CvePopover'; +import CreateVulnerability from './CreateVulnerability'; +import VulnerabilityDetail from './VulnerabilityDetail'; +import VulnerabilityPopover from './VulnerabilityPopover'; const useStyles = makeStyles()({ itemHead: { textTransform: 'uppercase' }, @@ -31,56 +31,56 @@ const useStyles = makeStyles()({ }); const inlineStyles: Record = ({ - cve_external_id: { width: '20%' }, - cve_cvss_v31: { width: '20%' }, - cve_published: { width: '60%' }, + vulnerability_external_id: { width: '20%' }, + vulnerability_cvss_v31: { width: '20%' }, + vulnerability_published: { width: '60%' }, }); -const Cves = () => { +const Vulnerabilities = () => { const { fldt, t } = useFormatter(); const { classes } = useStyles(); const bodyItemsStyles = useBodyItemsStyles(); const [loading, setLoading] = useState(true); - const [selectedCve, setSelectedCve] = useState(null); + const [selectedVulnerability, setSelectedVulnerability] = useState(null); // Filter const availableFilterNames = [ - 'cve_external_id', + 'vulnerability_external_id', ]; - const [cves, setCves] = useState([]); + const [vulnerabilities, setVulnerabilities] = useState([]); const [searchParams] = useSearchParams(); const [search] = searchParams.getAll('search'); - const { queryableHelpers, searchPaginationInput } = useQueryableWithLocalStorage('cve', buildSearchPagination({ - sorts: initSorting('cve_created_at', 'DESC'), + const { queryableHelpers, searchPaginationInput } = useQueryableWithLocalStorage('vulnerability', buildSearchPagination({ + sorts: initSorting('vulnerability_created_at', 'DESC'), textSearch: search, })); - const searchCvesToload = (input: SearchPaginationInput) => { + const searchVulnerabilitiesToload = (input: SearchPaginationInput) => { setLoading(true); - return searchCves(input).finally(() => { + return searchVulnerabilities(input).finally(() => { setLoading(false); }); }; const headers: Header[] = useMemo(() => [ { - field: 'cve_external_id', - label: 'CVE ID', + field: 'vulnerability_external_id', + label: 'VULNERABILITY ID', isSortable: true, - value: (cve: CveSimple) => cve.cve_external_id, + value: (vulnerability: VulnerabilitySimple) => vulnerability.vulnerability_external_id, }, { - field: 'cve_cvss_v31', + field: 'vulnerability_cvss_v31', label: 'CVSS', isSortable: true, - value: (cve: CveSimple) => ( - + value: (vulnerability: VulnerabilitySimple) => ( + ), }, { - field: 'cve_published', + field: 'vulnerability_published', label: 'NVD Published Date', isSortable: true, - value: (cve: CveSimple) => fldt(cve.cve_published), + value: (vulnerability: VulnerabilitySimple) => fldt(vulnerability.vulnerability_published), }, ], []); @@ -95,12 +95,12 @@ const Cves = () => { }]} /> { /> - {loading ? : cves.map(cve => ( + {loading ? : vulnerabilities.map(vulnerability => ( setCves(cves.map(a => (a.cve_id !== result.cve_id ? a : result)))} - onDelete={(result: string) => setCves(cves.filter(a => (a.cve_id !== result)))} + setVulnerabilities(vulnerabilities.map(a => (a.vulnerability_id !== result.vulnerability_id ? a : result)))} + onDelete={(result: string) => setVulnerabilities(vulnerabilities.filter(a => (a.vulnerability_id !== result)))} /> )} > setSelectedCve(cve)} + onClick={() => setSelectedVulnerability(vulnerability)} > @@ -151,7 +151,7 @@ const Cves = () => { ...inlineStyles[header.field], }} > - {header.value && header.value(cve)} + {header.value && header.value(vulnerability)} ))} @@ -162,20 +162,20 @@ const Cves = () => { ))} - setCves([result, ...cves])} + setVulnerabilities([result, ...vulnerabilities])} /> setSelectedCve(null)} - title={selectedCve?.cve_external_id ?? ''} - additionalTitle={selectedCve?.cve_cvss_v31 ? 'CVSS' : undefined} - additionalChipLabel={selectedCve?.cve_cvss_v31.toFixed(1)} + open={!!selectedVulnerability} + handleClose={() => setSelectedVulnerability(null)} + title={selectedVulnerability?.vulnerability_external_id ?? ''} + additionalTitle={selectedVulnerability?.vulnerability_cvss_v31 ? 'CVSS' : undefined} + additionalChipLabel={selectedVulnerability?.vulnerability_cvss_v31.toFixed(1)} > - {selectedCve && ( - )} @@ -185,4 +185,4 @@ const Cves = () => { ); }; -export default Cves; +export default Vulnerabilities; diff --git a/openaev-front/src/admin/components/settings/cves/CveDetail.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityDetail.tsx similarity index 54% rename from openaev-front/src/admin/components/settings/cves/CveDetail.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityDetail.tsx index 7638f89468..8a7ca101a2 100644 --- a/openaev-front/src/admin/components/settings/cves/CveDetail.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityDetail.tsx @@ -1,21 +1,21 @@ import { useEffect, useState } from 'react'; -import { fetchCve } from '../../../../actions/cve-actions'; +import { fetchVulnerability } from '../../../../actions/vulnerability-actions'; import Tabs, { type TabsEntry } from '../../../../components/common/tabs/Tabs'; import useTabs from '../../../../components/common/tabs/useTabs'; import { useFormatter } from '../../../../components/i18n'; -import { type CveOutput, type CveSimple } from '../../../../utils/api-types'; +import { type VulnerabilityOutput, type VulnerabilitySimple } from '../../../../utils/api-types'; import useEnterpriseEdition from '../../../../utils/hooks/useEnterpriseEdition'; -import CveTabPanel from './CveTabPanel'; import GeneralVulnerabilityInfoTab from './GeneralVulnerabilityInfoTab'; import RemediationInfoTab from './RemediationInfoTab'; import TabLabelWithEE from './TabLabelWithEE'; +import VulnerabilityTabPanel from './VulnerabilityTabPanel'; -interface Props { selectedCve: CveSimple } +interface Props { selectedVulnerability: VulnerabilitySimple } -export type CveStatus = 'loading' | 'loaded' | 'notAvailable'; +export type VulnerabilityStatus = 'loading' | 'loaded' | 'notAvailable'; -const CveDetail = ({ selectedCve }: Props) => { +const VulnerabilityDetail = ({ selectedVulnerability }: Props) => { const { t } = useFormatter(); const { @@ -24,21 +24,21 @@ const CveDetail = ({ selectedCve }: Props) => { setEEFeatureDetectedInfo, } = useEnterpriseEdition(); - const [cve, setCve] = useState(null); - const [cveStatus, setCveStatus] = useState('loading'); + const [vulnerability, setCve] = useState(null); + const [vulnerabilityStatus, setVulnerabilityStatus] = useState('loading'); useEffect(() => { - if (!selectedCve.cve_id) return; + if (!selectedVulnerability.vulnerability_id) return; - setCveStatus('loading'); + setVulnerabilityStatus('loading'); - fetchCve(selectedCve.cve_id) + fetchVulnerability(selectedVulnerability.vulnerability_id) .then((res) => { setCve(res.data); - setCveStatus(res.data ? 'loaded' : 'notAvailable'); + setVulnerabilityStatus(res.data ? 'loaded' : 'notAvailable'); }) - .catch(() => setCveStatus('notAvailable')); - }, [selectedCve]); + .catch(() => setVulnerabilityStatus('notAvailable')); + }, [selectedVulnerability]); const tabEntries: TabsEntry[] = [{ key: 'General', @@ -53,15 +53,15 @@ const CveDetail = ({ selectedCve }: Props) => { switch (currentTab) { case 'General': return ( - - - + + + ); case 'Remediation': return ( - - - + + + ); default: return null; @@ -88,4 +88,4 @@ const CveDetail = ({ selectedCve }: Props) => { ); }; -export default CveDetail; +export default VulnerabilityDetail; diff --git a/openaev-front/src/admin/components/settings/cves/CveForm.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityForm.tsx similarity index 62% rename from openaev-front/src/admin/components/settings/cves/CveForm.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityForm.tsx index f41de5a129..813d26b2b1 100644 --- a/openaev-front/src/admin/components/settings/cves/CveForm.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityForm.tsx @@ -8,7 +8,7 @@ import { z } from 'zod'; import Tabs, { type TabsEntry } from '../../../../components/common/tabs/Tabs'; import useTabs from '../../../../components/common/tabs/useTabs'; import { useFormatter } from '../../../../components/i18n'; -import { type CveCreateInput } from '../../../../utils/api-types'; +import { type VulnerabilityCreateInput } from '../../../../utils/api-types'; import useEnterpriseEdition from '../../../../utils/hooks/useEnterpriseEdition'; import { zodImplement } from '../../../../utils/Zod'; import EEChip from '../../common/entreprise_edition/EEChip'; @@ -16,29 +16,30 @@ import GeneralFormTab from './form/GeneralFormTab'; import RemediationFormTab from './form/RemediationFormTab'; interface Props { - onSubmit: SubmitHandler; + onSubmit: SubmitHandler; handleClose: () => void; editing?: boolean; - initialValues?: Partial; + initialValues?: Partial; } -const CveForm = ({ +const VulnerabilityForm = ({ onSubmit, handleClose, editing, initialValues = { - cve_external_id: '', - cve_cvss_v31: undefined, - cve_description: '', - cve_source_identifier: '', - cve_published: '', - cve_vuln_status: undefined, - cve_cisa_action_due: '', - cve_cisa_exploit_add: '', - cve_cisa_required_action: '', - cve_cisa_vulnerability_name: '', - cve_cwes: [], - cve_reference_urls: [], + vulnerability_external_id: '', + vulnerability_cvss_v31: undefined, + vulnerability_description: '', + vulnerability_source_identifier: '', + vulnerability_published: '', + vulnerability_vuln_status: undefined, + vulnerability_cisa_action_due: '', + vulnerability_cisa_exploit_add: '', + vulnerability_cisa_required_action: '', + vulnerability_cisa_vulnerability_name: '', + vulnerability_cwes: [], + vulnerability_reference_urls: [], + vulnerability_remediation: '', }, }: Props) => { // Standard hooks @@ -56,34 +57,34 @@ const CveForm = ({ cwe_source: z.string().optional(), }); - const schema = zodImplement().with({ - cve_external_id: z.string().min(1, { message: t('Should not be empty') }), - cve_cvss_v31: z.coerce.number().min(0).max(10), - cve_description: z.string().optional(), - cve_source_identifier: z.string().optional(), - cve_published: z.string().optional(), - cve_vuln_status: z.enum(['ANALYZED', 'DEFERRED', 'MODIFIED']).optional(), - cve_cisa_action_due: z.string().optional(), - cve_cisa_exploit_add: z.string().optional(), - cve_cisa_required_action: z.string().optional(), - cve_cisa_vulnerability_name: z.string().optional(), - cve_cwes: z.array(cwesObject).optional(), - cve_reference_urls: z.array(z.string().url({ message: t('Invalid URL') })).optional(), - cve_remediation: z.string().optional(), + const schema = zodImplement().with({ + vulnerability_external_id: z.string().min(1, { message: t('Should not be empty') }), + vulnerability_cvss_v31: z.coerce.number().min(0).max(10), + vulnerability_description: z.string().optional(), + vulnerability_source_identifier: z.string().optional(), + vulnerability_published: z.string().optional(), + vulnerability_vuln_status: z.enum(['ANALYZED', 'DEFERRED', 'MODIFIED']).optional(), + vulnerability_cisa_action_due: z.string().optional(), + vulnerability_cisa_exploit_add: z.string().optional(), + vulnerability_cisa_required_action: z.string().optional(), + vulnerability_cisa_vulnerability_name: z.string().optional(), + vulnerability_cwes: z.array(cwesObject).optional(), + vulnerability_reference_urls: z.array(z.string().url({ message: t('Invalid URL') })).optional(), + vulnerability_remediation: z.string().optional(), }); - const methods = useForm({ + const methods = useForm({ mode: 'onTouched', resolver: zodResolver(schema), defaultValues: { ...initialValues, - cve_published: initialValues?.cve_published ?? undefined, - cve_vuln_status: initialValues?.cve_vuln_status ?? undefined, - cve_cisa_action_due: initialValues?.cve_cisa_action_due ?? undefined, - cve_cisa_exploit_add: initialValues?.cve_cisa_exploit_add ?? undefined, - cve_cwes: initialValues.cve_cwes ?? [], - cve_reference_urls: initialValues.cve_reference_urls ?? [], - cve_remediation: initialValues.cve_remediation ?? '', + vulnerability_published: initialValues?.vulnerability_published ?? undefined, + vulnerability_vuln_status: initialValues?.vulnerability_vuln_status ?? undefined, + vulnerability_cisa_action_due: initialValues?.vulnerability_cisa_action_due ?? undefined, + vulnerability_cisa_exploit_add: initialValues?.vulnerability_cisa_exploit_add ?? undefined, + vulnerability_cwes: initialValues.vulnerability_cwes ?? [], + vulnerability_reference_urls: initialValues.vulnerability_reference_urls ?? [], + vulnerability_remediation: initialValues.vulnerability_remediation ?? '', }, }); @@ -131,7 +132,7 @@ const CveForm = ({ return (
); }; -export default CveForm; +export default VulnerabilityForm; diff --git a/openaev-front/src/admin/components/settings/cves/CvePopover.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityPopover.tsx similarity index 59% rename from openaev-front/src/admin/components/settings/cves/CvePopover.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityPopover.tsx index 487e70a1ad..c9ddf2f999 100644 --- a/openaev-front/src/admin/components/settings/cves/CvePopover.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityPopover.tsx @@ -1,29 +1,29 @@ import * as R from 'ramda'; import { type FunctionComponent, useContext, useEffect, useState } from 'react'; -import { deleteCve, fetchCve, updateCve } from '../../../../actions/cve-actions'; +import { deleteVulnerability, fetchVulnerability, updateVulnerability } from '../../../../actions/vulnerability-actions'; import ButtonPopover, { type PopoverEntry } from '../../../../components/common/ButtonPopover'; import DialogDelete from '../../../../components/common/DialogDelete'; import Drawer from '../../../../components/common/Drawer'; import { useFormatter } from '../../../../components/i18n'; import Loader from '../../../../components/Loader'; -import { type CveOutput, type CveSimple, type CveUpdateInput } from '../../../../utils/api-types'; +import { type VulnerabilityOutput, type VulnerabilitySimple, type VulnerabilityUpdateInput } from '../../../../utils/api-types'; import { MESSAGING$ } from '../../../../utils/Environment'; import { AbilityContext } from '../../../../utils/permissions/PermissionsProvider'; import { ACTIONS, SUBJECTS } from '../../../../utils/permissions/types'; -import CveForm from './CveForm'; +import VulnerabilityForm from './VulnerabilityForm'; interface Props { onDelete?: (result: string) => void; - onUpdate?: (result: CveSimple) => void; - cve: CveSimple; + onUpdate?: (result: VulnerabilitySimple) => void; + vulnerability: VulnerabilitySimple; } -const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { +const VulnerabilityPopover: FunctionComponent = ({ onDelete, onUpdate, vulnerability }) => { const { t } = useFormatter(); const ability = useContext(AbilityContext); - const [fullCve, setFullCve] = useState(null); + const [fullVulnerability, setFullVulnerability] = useState(null); const [loading, setLoading] = useState(false); const [openEdit, setOpenEdit] = useState(false); @@ -32,16 +32,16 @@ const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { const handleOpenEdit = () => setOpenEdit(true); const handleCloseEdit = () => { setOpenEdit(false); - setFullCve(null); + setFullVulnerability(null); }; const handleOpenDelete = () => setOpenDelete(true); const handleCloseDelete = () => setOpenDelete(false); const handleDelete = () => { - deleteCve(cve.cve_id) + deleteVulnerability(vulnerability.vulnerability_id) .then(() => { - onDelete?.(cve.cve_id); + onDelete?.(vulnerability.vulnerability_id); }) .catch(() => { MESSAGING$.notifyError(t('Failed to delete vulnerability.')); @@ -51,8 +51,8 @@ const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { }); }; - const handleSubmitEdit = (data: CveUpdateInput) => { - updateCve(cve.cve_id, data) + const handleSubmitEdit = (data: VulnerabilityUpdateInput) => { + updateVulnerability(vulnerability.vulnerability_id, data) .then((response) => { onUpdate?.(response.data); }) @@ -65,14 +65,14 @@ const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { }; useEffect(() => { - if (!openEdit || !cve?.cve_id) return; + if (!openEdit || !vulnerability?.vulnerability_id) return; setLoading(true); - fetchCve(cve.cve_id) - .then(res => setFullCve(res.data)) - .catch(() => setFullCve(null)) + fetchVulnerability(vulnerability.vulnerability_id) + .then(res => setFullVulnerability(res.data)) + .catch(() => setFullVulnerability(null)) .finally(() => setLoading(false)); - }, [openEdit, cve?.cve_id]); + }, [openEdit, vulnerability?.vulnerability_id]); const entries: PopoverEntry[] = [ { @@ -87,22 +87,22 @@ const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { }, ]; - const initialValues = fullCve + const initialValues = fullVulnerability ? R.pick([ - 'cve_external_id', - 'cve_cvss_v31', - 'cve_description', - 'cve_source_identifier', - 'cve_published', - 'cve_vuln_status', - 'cve_cisa_action_due', - 'cve_cisa_exploit_add', - 'cve_cisa_required_action', - 'cve_cisa_vulnerability_name', - 'cve_cwes', - 'cve_reference_urls', - 'cve_remediation', - ], fullCve) + 'vulnerability_external_id', + 'vulnerability_cvss_v31', + 'vulnerability_description', + 'vulnerability_source_identifier', + 'vulnerability_published', + 'vulnerability_vuln_status', + 'vulnerability_cisa_action_due', + 'vulnerability_cisa_exploit_add', + 'vulnerability_cisa_required_action', + 'vulnerability_cisa_vulnerability_name', + 'vulnerability_cwes', + 'vulnerability_reference_urls', + 'vulnerability_remediation', + ], fullVulnerability) : {}; return ( @@ -113,7 +113,7 @@ const CvePopover: FunctionComponent = ({ onDelete, onUpdate, cve }) => { open={openDelete} handleClose={handleCloseDelete} handleSubmit={handleDelete} - text={`${t('Do you want to delete this vulnerability:')} ${cve.cve_external_id}?`} + text={`${t('Do you want to delete this vulnerability:')} ${vulnerability.vulnerability_external_id}?`} /> = ({ onDelete, onUpdate, cve }) => { handleClose={handleCloseEdit} title={t('Update the vulnerability')} > - {loading || !fullCve ? ( + {loading || !fullVulnerability ? ( ) : ( - = ({ onDelete, onUpdate, cve }) => { ); }; -export default CvePopover; +export default VulnerabilityPopover; diff --git a/openaev-front/src/admin/components/settings/cves/CveTabPanel.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityTabPanel.tsx similarity index 64% rename from openaev-front/src/admin/components/settings/cves/CveTabPanel.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityTabPanel.tsx index 2d89e9c314..980dfc578e 100644 --- a/openaev-front/src/admin/components/settings/cves/CveTabPanel.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/VulnerabilityTabPanel.tsx @@ -3,16 +3,16 @@ import { useTheme } from '@mui/material/styles'; import { useFormatter } from '../../../../components/i18n'; import Loader from '../../../../components/Loader'; -import { type CveOutput } from '../../../../utils/api-types'; -import { type CveStatus } from './CveDetail'; +import { type VulnerabilityOutput } from '../../../../utils/api-types'; +import { type VulnerabilityStatus } from './VulnerabilityDetail'; interface CveTabPanelProps { - status: CveStatus; - cve: CveOutput | null; + status: VulnerabilityStatus; + vulnerability: VulnerabilityOutput | null; children: React.ReactNode; } -const CveTabPanel = ({ status, cve, children }: CveTabPanelProps) => { +const VulnerabilityTabPanel = ({ status, vulnerability, children }: CveTabPanelProps) => { const theme = useTheme(); const { t } = useFormatter(); @@ -28,10 +28,10 @@ const CveTabPanel = ({ status, cve, children }: CveTabPanelProps) => { ); case 'loaded': - return cve ? <>{children} : null; + return vulnerability ? <>{children} : null; default: return null; } }; -export default CveTabPanel; +export default VulnerabilityTabPanel; diff --git a/openaev-front/src/admin/components/settings/cves/form/GeneralFormTab.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/form/GeneralFormTab.tsx similarity index 69% rename from openaev-front/src/admin/components/settings/cves/form/GeneralFormTab.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/form/GeneralFormTab.tsx index 050147462a..99f1ff3b9b 100644 --- a/openaev-front/src/admin/components/settings/cves/form/GeneralFormTab.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/form/GeneralFormTab.tsx @@ -17,11 +17,11 @@ const GeneralFormTab = ({ editing = false }: Props) => { const { fields: cwesFields, append: cwesAppend, remove: cwesRemove } = useFieldArray({ control, - name: 'cve_cwes', + name: 'vulnerability_cwes', }); const { fields: referencesFields, append: referencesAppend, remove: referencesRemove } = useFieldArray({ control, - name: 'cve_reference_urls', + name: 'vulnerability_reference_urls', }); const vulnerabilityStatus = [ @@ -39,29 +39,29 @@ const GeneralFormTab = ({ editing = false }: Props) => { return ( <> - - + + - + {/* QUICK INFO */} {t('Quick Info')} - - - + + + {/* CISA */} {t('CISA\'s Known Exploited Vulnerabilities Catalog')} - - + +
- - + +
{/* CWES */} @@ -74,8 +74,8 @@ const GeneralFormTab = ({ editing = false }: Props) => { }} key={cwesField.id} > - - + + cwesRemove(cwesIndex)} size="small" @@ -112,7 +112,7 @@ const GeneralFormTab = ({ editing = false }: Props) => { }} key={referencesField.id} > - + referencesRemove(referencesIndex)} size="small" diff --git a/openaev-front/src/admin/components/settings/cves/form/RemediationFormTab.tsx b/openaev-front/src/admin/components/settings/vulnerabilities/form/RemediationFormTab.tsx similarity index 68% rename from openaev-front/src/admin/components/settings/cves/form/RemediationFormTab.tsx rename to openaev-front/src/admin/components/settings/vulnerabilities/form/RemediationFormTab.tsx index b98ecbc67e..bdbcc22ed6 100644 --- a/openaev-front/src/admin/components/settings/cves/form/RemediationFormTab.tsx +++ b/openaev-front/src/admin/components/settings/vulnerabilities/form/RemediationFormTab.tsx @@ -6,7 +6,7 @@ const RemediationFormTab = () => { return ( <> - + ); }; diff --git a/openaev-front/src/components/FindingIcon.tsx b/openaev-front/src/components/FindingIcon.tsx index e1b1de2b07..67106a8641 100644 --- a/openaev-front/src/components/FindingIcon.tsx +++ b/openaev-front/src/components/FindingIcon.tsx @@ -24,7 +24,7 @@ const renderIcon = (findingType: string) => { return ; case 'credentials': return ; - case 'cve': + case 'vulnerability': return ; default: return ; diff --git a/openaev-front/src/utils/api-types.d.ts b/openaev-front/src/utils/api-types.d.ts index 1c4b2c22e6..abfd3c2a0d 100644 --- a/openaev-front/src/utils/api-types.d.ts +++ b/openaev-front/src/utils/api-types.d.ts @@ -1187,48 +1187,7 @@ export interface CveSimple { cve_published?: string; } -/** Payload to update a CVE */ -export interface CveUpdateInput { - /** - * Date when action is due by CISA - * @format date-time - */ - cve_cisa_action_due?: string; - /** - * Date when CISA added the CVE to the exploited list - * @format date-time - */ - cve_cisa_exploit_add?: string; - /** Action required by CISA */ - cve_cisa_required_action?: string; - /** Vulnerability name used by CISA */ - cve_cisa_vulnerability_name?: string; - /** List of linked CWEs */ - cve_cwes?: CweInput[]; - /** Description of the CVE */ - cve_description?: string; - /** - * Publication date of the CVE - * @format date-time - */ - cve_published?: string; - /** List of reference URLs */ - cve_reference_urls?: string[]; - /** Suggested remediation */ - cve_remediation?: string; - /** - * Identifier of the CVE source - * @example "MITRE" - */ - cve_source_identifier?: string; - /** - * Vulnerability status - * @example "ANALYZED" - */ - cve_vuln_status?: "ANALYZED" | "DEFERRED" | "MODIFIED"; -} - -/** CWE input used in CVE creation/update */ +/** CWE input used in vulnerability creation/update */ export interface CweInput { /** * External CWE identifier @@ -4551,6 +4510,25 @@ export interface PageUserOutput { totalPages?: number; } +export interface PageVulnerabilitySimple { + content?: VulnerabilitySimple[]; + empty?: boolean; + first?: boolean; + last?: boolean; + /** @format int32 */ + number?: number; + /** @format int32 */ + numberOfElements?: number; + pageable?: PageableObject; + /** @format int32 */ + size?: number; + sort?: SortObject[]; + /** @format int64 */ + totalElements?: number; + /** @format int32 */ + totalPages?: number; +} + export interface PageableObject { /** @format int64 */ offset?: number; @@ -6266,6 +6244,180 @@ export interface ViolationErrorBag { type?: string; } +export interface VulnerabilityBulkInsertInput { + initial_dataset_completed?: boolean; + /** @format int32 */ + last_index?: number; + /** @format date-time */ + last_modified_date_fetched?: string; + source_identifier: string; + vulnerabilities: VulnerabilityCreateInput[]; +} + +/** Payload to create a Vulnerabilty */ +export interface VulnerabilityCreateInput { + /** + * CVSS score + * @min 0 + * @exclusiveMin false + * @max 10 + * @exclusiveMax false + * @example 7.5 + */ + vulnerability_cvss_v31: number; + /** + * Date when action is due by CISA + * @format date-time + */ + vulnerability_cisa_action_due?: string; + /** + * Date when CISA added the vulnerability to the exploited list + * @format date-time + */ + vulnerability_cisa_exploit_add?: string; + /** Action required by CISA */ + vulnerability_cisa_required_action?: string; + /** Vulnerability name used by CISA */ + vulnerability_cisa_vulnerability_name?: string; + /** List of linked CWEs */ + vulnerability_cwes?: CweInput[]; + /** Description of the vulnerability */ + vulnerability_description?: string; + /** + * External Unique Vulnerabilty Identifier + * @example "CVE-2024-0001" + */ + vulnerability_external_id: string; + /** + * Publication date of the vulnerability + * @format date-time + */ + vulnerability_published?: string; + /** List of reference URLs */ + vulnerability_reference_urls?: string[]; + /** Suggested remediation */ + vulnerability_remediation?: string; + /** + * Identifier of the vulnerability source + * @example "MITRE" + */ + vulnerability_source_identifier?: string; + /** + * Vulnerability status + * @example "ANALYZED" + */ + vulnerability_vuln_status?: "ANALYZED" | "DEFERRED" | "MODIFIED"; +} + +/** Full vulnerability output including references and CWEs */ +export interface VulnerabilityOutput { + /** + * CVSS score + * @example 7.8 + */ + vulnerability_cvss_v31: number; + /** + * CISA required action due date + * @format date-time + */ + vulnerability_cisa_action_due?: string; + /** + * CISA exploit addition date + * @format date-time + */ + vulnerability_cisa_exploit_add?: string; + /** Action required by CISA */ + vulnerability_cisa_required_action?: string; + /** Name used by CISA for the vulnerability */ + vulnerability_cisa_vulnerability_name?: string; + /** List of CWE outputs */ + vulnerability_cwes?: CweOutput[]; + /** Detailed vulnerability description */ + vulnerability_description?: string; + /** + * External Vulnerability identifier + * @example "CVE-2024-0001" + */ + vulnerability_external_id: string; + /** Id */ + vulnerability_id: string; + /** + * Vulnerability published date + * @format date-time + */ + vulnerability_published?: string; + /** External references */ + vulnerability_reference_urls?: string[]; + /** Remediation suggestions */ + vulnerability_remediation?: string; + /** Source identifier */ + vulnerability_source_identifier?: string; + /** Status of the vulnerability */ + vulnerability_vuln_status?: "ANALYZED" | "DEFERRED" | "MODIFIED"; +} + +/** Simplified Vulnerability representation */ +export interface VulnerabilitySimple { + /** + * CVSS score + * @example 7.8 + */ + vulnerability_cvss_v31: number; + /** + * External Vulnerability identifier + * @example "CVE-2024-0001" + */ + vulnerability_external_id: string; + /** Id */ + vulnerability_id: string; + /** + * Vulnerability published date + * @format date-time + */ + vulnerability_published?: string; +} + +/** Payload to update a vulnerability */ +export interface VulnerabilityUpdateInput { + /** + * Date when action is due by CISA + * @format date-time + */ + vulnerability_cisa_action_due?: string; + /** + * Date when CISA added the vulnerability to the exploited list + * @format date-time + */ + vulnerability_cisa_exploit_add?: string; + /** Action required by CISA */ + vulnerability_cisa_required_action?: string; + /** Vulnerability name used by CISA */ + vulnerability_cisa_vulnerability_name?: string; + /** List of linked CWEs */ + vulnerability_cwes?: CweInput[]; + /** Description of the vulnerability */ + vulnerability_description?: string; + /** + * Publication date of the vulnerability + * @format date-time + */ + vulnerability_published?: string; + /** List of reference URLs */ + vulnerability_reference_urls?: string[]; + /** Suggested remediation */ + vulnerability_remediation?: string; + /** + * Identifier of the vulnerability source + * @example "MITRE" + */ + vulnerability_source_identifier?: string; + /** + * Vulnerability status + * @example "ANALYZED" + */ + vulnerability_vuln_status?: "ANALYZED" | "DEFERRED" | "MODIFIED"; +} + export interface Widget { listened?: boolean; widget_config: diff --git a/openaev-front/src/utils/lang/en.json b/openaev-front/src/utils/lang/en.json index 215e3ac0f3..afdd0c532b 100644 --- a/openaev-front/src/utils/lang/en.json +++ b/openaev-front/src/utils/lang/en.json @@ -384,8 +384,8 @@ "custom_dashboard_step_type": "Visualization", "Customization": "Customization", "Customize columns": "Customize columns", - "CVE ID": "CVE ID", - "cve_external_id": "CVE ID", + "VULNERABILITY ID": "VULNERABILITY ID", + "vulnerability_external_id": "VULNERABILITY ID", "CVSS must be at least 0.0": "CVSS must be at least 0.0", "CVSS must be at most 10.0": "CVSS must be at most 10.0", "CVSS score is required": "CVSS score is required", diff --git a/openaev-front/src/utils/lang/fr.json b/openaev-front/src/utils/lang/fr.json index 9206314301..b9dfa79990 100644 --- a/openaev-front/src/utils/lang/fr.json +++ b/openaev-front/src/utils/lang/fr.json @@ -384,8 +384,8 @@ "custom_dashboard_step_type": "Visualisation", "Customization": "Personnalisation", "Customize columns": "Personnaliser les colonnes", - "CVE ID": "ID CVE", - "cve_external_id": "ID CVE", + "VULNERABILITY ID": "ID VULNERABILITY", + "vulnerability_external_id": "ID VULNERABILITY", "CVSS must be at least 0.0": "CVSS doit être au moins égal à 0.0", "CVSS must be at most 10.0": "CVSS doit être au plus égal à 10.0", "CVSS score is required": "Le score CVSS est requis", diff --git a/openaev-front/src/utils/lang/zh.json b/openaev-front/src/utils/lang/zh.json index 687f0a1e46..23cc0f1052 100644 --- a/openaev-front/src/utils/lang/zh.json +++ b/openaev-front/src/utils/lang/zh.json @@ -384,8 +384,8 @@ "custom_dashboard_step_type": "可视化", "Customization": "自定义", "Customize columns": "专栏标准化", - "CVE ID": "CVE ID", - "cve_external_id": "CVE ID", + "VULNERABILITY ID": "VULNERABILITY ID", + "vulnerability_external_id": "VULNERABILITY ID", "CVSS must be at least 0.0": "CVSS 必须至少为 0.0", "CVSS must be at most 10.0": "CVSS 必须至少为 10.0", "CVSS score is required": "CVSS 分数是必需的", diff --git a/openaev-model/src/main/java/io/openaev/database/model/Capability.java b/openaev-model/src/main/java/io/openaev/database/model/Capability.java index 1d4ed1bfdc..2bad859993 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/Capability.java +++ b/openaev-model/src/main/java/io/openaev/database/model/Capability.java @@ -189,8 +189,8 @@ MANAGE_SECURITY_PLATFORMS, pair(ResourceType.SECURITY_PLATFORM, Action.DELETE)), pair(ResourceType.TAG, Action.CREATE), pair(ResourceType.TAG_RULE, Action.WRITE), pair(ResourceType.TAG_RULE, Action.CREATE), - pair(ResourceType.CVE, Action.WRITE), - pair(ResourceType.CVE, Action.CREATE), + pair(ResourceType.VULNERABILITY, Action.WRITE), + pair(ResourceType.VULNERABILITY, Action.CREATE), pair(ResourceType.COLLECTOR, Action.WRITE), pair(ResourceType.COLLECTOR, Action.CREATE), pair(ResourceType.INJECTOR, Action.WRITE), @@ -210,7 +210,7 @@ MANAGE_SECURITY_PLATFORMS, pair(ResourceType.SECURITY_PLATFORM, Action.DELETE)), pair(ResourceType.KILL_CHAIN_PHASE, Action.DELETE), pair(ResourceType.TAG, Action.DELETE), pair(ResourceType.TAG_RULE, Action.DELETE), - pair(ResourceType.CVE, Action.DELETE), + pair(ResourceType.VULNERABILITY, Action.DELETE), pair(ResourceType.COLLECTOR, Action.DELETE), pair(ResourceType.INJECTOR, Action.DELETE), pair(ResourceType.INJECTOR_CONTRACT, Action.DELETE), diff --git a/openaev-model/src/main/java/io/openaev/database/model/Cve.java b/openaev-model/src/main/java/io/openaev/database/model/Cve.java index 137fabe709..32e54b5b34 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/Cve.java +++ b/openaev-model/src/main/java/io/openaev/database/model/Cve.java @@ -127,5 +127,5 @@ public enum VulnerabilityStatus { @Getter(onMethod_ = @JsonIgnore) @Transient - private final ResourceType resourceType = ResourceType.CVE; + private final ResourceType resourceType = ResourceType.VULNERABILITY; } diff --git a/openaev-model/src/main/java/io/openaev/database/model/Cwe.java b/openaev-model/src/main/java/io/openaev/database/model/Cwe.java index 7972d8eca9..e74b6d83ab 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/Cwe.java +++ b/openaev-model/src/main/java/io/openaev/database/model/Cwe.java @@ -38,7 +38,7 @@ public class Cwe implements Base { private String source; @ManyToMany(mappedBy = "cwes") - private List cves; + private List vulnerabilities; // -- AUDIT -- diff --git a/openaev-model/src/main/java/io/openaev/database/model/InjectorContract.java b/openaev-model/src/main/java/io/openaev/database/model/InjectorContract.java index 6a26d0a8ef..92be403c5a 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/InjectorContract.java +++ b/openaev-model/src/main/java/io/openaev/database/model/InjectorContract.java @@ -141,10 +141,10 @@ public void setAttackPatterns(List attackPatterns) { @JsonSerialize(using = MultiIdSetDeserializer.class) @JsonProperty("injector_contract_vulnerabilities") @Queryable(searchable = true, filterable = true, path = "vulnerabilities.externalId") - private Set vulnerabilities = new HashSet<>(); + private Set vulnerabilities = new HashSet<>(); // UpdatedAt now used to sync with linked object - public void setVulnerabilities(Set vulnerabilities) { + public void setVulnerabilities(Set vulnerabilities) { this.updatedAt = now(); this.vulnerabilities = vulnerabilities; } diff --git a/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java b/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java index 6cbc671646..cf08de5ea6 100644 --- a/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java +++ b/openaev-model/src/main/java/io/openaev/database/model/ResourceType.java @@ -29,7 +29,7 @@ public enum ResourceType { KILL_CHAIN_PHASE, ATTACK_PATTERN, ASSET_GROUP, - CVE, + VULNERABILITY, USER_GROUP, INJECTOR, INJECTOR_CONTRACT, @@ -46,6 +46,9 @@ public enum ResourceType { public static ResourceType fromString(@NotNull String name) { try { + if ("CVE".equals(name)) { + return VULNERABILITY; + } return ResourceType.valueOf(name.toUpperCase()); } catch (IllegalArgumentException e) { return UNKNOWN; diff --git a/openaev-model/src/main/java/io/openaev/database/model/Vulnerability.java b/openaev-model/src/main/java/io/openaev/database/model/Vulnerability.java new file mode 100644 index 0000000000..82d5bfaf63 --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/database/model/Vulnerability.java @@ -0,0 +1,133 @@ +package io.openaev.database.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openaev.annotation.Queryable; +import io.openaev.database.audit.ModelBaseListener; +import io.openaev.helper.MultiIdListDeserializer; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.annotations.UuidGenerator; + +@Entity +@Table(name = "vulnerabilities") +@EntityListeners(ModelBaseListener.class) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class Vulnerability implements Base { + + public enum VulnerabilityStatus { + ANALYZED, + DEFERRED, + MODIFIED, + } + + @Id + @Column(name = "vulnerability_id") + @JsonProperty("vulnerability_id") + @GeneratedValue(generator = "UUID") + @UuidGenerator + @EqualsAndHashCode.Include + @NotBlank + private String id; + + @Column(name = "vulnerability_external_id") + @JsonProperty("vulnerability_external_id") + @NotBlank + @Queryable(searchable = true, filterable = true, sortable = true) + private String externalId; + + @Column(name = "vulnerability_source_identifier") + @JsonProperty("vulnerability_source_identifier") + private String sourceIdentifier; + + @Column(name = "vulnerability_published") + @JsonProperty("vulnerability_published") + @Queryable(sortable = true) + private Instant published; + + @Enumerated(EnumType.STRING) + @Column(name = "vulnerability_vuln_status") + @JsonProperty("vulnerability_vuln_status") + private VulnerabilityStatus vulnStatus; + + @Column(name = "vulnerability_description") + @JsonProperty("vulnerability_description") + private String description; + + @Column(name = "vulnerability_remediation") + @JsonProperty("vulnerability_remediation") + private String remediation; + + @Column(name = "vulnerability_cisa_exploit_add") + @JsonProperty("vulnerability_cisa_exploit_add") + private Instant cisaExploitAdd; + + @Column(name = "vulnerability_cisa_action_due") + @JsonProperty("vulnerability_cisa_action_due") + private Instant cisaActionDue; + + @Column(name = "vulnerability_cisa_required_action") + @JsonProperty("vulnerability_cisa_required_action") + private String cisaRequiredAction; + + @Column(name = "vulnerability_cisa_vulnerability_name") + @JsonProperty("vulnerability_cisa_vulnerability_name") + private String cisaVulnerabilityName; + + @ElementCollection + @CollectionTable( + name = "vulnerability_reference_urls", + joinColumns = @JoinColumn(name = "vulnerability_id")) + @Column(name = "vulnerability_reference_url") + @JsonProperty("vulnerability_reference_urls") + private List referenceUrls = new ArrayList<>(); + + @NotNull + @Column(name = "vulnerability_cvss_v31", precision = 3, scale = 1) + @JsonProperty("vulnerability_cvss_v31") + @Queryable(searchable = true, filterable = true, sortable = true) + private BigDecimal cvssV31; + + @ArraySchema(schema = @Schema(type = "string")) + @ManyToMany + @JoinTable( + name = "vulnerabilities_cwes", + joinColumns = @JoinColumn(name = "vulnerability_id"), + inverseJoinColumns = @JoinColumn(name = "cwe_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("vulnerabilities_cwes") + private List cwes = new ArrayList<>(); + + // -- AUDIT -- + + @CreationTimestamp + @Queryable(filterable = true, sortable = true, label = "created at") + @Column(name = "vulnerability_created_at", updatable = false) + @JsonProperty("vulnerability_created_at") + private Instant creationDate; + + @UpdateTimestamp + @Queryable(filterable = true, sortable = true, label = "updated at") + @Column(name = "vulnerability_updated_at") + @JsonProperty("vulnerability_updated_at") + private Instant updateDate; + + @Getter(onMethod_ = @JsonIgnore) + @Transient + private final ResourceType resourceType = ResourceType.VULNERABILITY; +} diff --git a/openaev-model/src/main/java/io/openaev/database/repository/InjectorContractRepository.java b/openaev-model/src/main/java/io/openaev/database/repository/InjectorContractRepository.java index a9847b05e7..d5b4a6abe2 100644 --- a/openaev-model/src/main/java/io/openaev/database/repository/InjectorContractRepository.java +++ b/openaev-model/src/main/java/io/openaev/database/repository/InjectorContractRepository.java @@ -70,15 +70,15 @@ Optional findInjectorContractByInjectorAndPayload( FROM ( SELECT ic.*, ROW_NUMBER() OVER ( - PARTITION BY cve.cve_external_id + PARTITION BY vulnerability.vulnerability_external_id ORDER BY ic.injector_contract_updated_at DESC ) AS rn FROM injectors_contracts ic JOIN injectors_contracts_vulnerabilities icv ON ic.injector_contract_id = icv.injector_contract_id - JOIN cves cve - ON icv.vulnerability_id = cve.cve_id - WHERE LOWER(cve.cve_external_id) IN (:externalIds) + JOIN vulnerabilities vulnerability + ON icv.vulnerability_id = vulnerability.vulnerability_id + WHERE LOWER(vulnerability.vulnerability_external_id) IN (:externalIds) ) ranked WHERE ranked.rn <= :contractsPerVulnerability """, diff --git a/openaev-model/src/main/java/io/openaev/database/repository/VulnerabilityRepository.java b/openaev-model/src/main/java/io/openaev/database/repository/VulnerabilityRepository.java new file mode 100644 index 0000000000..b08a19294a --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/database/repository/VulnerabilityRepository.java @@ -0,0 +1,19 @@ +package io.openaev.database.repository; + +import io.openaev.database.model.Vulnerability; +import java.util.Optional; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VulnerabilityRepository + extends CrudRepository, JpaSpecificationExecutor { + + Optional findByExternalId(String externalId); + + Set getAllByIdInIgnoreCase(Set ids); + + Set getAllByExternalIdInIgnoreCase(Set externalIds); +}