From f1e765e87f9d33e761361f66033f201bd9b0d107 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Sun, 12 Jul 2026 23:59:17 +0400 Subject: [PATCH 01/12] feat(enrichment): ingest KEV,EPSS,OSV feeds with vulnerability rescoring --- backend/apps/api/build.gradle.kts | 1 + .../dev/cleat/api/CleatApiApplication.java | 5 +- .../dto/request/VulnerabilityRequestDto.java | 12 ++++ .../response/VulnerabilityResponseDto.java | 53 ++++++++++++++ .../common/exception/FeedSyncException.java | 11 +++ .../dev/cleat/domain/PriorityCalculator.java | 18 +++-- .../dev/cleat/domain/model/Vulnerability.java | 6 +- .../cleat/enrichment/client/EpssClient.java | 29 ++++++++ .../cleat/enrichment/client/KevClient.java | 29 ++++++++ .../cleat/enrichment/client/OsvClient.java | 26 +++++++ .../enrichment/config/RestTemplateConfig.java | 14 ++++ .../dev/cleat/enrichment/dto/EpssData.java | 27 +++++++ .../cleat/enrichment/dto/EpssResponse.java | 18 +++++ .../dev/cleat/enrichment/dto/FeedResult.java | 3 + .../dev/cleat/enrichment/dto/KevResponse.java | 18 +++++ .../enrichment/dto/KevVulnerability.java | 18 +++++ .../dev/cleat/enrichment/dto/OsvPackage.java | 25 +++++++ .../dev/cleat/enrichment/dto/OsvRequest.java | 17 +++++ .../dev/cleat/enrichment/dto/OsvResponse.java | 28 ++++++++ .../cleat/enrichment/service/FeedService.java | 52 ++++++++++++++ .../entity/VulnerabilityEntity.java | 14 ++++ .../mapper/VulnerabilityMapper.java | 2 + .../V6__alter_vulnerability_table.sql | 1 + backend/libs/scanning/build.gradle.kts | 1 + .../cleat/scanning/VulnerabilityScanner.java | 12 +++- .../scanning/service/FeedSyncService.java | 66 +++++++++++++++++ .../src/main/resources/application.yml | 3 + .../cleat/scanning/FeedSyncServiceTest.java | 71 +++++++++++++++++++ .../scanning/VulnerabilityScannerTest.java | 18 +++-- 29 files changed, 583 insertions(+), 15 deletions(-) create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/exception/FeedSyncException.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/OsvClient.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssData.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssResponse.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/FeedResult.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevResponse.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvPackage.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvRequest.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvResponse.java create mode 100644 backend/libs/enrichment/src/main/java/dev/cleat/enrichment/service/FeedService.java create mode 100644 backend/libs/persistence/src/main/resources/db/migration/V6__alter_vulnerability_table.sql create mode 100644 backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java create mode 100644 backend/libs/scanning/src/main/resources/application.yml create mode 100644 backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java diff --git a/backend/apps/api/build.gradle.kts b/backend/apps/api/build.gradle.kts index 2d4da1d0..f1c13f33 100644 --- a/backend/apps/api/build.gradle.kts +++ b/backend/apps/api/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(project(":libs:persistence")) implementation(project(":libs:github-client")) implementation(project(":libs:scanning")) + implementation(project(":libs:enrichment")) implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") diff --git a/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java b/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java index b5f0ddab..41794b2d 100644 --- a/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java +++ b/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication( scanBasePackages = { @@ -11,10 +12,12 @@ "dev.cleat.persistence", "dev.cleat.domain", "dev.cleat.common", - "dev.cleat.scanning" + "dev.cleat.scanning", + "dev.cleat.enrichment" }) @EnableJpaRepositories(basePackages = "dev.cleat.persistence.repository") @EntityScan(basePackages = "dev.cleat.persistence.entity") +@EnableScheduling public class CleatApiApplication { public static void main(String[] args) { SpringApplication.run(CleatApiApplication.class); diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java index ebb420fe..fb64f5dc 100644 --- a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java @@ -18,6 +18,7 @@ public class VulnerabilityRequestDto { private Reachable reachable; private UUID advisoryId; private String cwe; + private String cve; private String title; private List affectedRepos; private Boolean hasFixPr; @@ -36,6 +37,7 @@ public VulnerabilityRequestDto( Reachable reachable, UUID advisoryId, String cwe, + String cve, String title, List affectedRepos, Boolean hasFixPr) { @@ -51,6 +53,7 @@ public VulnerabilityRequestDto( this.reachable = reachable; this.advisoryId = advisoryId; this.cwe = cwe; + this.cve = cve; this.title = title; this.affectedRepos = affectedRepos; this.hasFixPr = hasFixPr; @@ -155,6 +158,15 @@ public VulnerabilityRequestDto setCwe(String cwe) { return this; } + public String getCve() { + return cve; + } + + public VulnerabilityRequestDto setCve(String cve) { + this.cve = cve; + return this; + } + public String getTitle() { return title; } diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java index ed3f8b0b..a5c7c412 100644 --- a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java @@ -22,11 +22,55 @@ public class VulnerabilityResponseDto { private Reachable reachable; private UUID advisoryId; private String cwe; + private String cve; private String title; private List affectedRepos; private Boolean hasFixPr; private OffsetDateTime publishedAt; + public VulnerabilityResponseDto() {} + + public VulnerabilityResponseDto( + UUID id, + UUID accountId, + String packageName, + String ecosystem, + String currentVersion, + String fixedVersion, + Double cvss, + Severity severity, + Priority priority, + Double epss, + Boolean kev, + Reachable reachable, + UUID advisoryId, + String cwe, + String cve, + String title, + List affectedRepos, + Boolean hasFixPr, + OffsetDateTime publishedAt) { + this.id = id; + this.accountId = accountId; + this.packageName = packageName; + this.ecosystem = ecosystem; + this.currentVersion = currentVersion; + this.fixedVersion = fixedVersion; + this.cvss = cvss; + this.severity = severity; + this.priority = priority; + this.epss = epss; + this.kev = kev; + this.reachable = reachable; + this.advisoryId = advisoryId; + this.cwe = cwe; + this.cve = cve; + this.title = title; + this.affectedRepos = affectedRepos; + this.hasFixPr = hasFixPr; + this.publishedAt = publishedAt; + } + public UUID getId() { return id; } @@ -153,6 +197,15 @@ public VulnerabilityResponseDto setCwe(String cwe) { return this; } + public String getCve() { + return cve; + } + + public VulnerabilityResponseDto setCve(String cve) { + this.cve = cve; + return this; + } + public String getTitle() { return title; } diff --git a/backend/libs/common/src/main/java/dev/cleat/common/exception/FeedSyncException.java b/backend/libs/common/src/main/java/dev/cleat/common/exception/FeedSyncException.java new file mode 100644 index 00000000..ffc2fc6c --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/exception/FeedSyncException.java @@ -0,0 +1,11 @@ +package dev.cleat.common.exception; + +public class FeedSyncException extends RuntimeException { + public FeedSyncException(String msg) { + super(msg); + } + + public FeedSyncException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/backend/libs/domain/src/main/java/dev/cleat/domain/PriorityCalculator.java b/backend/libs/domain/src/main/java/dev/cleat/domain/PriorityCalculator.java index 95018fa3..3b8cac79 100644 --- a/backend/libs/domain/src/main/java/dev/cleat/domain/PriorityCalculator.java +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/PriorityCalculator.java @@ -1,6 +1,8 @@ package dev.cleat.domain; import dev.cleat.common.enums.Priority; +import dev.cleat.common.enums.Reachable; +import dev.cleat.common.enums.Severity; import dev.cleat.domain.model.Vulnerability; import org.springframework.stereotype.Component; @@ -11,14 +13,18 @@ public Priority calculate(Vulnerability vulnerability) { if (vulnerability.cvss() < 0 || vulnerability.cvss() > 10) { throw new IllegalArgumentException("CVSS must be between 0 and 10"); } - if (vulnerability.kev() || vulnerability.cvss() >= 9.0) { + if (vulnerability.kev() || vulnerability.cvss() >= 9.0 && vulnerability.reachable() == Reachable.REACHABLE) { return Priority.URGENT; } - return switch (vulnerability.severity()) { - case CRITICAL -> Priority.HIGH; - case HIGH -> Priority.MEDIUM; - default -> Priority.LOW; - }; + if (vulnerability.epss() > 0.1 || vulnerability.severity() == Severity.CRITICAL) { + return Priority.HIGH; + } + + if (vulnerability.cvss() >= 7.0) { + return Priority.MEDIUM; + } + + return Priority.LOW; } } diff --git a/backend/libs/domain/src/main/java/dev/cleat/domain/model/Vulnerability.java b/backend/libs/domain/src/main/java/dev/cleat/domain/model/Vulnerability.java index ee40c004..b3291350 100644 --- a/backend/libs/domain/src/main/java/dev/cleat/domain/model/Vulnerability.java +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/model/Vulnerability.java @@ -1,12 +1,16 @@ package dev.cleat.domain.model; +import dev.cleat.common.enums.Reachable; import dev.cleat.common.enums.Severity; import java.util.Objects; -public record Vulnerability(Boolean kev, Double cvss, Severity severity) { +public record Vulnerability(Boolean kev, Double cvss, Severity severity, Double epss, Reachable reachable) { public Vulnerability { Objects.requireNonNull(cvss, "cvss cannot be null"); Objects.requireNonNull(severity, "severity cannot be null"); + kev = (kev != null) ? kev : false; + epss = (epss != null) ? epss : 0.0; + reachable = (reachable != null) ? reachable : Reachable.UNKNOWN; } } diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java new file mode 100644 index 00000000..cd660a96 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java @@ -0,0 +1,29 @@ +package dev.cleat.enrichment.client; + +import dev.cleat.enrichment.dto.EpssResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class EpssClient { + private final RestTemplate restTemplate; + + public EpssClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + private static final String URL = "https://api.first.org/data/v1/epss?cve="; + + public Double fetchScore(String cve) { + + EpssResponse response = restTemplate.getForObject(URL + cve, EpssResponse.class); + if (response == null || response.getData() == null || response.getData().isEmpty()) { + return null; + } + return response.getData().getFirst().getEpss(); + } + + public EpssResponse fetchFeed(String cve) { + return restTemplate.getForObject(URL + cve, EpssResponse.class); + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java new file mode 100644 index 00000000..671fb287 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java @@ -0,0 +1,29 @@ +package dev.cleat.enrichment.client; + +import dev.cleat.enrichment.dto.KevResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class KevClient { + private final RestTemplate restTemplate; + + public KevClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + private static final String URL = + "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"; + + public boolean isKev(String cve) { + KevResponse response = restTemplate.getForObject(URL, KevResponse.class); + if (response == null || response.getVulnerabilities() == null) { + return false; + } + return response.getVulnerabilities().stream().anyMatch(v -> cve.equals(v.getCveId())); + } + + public KevResponse fetchFeed() { + return restTemplate.getForObject(URL, KevResponse.class); + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/OsvClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/OsvClient.java new file mode 100644 index 00000000..623b2c0e --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/OsvClient.java @@ -0,0 +1,26 @@ +package dev.cleat.enrichment.client; + +import dev.cleat.enrichment.dto.OsvPackage; +import dev.cleat.enrichment.dto.OsvRequest; +import dev.cleat.enrichment.dto.OsvResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class OsvClient { + private final RestTemplate restTemplate; + + public OsvClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + private static final String URL = "https://api.osv.dev/v1/query"; + + public OsvResponse query(String packageName, String ecosystem) { + OsvPackage osvPackage = new OsvPackage().setName(packageName).setEcosystem(ecosystem); + + OsvRequest osvRequest = new OsvRequest().setPkg(osvPackage); + + return restTemplate.postForObject(URL, osvRequest, OsvResponse.class); + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java new file mode 100644 index 00000000..bafb5e16 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package dev.cleat.enrichment.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssData.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssData.java new file mode 100644 index 00000000..0285d2d9 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssData.java @@ -0,0 +1,27 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EpssData { + private String cve; + private Double epss; + + public String getCve() { + return cve; + } + + public EpssData setCve(String cve) { + this.cve = cve; + return this; + } + + public Double getEpss() { + return epss; + } + + public EpssData setEpss(Double epss) { + this.epss = epss; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssResponse.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssResponse.java new file mode 100644 index 00000000..faeaa913 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/EpssResponse.java @@ -0,0 +1,18 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties() +public class EpssResponse { + private List data; + + public List getData() { + return data; + } + + public EpssResponse setData(List data) { + this.data = data; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/FeedResult.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/FeedResult.java new file mode 100644 index 00000000..9013b593 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/FeedResult.java @@ -0,0 +1,3 @@ +package dev.cleat.enrichment.dto; + +public record FeedResult(boolean kev, Double epss, OsvResponse osv) {} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevResponse.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevResponse.java new file mode 100644 index 00000000..95843135 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevResponse.java @@ -0,0 +1,18 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class KevResponse { + private List vulnerabilities; + + public List getVulnerabilities() { + return vulnerabilities; + } + + public KevResponse setVulnerabilities(List vulnerabilities) { + this.vulnerabilities = vulnerabilities; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java new file mode 100644 index 00000000..49b3a948 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java @@ -0,0 +1,18 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class KevVulnerability { + + private String cveId; + + public String getCveId() { + return cveId; + } + + public KevVulnerability setCveId(String cveId) { + this.cveId = cveId; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvPackage.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvPackage.java new file mode 100644 index 00000000..6775f276 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvPackage.java @@ -0,0 +1,25 @@ +package dev.cleat.enrichment.dto; + +public class OsvPackage { + + private String name; + private String ecosystem; + + public String getName() { + return name; + } + + public OsvPackage setName(String name) { + this.name = name; + return this; + } + + public String getEcosystem() { + return ecosystem; + } + + public OsvPackage setEcosystem(String ecosystem) { + this.ecosystem = ecosystem; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvRequest.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvRequest.java new file mode 100644 index 00000000..9687ecff --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvRequest.java @@ -0,0 +1,17 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OsvRequest { + @JsonProperty("package") + private OsvPackage pkg; + + public OsvPackage getPkg() { + return pkg; + } + + public OsvRequest setPkg(OsvPackage pkg) { + this.pkg = pkg; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvResponse.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvResponse.java new file mode 100644 index 00000000..3699ca99 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/OsvResponse.java @@ -0,0 +1,28 @@ +package dev.cleat.enrichment.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OsvResponse { + + private String id; + private String summary; + + public String getId() { + return id; + } + + public OsvResponse setId(String id) { + this.id = id; + return this; + } + + public String getSummary() { + return summary; + } + + public OsvResponse setSummary(String summary) { + this.summary = summary; + return this; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/service/FeedService.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/service/FeedService.java new file mode 100644 index 00000000..cb9a2000 --- /dev/null +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/service/FeedService.java @@ -0,0 +1,52 @@ +package dev.cleat.enrichment.service; + +import dev.cleat.common.exception.FeedSyncException; +import dev.cleat.enrichment.client.EpssClient; +import dev.cleat.enrichment.client.KevClient; +import dev.cleat.enrichment.client.OsvClient; +import dev.cleat.enrichment.dto.FeedResult; +import dev.cleat.enrichment.dto.OsvResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class FeedService { + private final KevClient kevClient; + private final EpssClient epssClient; + private final OsvClient osvClient; + + public FeedService(KevClient kevClient, EpssClient epssClient, OsvClient osvClient) { + this.kevClient = kevClient; + this.epssClient = epssClient; + this.osvClient = osvClient; + } + + private static final Logger LOG = LoggerFactory.getLogger(FeedService.class); + + public boolean isKev(String cve) { + return kevClient.isKev(cve); + } + + public Double fetchScore(String cve) { + return epssClient.fetchScore(cve); + } + + public OsvResponse query(String packageName, String ecosystem) { + return osvClient.query(packageName, ecosystem); + } + + public dev.cleat.enrichment.dto.FeedResult fetchFeeds(String cve, String packageName, String ecosystem) { + + LOG.info("Fetching feeds for CVE: {}", cve); + try { + FeedResult result = new FeedResult( + kevClient.isKev(cve), epssClient.fetchScore(cve), osvClient.query(packageName, ecosystem)); + LOG.info("Successfully fetched feeds for CVE: {}", cve); + return result; + } catch (Exception e) { + LOG.error("Failed to fetch feeds for {}", cve, e); + throw new FeedSyncException("Failed to fetch enrichment feeds", e); + } + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java index 4d8d16e8..c8214ac6 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java @@ -68,6 +68,9 @@ public class VulnerabilityEntity { @Column(name = "cwe") private String cwe; + @Column(name = "cve") + private String cve; + @Column(name = "title") private String title; @@ -99,6 +102,7 @@ public VulnerabilityEntity( Reachable reachable, UUID advisoryId, String cwe, + String cve, String title, List affectedRepos, Boolean hasFixPr, @@ -117,6 +121,7 @@ public VulnerabilityEntity( this.reachable = reachable; this.advisoryId = advisoryId; this.cwe = cwe; + this.cve = cve; this.title = title; this.affectedRepos = affectedRepos; this.hasFixPr = hasFixPr; @@ -249,6 +254,15 @@ public VulnerabilityEntity setCwe(String cwe) { return this; } + public String getCve() { + return cve; + } + + public VulnerabilityEntity setCve(String cve) { + this.cve = cve; + return this; + } + public String getTitle() { return title; } diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/VulnerabilityMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/VulnerabilityMapper.java index 52906cb0..12e8c0b0 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/VulnerabilityMapper.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/VulnerabilityMapper.java @@ -23,6 +23,7 @@ public VulnerabilityResponseDto toVulnerabilityDto(VulnerabilityEntity vulnerabi .setReachable(vulnerabilityEntity.getReachable()) .setAdvisoryId(vulnerabilityEntity.getAdvisoryId()) .setCwe(vulnerabilityEntity.getCwe()) + .setCve(vulnerabilityEntity.getCve()) .setTitle(vulnerabilityEntity.getTitle()) .setAffectedRepos(vulnerabilityEntity.getAffectedRepos()) .setHasFixPr(vulnerabilityEntity.getHasFixPr()) @@ -45,6 +46,7 @@ public VulnerabilityEntity toVulnerabilityEntity(VulnerabilityRequestDto vulnera .setReachable(vulnerabilityRequestDto.getReachable()) .setAdvisoryId(vulnerabilityRequestDto.getAdvisoryId()) .setCwe(vulnerabilityRequestDto.getCwe()) + .setCve(vulnerabilityRequestDto.getCve()) .setTitle(vulnerabilityRequestDto.getTitle()) .setAffectedRepos(vulnerabilityRequestDto.getAffectedRepos()) .setHasFixPr(vulnerabilityRequestDto.getHasFixPr()); diff --git a/backend/libs/persistence/src/main/resources/db/migration/V6__alter_vulnerability_table.sql b/backend/libs/persistence/src/main/resources/db/migration/V6__alter_vulnerability_table.sql new file mode 100644 index 00000000..5180a1f0 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V6__alter_vulnerability_table.sql @@ -0,0 +1 @@ +ALTER TABLE vulnerability ADD COLUMN cve VARCHAR(50); \ No newline at end of file diff --git a/backend/libs/scanning/build.gradle.kts b/backend/libs/scanning/build.gradle.kts index 41a5c34d..99ae6d17 100644 --- a/backend/libs/scanning/build.gradle.kts +++ b/backend/libs/scanning/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { implementation(project(":libs:common")) implementation(project(":libs:persistence")) implementation(project(":libs:github-client")) + implementation(project(":libs:enrichment")) implementation("org.springframework.boot:spring-boot-starter") diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/VulnerabilityScanner.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/VulnerabilityScanner.java index 63a2fa61..6635ec64 100644 --- a/backend/libs/scanning/src/main/java/dev/cleat/scanning/VulnerabilityScanner.java +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/VulnerabilityScanner.java @@ -27,12 +27,18 @@ public void processAndSave(VulnerabilityEntity vulnerabilityEntity) { } if (vulnerabilityEntity.getCvss() == null || vulnerabilityEntity.getSeverity() == null - || vulnerabilityEntity.getKev() == null) { - throw new IllegalArgumentException("cvss,severity and kev are required"); + || vulnerabilityEntity.getKev() == null + || vulnerabilityEntity.getEpss() == null + || vulnerabilityEntity.getReachable() == null) { + throw new IllegalArgumentException("cvss,severity kev,epss,reachable are required"); } try { Vulnerability vulnerability = new Vulnerability( - vulnerabilityEntity.getKev(), vulnerabilityEntity.getCvss(), vulnerabilityEntity.getSeverity()); + vulnerabilityEntity.getKev(), + vulnerabilityEntity.getCvss(), + vulnerabilityEntity.getSeverity(), + vulnerabilityEntity.getEpss(), + vulnerabilityEntity.getReachable()); vulnerabilityEntity.setPriority(priorityCalculator.calculate(vulnerability)); vulnerabilityRepository.save(vulnerabilityEntity); LOG.info("Vulnerability processed successfully for ID: {}", vulnerabilityEntity.getId()); diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java new file mode 100644 index 00000000..6407ad0a --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java @@ -0,0 +1,66 @@ +package dev.cleat.scanning.service; + +import dev.cleat.domain.PriorityCalculator; +import dev.cleat.domain.model.Vulnerability; +import dev.cleat.enrichment.dto.FeedResult; +import dev.cleat.enrichment.service.FeedService; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class FeedSyncService { + + private final VulnerabilityRepository vulnerabilityRepository; + + private final PriorityCalculator priorityCalculator; + + private final FeedService feedService; + + public FeedSyncService( + VulnerabilityRepository vulnerabilityRepository, + FeedService feedService, + PriorityCalculator priorityCalculator) { + this.vulnerabilityRepository = vulnerabilityRepository; + this.feedService = feedService; + this.priorityCalculator = priorityCalculator; + } + + private static final Logger LOG = LoggerFactory.getLogger(FeedSyncService.class); + + @Transactional + @Scheduled(fixedDelayString = "${feed.sync.delay}") + public void syncFeeds() { + LOG.info("Feed synchronization started"); + List vulnerabilities = vulnerabilityRepository.findAll(); + for (VulnerabilityEntity v : vulnerabilities) { + if (v.getCve() == null) { + LOG.warn("Skipping vulnerability {} because CVE is missing", v.getId()); + continue; + } + try { + FeedResult feed = feedService.fetchFeeds(v.getCve(), v.getPackageName(), v.getEcosystem()); + + v.setKev(feed.kev()); + v.setEpss(feed.epss()); + + if (feed.osv() != null && feed.osv().getSummary() != null) { + v.setTitle(feed.osv().getSummary()); + } + Vulnerability vulnerability = + new Vulnerability(v.getKev(), v.getCvss(), v.getSeverity(), v.getEpss(), v.getReachable()); + v.setPriority(priorityCalculator.calculate(vulnerability)); + LOG.info("Updated vulnerability {}", v.getId()); + } catch (Exception e) { + LOG.error("Failed to update vulnerability {}", v.getId(), e); + } + } + vulnerabilityRepository.saveAll(vulnerabilities); + LOG.info("Feed synchronization completed"); + } +} diff --git a/backend/libs/scanning/src/main/resources/application.yml b/backend/libs/scanning/src/main/resources/application.yml new file mode 100644 index 00000000..f33f21a2 --- /dev/null +++ b/backend/libs/scanning/src/main/resources/application.yml @@ -0,0 +1,3 @@ +feed: + sync: + delay: 3600000 \ No newline at end of file diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java new file mode 100644 index 00000000..195db3a8 --- /dev/null +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java @@ -0,0 +1,71 @@ +package dev.cleat.scanning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.cleat.common.enums.Priority; +import dev.cleat.common.enums.Reachable; +import dev.cleat.common.enums.Severity; +import dev.cleat.domain.PriorityCalculator; +import dev.cleat.enrichment.dto.FeedResult; +import dev.cleat.enrichment.dto.OsvResponse; +import dev.cleat.enrichment.service.FeedService; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import dev.cleat.scanning.service.FeedSyncService; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class FeedSyncServiceTest { + + @Mock + VulnerabilityRepository vulnerabilityRepository; + + @Mock + FeedService feedService; + + @Mock + PriorityCalculator priorityCalculator; + + @InjectMocks + FeedSyncService feedSyncService; + + @Test + void shouldRescoreAllVulnerabilities() { + + VulnerabilityEntity vulnerabilityEntity = new VulnerabilityEntity() + .setCve("CVE-2025-1234") + .setPackageName("spring-core") + .setEcosystem("maven") + .setCvss(9.5) + .setSeverity(Severity.HIGH) + .setReachable(Reachable.REACHABLE); + + FeedResult feed = new FeedResult(true, 0.95, new OsvResponse()); + + when(vulnerabilityRepository.findAll()).thenReturn(List.of(vulnerabilityEntity)); + + when(feedService.fetchFeeds( + vulnerabilityEntity.getCve(), + vulnerabilityEntity.getPackageName(), + vulnerabilityEntity.getEcosystem())) + .thenReturn(feed); + + when(priorityCalculator.calculate(any())).thenReturn(Priority.URGENT); + + feedSyncService.syncFeeds(); + + verify(vulnerabilityRepository).saveAll(any()); + + assertEquals(true, vulnerabilityEntity.getKev()); + assertEquals(0.95, vulnerabilityEntity.getEpss()); + assertEquals(Priority.URGENT, vulnerabilityEntity.getPriority()); + } +} diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/VulnerabilityScannerTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/VulnerabilityScannerTest.java index f090187e..eafba43d 100644 --- a/backend/libs/scanning/src/test/java/dev/cleat/scanning/VulnerabilityScannerTest.java +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/VulnerabilityScannerTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.when; import dev.cleat.common.enums.Priority; +import dev.cleat.common.enums.Reachable; import dev.cleat.common.enums.Severity; import dev.cleat.domain.PriorityCalculator; import dev.cleat.persistence.entity.VulnerabilityEntity; @@ -35,8 +36,13 @@ public class VulnerabilityScannerTest { void givenVulnerabilityWithKevAndHighCvssWhenProcessingThenPriorityShouldBeUrgent() { // given UUID uuid = UUID.randomUUID(); - VulnerabilityEntity vulnerabilityEntity = - new VulnerabilityEntity().setId(uuid).setKev(true).setCvss(9.5).setSeverity(Severity.HIGH); + VulnerabilityEntity vulnerabilityEntity = new VulnerabilityEntity() + .setId(uuid) + .setKev(true) + .setCvss(9.5) + .setSeverity(Severity.HIGH) + .setEpss(0.01) + .setReachable(Reachable.REACHABLE); when(priorityCalculator.calculate(any())).thenReturn(Priority.URGENT); @@ -54,8 +60,12 @@ void givenVulnerabilityWithKevAndHighCvssWhenProcessingThenPriorityShouldBeUrgen void whenCalculatorThrowsExceptionThenScannerWrapsItInRuntimeException() { // given - VulnerabilityEntity vulnerabilityEntity = - new VulnerabilityEntity().setKev(true).setCvss(9.0).setSeverity(Severity.HIGH); + VulnerabilityEntity vulnerabilityEntity = new VulnerabilityEntity() + .setKev(true) + .setCvss(9.0) + .setSeverity(Severity.HIGH) + .setEpss(0.01) + .setReachable(Reachable.REACHABLE); when(priorityCalculator.calculate(any())).thenThrow(new IllegalArgumentException("Invalid CVSS")); From 4f3b39ab5f2a59a45173bf02dee24e9d48cd1fcb Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 00:27:42 +0400 Subject: [PATCH 02/12] fix(config): add feed sync delay propertiy --- backend/apps/api/src/main/resources/application.yml | 4 ++++ .../main/java/dev/cleat/worker/CleatWorkerApplication.java | 3 ++- backend/apps/worker/src/main/resources/application.yml | 6 +++++- backend/libs/scanning/src/main/resources/application.yml | 3 --- 4 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 backend/libs/scanning/src/main/resources/application.yml diff --git a/backend/apps/api/src/main/resources/application.yml b/backend/apps/api/src/main/resources/application.yml index 06bbefc3..7398d716 100644 --- a/backend/apps/api/src/main/resources/application.yml +++ b/backend/apps/api/src/main/resources/application.yml @@ -29,3 +29,7 @@ management: enabled: true redis: enabled: true + +feed: + sync: + delay: 3600000 diff --git a/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java b/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java index 8ebbdb3b..66bf2374 100644 --- a/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java +++ b/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java @@ -12,7 +12,8 @@ "dev.cleat.scanning", "dev.cleat.worker", "dev.cleat.domain", - "dev.cleat.common" + "dev.cleat.common", + "dev.cleat.enrichment" }) @EnableJpaRepositories(basePackages = "dev.cleat.persistence.repository") @EntityScan(basePackages = "dev.cleat.persistence.entity") diff --git a/backend/apps/worker/src/main/resources/application.yml b/backend/apps/worker/src/main/resources/application.yml index 2fc5ab65..cf8836dd 100644 --- a/backend/apps/worker/src/main/resources/application.yml +++ b/backend/apps/worker/src/main/resources/application.yml @@ -32,4 +32,8 @@ management: app: security: - scan-interval: 600000 \ No newline at end of file + scan-interval: 600000 + +feed: + sync: + delay: 3600000 \ No newline at end of file diff --git a/backend/libs/scanning/src/main/resources/application.yml b/backend/libs/scanning/src/main/resources/application.yml deleted file mode 100644 index f33f21a2..00000000 --- a/backend/libs/scanning/src/main/resources/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -feed: - sync: - delay: 3600000 \ No newline at end of file From 948bc9926c386b42b6c5e6b7bb349fabb38d1037 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 01:12:25 +0400 Subject: [PATCH 03/12] chore(config): configure scheduler thread pool and HTTP timeouts --- .../cleat/api/config/SchedulingConfig.java | 19 +++++++++++++++++++ .../enrichment/config/RestTemplateConfig.java | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java diff --git a/backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java b/backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java new file mode 100644 index 00000000..c64cbfbe --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java @@ -0,0 +1,19 @@ +package dev.cleat.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +public class SchedulingConfig { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler schedular = new ThreadPoolTaskScheduler(); + schedular.setPoolSize(4); + schedular.setThreadNamePrefix("scheduler-"); + schedular.initialize(); + return schedular; + } +} diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java index bafb5e16..e6177679 100644 --- a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/config/RestTemplateConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration @@ -9,6 +10,9 @@ public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { - return new RestTemplate(); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(4000); + factory.setReadTimeout(10000); + return new RestTemplate(factory); } } From 8118339cd660ed86552298b42450795d61cca265 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 01:24:09 +0400 Subject: [PATCH 04/12] refactor(enrichment): use URI template for epss requests --- .../main/java/dev/cleat/enrichment/client/EpssClient.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java index cd660a96..e358e6bd 100644 --- a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/EpssClient.java @@ -12,11 +12,11 @@ public EpssClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; } - private static final String URL = "https://api.first.org/data/v1/epss?cve="; + private static final String URL = "https://api.first.org/data/v1/epss?cve={cve}"; public Double fetchScore(String cve) { - EpssResponse response = restTemplate.getForObject(URL + cve, EpssResponse.class); + EpssResponse response = restTemplate.getForObject(URL, EpssResponse.class, cve); if (response == null || response.getData() == null || response.getData().isEmpty()) { return null; } @@ -24,6 +24,6 @@ public Double fetchScore(String cve) { } public EpssResponse fetchFeed(String cve) { - return restTemplate.getForObject(URL + cve, EpssResponse.class); + return restTemplate.getForObject(URL, EpssResponse.class, cve); } } From aa6bc67a432631f880434d80813cd9beaea7d426 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 01:59:46 +0400 Subject: [PATCH 05/12] perf(enrichment): cache KEV feed lookups --- .../cleat/enrichment/client/KevClient.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java index 671fb287..65b66e37 100644 --- a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java @@ -1,12 +1,18 @@ package dev.cleat.enrichment.client; import dev.cleat.enrichment.dto.KevResponse; +import dev.cleat.enrichment.dto.KevVulnerability; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class KevClient { private final RestTemplate restTemplate; + private Set kevCache; + private Long cacheTimestamp; + private static final long CACHE_TTL_MS = 3600_000L; public KevClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; @@ -15,12 +21,22 @@ public KevClient(RestTemplate restTemplate) { private static final String URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"; - public boolean isKev(String cve) { - KevResponse response = restTemplate.getForObject(URL, KevResponse.class); - if (response == null || response.getVulnerabilities() == null) { - return false; + public Set getKevCache() { + + if (kevCache == null || System.currentTimeMillis() - cacheTimestamp > CACHE_TTL_MS) { + KevResponse response = restTemplate.getForObject(URL, KevResponse.class); + kevCache = (response == null || response.getVulnerabilities() == null) + ? Set.of() + : response.getVulnerabilities().stream() + .map(KevVulnerability::getCveId) + .collect(Collectors.toSet()); + cacheTimestamp = System.currentTimeMillis(); } - return response.getVulnerabilities().stream().anyMatch(v -> cve.equals(v.getCveId())); + return kevCache; + } + + public boolean isKev(String cve) { + return getKevCache().contains(cve); } public KevResponse fetchFeed() { From f1e42356046cd07533a75ed895dfd5e11ba58d3e Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 02:15:40 +0400 Subject: [PATCH 06/12] refactor(enrichment): map KEV cveID field explicitly --- .../main/java/dev/cleat/enrichment/dto/KevVulnerability.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java index 49b3a948..7b706c8e 100644 --- a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/dto/KevVulnerability.java @@ -1,10 +1,11 @@ package dev.cleat.enrichment.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class KevVulnerability { - + @JsonProperty("cveID") private String cveId; public String getCveId() { From ca1d9c5f9903410b6367e5e42b14a6c815a5d592 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 02:48:03 +0400 Subject: [PATCH 07/12] refactor(scanning): move vulnerability updates into transactional service --- .../scanning/service/FeedSyncService.java | 24 +++---------- .../service/VulnerabilityUpdateService.java | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityUpdateService.java diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java index 6407ad0a..b044dcbe 100644 --- a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java @@ -1,7 +1,5 @@ package dev.cleat.scanning.service; -import dev.cleat.domain.PriorityCalculator; -import dev.cleat.domain.model.Vulnerability; import dev.cleat.enrichment.dto.FeedResult; import dev.cleat.enrichment.service.FeedService; import dev.cleat.persistence.entity.VulnerabilityEntity; @@ -11,29 +9,25 @@ import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Service public class FeedSyncService { private final VulnerabilityRepository vulnerabilityRepository; - - private final PriorityCalculator priorityCalculator; - private final FeedService feedService; + private final VulnerabilityUpdateService vulnerabilityUpdateService; public FeedSyncService( VulnerabilityRepository vulnerabilityRepository, FeedService feedService, - PriorityCalculator priorityCalculator) { + VulnerabilityUpdateService vulnerabilityUpdateService) { this.vulnerabilityRepository = vulnerabilityRepository; this.feedService = feedService; - this.priorityCalculator = priorityCalculator; + this.vulnerabilityUpdateService = vulnerabilityUpdateService; } private static final Logger LOG = LoggerFactory.getLogger(FeedSyncService.class); - @Transactional @Scheduled(fixedDelayString = "${feed.sync.delay}") public void syncFeeds() { LOG.info("Feed synchronization started"); @@ -45,22 +39,12 @@ public void syncFeeds() { } try { FeedResult feed = feedService.fetchFeeds(v.getCve(), v.getPackageName(), v.getEcosystem()); - - v.setKev(feed.kev()); - v.setEpss(feed.epss()); - - if (feed.osv() != null && feed.osv().getSummary() != null) { - v.setTitle(feed.osv().getSummary()); - } - Vulnerability vulnerability = - new Vulnerability(v.getKev(), v.getCvss(), v.getSeverity(), v.getEpss(), v.getReachable()); - v.setPriority(priorityCalculator.calculate(vulnerability)); + vulnerabilityUpdateService.update(v, feed); LOG.info("Updated vulnerability {}", v.getId()); } catch (Exception e) { LOG.error("Failed to update vulnerability {}", v.getId(), e); } } - vulnerabilityRepository.saveAll(vulnerabilities); LOG.info("Feed synchronization completed"); } } diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityUpdateService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityUpdateService.java new file mode 100644 index 00000000..b8f6cc90 --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityUpdateService.java @@ -0,0 +1,36 @@ +package dev.cleat.scanning.service; + +import dev.cleat.domain.PriorityCalculator; +import dev.cleat.domain.model.Vulnerability; +import dev.cleat.enrichment.dto.FeedResult; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class VulnerabilityUpdateService { + + private final PriorityCalculator priorityCalculator; + private final VulnerabilityRepository vulnerabilityRepository; + + public VulnerabilityUpdateService( + PriorityCalculator priorityCalculator, VulnerabilityRepository vulnerabilityRepository) { + this.priorityCalculator = priorityCalculator; + this.vulnerabilityRepository = vulnerabilityRepository; + } + + public void update(VulnerabilityEntity v, FeedResult feed) { + v.setKev(feed.kev()); + v.setEpss(feed.epss()); + if (feed.osv() != null && feed.osv().getSummary() != null) { + v.setTitle(feed.osv().getSummary()); + } + Vulnerability vulnerability = + new Vulnerability(v.getKev(), v.getCvss(), v.getSeverity(), v.getEpss(), v.getReachable()); + v.setPriority(priorityCalculator.calculate(vulnerability)); + + vulnerabilityRepository.save(v); + } +} From db8d1e8360d65ae94e37d2781815f11d7630dc10 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 03:16:39 +0400 Subject: [PATCH 08/12] test(scanning): update FeedSyncService test after refactor --- .../cleat/scanning/FeedSyncServiceTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java index 195db3a8..36165d71 100644 --- a/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java @@ -1,20 +1,19 @@ package dev.cleat.scanning; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import dev.cleat.common.enums.Priority; import dev.cleat.common.enums.Reachable; import dev.cleat.common.enums.Severity; -import dev.cleat.domain.PriorityCalculator; import dev.cleat.enrichment.dto.FeedResult; import dev.cleat.enrichment.dto.OsvResponse; import dev.cleat.enrichment.service.FeedService; import dev.cleat.persistence.entity.VulnerabilityEntity; import dev.cleat.persistence.repository.VulnerabilityRepository; import dev.cleat.scanning.service.FeedSyncService; +import dev.cleat.scanning.service.VulnerabilityUpdateService; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,7 +31,7 @@ public class FeedSyncServiceTest { FeedService feedService; @Mock - PriorityCalculator priorityCalculator; + VulnerabilityUpdateService vulnerabilityUpdateService; @InjectMocks FeedSyncService feedSyncService; @@ -58,14 +57,15 @@ void shouldRescoreAllVulnerabilities() { vulnerabilityEntity.getEcosystem())) .thenReturn(feed); - when(priorityCalculator.calculate(any())).thenReturn(Priority.URGENT); + doNothing().when(vulnerabilityUpdateService).update(any(VulnerabilityEntity.class), any(FeedResult.class)); feedSyncService.syncFeeds(); - verify(vulnerabilityRepository).saveAll(any()); - - assertEquals(true, vulnerabilityEntity.getKev()); - assertEquals(0.95, vulnerabilityEntity.getEpss()); - assertEquals(Priority.URGENT, vulnerabilityEntity.getPriority()); + verify(feedService) + .fetchFeeds( + vulnerabilityEntity.getCve(), + vulnerabilityEntity.getPackageName(), + vulnerabilityEntity.getEcosystem()); + verify(vulnerabilityUpdateService).update(any(VulnerabilityEntity.class), any(FeedResult.class)); } } From 3635b6bec1259340db8e2ad41d2b4a4525329949 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 03:36:48 +0400 Subject: [PATCH 09/12] test(scanning): update FeedSyncService test for paginated processing --- .../scanning/service/FeedSyncService.java | 34 +++++++++++-------- .../cleat/scanning/FeedSyncServiceTest.java | 6 +++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java index b044dcbe..c6fa03a5 100644 --- a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/FeedSyncService.java @@ -4,9 +4,10 @@ import dev.cleat.enrichment.service.FeedService; import dev.cleat.persistence.entity.VulnerabilityEntity; import dev.cleat.persistence.repository.VulnerabilityRepository; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -30,21 +31,26 @@ public FeedSyncService( @Scheduled(fixedDelayString = "${feed.sync.delay}") public void syncFeeds() { + int page = 0; + Page vulnerabilities; LOG.info("Feed synchronization started"); - List vulnerabilities = vulnerabilityRepository.findAll(); - for (VulnerabilityEntity v : vulnerabilities) { - if (v.getCve() == null) { - LOG.warn("Skipping vulnerability {} because CVE is missing", v.getId()); - continue; + do { + vulnerabilities = vulnerabilityRepository.findAll(PageRequest.of(page, 100)); + for (VulnerabilityEntity v : vulnerabilities.getContent()) { + if (v.getCve() == null) { + LOG.warn("Skipping vulnerability {} because CVE is missing", v.getId()); + continue; + } + try { + FeedResult feed = feedService.fetchFeeds(v.getCve(), v.getPackageName(), v.getEcosystem()); + vulnerabilityUpdateService.update(v, feed); + LOG.info("Updated vulnerability {}", v.getId()); + } catch (Exception e) { + LOG.error("Failed to update vulnerability {}", v.getId(), e); + } } - try { - FeedResult feed = feedService.fetchFeeds(v.getCve(), v.getPackageName(), v.getEcosystem()); - vulnerabilityUpdateService.update(v, feed); - LOG.info("Updated vulnerability {}", v.getId()); - } catch (Exception e) { - LOG.error("Failed to update vulnerability {}", v.getId(), e); - } - } + page++; + } while (vulnerabilities.hasNext()); LOG.info("Feed synchronization completed"); } } diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java index 36165d71..3f2fdb65 100644 --- a/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/FeedSyncServiceTest.java @@ -20,6 +20,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; @ExtendWith(MockitoExtension.class) public class FeedSyncServiceTest { @@ -48,8 +51,9 @@ void shouldRescoreAllVulnerabilities() { .setReachable(Reachable.REACHABLE); FeedResult feed = new FeedResult(true, 0.95, new OsvResponse()); + Page page = new PageImpl<>(List.of(vulnerabilityEntity)); - when(vulnerabilityRepository.findAll()).thenReturn(List.of(vulnerabilityEntity)); + when(vulnerabilityRepository.findAll(any(Pageable.class))).thenReturn(page); when(feedService.fetchFeeds( vulnerabilityEntity.getCve(), From 49378bd571019cb4ce0526416a1c39b722e78d69 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 09:34:22 +0400 Subject: [PATCH 10/12] refactor(worker): move task scheduler configuration to worker application --- .../dev/cleat/worker}/config/SchedulingConfig.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename backend/apps/{api/src/main/java/dev/cleat/api => worker/src/main/java/dev/cleat/worker}/config/SchedulingConfig.java (59%) diff --git a/backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java b/backend/apps/worker/src/main/java/dev/cleat/worker/config/SchedulingConfig.java similarity index 59% rename from backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java rename to backend/apps/worker/src/main/java/dev/cleat/worker/config/SchedulingConfig.java index c64cbfbe..981d5045 100644 --- a/backend/apps/api/src/main/java/dev/cleat/api/config/SchedulingConfig.java +++ b/backend/apps/worker/src/main/java/dev/cleat/worker/config/SchedulingConfig.java @@ -1,4 +1,4 @@ -package dev.cleat.api.config; +package dev.cleat.worker.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,10 +10,10 @@ public class SchedulingConfig { @Bean public TaskScheduler taskScheduler() { - ThreadPoolTaskScheduler schedular = new ThreadPoolTaskScheduler(); - schedular.setPoolSize(4); - schedular.setThreadNamePrefix("scheduler-"); - schedular.initialize(); - return schedular; + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(4); + scheduler.setThreadNamePrefix("scheduler-"); + scheduler.initialize(); + return scheduler; } } From 16c354ffd82b14c836e68837c5f97e488d761636 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 09:48:21 +0400 Subject: [PATCH 11/12] refactor(eenrichment): make KEV cache access thread-safe --- .../src/main/java/dev/cleat/enrichment/client/KevClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java index 65b66e37..6628eb2b 100644 --- a/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java +++ b/backend/libs/enrichment/src/main/java/dev/cleat/enrichment/client/KevClient.java @@ -21,7 +21,7 @@ public KevClient(RestTemplate restTemplate) { private static final String URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"; - public Set getKevCache() { + public synchronized Set getKevCache() { if (kevCache == null || System.currentTimeMillis() - cacheTimestamp > CACHE_TTL_MS) { KevResponse response = restTemplate.getForObject(URL, KevResponse.class); From 4b9280fa98cc82e451f59c6e1a7e55f74e8609e5 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Mon, 13 Jul 2026 10:01:48 +0400 Subject: [PATCH 12/12] feat(persistence): add optimistic locking for vulnerabilities --- .../entity/VulnerabilityEntity.java | 18 +++++++++++++++++- .../V7__alter_vulnerability_table.sql | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 backend/libs/persistence/src/main/resources/db/migration/V7__alter_vulnerability_table.sql diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java index c8214ac6..3af295cc 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java @@ -14,6 +14,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; +import jakarta.persistence.Version; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -85,6 +86,10 @@ public class VulnerabilityEntity { @Column(name = "published_at") private OffsetDateTime publishedAt; + @Version + @Column(name = "version", nullable = false) + private Long version; + public VulnerabilityEntity() {} public VulnerabilityEntity( @@ -106,7 +111,8 @@ public VulnerabilityEntity( String title, List affectedRepos, Boolean hasFixPr, - OffsetDateTime publishedAt) { + OffsetDateTime publishedAt, + Long version) { this.id = id; this.accountId = accountId; this.packageName = packageName; @@ -126,6 +132,7 @@ public VulnerabilityEntity( this.affectedRepos = affectedRepos; this.hasFixPr = hasFixPr; this.publishedAt = publishedAt; + this.version = version; } public UUID getId() { @@ -298,4 +305,13 @@ public VulnerabilityEntity setPublishedAt(OffsetDateTime publishedAt) { this.publishedAt = publishedAt; return this; } + + public Long getVersion() { + return version; + } + + public VulnerabilityEntity setVersion(Long version) { + this.version = version; + return this; + } } diff --git a/backend/libs/persistence/src/main/resources/db/migration/V7__alter_vulnerability_table.sql b/backend/libs/persistence/src/main/resources/db/migration/V7__alter_vulnerability_table.sql new file mode 100644 index 00000000..18f18c52 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V7__alter_vulnerability_table.sql @@ -0,0 +1 @@ +ALTER TABLE vulnerability ADD COLUMN version BIGINT NOT NULL DEFAULT 0; \ No newline at end of file