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 78331baf..b5f0ddab 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 @@ -5,7 +5,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -@SpringBootApplication(scanBasePackages = {"dev.cleat.api", "dev.cleat.persistence", "dev.cleat.common"}) +@SpringBootApplication( + scanBasePackages = { + "dev.cleat.api", + "dev.cleat.persistence", + "dev.cleat.domain", + "dev.cleat.common", + "dev.cleat.scanning" + }) @EnableJpaRepositories(basePackages = "dev.cleat.persistence.repository") @EntityScan(basePackages = "dev.cleat.persistence.entity") public class CleatApiApplication { diff --git a/backend/apps/api/src/main/java/dev/cleat/api/controller/CodeScanAlertController.java b/backend/apps/api/src/main/java/dev/cleat/api/controller/CodeScanAlertController.java new file mode 100644 index 00000000..2cc7198a --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/controller/CodeScanAlertController.java @@ -0,0 +1,28 @@ +package dev.cleat.api.controller; + +import dev.cleat.common.dto.request.CodeScanAlertRequestDto; +import dev.cleat.common.dto.response.CodeScanAlertResponseDto; +import dev.cleat.scanning.service.CodeScanAlertService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/alerts") +public class CodeScanAlertController { + + private final CodeScanAlertService codeScanAlertService; + + public CodeScanAlertController(CodeScanAlertService codeScanAlertService) { + this.codeScanAlertService = codeScanAlertService; + } + + @PostMapping + public ResponseEntity create( + @RequestBody CodeScanAlertRequestDto codeScanAlertRequestDto) { + return ResponseEntity.status(HttpStatus.CREATED).body(codeScanAlertService.create(codeScanAlertRequestDto)); + } +} diff --git a/backend/apps/api/src/main/java/dev/cleat/api/controller/SecretFindingController.java b/backend/apps/api/src/main/java/dev/cleat/api/controller/SecretFindingController.java new file mode 100644 index 00000000..f3c6e0f4 --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/controller/SecretFindingController.java @@ -0,0 +1,28 @@ +package dev.cleat.api.controller; + +import dev.cleat.common.dto.request.SecretFindingRequestDto; +import dev.cleat.common.dto.response.SecretFindingResponseDto; +import dev.cleat.scanning.service.SecretFindingService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/secrets") +public class SecretFindingController { + + private final SecretFindingService secretFindingService; + + public SecretFindingController(SecretFindingService secretFindingService) { + this.secretFindingService = secretFindingService; + } + + @PostMapping + public ResponseEntity createSecret( + @RequestBody SecretFindingRequestDto secretFindingRequestDto) { + return ResponseEntity.status(HttpStatus.CREATED).body(secretFindingService.create(secretFindingRequestDto)); + } +} diff --git a/backend/apps/api/src/main/java/dev/cleat/api/controller/VulnerabilityController.java b/backend/apps/api/src/main/java/dev/cleat/api/controller/VulnerabilityController.java new file mode 100644 index 00000000..46c03177 --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/controller/VulnerabilityController.java @@ -0,0 +1,36 @@ +package dev.cleat.api.controller; + +import dev.cleat.common.dto.request.VulnerabilityRequestDto; +import dev.cleat.common.dto.response.VulnerabilityResponseDto; +import dev.cleat.scanning.service.VulnerabilityService; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/vulnerabilities") +public class VulnerabilityController { + + private final VulnerabilityService vulnerabilityService; + + public VulnerabilityController(VulnerabilityService vulnerabilityService) { + this.vulnerabilityService = vulnerabilityService; + } + + @PostMapping + public ResponseEntity createVulnerability( + @RequestBody VulnerabilityRequestDto vulnerabilityRequestDto) { + return ResponseEntity.status(HttpStatus.CREATED).body(vulnerabilityService.create(vulnerabilityRequestDto)); + } + + @GetMapping("/{id}") + public ResponseEntity getVulnerability(@PathVariable UUID id) { + return ResponseEntity.ok(vulnerabilityService.findById(id)); + } +} diff --git a/backend/apps/api/src/main/java/dev/cleat/api/exception/GlobalExceptionHandler.java b/backend/apps/api/src/main/java/dev/cleat/api/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..6ce43e65 --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/exception/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package dev.cleat.api.exception; + +import dev.cleat.common.exception.NotFoundException; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity> handleNotFound(NotFoundException ex) { + LOG.info("Resource not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneralException(Exception ex) { + + if (ex instanceof ResponseStatusException) { + throw (ResponseStatusException) ex; + } + LOG.error("Unhandled exception occurred: ", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "An unexpected error occurred")); + } +} diff --git a/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java b/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java index 7af426b4..de0c0302 100644 --- a/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java +++ b/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java @@ -1,6 +1,5 @@ package dev.cleat.api; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.PostgreSQLContainer; @@ -8,7 +7,6 @@ import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers -@SpringBootTest public abstract class AbstractIntegrationTest { @Container diff --git a/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java b/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java index eb41e9f1..98f0e225 100644 --- a/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java +++ b/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; -@SpringBootTest +@SpringBootTest(classes = CleatApiApplication.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class CleatApiTests extends AbstractIntegrationTest { @Test diff --git a/backend/apps/api/src/test/java/dev/cleat/api/CodeScanAlertControllerTest.java b/backend/apps/api/src/test/java/dev/cleat/api/CodeScanAlertControllerTest.java new file mode 100644 index 00000000..74df13d3 --- /dev/null +++ b/backend/apps/api/src/test/java/dev/cleat/api/CodeScanAlertControllerTest.java @@ -0,0 +1,45 @@ +package dev.cleat.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.cleat.api.controller.CodeScanAlertController; +import dev.cleat.common.dto.request.CodeScanAlertRequestDto; +import dev.cleat.scanning.service.CodeScanAlertService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CodeScanAlertController.class) +@AutoConfigureMockMvc(addFilters = false) +@ContextConfiguration(classes = CodeScanAlertController.class) +public class CodeScanAlertControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private CodeScanAlertService codeScanAlertService; + + @Test + void whenCreateCodeScanAlertThenShouldBeReturn201() throws Exception { + + // given + CodeScanAlertRequestDto codeScanAlertRequestDto = new CodeScanAlertRequestDto(); + + // then + mockMvc.perform(post("/api/v1/alerts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(codeScanAlertRequestDto))) + .andExpect(status().isCreated()); + } +} diff --git a/backend/apps/api/src/test/java/dev/cleat/api/SecretFindingControllerTest.java b/backend/apps/api/src/test/java/dev/cleat/api/SecretFindingControllerTest.java new file mode 100644 index 00000000..45461372 --- /dev/null +++ b/backend/apps/api/src/test/java/dev/cleat/api/SecretFindingControllerTest.java @@ -0,0 +1,45 @@ +package dev.cleat.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.cleat.api.controller.SecretFindingController; +import dev.cleat.common.dto.request.SecretFindingRequestDto; +import dev.cleat.scanning.service.SecretFindingService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(SecretFindingController.class) +@AutoConfigureMockMvc(addFilters = false) +@ContextConfiguration(classes = {SecretFindingController.class}) +public class SecretFindingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private SecretFindingService secretFindingService; + + @Test + void whenCreateSecretThenShouldReturn201() throws Exception { + + // given + SecretFindingRequestDto secretFindingRequestDto = new SecretFindingRequestDto(); + + // then + mockMvc.perform(post("/api/v1/secrets") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(secretFindingRequestDto))) + .andExpect(status().isCreated()); + } +} diff --git a/backend/apps/api/src/test/java/dev/cleat/api/VulnerabilityControllerTest.java b/backend/apps/api/src/test/java/dev/cleat/api/VulnerabilityControllerTest.java new file mode 100644 index 00000000..fe29f355 --- /dev/null +++ b/backend/apps/api/src/test/java/dev/cleat/api/VulnerabilityControllerTest.java @@ -0,0 +1,45 @@ +package dev.cleat.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.cleat.api.controller.VulnerabilityController; +import dev.cleat.common.dto.request.VulnerabilityRequestDto; +import dev.cleat.scanning.service.VulnerabilityService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(VulnerabilityController.class) +@AutoConfigureMockMvc(addFilters = false) +@ContextConfiguration(classes = {VulnerabilityController.class}) +public class VulnerabilityControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private VulnerabilityService vulnerabilityService; + + @Test + void whenCreateVulnerabilityThenShouldReturn201() throws Exception { + + // given + VulnerabilityRequestDto vulnerabilityRequestDto = new VulnerabilityRequestDto(); + + // then + mockMvc.perform(post("/api/v1/vulnerabilities") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(vulnerabilityRequestDto))) + .andExpect(status().isCreated()); + } +} diff --git a/backend/apps/worker/build.gradle.kts b/backend/apps/worker/build.gradle.kts index 4ae7e159..b190c03b 100644 --- a/backend/apps/worker/build.gradle.kts +++ b/backend/apps/worker/build.gradle.kts @@ -14,8 +14,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.testcontainers:junit-jupiter") - testImplementation("org.testcontainers:postgresql") - testImplementation("org.testcontainers:testcontainers") - testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:postgresql") + testImplementation("org.testcontainers:testcontainers") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + } 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 c28eff76..8ebbdb3b 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 @@ -2,9 +2,20 @@ import org.springframework.boot.SpringApplication; 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 +@SpringBootApplication( + scanBasePackages = { + "dev.cleat.persistence", + "dev.cleat.scanning", + "dev.cleat.worker", + "dev.cleat.domain", + "dev.cleat.common" + }) +@EnableJpaRepositories(basePackages = "dev.cleat.persistence.repository") +@EntityScan(basePackages = "dev.cleat.persistence.entity") @EnableScheduling public class CleatWorkerApplication { diff --git a/backend/apps/worker/src/main/java/dev/cleat/worker/SecurityScanWorker.java b/backend/apps/worker/src/main/java/dev/cleat/worker/SecurityScanWorker.java new file mode 100644 index 00000000..d5feec6a --- /dev/null +++ b/backend/apps/worker/src/main/java/dev/cleat/worker/SecurityScanWorker.java @@ -0,0 +1,78 @@ +package dev.cleat.worker; + +import dev.cleat.persistence.repository.CodeScanAlertRepository; +import dev.cleat.persistence.repository.SecretFindingRepository; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import dev.cleat.scanning.CodeScanAlertScanner; +import dev.cleat.scanning.SecretFindingScanner; +import dev.cleat.scanning.VulnerabilityScanner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class SecurityScanWorker { + private final VulnerabilityScanner vulnerabilityScanner; + private final VulnerabilityRepository vulnerabilityRepository; + private static final Logger LOG = LoggerFactory.getLogger(SecurityScanWorker.class); + private final SecretFindingRepository secretFindingRepository; + private final SecretFindingScanner secretFindingScanner; + private final CodeScanAlertRepository codeScanAlertRepository; + private final CodeScanAlertScanner codeScanAlertScanner; + + public SecurityScanWorker( + VulnerabilityScanner vulnerabilityScanner, + VulnerabilityRepository vulnerabilityRepository, + SecretFindingRepository secretFindingRepository, + SecretFindingScanner secretFindingScanner, + CodeScanAlertRepository codeScanAlertRepository, + CodeScanAlertScanner codeScanAlertScanner) { + this.vulnerabilityScanner = vulnerabilityScanner; + this.vulnerabilityRepository = vulnerabilityRepository; + this.secretFindingRepository = secretFindingRepository; + this.secretFindingScanner = secretFindingScanner; + this.codeScanAlertRepository = codeScanAlertRepository; + this.codeScanAlertScanner = codeScanAlertScanner; + } + + @Scheduled(fixedDelayString = "${app.security.scan-interval}") + public void runSecurityScan() { + LOG.info("Starting scheduled security scan..."); + processVulns(); + processSecrets(); + processAlerts(); + LOG.info("Scheduled security scan completed."); + } + + private void processVulns() { + vulnerabilityRepository.findAll().forEach(vulnerability -> { + try { + vulnerabilityScanner.processAndSave(vulnerability); + } catch (Exception ex) { + LOG.error("Failed to process vulnerability ID: {}.Skipping...", vulnerability.getId(), ex); + } + }); + } + + private void processSecrets() { + secretFindingRepository.findAll().forEach(secret -> { + try { + secretFindingScanner.process(secret); + } catch (Exception ex) { + LOG.error("Failed to process secret ID: {}", secret.getId(), ex); + } + }); + } + + private void processAlerts() { + codeScanAlertRepository.findAll().forEach(alert -> { + try { + codeScanAlertScanner.process(alert); + codeScanAlertRepository.save(alert); + } catch (Exception ex) { + LOG.error("Failed to process ID: {}", alert.getId(), ex); + } + }); + } +} diff --git a/backend/apps/worker/src/main/resources/application.yml b/backend/apps/worker/src/main/resources/application.yml index ce6f2f6b..2fc5ab65 100644 --- a/backend/apps/worker/src/main/resources/application.yml +++ b/backend/apps/worker/src/main/resources/application.yml @@ -28,4 +28,8 @@ management: db: enabled: true redis: - enabled: true \ No newline at end of file + enabled: true + +app: + security: + scan-interval: 600000 \ No newline at end of file diff --git a/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java b/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java index 63842d66..16315f1a 100644 --- a/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java +++ b/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java @@ -1,6 +1,5 @@ package dev.cleat.worker; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.PostgreSQLContainer; @@ -8,7 +7,6 @@ import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers -@SpringBootTest public abstract class AbstractIntegrationTest { @Container diff --git a/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java b/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java index 7a6f1f44..3d980caf 100644 --- a/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java +++ b/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; -@SpringBootTest +@SpringBootTest(classes = CleatWorkerApplication.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class CleatWorkerApplicationTests extends AbstractIntegrationTest { @Test diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/CodeScanAlertRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/CodeScanAlertRequestDto.java new file mode 100644 index 00000000..40e23d51 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/CodeScanAlertRequestDto.java @@ -0,0 +1,133 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.Severity; +import java.util.UUID; + +public class CodeScanAlertRequestDto { + + private UUID accountId; + private UUID repo; + private String rule; + private UUID ruleId; + private Severity severity; + private String file; + private Integer line; + private String branch; + private String tool; + private String description; + + public CodeScanAlertRequestDto() {} + + public CodeScanAlertRequestDto( + UUID accountId, + UUID repo, + String rule, + UUID ruleId, + Severity severity, + String file, + Integer line, + String branch, + String tool, + String description) { + this.accountId = accountId; + this.repo = repo; + this.rule = rule; + this.ruleId = ruleId; + this.severity = severity; + this.file = file; + this.line = line; + this.branch = branch; + this.tool = tool; + this.description = description; + } + + public UUID getAccountId() { + return accountId; + } + + public CodeScanAlertRequestDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public UUID getRepo() { + return repo; + } + + public CodeScanAlertRequestDto setRepo(UUID repo) { + this.repo = repo; + return this; + } + + public String getRule() { + return rule; + } + + public CodeScanAlertRequestDto setRule(String rule) { + this.rule = rule; + return this; + } + + public UUID getRuleId() { + return ruleId; + } + + public CodeScanAlertRequestDto setRuleId(UUID ruleId) { + this.ruleId = ruleId; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public CodeScanAlertRequestDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getFile() { + return file; + } + + public CodeScanAlertRequestDto setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public CodeScanAlertRequestDto setLine(Integer line) { + this.line = line; + return this; + } + + public String getBranch() { + return branch; + } + + public CodeScanAlertRequestDto setBranch(String branch) { + this.branch = branch; + return this; + } + + public String getTool() { + return tool; + } + + public CodeScanAlertRequestDto setTool(String tool) { + this.tool = tool; + return this; + } + + public String getDescription() { + return description; + } + + public CodeScanAlertRequestDto setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java index a458c31f..189f93d5 100644 --- a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java @@ -3,8 +3,11 @@ import dev.cleat.common.enums.Severity; import dev.cleat.common.enums.Validity; import java.time.OffsetDateTime; +import java.util.UUID; public class SecretFindingRequestDto { + private UUID accountId; + private UUID repoId; private String provider; private String secretType; private String file; @@ -19,6 +22,8 @@ public class SecretFindingRequestDto { public SecretFindingRequestDto() {} public SecretFindingRequestDto( + UUID accountId, + UUID repoId, String provider, String secretType, String file, @@ -30,6 +35,8 @@ public SecretFindingRequestDto( Severity severity, Boolean pushProtectionBlocked) { + this.accountId = accountId; + this.repoId = repoId; this.provider = provider; this.secretType = secretType; this.file = file; @@ -42,6 +49,24 @@ public SecretFindingRequestDto( this.pushProtectionBlocked = pushProtectionBlocked; } + public UUID getAccountId() { + return accountId; + } + + public SecretFindingRequestDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public UUID getRepoId() { + return repoId; + } + + public SecretFindingRequestDto setRepoId(UUID repoId) { + this.repoId = repoId; + return this; + } + public String getProvider() { return provider; } diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/CodeScanAlertResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/CodeScanAlertResponseDto.java new file mode 100644 index 00000000..ba919bdb --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/CodeScanAlertResponseDto.java @@ -0,0 +1,171 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Status; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class CodeScanAlertResponseDto { + + private UUID id; + private UUID accountId; + private UUID repo; + private String rule; + private UUID ruleId; + private Severity severity; + private String file; + private Integer line; + private String branch; + private Status status; + private String tool; + private OffsetDateTime detectedAt; + private String description; + + public CodeScanAlertResponseDto() {} + + public CodeScanAlertResponseDto( + UUID id, + UUID accountId, + UUID repo, + String rule, + UUID ruleId, + Severity severity, + String file, + Integer line, + String branch, + Status status, + String tool, + OffsetDateTime detectedAt, + String description) { + this.id = id; + this.accountId = accountId; + this.repo = repo; + this.rule = rule; + this.ruleId = ruleId; + this.severity = severity; + this.file = file; + this.line = line; + this.branch = branch; + this.status = status; + this.tool = tool; + this.detectedAt = detectedAt; + this.description = description; + } + + public UUID getId() { + return id; + } + + public CodeScanAlertResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public CodeScanAlertResponseDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public UUID getRepo() { + return repo; + } + + public CodeScanAlertResponseDto setRepo(UUID repo) { + this.repo = repo; + return this; + } + + public String getRule() { + return rule; + } + + public CodeScanAlertResponseDto setRule(String rule) { + this.rule = rule; + return this; + } + + public UUID getRuleId() { + return ruleId; + } + + public CodeScanAlertResponseDto setRuleId(UUID ruleId) { + this.ruleId = ruleId; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public CodeScanAlertResponseDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getFile() { + return file; + } + + public CodeScanAlertResponseDto setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public CodeScanAlertResponseDto setLine(Integer line) { + this.line = line; + return this; + } + + public String getBranch() { + return branch; + } + + public CodeScanAlertResponseDto setBranch(String branch) { + this.branch = branch; + return this; + } + + public Status getStatus() { + return status; + } + + public CodeScanAlertResponseDto setStatus(Status status) { + this.status = status; + return this; + } + + public String getTool() { + return tool; + } + + public CodeScanAlertResponseDto setTool(String tool) { + this.tool = tool; + return this; + } + + public OffsetDateTime getDetectedAt() { + return detectedAt; + } + + public CodeScanAlertResponseDto setDetectedAt(OffsetDateTime detectedAt) { + this.detectedAt = detectedAt; + return this; + } + + public String getDescription() { + return description; + } + + public CodeScanAlertResponseDto setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java index 0e380a7f..ba7eb54c 100644 --- a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java @@ -8,6 +8,7 @@ public class DatasetDto { private List repos; private List secrets; private List vulnerabilities; + private List codeAlerts; private UsageResponseDto usage; private List members; private List events; @@ -19,6 +20,7 @@ public DatasetDto( List repos, List secrets, List vulnerabilities, + List codeAlerts, UsageResponseDto usage, List members, List events) { @@ -26,6 +28,7 @@ public DatasetDto( this.repos = repos; this.secrets = secrets; this.vulnerabilities = vulnerabilities; + this.codeAlerts = codeAlerts; this.usage = usage; this.members = members; this.events = events; @@ -67,6 +70,15 @@ public DatasetDto setVulnerabilities(List vulnerabilit return this; } + public List getCodeAlerts() { + return codeAlerts; + } + + public DatasetDto setCodeAlerts(List codeAlerts) { + this.codeAlerts = codeAlerts; + return this; + } + public UsageResponseDto getUsage() { return usage; } 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 12f2280b..ed3f8b0b 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 @@ -1,5 +1,6 @@ package dev.cleat.common.dto.response; +import dev.cleat.common.enums.Priority; import dev.cleat.common.enums.Reachable; import dev.cleat.common.enums.Severity; import java.time.OffsetDateTime; @@ -15,6 +16,7 @@ public class VulnerabilityResponseDto { private String fixedVersion; private Double cvss; private Severity severity; + private Priority priority; private Double epss; private Boolean kev; private Reachable reachable; @@ -97,6 +99,15 @@ public VulnerabilityResponseDto setSeverity(Severity severity) { return this; } + public Priority getPriority() { + return priority; + } + + public VulnerabilityResponseDto setPriority(Priority priority) { + this.priority = priority; + return this; + } + public Double getEpss() { return epss; } diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Priority.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Priority.java new file mode 100644 index 00000000..e209a863 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Priority.java @@ -0,0 +1,21 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Priority { + URGENT("urgent"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String value; + + Priority(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Status.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Status.java new file mode 100644 index 00000000..8b89b81a --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Status.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Status { + OPEN("open"), + FIXED("fixed"), + DISMISSED("dismissed"); + + private final String value; + + Status(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/domain/build.gradle.kts b/backend/libs/domain/build.gradle.kts index 7f5e077b..25a798f4 100644 --- a/backend/libs/domain/build.gradle.kts +++ b/backend/libs/domain/build.gradle.kts @@ -5,4 +5,5 @@ plugins { dependencies { implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") + implementation(project(":libs:common")) } 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 new file mode 100644 index 00000000..95018fa3 --- /dev/null +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/PriorityCalculator.java @@ -0,0 +1,24 @@ +package dev.cleat.domain; + +import dev.cleat.common.enums.Priority; +import dev.cleat.domain.model.Vulnerability; +import org.springframework.stereotype.Component; + +@Component +public class PriorityCalculator { + 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) { + return Priority.URGENT; + } + + return switch (vulnerability.severity()) { + case CRITICAL -> Priority.HIGH; + case HIGH -> Priority.MEDIUM; + default -> 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 new file mode 100644 index 00000000..ee40c004 --- /dev/null +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/model/Vulnerability.java @@ -0,0 +1,12 @@ +package dev.cleat.domain.model; + +import dev.cleat.common.enums.Severity; +import java.util.Objects; + +public record Vulnerability(Boolean kev, Double cvss, Severity severity) { + public Vulnerability { + Objects.requireNonNull(cvss, "cvss cannot be null"); + Objects.requireNonNull(severity, "severity cannot be null"); + kev = (kev != null) ? kev : false; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java index 01f85ce0..3763f717 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java @@ -4,9 +4,17 @@ import dev.cleat.common.exception.NotFoundException; import dev.cleat.persistence.entity.AccountEntity; import dev.cleat.persistence.entity.UsageEntity; -import dev.cleat.persistence.mapper.CleatMapper; +import dev.cleat.persistence.mapper.AccountMapper; +import dev.cleat.persistence.mapper.ActivityEventMapper; +import dev.cleat.persistence.mapper.CodeScanAlertMapper; +import dev.cleat.persistence.mapper.MemberMapper; +import dev.cleat.persistence.mapper.RepoMapper; +import dev.cleat.persistence.mapper.SecretFindingMapper; +import dev.cleat.persistence.mapper.UsageMapper; +import dev.cleat.persistence.mapper.VulnerabilityMapper; import dev.cleat.persistence.repository.AccountRepository; import dev.cleat.persistence.repository.ActivityEventRepository; +import dev.cleat.persistence.repository.CodeScanAlertRepository; import dev.cleat.persistence.repository.MemberRepository; import dev.cleat.persistence.repository.RepoRepository; import dev.cleat.persistence.repository.SecretFindingRepository; @@ -25,7 +33,15 @@ public class DashboardService { private final UsageRepository usageRepository; private final MemberRepository memberRepository; private final ActivityEventRepository activityEventRepository; - private final CleatMapper cleatMapper; + private final AccountMapper accountMapper; + private final ActivityEventMapper activityEventMapper; + private final MemberMapper memberMapper; + private final RepoMapper repoMapper; + private final UsageMapper usageMapper; + private final SecretFindingMapper secretFindingMapper; + private final VulnerabilityMapper vulnerabilityMapper; + private final CodeScanAlertMapper codeScanAlertMapper; + private final CodeScanAlertRepository codeScanAlertRepository; public DashboardService( AccountRepository accountRepository, @@ -35,7 +51,15 @@ public DashboardService( UsageRepository usageRepository, MemberRepository memberRepository, ActivityEventRepository activityEventRepository, - CleatMapper cleatMapper) { + AccountMapper accountMapper, + ActivityEventMapper activityEventMapper, + MemberMapper memberMapper, + RepoMapper repoMapper, + UsageMapper usageMapper, + SecretFindingMapper secretFindingMapper, + VulnerabilityMapper vulnerabilityMapper, + CodeScanAlertMapper codeScanAlertMapper, + CodeScanAlertRepository codeScanAlertRepository) { this.accountRepository = accountRepository; this.repoRepository = repoRepository; this.secretFindingRepository = secretFindingRepository; @@ -43,7 +67,15 @@ public DashboardService( this.usageRepository = usageRepository; this.memberRepository = memberRepository; this.activityEventRepository = activityEventRepository; - this.cleatMapper = cleatMapper; + this.accountMapper = accountMapper; + this.activityEventMapper = activityEventMapper; + this.memberMapper = memberMapper; + this.repoMapper = repoMapper; + this.usageMapper = usageMapper; + this.secretFindingMapper = secretFindingMapper; + this.vulnerabilityMapper = vulnerabilityMapper; + this.codeScanAlertMapper = codeScanAlertMapper; + this.codeScanAlertRepository = codeScanAlertRepository; } @Transactional(readOnly = true) @@ -52,23 +84,26 @@ public DatasetDto getDataset(UUID accountId) { AccountEntity account = accountRepository.findById(accountId).orElseThrow(() -> new NotFoundException("Account not found")); return new DatasetDto( - cleatMapper.toAccountDto(account), + accountMapper.toAccountDto(account), repoRepository.findAllByAccountId(accountId).stream() - .map(cleatMapper::toRepoDto) + .map(repoMapper::toRepoDto) .toList(), secretFindingRepository.findAllByAccountId(accountId).stream() - .map(cleatMapper::toSecretFindingDto) + .map(secretFindingMapper::toSecretFindingDto) .toList(), vulnerabilityRepository.findAllByAccountId(accountId).stream() - .map(cleatMapper::toVulnerabilityDto) + .map(vulnerabilityMapper::toVulnerabilityDto) .toList(), - cleatMapper.toUsageDto( + codeScanAlertRepository.findAllByAccountId(accountId).stream() + .map(codeScanAlertMapper::toCodeScanAlertDto) + .toList(), + usageMapper.toUsageDto( usageRepository.findByAccountId(accountId).orElse(new UsageEntity())), memberRepository.findAllByAccountId(accountId).stream() - .map(cleatMapper::toMemberDto) + .map(memberMapper::toMemberDto) .toList(), activityEventRepository.findAllByAccountId(accountId).stream() - .map(cleatMapper::toActivityEventDto) + .map(activityEventMapper::toActivityEventDto) .toList()); } } diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/CodeScanAlertEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/CodeScanAlertEntity.java new file mode 100644 index 00000000..800ff30d --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/CodeScanAlertEntity.java @@ -0,0 +1,217 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Status; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "code_scan_alerts") +public class CodeScanAlertEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "account_id") + private UUID accountId; + + @Column(name = "repo") + private UUID repo; + + @Column(name = "rule") + private String rule; + + @Column(name = "rule_id") + private UUID ruleId; + + @Enumerated(EnumType.STRING) + @Column(name = "severity") + private Severity severity; + + @Column(name = "file") + private String file; + + @Column(name = "line") + private Integer line; + + @Column(name = "branch") + private String branch; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private Status status; + + @Column(name = "tool") + private String tool; + + @Column(name = "detected_at", insertable = false, updatable = true) + private OffsetDateTime detectedAt; + + @Column(name = "description") + private String description; + + public CodeScanAlertEntity() {} + + public CodeScanAlertEntity( + UUID id, + UUID accountId, + UUID repo, + String rule, + UUID ruleId, + Severity severity, + String file, + Integer line, + String branch, + Status status, + String tool, + OffsetDateTime detectedAt, + String description) { + this.id = id; + this.accountId = accountId; + this.repo = repo; + this.rule = rule; + this.ruleId = ruleId; + this.severity = severity; + this.file = file; + this.line = line; + this.branch = branch; + this.status = status; + this.tool = tool; + this.detectedAt = detectedAt; + this.description = description; + } + + public UUID getId() { + return id; + } + + public CodeScanAlertEntity setId(UUID id) { + this.id = id; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public CodeScanAlertEntity setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public UUID getRepo() { + return repo; + } + + public CodeScanAlertEntity setRepo(UUID repo) { + this.repo = repo; + return this; + } + + public String getRule() { + return rule; + } + + public CodeScanAlertEntity setRule(String rule) { + this.rule = rule; + return this; + } + + public UUID getRuleId() { + return ruleId; + } + + public CodeScanAlertEntity setRuleId(UUID ruleId) { + this.ruleId = ruleId; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public CodeScanAlertEntity setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getFile() { + return file; + } + + public CodeScanAlertEntity setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public CodeScanAlertEntity setLine(Integer line) { + this.line = line; + return this; + } + + public String getBranch() { + return branch; + } + + public CodeScanAlertEntity setBranch(String branch) { + this.branch = branch; + return this; + } + + public Status getStatus() { + return status; + } + + public CodeScanAlertEntity setStatus(Status status) { + this.status = status; + return this; + } + + public String getTool() { + return tool; + } + + public CodeScanAlertEntity setTool(String tool) { + this.tool = tool; + return this; + } + + public OffsetDateTime getDetectedAt() { + return detectedAt; + } + + public CodeScanAlertEntity setDetectedAt(OffsetDateTime detectedAt) { + this.detectedAt = detectedAt; + return this; + } + + public String getDescription() { + return description; + } + + public CodeScanAlertEntity setDescription(String description) { + this.description = description; + return this; + } + + @PrePersist + protected void onCreate() { + if (this.detectedAt == null) { + this.detectedAt = OffsetDateTime.now(); + } + } +} 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 a8873271..4d8d16e8 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 @@ -1,5 +1,6 @@ package dev.cleat.persistence.entity; +import dev.cleat.common.enums.Priority; import dev.cleat.common.enums.Reachable; import dev.cleat.common.enums.Severity; import jakarta.persistence.CollectionTable; @@ -47,6 +48,10 @@ public class VulnerabilityEntity { @Column(name = "severity") private Severity severity; + @Enumerated(EnumType.STRING) + @Column(name = "priority") + private Priority priority; + @Column(name = "epss") private Double epss; @@ -88,6 +93,7 @@ public VulnerabilityEntity( String fixedVersion, Double cvss, Severity severity, + Priority priority, Double epss, Boolean kev, Reachable reachable, @@ -105,6 +111,7 @@ public VulnerabilityEntity( this.fixedVersion = fixedVersion; this.cvss = cvss; this.severity = severity; + this.priority = priority; this.epss = epss; this.kev = kev; this.reachable = reachable; @@ -188,6 +195,15 @@ public VulnerabilityEntity setSeverity(Severity severity) { return this; } + public Priority getPriority() { + return priority; + } + + public VulnerabilityEntity setPriority(Priority priority) { + this.priority = priority; + return this; + } + public Double getEpss() { return epss; } diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/AccountMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/AccountMapper.java new file mode 100644 index 00000000..7a003042 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/AccountMapper.java @@ -0,0 +1,38 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.AccountRequestDto; +import dev.cleat.common.dto.response.AccountResponseDto; +import dev.cleat.persistence.entity.AccountEntity; +import org.springframework.stereotype.Component; + +@Component +public class AccountMapper { + public AccountResponseDto toAccountDto(AccountEntity accountEntity) { + if (accountEntity == null) { + return null; + } + + return new AccountResponseDto() + .setId(accountEntity.getId()) + .setLogin(accountEntity.getLogin()) + .setName(accountEntity.getName()) + .setType(accountEntity.getType()) + .setPlan(accountEntity.getPlan()) + .setRepoCount(accountEntity.getRepoCount()) + .setMemberCount(accountEntity.getMemberCount()) + .setPostureScore(accountEntity.getPostureScore()) + .setMonthlySpend(accountEntity.getMonthlySpend()) + .setReclaimable(accountEntity.getReclaimable()); + } + + public AccountEntity toAccountEntity(AccountRequestDto accountRequestDto) { + if (accountRequestDto == null) { + return null; + } + return new AccountEntity() + .setLogin(accountRequestDto.getLogin()) + .setName(accountRequestDto.getName()) + .setType(accountRequestDto.getType()) + .setPlan(accountRequestDto.getPlan()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ActivityEventMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ActivityEventMapper.java new file mode 100644 index 00000000..e6034a5c --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ActivityEventMapper.java @@ -0,0 +1,41 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.ActivityEventRequestDto; +import dev.cleat.common.dto.response.ActivityEventResponseDto; +import dev.cleat.persistence.entity.ActivityEventEntity; +import org.springframework.stereotype.Component; + +@Component +public class ActivityEventMapper { + + public ActivityEventResponseDto toActivityEventDto(ActivityEventEntity activityEventEntity) { + if (activityEventEntity == null) { + return null; + } + return new ActivityEventResponseDto() + .setId(activityEventEntity.getId()) + .setAccountId(activityEventEntity.getAccountId()) + .setType(activityEventEntity.getType()) + .setSeverity(activityEventEntity.getSeverity()) + .setActor(activityEventEntity.getActor()) + .setTarget(activityEventEntity.getTarget()) + .setRepo( + activityEventEntity.getRepo() != null + ? activityEventEntity.getRepo().getName() + : "Unknown") + .setMessage(activityEventEntity.getMessage()) + .setCreatedAt(activityEventEntity.getCreatedAt()); + } + + public ActivityEventEntity toActiveEventEntity(ActivityEventRequestDto activityEventRequestDto) { + if (activityEventRequestDto == null) { + return null; + } + return new ActivityEventEntity() + .setType(activityEventRequestDto.getType()) + .setSeverity(activityEventRequestDto.getSeverity()) + .setActor(activityEventRequestDto.getActor()) + .setTarget(activityEventRequestDto.getTarget()) + .setMessage(activityEventRequestDto.getMessage()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java deleted file mode 100644 index 5e1ac1e2..00000000 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java +++ /dev/null @@ -1,292 +0,0 @@ -package dev.cleat.persistence.mapper; - -import dev.cleat.common.dto.UsagePointDto; -import dev.cleat.common.dto.request.AccountRequestDto; -import dev.cleat.common.dto.request.ActivityEventRequestDto; -import dev.cleat.common.dto.request.MemberRequestDto; -import dev.cleat.common.dto.request.RepoRequestDto; -import dev.cleat.common.dto.request.SecretFindingRequestDto; -import dev.cleat.common.dto.request.VulnerabilityRequestDto; -import dev.cleat.common.dto.response.AccountResponseDto; -import dev.cleat.common.dto.response.ActivityEventResponseDto; -import dev.cleat.common.dto.response.DatasetDto; -import dev.cleat.common.dto.response.MemberResponseDto; -import dev.cleat.common.dto.response.RepoResponseDto; -import dev.cleat.common.dto.response.ScorecardCheckResponseDto; -import dev.cleat.common.dto.response.SecretFindingResponseDto; -import dev.cleat.common.dto.response.UsageResponseDto; -import dev.cleat.common.dto.response.VulnerabilityResponseDto; -import dev.cleat.persistence.entity.AccountEntity; -import dev.cleat.persistence.entity.ActivityEventEntity; -import dev.cleat.persistence.entity.MemberEntity; -import dev.cleat.persistence.entity.RepoEntity; -import dev.cleat.persistence.entity.ScorecardCheckEntity; -import dev.cleat.persistence.entity.SecretFindingEntity; -import dev.cleat.persistence.entity.UsageEntity; -import dev.cleat.persistence.entity.UsagePointEntity; -import dev.cleat.persistence.entity.VulnerabilityEntity; -import java.util.List; -import org.springframework.stereotype.Component; - -@Component -public class CleatMapper { - - public AccountResponseDto toAccountDto(AccountEntity accountEntity) { - if (accountEntity == null) { - return null; - } - - return new AccountResponseDto() - .setId(accountEntity.getId()) - .setLogin(accountEntity.getLogin()) - .setName(accountEntity.getName()) - .setType(accountEntity.getType()) - .setPlan(accountEntity.getPlan()) - .setRepoCount(accountEntity.getRepoCount()) - .setMemberCount(accountEntity.getMemberCount()) - .setPostureScore(accountEntity.getPostureScore()) - .setMonthlySpend(accountEntity.getMonthlySpend()) - .setReclaimable(accountEntity.getReclaimable()); - } - - public AccountEntity toAccountEntity(AccountRequestDto accountRequestDto) { - if (accountRequestDto == null) { - return null; - } - return new AccountEntity() - .setLogin(accountRequestDto.getLogin()) - .setName(accountRequestDto.getName()) - .setType(accountRequestDto.getType()) - .setPlan(accountRequestDto.getPlan()); - } - - public RepoResponseDto toRepoDto(RepoEntity repoEntity) { - if (repoEntity == null) { - return null; - } - return new RepoResponseDto() - .setId(repoEntity.getId()) - .setName(repoEntity.getName()) - .setAccountId(repoEntity.getAccount().getId()) - .setVisibility(repoEntity.getVisibility()) - .setLanguage(repoEntity.getLanguage()) - .setStars(repoEntity.getStars()) - .setDefaultBranch(repoEntity.getDefaultBranch()) - .setBranchProtected(repoEntity.getBranchProtected()) - .setHasReadme(repoEntity.getHasReadme()) - .setHasLicense(repoEntity.getHasLicense()) - .setHasContributing(repoEntity.getHasContributing()) - .setHasCodeowners(repoEntity.getHasCodeowners()) - .setHasCI(repoEntity.getHasCi()) - .setSizeMb(repoEntity.getSizeMb()) - .setLastPushedAt(repoEntity.getLastPushedAt()) - .setArchived(repoEntity.getArchived()) - .setOpenVulns(repoEntity.getOpenVulns()) - .setOpenSecrets(repoEntity.getOpenSecrets()) - .setOpenCodeAlerts(repoEntity.getOpenCodeAlerts()) - .setStaleBranches(repoEntity.getStaleBranches()) - .setOpenPRs(repoEntity.getOpenPRs()) - .setHygieneScore(repoEntity.getHygieneScore()) - .setScorecard((repoEntity.getScorecard()) - .stream().map(this::toScorecardCheckResponseDto).toList()) - .setTopics(repoEntity.getTopics()); - } - - public RepoEntity toRepoEntity(RepoRequestDto repoRequestDto) { - if (repoRequestDto == null) { - return null; - } - return new RepoEntity() - .setName(repoRequestDto.getName()) - .setVisibility(repoRequestDto.getVisibility()) - .setLanguage(repoRequestDto.getLanguage()) - .setDefaultBranch(repoRequestDto.getDefaultBranch()) - .setTopics(repoRequestDto.getTopics()); - } - - public ScorecardCheckResponseDto toScorecardCheckResponseDto(ScorecardCheckEntity scorecardCheckEntity) { - if (scorecardCheckEntity == null) { - return null; - } - return new ScorecardCheckResponseDto() - .setId(scorecardCheckEntity.getId()) - .setName(scorecardCheckEntity.getName()) - .setReason(scorecardCheckEntity.getReason()) - .setScore(scorecardCheckEntity.getScore()); - } - - public ActivityEventResponseDto toActivityEventDto(ActivityEventEntity activityEventEntity) { - if (activityEventEntity == null) { - return null; - } - return new ActivityEventResponseDto() - .setId(activityEventEntity.getId()) - .setAccountId(activityEventEntity.getAccountId()) - .setType(activityEventEntity.getType()) - .setSeverity(activityEventEntity.getSeverity()) - .setActor(activityEventEntity.getActor()) - .setTarget(activityEventEntity.getTarget()) - .setRepo(activityEventEntity.getRepo().getName()) - .setMessage(activityEventEntity.getMessage()) - .setCreatedAt(activityEventEntity.getCreatedAt()); - } - - public ActivityEventEntity toActiveEventEntity(ActivityEventRequestDto activityEventRequestDto) { - if (activityEventRequestDto == null) { - return null; - } - return new ActivityEventEntity() - .setType(activityEventRequestDto.getType()) - .setSeverity(activityEventRequestDto.getSeverity()) - .setActor(activityEventRequestDto.getActor()) - .setTarget(activityEventRequestDto.getTarget()) - .setMessage(activityEventRequestDto.getMessage()); - } - - public DatasetDto toDatasetDto( - AccountResponseDto account, - List repos, - List secrets, - List vulnerabilities, - UsageResponseDto usage, - List members, - List events) { - return new DatasetDto() - .setAccount(account) - .setRepos(repos) - .setSecrets(secrets) - .setVulnerabilities(vulnerabilities) - .setUsage(usage) - .setMembers(members) - .setEvents(events); - } - - public MemberResponseDto toMemberDto(MemberEntity memberEntity) { - return new MemberResponseDto() - .setId(memberEntity.getId()) - .setLogin(memberEntity.getLogin()) - .setName(memberEntity.getName()) - .setRole(memberEntity.getRole()) - .setTwoFactor(memberEntity.getTwoFactor()) - .setLastActiveAt(memberEntity.getLastActiveAt()) - .setTeams(memberEntity.getTeams()) - .setOutsideCollaborator(memberEntity.getOutsideCollaborator()) - .setRepoAccess(memberEntity.getRepoAccess()); - } - - public MemberEntity toMemberEntity(MemberRequestDto memberRequestDto) { - if (memberRequestDto == null) { - return null; - } - return new MemberEntity() - .setLogin(memberRequestDto.getLogin()) - .setName(memberRequestDto.getName()) - .setRole(memberRequestDto.getRole()) - .setTwoFactor(memberRequestDto.getTwoFactor()) - .setTeams(memberRequestDto.getTeams()); - } - - public SecretFindingResponseDto toSecretFindingDto(SecretFindingEntity secretFindingEntity) { - return new SecretFindingResponseDto() - .setId(secretFindingEntity.getId()) - .setAccountId(secretFindingEntity.getAccountId()) - .setRepo(secretFindingEntity.getRepo().getName()) - .setProvider(secretFindingEntity.getProvider()) - .setSecretType(secretFindingEntity.getSecretType()) - .setFile(secretFindingEntity.getFile()) - .setLine(secretFindingEntity.getLine()) - .setCommit(secretFindingEntity.getCommit()) - .setAuthor(secretFindingEntity.getAuthor()) - .setDetectedAt(secretFindingEntity.getDetectedAt()) - .setValidity(secretFindingEntity.getValidity()) - .setSeverity(secretFindingEntity.getSeverity()) - .setPushProtectionBlocked(secretFindingEntity.getPushProtectionBlocked()); - } - - public SecretFindingEntity toSecretFindingEntity(SecretFindingRequestDto secretFindingRequestDto) { - if (secretFindingRequestDto == null) { - return null; - } - return new SecretFindingEntity() - .setProvider(secretFindingRequestDto.getProvider()) - .setSecretType(secretFindingRequestDto.getSecretType()) - .setFile(secretFindingRequestDto.getFile()) - .setLine(secretFindingRequestDto.getLine()) - .setCommit(secretFindingRequestDto.getCommit()) - .setAuthor(secretFindingRequestDto.getAuthor()) - .setDetectedAt(secretFindingRequestDto.getDetectedAt()) - .setValidity(secretFindingRequestDto.getValidity()) - .setSeverity(secretFindingRequestDto.getSeverity()) - .setPushProtectionBlocked(secretFindingRequestDto.getPushProtectionBlocked()); - } - - public VulnerabilityResponseDto toVulnerabilityDto(VulnerabilityEntity vulnerabilityEntity) { - return new VulnerabilityResponseDto() - .setId(vulnerabilityEntity.getId()) - .setAccountId(vulnerabilityEntity.getAccountId()) - .setPackageName(vulnerabilityEntity.getPackageName()) - .setEcosystem(vulnerabilityEntity.getEcosystem()) - .setCurrentVersion(vulnerabilityEntity.getCurrentVersion()) - .setFixedVersion(vulnerabilityEntity.getFixedVersion()) - .setCvss(vulnerabilityEntity.getCvss()) - .setSeverity(vulnerabilityEntity.getSeverity()) - .setEpss(vulnerabilityEntity.getEpss()) - .setKev(vulnerabilityEntity.getKev()) - .setReachable(vulnerabilityEntity.getReachable()) - .setAdvisoryId(vulnerabilityEntity.getAdvisoryId()) - .setCwe(vulnerabilityEntity.getCwe()) - .setTitle(vulnerabilityEntity.getTitle()) - .setAffectedRepos(vulnerabilityEntity.getAffectedRepos()) - .setHasFixPr(vulnerabilityEntity.getHasFixPr()) - .setPublishedAt(vulnerabilityEntity.getPublishedAt()); - } - - public VulnerabilityEntity toVulnerabilityEntity(VulnerabilityRequestDto vulnerabilityRequestDto) { - if (vulnerabilityRequestDto == null) { - return null; - } - return new VulnerabilityEntity() - .setPackageName(vulnerabilityRequestDto.getPackageName()) - .setEcosystem(vulnerabilityRequestDto.getEcosystem()) - .setCurrentVersion(vulnerabilityRequestDto.getCurrentVersion()) - .setFixedVersion(vulnerabilityRequestDto.getFixedVersion()) - .setCvss(vulnerabilityRequestDto.getCvss()) - .setSeverity(vulnerabilityRequestDto.getSeverity()) - .setEpss(vulnerabilityRequestDto.getEpss()) - .setKev(vulnerabilityRequestDto.getKev()) - .setReachable(vulnerabilityRequestDto.getReachable()) - .setAdvisoryId(vulnerabilityRequestDto.getAdvisoryId()) - .setCwe(vulnerabilityRequestDto.getCwe()) - .setTitle(vulnerabilityRequestDto.getTitle()) - .setAffectedRepos(vulnerabilityRequestDto.getAffectedRepos()) - .setHasFixPr(vulnerabilityRequestDto.getHasFixPr()); - } - - public UsageResponseDto toUsageDto(UsageEntity usageEntity) { - if (usageEntity == null) { - return null; - } - return new UsageResponseDto() - .setActionsMinutes(usageEntity.getActionsMinutes()) - .setMinutesIncluded(usageEntity.getMinutesIncluded()) - .setStorageGb(usageEntity.getStorageGb()) - .setStorageIncludedGb(usageEntity.getStorageIncludedGb()) - .setMonthlyCost(usageEntity.getMonthlyCost()) - .setReclaimable(usageEntity.getReclaimable()) - .setBreakdown(usageEntity.getBreakdown()) - .setSeries(usageEntity.getSeries().stream() - .map(this::toUsagePointDto) - .toList()); - } - - public UsagePointDto toUsagePointDto(UsagePointEntity usagePointEntity) { - if (usagePointEntity == null) { - return null; - } - return new UsagePointDto() - .setLabel(usagePointEntity.getLabel()) - .setMinutes(usagePointEntity.getMinutes()) - .setStorageGb(usagePointEntity.getStorageGb()) - .setCost(usagePointEntity.getCost()); - } -} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CodeScanAlertMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CodeScanAlertMapper.java new file mode 100644 index 00000000..c50f2704 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CodeScanAlertMapper.java @@ -0,0 +1,50 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.CodeScanAlertRequestDto; +import dev.cleat.common.dto.response.CodeScanAlertResponseDto; +import dev.cleat.persistence.entity.CodeScanAlertEntity; +import org.springframework.stereotype.Component; + +@Component +public class CodeScanAlertMapper { + + public CodeScanAlertResponseDto toCodeScanAlertDto(CodeScanAlertEntity codeScanAlertEntity) { + + if (codeScanAlertEntity == null) { + return null; + } + return new CodeScanAlertResponseDto( + codeScanAlertEntity.getId(), + codeScanAlertEntity.getAccountId(), + codeScanAlertEntity.getRepo(), + codeScanAlertEntity.getRule(), + codeScanAlertEntity.getRuleId(), + codeScanAlertEntity.getSeverity(), + codeScanAlertEntity.getFile(), + codeScanAlertEntity.getLine(), + codeScanAlertEntity.getBranch(), + codeScanAlertEntity.getStatus(), + codeScanAlertEntity.getTool(), + codeScanAlertEntity.getDetectedAt(), + codeScanAlertEntity.getDescription()); + } + + public CodeScanAlertEntity toCodeScanAlertEntity(CodeScanAlertRequestDto codeScanAlertRequestDto) { + + if (codeScanAlertRequestDto == null) { + return null; + } + + return new CodeScanAlertEntity() + .setAccountId(codeScanAlertRequestDto.getAccountId()) + .setRepo(codeScanAlertRequestDto.getRepo()) + .setRule(codeScanAlertRequestDto.getRule()) + .setRuleId(codeScanAlertRequestDto.getRuleId()) + .setSeverity(codeScanAlertRequestDto.getSeverity()) + .setFile(codeScanAlertRequestDto.getFile()) + .setLine(codeScanAlertRequestDto.getLine()) + .setBranch(codeScanAlertRequestDto.getBranch()) + .setTool(codeScanAlertRequestDto.getTool()) + .setDescription(codeScanAlertRequestDto.getDescription()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/MemberMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/MemberMapper.java new file mode 100644 index 00000000..5e1cea74 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/MemberMapper.java @@ -0,0 +1,35 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.MemberRequestDto; +import dev.cleat.common.dto.response.MemberResponseDto; +import dev.cleat.persistence.entity.MemberEntity; +import org.springframework.stereotype.Component; + +@Component +public class MemberMapper { + + public MemberResponseDto toMemberDto(MemberEntity memberEntity) { + return new MemberResponseDto() + .setId(memberEntity.getId()) + .setLogin(memberEntity.getLogin()) + .setName(memberEntity.getName()) + .setRole(memberEntity.getRole()) + .setTwoFactor(memberEntity.getTwoFactor()) + .setLastActiveAt(memberEntity.getLastActiveAt()) + .setTeams(memberEntity.getTeams()) + .setOutsideCollaborator(memberEntity.getOutsideCollaborator()) + .setRepoAccess(memberEntity.getRepoAccess()); + } + + public MemberEntity toMemberEntity(MemberRequestDto memberRequestDto) { + if (memberRequestDto == null) { + return null; + } + return new MemberEntity() + .setLogin(memberRequestDto.getLogin()) + .setName(memberRequestDto.getName()) + .setRole(memberRequestDto.getRole()) + .setTwoFactor(memberRequestDto.getTwoFactor()) + .setTeams(memberRequestDto.getTeams()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/RepoMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/RepoMapper.java new file mode 100644 index 00000000..c547df20 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/RepoMapper.java @@ -0,0 +1,62 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.RepoRequestDto; +import dev.cleat.common.dto.response.RepoResponseDto; +import dev.cleat.persistence.entity.RepoEntity; +import org.springframework.stereotype.Component; + +@Component +public class RepoMapper { + + private final ScorecardCheckMapper scorecardCheckMapper; + + public RepoMapper(ScorecardCheckMapper scorecardCheckMapper) { + this.scorecardCheckMapper = scorecardCheckMapper; + } + + public RepoResponseDto toRepoDto(RepoEntity repoEntity) { + if (repoEntity == null) { + return null; + } + return new RepoResponseDto() + .setId(repoEntity.getId()) + .setName(repoEntity.getName()) + .setAccountId(repoEntity.getAccount().getId()) + .setVisibility(repoEntity.getVisibility()) + .setLanguage(repoEntity.getLanguage()) + .setStars(repoEntity.getStars()) + .setDefaultBranch(repoEntity.getDefaultBranch()) + .setBranchProtected(repoEntity.getBranchProtected()) + .setHasReadme(repoEntity.getHasReadme()) + .setHasLicense(repoEntity.getHasLicense()) + .setHasContributing(repoEntity.getHasContributing()) + .setHasCodeowners(repoEntity.getHasCodeowners()) + .setHasCI(repoEntity.getHasCi()) + .setSizeMb(repoEntity.getSizeMb()) + .setLastPushedAt(repoEntity.getLastPushedAt()) + .setArchived(repoEntity.getArchived()) + .setOpenVulns(repoEntity.getOpenVulns()) + .setOpenSecrets(repoEntity.getOpenSecrets()) + .setOpenCodeAlerts(repoEntity.getOpenCodeAlerts()) + .setStaleBranches(repoEntity.getStaleBranches()) + .setOpenPRs(repoEntity.getOpenPRs()) + .setHygieneScore(repoEntity.getHygieneScore()) + .setScorecard((repoEntity.getScorecard()) + .stream() + .map(scorecardCheckMapper::toScorecardCheckResponseDto) + .toList()) + .setTopics(repoEntity.getTopics()); + } + + public RepoEntity toRepoEntity(RepoRequestDto repoRequestDto) { + if (repoRequestDto == null) { + return null; + } + return new RepoEntity() + .setName(repoRequestDto.getName()) + .setVisibility(repoRequestDto.getVisibility()) + .setLanguage(repoRequestDto.getLanguage()) + .setDefaultBranch(repoRequestDto.getDefaultBranch()) + .setTopics(repoRequestDto.getTopics()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ScorecardCheckMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ScorecardCheckMapper.java new file mode 100644 index 00000000..8ae2b139 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/ScorecardCheckMapper.java @@ -0,0 +1,20 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.response.ScorecardCheckResponseDto; +import dev.cleat.persistence.entity.ScorecardCheckEntity; +import org.springframework.stereotype.Component; + +@Component +public class ScorecardCheckMapper { + + public ScorecardCheckResponseDto toScorecardCheckResponseDto(ScorecardCheckEntity scorecardCheckEntity) { + if (scorecardCheckEntity == null) { + return null; + } + return new ScorecardCheckResponseDto() + .setId(scorecardCheckEntity.getId()) + .setName(scorecardCheckEntity.getName()) + .setReason(scorecardCheckEntity.getReason()) + .setScore(scorecardCheckEntity.getScore()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/SecretFindingMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/SecretFindingMapper.java new file mode 100644 index 00000000..a220b8ac --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/SecretFindingMapper.java @@ -0,0 +1,48 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.SecretFindingRequestDto; +import dev.cleat.common.dto.response.SecretFindingResponseDto; +import dev.cleat.persistence.entity.SecretFindingEntity; +import org.springframework.stereotype.Component; + +@Component +public class SecretFindingMapper { + + public SecretFindingResponseDto toSecretFindingDto(SecretFindingEntity secretFindingEntity) { + return new SecretFindingResponseDto() + .setId(secretFindingEntity.getId()) + .setAccountId(secretFindingEntity.getAccountId()) + .setRepo( + secretFindingEntity.getRepo() != null + ? secretFindingEntity.getRepo().getName() + : "Unknown") + .setProvider(secretFindingEntity.getProvider()) + .setSecretType(secretFindingEntity.getSecretType()) + .setFile(secretFindingEntity.getFile()) + .setLine(secretFindingEntity.getLine()) + .setCommit(secretFindingEntity.getCommit()) + .setAuthor(secretFindingEntity.getAuthor()) + .setDetectedAt(secretFindingEntity.getDetectedAt()) + .setValidity(secretFindingEntity.getValidity()) + .setSeverity(secretFindingEntity.getSeverity()) + .setPushProtectionBlocked(secretFindingEntity.getPushProtectionBlocked()); + } + + public SecretFindingEntity toSecretFindingEntity(SecretFindingRequestDto secretFindingRequestDto) { + if (secretFindingRequestDto == null) { + return null; + } + return new SecretFindingEntity() + .setAccountId(secretFindingRequestDto.getAccountId()) + .setProvider(secretFindingRequestDto.getProvider()) + .setSecretType(secretFindingRequestDto.getSecretType()) + .setFile(secretFindingRequestDto.getFile()) + .setLine(secretFindingRequestDto.getLine()) + .setCommit(secretFindingRequestDto.getCommit()) + .setAuthor(secretFindingRequestDto.getAuthor()) + .setDetectedAt(secretFindingRequestDto.getDetectedAt()) + .setValidity(secretFindingRequestDto.getValidity()) + .setSeverity(secretFindingRequestDto.getSeverity()) + .setPushProtectionBlocked(secretFindingRequestDto.getPushProtectionBlocked()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsageMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsageMapper.java new file mode 100644 index 00000000..42e43ee5 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsageMapper.java @@ -0,0 +1,36 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.response.UsageResponseDto; +import dev.cleat.persistence.entity.UsageEntity; +import java.util.Collections; +import org.springframework.stereotype.Component; + +@Component +public class UsageMapper { + + private final UsagePointMapper usagePointMapper; + + public UsageMapper(UsagePointMapper usagePointMapper) { + this.usagePointMapper = usagePointMapper; + } + + public UsageResponseDto toUsageDto(UsageEntity usageEntity) { + if (usageEntity == null) { + return null; + } + return new UsageResponseDto() + .setActionsMinutes(usageEntity.getActionsMinutes()) + .setMinutesIncluded(usageEntity.getMinutesIncluded()) + .setStorageGb(usageEntity.getStorageGb()) + .setStorageIncludedGb(usageEntity.getStorageIncludedGb()) + .setMonthlyCost(usageEntity.getMonthlyCost()) + .setReclaimable(usageEntity.getReclaimable()) + .setBreakdown(usageEntity.getBreakdown()) + .setSeries( + usageEntity.getSeries() != null + ? usageEntity.getSeries().stream() + .map(usagePointMapper::toUsagePointDto) + .toList() + : Collections.emptyList()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsagePointMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsagePointMapper.java new file mode 100644 index 00000000..4a68ea7b --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/UsagePointMapper.java @@ -0,0 +1,20 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.UsagePointDto; +import dev.cleat.persistence.entity.UsagePointEntity; +import org.springframework.stereotype.Component; + +@Component +public class UsagePointMapper { + + public UsagePointDto toUsagePointDto(UsagePointEntity usagePointEntity) { + if (usagePointEntity == null) { + return null; + } + return new UsagePointDto() + .setLabel(usagePointEntity.getLabel()) + .setMinutes(usagePointEntity.getMinutes()) + .setStorageGb(usagePointEntity.getStorageGb()) + .setCost(usagePointEntity.getCost()); + } +} 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 new file mode 100644 index 00000000..52906cb0 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/VulnerabilityMapper.java @@ -0,0 +1,52 @@ +package dev.cleat.persistence.mapper; + +import dev.cleat.common.dto.request.VulnerabilityRequestDto; +import dev.cleat.common.dto.response.VulnerabilityResponseDto; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import org.springframework.stereotype.Component; + +@Component +public class VulnerabilityMapper { + public VulnerabilityResponseDto toVulnerabilityDto(VulnerabilityEntity vulnerabilityEntity) { + return new VulnerabilityResponseDto() + .setId(vulnerabilityEntity.getId()) + .setAccountId(vulnerabilityEntity.getAccountId()) + .setPackageName(vulnerabilityEntity.getPackageName()) + .setEcosystem(vulnerabilityEntity.getEcosystem()) + .setCurrentVersion(vulnerabilityEntity.getCurrentVersion()) + .setFixedVersion(vulnerabilityEntity.getFixedVersion()) + .setCvss(vulnerabilityEntity.getCvss()) + .setSeverity(vulnerabilityEntity.getSeverity()) + .setPriority(vulnerabilityEntity.getPriority()) + .setEpss(vulnerabilityEntity.getEpss()) + .setKev(vulnerabilityEntity.getKev()) + .setReachable(vulnerabilityEntity.getReachable()) + .setAdvisoryId(vulnerabilityEntity.getAdvisoryId()) + .setCwe(vulnerabilityEntity.getCwe()) + .setTitle(vulnerabilityEntity.getTitle()) + .setAffectedRepos(vulnerabilityEntity.getAffectedRepos()) + .setHasFixPr(vulnerabilityEntity.getHasFixPr()) + .setPublishedAt(vulnerabilityEntity.getPublishedAt()); + } + + public VulnerabilityEntity toVulnerabilityEntity(VulnerabilityRequestDto vulnerabilityRequestDto) { + if (vulnerabilityRequestDto == null) { + return null; + } + return new VulnerabilityEntity() + .setPackageName(vulnerabilityRequestDto.getPackageName()) + .setEcosystem(vulnerabilityRequestDto.getEcosystem()) + .setCurrentVersion(vulnerabilityRequestDto.getCurrentVersion()) + .setFixedVersion(vulnerabilityRequestDto.getFixedVersion()) + .setCvss(vulnerabilityRequestDto.getCvss()) + .setSeverity(vulnerabilityRequestDto.getSeverity()) + .setEpss(vulnerabilityRequestDto.getEpss()) + .setKev(vulnerabilityRequestDto.getKev()) + .setReachable(vulnerabilityRequestDto.getReachable()) + .setAdvisoryId(vulnerabilityRequestDto.getAdvisoryId()) + .setCwe(vulnerabilityRequestDto.getCwe()) + .setTitle(vulnerabilityRequestDto.getTitle()) + .setAffectedRepos(vulnerabilityRequestDto.getAffectedRepos()) + .setHasFixPr(vulnerabilityRequestDto.getHasFixPr()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/CodeScanAlertRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/CodeScanAlertRepository.java new file mode 100644 index 00000000..6ad285cd --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/CodeScanAlertRepository.java @@ -0,0 +1,11 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.CodeScanAlertEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CodeScanAlertRepository extends JpaRepository { + + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql b/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql index 93df4f95..971a811a 100644 --- a/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql +++ b/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql @@ -160,4 +160,30 @@ CREATE TABLE vulnerability_affected_repos( ); +CREATE INDEX idx_scorecard_repo_id on scorecard_check(repo_id); + +CREATE INDEX idx_repo_topics_repo_id on repo_topics(repo_id); + +CREATE INDEX idx_activity_account_id on activity_event(account_id); +CREATE INDEX idx_activity_repo_id on activity_event(repo); +CREATE INDEX idx_activity_created_at on activity_event(created_at); + +CREATE INDEX idx_member_account_id on member(account_id); +CREATE INDEX idx_member_login on member(login); + +CREATE INDEX idx_secret_account_id on secret_finding(account_id); +CREATE INDEX idx_secret_repo_id on secret_finding(repo); +CREATE INDEX idx_secret_severity on secret_finding(severity); + +CREATE INDEX idx_usage_account_id on usage(account_id); + +CREATE INDEX idx_usage_point_usage_id on usage_point(usage_id); + +CREATE INDEX idx_vulnerability_account_id on vulnerability(account_id); +CREATE INDEX idx_vulnerability_severity on vulnerability(severity); +CREATE INDEX idx_vulnerability_kev on vulnerability(kev); +CREATE INDEX idx_vulnerability_cvss on vulnerability(cvss); + +CREATE INDEX idx_vulnerability_affected_repos_id on vulnerability_affected_repos(vulnerability_id); + diff --git a/backend/libs/persistence/src/main/resources/db/migration/V3__alter_vulnerability_table.sql b/backend/libs/persistence/src/main/resources/db/migration/V3__alter_vulnerability_table.sql new file mode 100644 index 00000000..9a165474 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V3__alter_vulnerability_table.sql @@ -0,0 +1 @@ +ALTER TABLE vulnerability ADD COLUMN priority VARCHAR(50 ); diff --git a/backend/libs/persistence/src/main/resources/db/migration/V4__create_code_scan_alerts_table.sql b/backend/libs/persistence/src/main/resources/db/migration/V4__create_code_scan_alerts_table.sql new file mode 100644 index 00000000..59bb1513 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V4__create_code_scan_alerts_table.sql @@ -0,0 +1,37 @@ +CREATE TABLE code_scan_alerts( + id UUID PRIMARY KEY, + account_id UUID NOT NULL, + severity VARCHAR(50) NOT NULL, + file VARCHAR(255) NOT NULL, + line INTEGER CHECK (line>=0), + branch VARCHAR(250), + status VARCHAR(50) NOT NULL , + tool VARCHAR(255) NOT NULL, + detected_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + description TEXT, + repo UUID NOT NULL, + rule VARCHAR(250) NOT NULL, + rule_id UUID NOT NULL, + + CONSTRAINT fk_account + FOREIGN KEY(account_id) + REFERENCES account(id) + ON DELETE CASCADE, + + CONSTRAINT fk_repo + FOREIGN KEY(repo) + REFERENCES repo(id) + ON DELETE CASCADE, + + CONSTRAINT chk_severity + CHECK (severity + IN('CRITICAL','HIGH','MEDIUM','LOW')), + + CONSTRAINT chk_status + CHECK (status IN('OPEN','FIXED','DISMISSED')) + +); + +CREATE INDEX idx_scan_alerts_repo_severity on code_scan_alerts(repo,severity); +CREATE INDEX idx_scan_alerts_detected_at on code_scan_alerts(detected_at); + diff --git a/backend/libs/persistence/src/main/resources/db/migration/V5__alter_secret_finding_table.sql b/backend/libs/persistence/src/main/resources/db/migration/V5__alter_secret_finding_table.sql new file mode 100644 index 00000000..6464da71 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V5__alter_secret_finding_table.sql @@ -0,0 +1,3 @@ +ALTER TABLE secret_finding +ALTER COLUMN detected_at + SET DEFAULT CURRENT_TIMESTAMP; diff --git a/backend/libs/scanning/build.gradle.kts b/backend/libs/scanning/build.gradle.kts index 0d4d4aca..41a5c34d 100644 --- a/backend/libs/scanning/build.gradle.kts +++ b/backend/libs/scanning/build.gradle.kts @@ -11,4 +11,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter") testImplementation("org.springframework.boot:spring-boot-starter-test") + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} +tasks.test{ + useJUnitPlatform() } diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/CodeScanAlertScanner.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/CodeScanAlertScanner.java new file mode 100644 index 00000000..5fc861ec --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/CodeScanAlertScanner.java @@ -0,0 +1,23 @@ +package dev.cleat.scanning; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Status; +import dev.cleat.persistence.entity.CodeScanAlertEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class CodeScanAlertScanner { + private static final Logger LOG = LoggerFactory.getLogger(CodeScanAlertScanner.class); + + public void process(CodeScanAlertEntity codeScanAlertEntity) { + LOG.info("Analyzing code scan alert for rule: {}", codeScanAlertEntity.getRule()); + if (Severity.CRITICAL.equals(codeScanAlertEntity.getSeverity()) + || Severity.HIGH.equals(codeScanAlertEntity.getSeverity())) { + codeScanAlertEntity.setStatus(Status.OPEN); + } else { + codeScanAlertEntity.setStatus(Status.OPEN); + } + } +} diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/SecretFindingScanner.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/SecretFindingScanner.java new file mode 100644 index 00000000..0ae459eb --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/SecretFindingScanner.java @@ -0,0 +1,32 @@ +package dev.cleat.scanning; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Validity; +import dev.cleat.persistence.entity.SecretFindingEntity; +import dev.cleat.persistence.repository.SecretFindingRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class SecretFindingScanner { + private static final Logger LOG = LoggerFactory.getLogger(SecretFindingScanner.class); + private final SecretFindingRepository secretFindingRepository; + + public SecretFindingScanner(SecretFindingRepository secretFindingRepository) { + this.secretFindingRepository = secretFindingRepository; + } + + public void process(SecretFindingEntity secretFindingEntity) { + LOG.info( + "Processing secret finding for repo: {}", + secretFindingEntity.getRepo().getName()); + if (Boolean.FALSE.equals(secretFindingEntity.getPushProtectionBlocked())) { + secretFindingEntity.setSeverity(Severity.CRITICAL).setValidity(Validity.ACTIVE); + } else { + secretFindingEntity.setSeverity(Severity.LOW).setValidity(Validity.REVOKED); + } + secretFindingRepository.save(secretFindingEntity); + LOG.info("Secret finding processed successfully: {}", secretFindingEntity.getId()); + } +} 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 new file mode 100644 index 00000000..63a2fa61 --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/VulnerabilityScanner.java @@ -0,0 +1,44 @@ +package dev.cleat.scanning; + +import dev.cleat.domain.PriorityCalculator; +import dev.cleat.domain.model.Vulnerability; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class VulnerabilityScanner { + private final PriorityCalculator priorityCalculator; + private final VulnerabilityRepository vulnerabilityRepository; + private static final Logger LOG = LoggerFactory.getLogger(VulnerabilityScanner.class); + + public VulnerabilityScanner( + PriorityCalculator priorityCalculator, VulnerabilityRepository vulnerabilityRepository) { + this.priorityCalculator = priorityCalculator; + this.vulnerabilityRepository = vulnerabilityRepository; + } + + public void processAndSave(VulnerabilityEntity vulnerabilityEntity) { + + if (vulnerabilityEntity == null) { + throw new IllegalArgumentException("VulnerabilityEntity cannot be null"); + } + if (vulnerabilityEntity.getCvss() == null + || vulnerabilityEntity.getSeverity() == null + || vulnerabilityEntity.getKev() == null) { + throw new IllegalArgumentException("cvss,severity and kev are required"); + } + try { + Vulnerability vulnerability = new Vulnerability( + vulnerabilityEntity.getKev(), vulnerabilityEntity.getCvss(), vulnerabilityEntity.getSeverity()); + vulnerabilityEntity.setPriority(priorityCalculator.calculate(vulnerability)); + vulnerabilityRepository.save(vulnerabilityEntity); + LOG.info("Vulnerability processed successfully for ID: {}", vulnerabilityEntity.getId()); + } catch (Exception e) { + LOG.error("Failed to process vulnerability with ID: {}", vulnerabilityEntity.getId(), e); + throw new RuntimeException("Scanner processing error", e); + } + } +} diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/CodeScanAlertService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/CodeScanAlertService.java new file mode 100644 index 00000000..3edef352 --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/CodeScanAlertService.java @@ -0,0 +1,49 @@ +package dev.cleat.scanning.service; + +import dev.cleat.common.dto.request.CodeScanAlertRequestDto; +import dev.cleat.common.dto.response.CodeScanAlertResponseDto; +import dev.cleat.common.exception.NotFoundException; +import dev.cleat.persistence.entity.CodeScanAlertEntity; +import dev.cleat.persistence.entity.RepoEntity; +import dev.cleat.persistence.mapper.CodeScanAlertMapper; +import dev.cleat.persistence.repository.CodeScanAlertRepository; +import dev.cleat.persistence.repository.RepoRepository; +import dev.cleat.scanning.CodeScanAlertScanner; +import org.springframework.stereotype.Service; + +@Service +public class CodeScanAlertService { + + private final CodeScanAlertMapper codeScanAlertMapper; + private final CodeScanAlertScanner codeScanAlertScanner; + private final CodeScanAlertRepository codeScanAlertRepository; + private final RepoRepository repoRepository; + + public CodeScanAlertService( + CodeScanAlertMapper codeScanAlertMapper, + CodeScanAlertScanner codeScanAlertScanner, + CodeScanAlertRepository codeScanAlertRepository, + RepoRepository repoRepository) { + this.codeScanAlertMapper = codeScanAlertMapper; + this.codeScanAlertScanner = codeScanAlertScanner; + this.codeScanAlertRepository = codeScanAlertRepository; + this.repoRepository = repoRepository; + } + + public CodeScanAlertResponseDto create(CodeScanAlertRequestDto codeScanAlertRequestDto) { + CodeScanAlertEntity codeScanAlertEntity = codeScanAlertMapper.toCodeScanAlertEntity(codeScanAlertRequestDto); + + if (codeScanAlertEntity.getBranch() == null + || codeScanAlertEntity.getBranch().isBlank()) { + RepoEntity repoEntity = repoRepository + .findById(codeScanAlertRequestDto.getRepo()) + .orElseThrow(() -> new NotFoundException("Repo not found")); + + codeScanAlertEntity.setBranch(repoEntity.getDefaultBranch()); + } + codeScanAlertScanner.process(codeScanAlertEntity); + + CodeScanAlertEntity savedEntity = codeScanAlertRepository.save(codeScanAlertEntity); + return codeScanAlertMapper.toCodeScanAlertDto(savedEntity); + } +} diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/SecretFindingService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/SecretFindingService.java new file mode 100644 index 00000000..f9cc3f36 --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/SecretFindingService.java @@ -0,0 +1,41 @@ +package dev.cleat.scanning.service; + +import dev.cleat.common.dto.request.SecretFindingRequestDto; +import dev.cleat.common.dto.response.SecretFindingResponseDto; +import dev.cleat.common.exception.NotFoundException; +import dev.cleat.persistence.entity.RepoEntity; +import dev.cleat.persistence.entity.SecretFindingEntity; +import dev.cleat.persistence.mapper.SecretFindingMapper; +import dev.cleat.persistence.repository.RepoRepository; +import dev.cleat.scanning.SecretFindingScanner; +import org.springframework.stereotype.Service; + +@Service +public class SecretFindingService { + + private final SecretFindingMapper secretFindingMapper; + private final SecretFindingScanner secretScanner; + private final RepoRepository repoRepository; + + public SecretFindingService( + SecretFindingMapper secretFindingMapper, + SecretFindingScanner secretScanner, + RepoRepository repoRepository) { + this.secretFindingMapper = secretFindingMapper; + this.secretScanner = secretScanner; + this.repoRepository = repoRepository; + } + + public SecretFindingResponseDto create(SecretFindingRequestDto secretFindingRequestDto) { + + SecretFindingEntity secretFindingEntity = secretFindingMapper.toSecretFindingEntity(secretFindingRequestDto); + RepoEntity repoEntity = repoRepository + .findById(secretFindingRequestDto.getRepoId()) + .orElseThrow(() -> new NotFoundException("Repo not found")); + secretFindingEntity.setAccountId(secretFindingRequestDto.getAccountId()); + secretFindingEntity.setRepo(repoEntity); + secretScanner.process(secretFindingEntity); + + return secretFindingMapper.toSecretFindingDto(secretFindingEntity); + } +} diff --git a/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityService.java b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityService.java new file mode 100644 index 00000000..b0b0e95d --- /dev/null +++ b/backend/libs/scanning/src/main/java/dev/cleat/scanning/service/VulnerabilityService.java @@ -0,0 +1,48 @@ +package dev.cleat.scanning.service; + +import dev.cleat.common.dto.request.VulnerabilityRequestDto; +import dev.cleat.common.dto.response.VulnerabilityResponseDto; +import dev.cleat.common.exception.NotFoundException; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.mapper.VulnerabilityMapper; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import dev.cleat.scanning.VulnerabilityScanner; +import java.time.OffsetDateTime; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class VulnerabilityService { + + private final VulnerabilityMapper vulnerabilityMapper; + private final VulnerabilityScanner vulnerabilityScanner; + private final VulnerabilityRepository vulnerabilityRepository; + + public VulnerabilityService( + VulnerabilityMapper vulnerabilityMapper, + VulnerabilityScanner vulnerabilityScanner, + VulnerabilityRepository vulnerabilityRepository) { + this.vulnerabilityMapper = vulnerabilityMapper; + this.vulnerabilityScanner = vulnerabilityScanner; + this.vulnerabilityRepository = vulnerabilityRepository; + } + + @Transactional + public VulnerabilityResponseDto create(VulnerabilityRequestDto vulnerabilityRequestDto) { + VulnerabilityEntity vulnerabilityEntity = vulnerabilityMapper.toVulnerabilityEntity(vulnerabilityRequestDto); + if (vulnerabilityEntity.getPublishedAt() == null) { + vulnerabilityEntity.setPublishedAt(OffsetDateTime.now()); + } + vulnerabilityScanner.processAndSave(vulnerabilityEntity); + return vulnerabilityMapper.toVulnerabilityDto(vulnerabilityEntity); + } + + @Transactional(readOnly = true) + public VulnerabilityResponseDto findById(UUID id) { + return vulnerabilityRepository + .findById(id) + .map(vulnerabilityMapper::toVulnerabilityDto) + .orElseThrow(() -> new NotFoundException("Vulnerability not found")); + } +} diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/CodeScanAlertScannerTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/CodeScanAlertScannerTest.java new file mode 100644 index 00000000..bde870aa --- /dev/null +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/CodeScanAlertScannerTest.java @@ -0,0 +1,30 @@ +package dev.cleat.scanning; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Status; +import dev.cleat.persistence.entity.CodeScanAlertEntity; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class CodeScanAlertScannerTest { + + @InjectMocks + CodeScanAlertScanner codeScanAlertScanner; + + @Test + void whenProcessAlertThenStatusShouldBeOpen() { + + // given + CodeScanAlertEntity codeScanAlertEntity = new CodeScanAlertEntity().setSeverity(Severity.CRITICAL); + + // when + codeScanAlertScanner.process(codeScanAlertEntity); + + // then + Assertions.assertEquals(Status.OPEN, codeScanAlertEntity.getStatus()); + } +} diff --git a/backend/libs/scanning/src/test/java/dev/cleat/scanning/SecretFindingScannerTest.java b/backend/libs/scanning/src/test/java/dev/cleat/scanning/SecretFindingScannerTest.java new file mode 100644 index 00000000..8aa51253 --- /dev/null +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/SecretFindingScannerTest.java @@ -0,0 +1,59 @@ +package dev.cleat.scanning; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Validity; +import dev.cleat.persistence.entity.RepoEntity; +import dev.cleat.persistence.entity.SecretFindingEntity; +import dev.cleat.persistence.repository.SecretFindingRepository; +import org.junit.jupiter.api.Assertions; +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 SecretFindingScannerTest { + + @InjectMocks + private SecretFindingScanner secretFindingScanner; + + @Mock + private SecretFindingRepository secretFindingRepository; + + @Test + void givenSecretNotBlockedByPushProtectionWhenProcessingThenSeverityShouldBeCritical() { + + // given + SecretFindingEntity secretFindingEntity = new SecretFindingEntity().setPushProtectionBlocked(false); + RepoEntity repoEntity = new RepoEntity().setName("test-repo"); + secretFindingEntity.setRepo(repoEntity); + // when + secretFindingScanner.process(secretFindingEntity); + + // then + Assertions.assertEquals(Severity.CRITICAL, secretFindingEntity.getSeverity()); + Assertions.assertEquals(Validity.ACTIVE, secretFindingEntity.getValidity()); + verify(secretFindingRepository).save(any()); + } + + @Test + void givenSecretBlockedByPushProtectionWhenProcessingThenSeverityShouldBeLow() { + + // given + SecretFindingEntity secretFindingEntity = new SecretFindingEntity().setPushProtectionBlocked(true); + RepoEntity repoEntity = new RepoEntity().setName("test-repo"); + secretFindingEntity.setRepo(repoEntity); + + // when + secretFindingScanner.process(secretFindingEntity); + + // then + Assertions.assertEquals(Severity.LOW, secretFindingEntity.getSeverity()); + Assertions.assertEquals(Validity.REVOKED, secretFindingEntity.getValidity()); + verify(secretFindingRepository).save(any()); + } +} 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 new file mode 100644 index 00000000..f090187e --- /dev/null +++ b/backend/libs/scanning/src/test/java/dev/cleat/scanning/VulnerabilityScannerTest.java @@ -0,0 +1,67 @@ +package dev.cleat.scanning; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.cleat.common.enums.Priority; +import dev.cleat.common.enums.Severity; +import dev.cleat.domain.PriorityCalculator; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import java.util.UUID; +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 VulnerabilityScannerTest { + @InjectMocks + private VulnerabilityScanner vulnerabilityScanner; + + @Mock + private VulnerabilityRepository vulnerabilityRepository; + + @Mock + private PriorityCalculator priorityCalculator; + + @Test + void givenVulnerabilityWithKevAndHighCvssWhenProcessingThenPriorityShouldBeUrgent() { + // given + UUID uuid = UUID.randomUUID(); + VulnerabilityEntity vulnerabilityEntity = + new VulnerabilityEntity().setId(uuid).setKev(true).setCvss(9.5).setSeverity(Severity.HIGH); + + when(priorityCalculator.calculate(any())).thenReturn(Priority.URGENT); + + // when + vulnerabilityScanner.processAndSave(vulnerabilityEntity); + + // then + verify(vulnerabilityRepository, times(1)).save(vulnerabilityEntity); + assertNotNull(vulnerabilityEntity.getPriority(), "Priority should not be null"); + assertEquals( + Priority.URGENT, vulnerabilityEntity.getPriority(), "Priority should be URGENT for high CVSS and KEV"); + } + + @Test + void whenCalculatorThrowsExceptionThenScannerWrapsItInRuntimeException() { + + // given + VulnerabilityEntity vulnerabilityEntity = + new VulnerabilityEntity().setKev(true).setCvss(9.0).setSeverity(Severity.HIGH); + + when(priorityCalculator.calculate(any())).thenThrow(new IllegalArgumentException("Invalid CVSS")); + + // then + assertThrows(RuntimeException.class, () -> { + vulnerabilityScanner.processAndSave(vulnerabilityEntity); + }); + } +}