From d4acc948814c88d537e3e5b04ea20fc4d8c7fa30 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Sat, 4 Oct 2025 17:25:22 -0400 Subject: [PATCH 01/13] feat[backend](incident-resources): added incident resource controller logs --- .../rest/incident/UtmIncidentResource.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java index bb3126878..11cd41455 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java @@ -20,6 +20,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import com.park.utmstack.aop.logging.AuditEvent; import javax.validation.Valid; import java.net.URI; @@ -74,6 +75,12 @@ public UtmIncidentResource(UtmIncidentService utmIncidentService, * @throws IllegalArgumentException if the input data is invalid. */ @PostMapping("/utm-incidents") + @om.park.utmstack.aop.logging.AuditEvent( + attemptType = ApplicationEventType.INCIDENT_CREATION_ATTEMPT, + attemptMessage = "Attempt to create a new incident initiated", + successType = ApplicationEventType.INCIDENT_CREATION_SUCCESS, + successMessage = "Incident created successfully" + ) public ResponseEntity createUtmIncident(@Valid @RequestBody NewIncidentDTO newIncidentDTO) { final String ctx = ".createUtmIncident"; try { @@ -123,6 +130,12 @@ public ResponseEntity createUtmIncident(@Valid @RequestBody NewInci * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("/utm-incidents/add-alerts") + @AuditEvent( + attemptType = ApplicationEventType.INCIDENT_ALERT_ADD_ATTEMPT, + attemptMessage = "Attempt to add alerts to incident initiated", + successType = ApplicationEventType.INCIDENT_ALERT_ADD_SUCCESS, + successMessage = "Alerts added to incident successfully" + ) public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody AddToIncidentDTO addToIncidentDTO) throws URISyntaxException { final String ctx = ".addAlertsToUtmIncident"; try { @@ -165,6 +178,12 @@ public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody Ad * @throws URISyntaxException if the Location URI syntax is incorrect */ @PutMapping("/utm-incidents/change-status") + @AuditEvent( + attemptType = ApplicationEventType.INCIDENT_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update incident status initiated", + successType = ApplicationEventType.INCIDENT_UPDATE_SUCCESS, + successMessage = "Incident status updated successfully" + ) public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmIncident utmIncident) throws URISyntaxException { final String ctx = ".updateUtmIncident"; try { From 066c47421cb9720d387c645335d86d6d3a0bad3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Mon, 6 Oct 2025 09:34:11 -0400 Subject: [PATCH 02/13] removed wrong AutditEvent path --- .../park/utmstack/web/rest/incident/UtmIncidentResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java index 11cd41455..9f10bf6a1 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java @@ -75,7 +75,7 @@ public UtmIncidentResource(UtmIncidentService utmIncidentService, * @throws IllegalArgumentException if the input data is invalid. */ @PostMapping("/utm-incidents") - @om.park.utmstack.aop.logging.AuditEvent( + @AuditEvent( attemptType = ApplicationEventType.INCIDENT_CREATION_ATTEMPT, attemptMessage = "Attempt to create a new incident initiated", successType = ApplicationEventType.INCIDENT_CREATION_SUCCESS, From 9bbadee20469bc7f6dfa2eb1858632d7a3457972 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 10:16:29 -0500 Subject: [PATCH 03/13] feat(logging): add MdcCleanupFilter for improved logging context management --- .../park/utmstack/config/LoggingConfiguration.java | 12 ++++++++++++ .../park/utmstack/config/SecurityConfiguration.java | 9 --------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java b/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java index 75b9a14d6..03a9aa9cb 100644 --- a/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java @@ -3,8 +3,11 @@ import ch.qos.logback.classic.LoggerContext; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.park.utmstack.loggin.filter.MdcCleanupFilter; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import tech.jhipster.config.JHipsterProperties; @@ -45,4 +48,13 @@ public LoggingConfiguration( addContextListener(context, customFields, loggingProperties); } } + + @Bean + public FilterRegistrationBean mdcCleanupFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new MdcCleanupFilter()); + registrationBean.setOrder(Integer.MAX_VALUE); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } } diff --git a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java index ef197e6e5..8f8a4ce6a 100644 --- a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java @@ -75,15 +75,6 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - public FilterRegistrationBean mdcCleanupFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new MdcCleanupFilter()); - registrationBean.setOrder(Integer.MAX_VALUE); - registrationBean.addUrlPatterns("/*"); - return registrationBean; - } - @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring() From 7e78db5fdb0752e755df195a7904e4e79998d402 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 10:34:29 -0500 Subject: [PATCH 04/13] feat(tfa): implement TFA enrollment resource and service for user authentication --- .../UtmConfigurationParameterService.java | 4 +- .../dto/tfa/enroll/TfaEnrollRequest.java | 13 +++++ .../service/tfa/TfaEnrollmentService.java | 54 +++++++++++++++++++ .../service/tfa/TfaMethodService.java | 2 +- .../park/utmstack/service/tfa/TfaService.java | 2 +- .../web/rest/tfa/TfaEnrollmentResource.java | 38 +++++++++++++ .../{TfaController.java => TfaResource.java} | 8 +-- 7 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java create mode 100644 backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java create mode 100644 backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java rename backend/src/main/java/com/park/utmstack/web/rest/tfa/{TfaController.java => TfaResource.java} (97%) diff --git a/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java b/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java index c90e5e6c1..48ceb04ff 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java @@ -162,13 +162,13 @@ public Map getValueMapForDateSetting() throws Exception { } } - public List getConfigParameterBySectionId(long sectionId) throws Exception { + public List getConfigParameterBySectionId(long sectionId) { final String ctx = CLASSNAME + ".getConfigParameterBySectionId"; try { return new ArrayList<>(configParamRepository .findAllBySectionId(sectionId)); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java new file mode 100644 index 000000000..7eeb0604d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java @@ -0,0 +1,13 @@ +package com.park.utmstack.service.dto.tfa.enroll; + +import com.park.utmstack.domain.tfa.TfaMethod; +import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; +import lombok.Data; + +@Data +public class TfaEnrollRequest { + private TfaMethod initMethod; + private TfaMethod verifyMethod; + private String verifyCode; + private TfaSaveRequest completeRequest; +} diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java new file mode 100644 index 000000000..c6d01dcff --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java @@ -0,0 +1,54 @@ +package com.park.utmstack.service.tfa; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.User; +import com.park.utmstack.domain.UtmConfigurationParameter; +import com.park.utmstack.service.UtmConfigurationParameterService; +import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; +import com.park.utmstack.service.dto.tfa.verify.TfaVerifyRequest; +import com.park.utmstack.util.exceptions.TfaVerificationException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; + +@Service +@RequiredArgsConstructor +public class TfaEnrollmentService { + + private final TfaService tfaService; + private final UtmConfigurationParameterService utmConfigurationParameterService; + + public void enrollTfa(User user, TfaEnrollRequest request) { + // --- INIT --- + if (request.getInitMethod() != null) { + tfaService.initiateSetup(user, request.getInitMethod()); + } + + // --- VERIFY --- + if (request.getVerifyMethod() != null && request.getVerifyCode() != null) { + var verifyRequest = new TfaVerifyRequest(request.getVerifyMethod(), request.getVerifyCode()); + var verifyResponse = tfaService.verifyCode(user, verifyRequest); + if (!verifyResponse.isValid()) { + throw new TfaVerificationException("Código TFA inválido: " + verifyResponse.getMessage()); + } + } + + // --- COMPLETE --- + if (request.getCompleteRequest() != null) { + List tfaParams = utmConfigurationParameterService.getConfigParameterBySectionId(Constants.TFA_SETTING_ID); + var complete = request.getCompleteRequest(); + for (UtmConfigurationParameter param : tfaParams) { + switch (param.getConfParamShort()) { + case PROP_TFA_METHOD -> param.setConfParamValue(String.valueOf(complete.getMethod())); + case Constants.PROP_TFA_ENABLE -> param.setConfParamValue(String.valueOf(complete.isEnable())); + } + } + tfaService.persistConfiguration(complete.getMethod()); + utmConfigurationParameterService.saveAllConfigParams(tfaParams); + tfaService.generateChallenge(user); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java index 1a55398ee..ac00cfe06 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java @@ -12,7 +12,7 @@ public interface TfaMethodService { TfaVerifyResponse verifyCode(User use, String code); - void persistConfiguration(User use) throws Exception; + void persistConfiguration(User use); void generateChallenge(User user); diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java index b11ea69c2..5e2c722f5 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java @@ -36,7 +36,7 @@ public TfaVerifyResponse verifyCode(User user, TfaVerifyRequest request) { return selected.verifyCode(user, request.getCode()); } - public void persistConfiguration(TfaMethod method) throws Exception { + public void persistConfiguration(TfaMethod method) { User user = userService.getCurrentUserLogin(); TfaMethodService selected = getMethodService(method); selected.persistConfiguration(user); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java new file mode 100644 index 000000000..1d2362f3d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java @@ -0,0 +1,38 @@ +package com.park.utmstack.web.rest.tfa; + +import com.park.utmstack.domain.User; +import com.park.utmstack.service.UserService; +import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; +import com.park.utmstack.service.tfa.TfaEnrollmentService; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 +@RequiredArgsConstructor +@Slf4j +@Hidden +@RequestMapping("api/tfa") +public class TfaEnrollmentResource { + + private static final String CLASSNAME = "TfaEnrollmentController"; + + private final UserService userService; + private final TfaEnrollmentService tfaEnrollmentService; + + + @PostMapping("/enroll") + public ResponseEntity enrollTfa(@RequestBody TfaEnrollRequest request) { + final String ctx = CLASSNAME + ".enrollTfa"; + User user = userService.getCurrentUserLogin(); + + tfaEnrollmentService.enrollTfa(user, request); + return ResponseEntity.ok().build(); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java similarity index 97% rename from backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java rename to backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java index d0cc00a0d..b4c0ddb2b 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java @@ -23,12 +23,9 @@ import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.exceptions.TfaVerificationException; import com.park.utmstack.util.exceptions.UtmMailException; -import com.park.utmstack.web.rest.util.HeaderUtil; import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -38,7 +35,6 @@ import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; @@ -46,8 +42,9 @@ @RestController @RequiredArgsConstructor @Slf4j +@Hidden @RequestMapping("api/tfa") -public class TfaController { +public class TfaResource { private static final String CLASSNAME = "TfaController"; @@ -90,7 +87,6 @@ public ResponseEntity verifyTfa(@RequestBody TfaVerifyRequest } @GetMapping("/generate-challenge") - @Hidden public ResponseEntity generateChallenge() { final String ctx = CLASSNAME + ".generateChallenge"; try { From e610bffeee1d6a07083a4759ffe0b49a7006dc5b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 11:45:36 -0500 Subject: [PATCH 05/13] feat(event-types): expand ApplicationEventType with incident-related events --- .../domain/application_events/enums/ApplicationEventType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 8e6397939..1b5c0fa3f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -34,5 +34,5 @@ public enum ApplicationEventType { ERROR, WARNING, INFO, - UNDEFINED + INCIDENT_CREATION_ATTEMPT, INCIDENT_CREATION_SUCCESS, INCIDENT_ALERT_ADD_ATTEMPT, INCIDENT_ALERT_ADD_SUCCESS, INCIDENT_UPDATE_ATTEMPT, INCIDENT_UPDATE_SUCCESS, UNDEFINED } From 6b8aac97a23ccc46e49a867b4cbbc294e71aef6e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 14:37:19 -0500 Subject: [PATCH 06/13] feat(tfa): implement TFA enrollment stages and exception handling --- .../park/utmstack/domain/tfa/TfaStage.java | 7 +++ .../dto/tfa/enroll/TfaEnrollRequest.java | 14 +++-- .../service/tfa/TfaEnrollmentService.java | 54 ------------------ .../exceptions/InvalidTfaStageException.java | 8 +++ .../web/rest/tfa/TfaEnrollmentResource.java | 55 ++++++++++++++++--- 5 files changed, 73 insertions(+), 65 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java delete mode 100644 backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java diff --git a/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java new file mode 100644 index 000000000..1bd87893f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java @@ -0,0 +1,7 @@ +package com.park.utmstack.domain.tfa; + +public enum TfaStage { + INIT, + VERIFY, + COMPLETE +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java index 7eeb0604d..b331422c3 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java @@ -1,13 +1,19 @@ package com.park.utmstack.service.dto.tfa.enroll; import com.park.utmstack.domain.tfa.TfaMethod; +import com.park.utmstack.domain.tfa.TfaStage; import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; +import com.park.utmstack.service.dto.tfa.verify.TfaVerifyRequest; import lombok.Data; @Data public class TfaEnrollRequest { - private TfaMethod initMethod; - private TfaMethod verifyMethod; - private String verifyCode; - private TfaSaveRequest completeRequest; + private TfaStage stage; + private TfaMethod method; + private String code; + private boolean enable; + + public TfaVerifyRequest toVerifyRequest() { + return new TfaVerifyRequest(method, code); + } } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java deleted file mode 100644 index c6d01dcff..000000000 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaEnrollmentService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.park.utmstack.service.tfa; - -import com.park.utmstack.config.Constants; -import com.park.utmstack.domain.User; -import com.park.utmstack.domain.UtmConfigurationParameter; -import com.park.utmstack.service.UtmConfigurationParameterService; -import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; -import com.park.utmstack.service.dto.tfa.verify.TfaVerifyRequest; -import com.park.utmstack.util.exceptions.TfaVerificationException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; - -import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; - -@Service -@RequiredArgsConstructor -public class TfaEnrollmentService { - - private final TfaService tfaService; - private final UtmConfigurationParameterService utmConfigurationParameterService; - - public void enrollTfa(User user, TfaEnrollRequest request) { - // --- INIT --- - if (request.getInitMethod() != null) { - tfaService.initiateSetup(user, request.getInitMethod()); - } - - // --- VERIFY --- - if (request.getVerifyMethod() != null && request.getVerifyCode() != null) { - var verifyRequest = new TfaVerifyRequest(request.getVerifyMethod(), request.getVerifyCode()); - var verifyResponse = tfaService.verifyCode(user, verifyRequest); - if (!verifyResponse.isValid()) { - throw new TfaVerificationException("Código TFA inválido: " + verifyResponse.getMessage()); - } - } - - // --- COMPLETE --- - if (request.getCompleteRequest() != null) { - List tfaParams = utmConfigurationParameterService.getConfigParameterBySectionId(Constants.TFA_SETTING_ID); - var complete = request.getCompleteRequest(); - for (UtmConfigurationParameter param : tfaParams) { - switch (param.getConfParamShort()) { - case PROP_TFA_METHOD -> param.setConfParamValue(String.valueOf(complete.getMethod())); - case Constants.PROP_TFA_ENABLE -> param.setConfParamValue(String.valueOf(complete.isEnable())); - } - } - tfaService.persistConfiguration(complete.getMethod()); - utmConfigurationParameterService.saveAllConfigParams(tfaParams); - tfaService.generateChallenge(user); - } - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java new file mode 100644 index 000000000..207554ca9 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java @@ -0,0 +1,8 @@ +package com.park.utmstack.util.exceptions; + +public class InvalidTfaStageException extends RuntimeException { + public InvalidTfaStageException(String message) { + super(message); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java index 1d2362f3d..94f6e36cb 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java @@ -1,9 +1,17 @@ package com.park.utmstack.web.rest.tfa; +import com.park.utmstack.config.Constants; import com.park.utmstack.domain.User; +import com.park.utmstack.domain.UtmConfigurationParameter; import com.park.utmstack.service.UserService; +import com.park.utmstack.service.UtmConfigurationParameterService; import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; -import com.park.utmstack.service.tfa.TfaEnrollmentService; +import com.park.utmstack.service.dto.tfa.init.TfaInitResponse; +import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; +import com.park.utmstack.service.dto.tfa.verify.TfaVerifyResponse; +import com.park.utmstack.service.tfa.TfaService; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.InvalidTfaStageException; import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,26 +21,59 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + +import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; + @RestController @RequiredArgsConstructor @Slf4j @Hidden -@RequestMapping("api/tfa") +@RequestMapping("api/enrollment/tfa") public class TfaEnrollmentResource { private static final String CLASSNAME = "TfaEnrollmentController"; private final UserService userService; - private final TfaEnrollmentService tfaEnrollmentService; + private final TfaService tfaService; + private final UtmConfigurationParameterService utmConfigurationParameterService; - @PostMapping("/enroll") + @PostMapping public ResponseEntity enrollTfa(@RequestBody TfaEnrollRequest request) { - final String ctx = CLASSNAME + ".enrollTfa"; User user = userService.getCurrentUserLogin(); - tfaEnrollmentService.enrollTfa(user, request); - return ResponseEntity.ok().build(); + return switch (request.getStage()) { + case INIT -> { + TfaInitResponse initResponse = tfaService.initiateSetup(user, request.getMethod()); + yield ResponseEntity.ok(initResponse); + } + case VERIFY -> { + TfaVerifyResponse verifyResponse = tfaService.verifyCode(user, request.toVerifyRequest()); + yield ResponseEntity.ok(verifyResponse); + } + case COMPLETE -> { + List tfaParams = utmConfigurationParameterService + .getConfigParameterBySectionId(Constants.TFA_SETTING_ID); + + for (UtmConfigurationParameter param : tfaParams) { + switch (param.getConfParamShort()) { + case PROP_TFA_METHOD: + param.setConfParamValue(String.valueOf(request.getMethod())); + break; + case Constants.PROP_TFA_ENABLE: + param.setConfParamValue(String.valueOf(request.isEnable())); + break; + } + } + + tfaService.persistConfiguration(request.getMethod()); + utmConfigurationParameterService.saveAllConfigParams(tfaParams); + tfaService.generateChallenge(user); + yield ResponseEntity.ok().build(); + } + default -> throw new InvalidTfaStageException("Invalid TFA stage: " + request.getStage()); + }; } } From e9e34422d0b77747c06c23b2ba57eacc6edbd233 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 7 Oct 2025 10:43:02 -0500 Subject: [PATCH 07/13] feat(tfa): update TFA expiration constants and enhance challenge generation --- .../com/park/utmstack/config/Constants.java | 4 +-- .../config/SecurityConfiguration.java | 5 ++- .../park/utmstack/service/UserService.java | 2 ++ .../service/dto/jwt/LoginResponseDTO.java | 1 + .../utmstack/service/tfa/EmailTfaService.java | 9 ++++-- .../service/tfa/EmailTotpService.java | 6 ++-- .../service/tfa/TfaMethodService.java | 2 ++ .../park/utmstack/service/tfa/TfaService.java | 4 ++- .../utmstack/service/tfa/TotpTfaService.java | 13 +++++--- .../utmstack/web/rest/UserJWTController.java | 4 ++- .../web/rest/tfa/TfaEnrollmentResource.java | 32 +++++++++++++++++-- .../utmstack/web/rest/tfa/TfaResource.java | 4 +-- 12 files changed, 68 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 8757f1260..b1f771850 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -36,8 +36,8 @@ public final class Constants { public static final String PROP_NETWORK_SCAN_API_URL = "utmstack.networkScan.apiUrl"; public static final String PROP_TFA_ENABLE = "utmstack.tfa.enable"; public static final String PROP_TFA_METHOD = "utmstack.tfa.method"; - public static final int EXPIRES_IN_SECONDS = 30; - public static final int INIT_EXPIRES_IN_SECONDS = 300; + public static final int EXPIRES_IN_SECONDS_TOTP = 30; // Google Authenticator + public static final int EXPIRES_IN_SECONDS_EMAIL = 120; // Email OTP public static final String TFA_ISSUER = "UTMStack"; // ---------------------------------------------------------------------------------- diff --git a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java index 8f8a4ce6a..9fe019ace 100644 --- a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java @@ -110,7 +110,10 @@ public void configure(HttpSecurity http) throws Exception { .antMatchers("/api/account/reset-password/init").permitAll() .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/images/all").permitAll() - .antMatchers("/api/tfa/**").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) + .antMatchers("/api/enrollment/**").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER) + .antMatchers("/api/tfa/verify-code").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN) + .antMatchers("/api/tfa/refresh").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN) + .antMatchers("/api/tfa/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) .antMatchers("/api/utm-incident-jobs").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/api/utm-incident-jobs/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/api/utm-incident-variables/**").hasAuthority(AuthoritiesConstants.ADMIN) diff --git a/backend/src/main/java/com/park/utmstack/service/UserService.java b/backend/src/main/java/com/park/utmstack/service/UserService.java index 578548dd7..ed1a6379b 100644 --- a/backend/src/main/java/com/park/utmstack/service/UserService.java +++ b/backend/src/main/java/com/park/utmstack/service/UserService.java @@ -218,6 +218,8 @@ public void updateUserTfaSecret(String userLogin, String tfaSecret, String tfaMe .orElseThrow(() -> new NoSuchElementException(String.format("User %1$s not found", userLogin))); user.setTfaMethod(tfaMethod); user.setTfaSecret(tfaSecret); + + userRepository.save(user); } public void deleteUser(String login) { diff --git a/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java index 69572ee49..c1a6594c2 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java @@ -13,5 +13,6 @@ public class LoginResponseDTO { private boolean forceTfa; private String method; private String token; + private long tfaExpiresInSeconds; } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java index 18d106da1..6136ff00c 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java @@ -92,7 +92,7 @@ public void generateChallenge(User user) { String secret = user.getTfaSecret(); String code = tfaService.generateCode(secret); - TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + Constants.EXPIRES_IN_SECONDS * 1000 * 10); + TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + (Constants.EXPIRES_IN_SECONDS_EMAIL * 4) * 1000 * 10); cache.storeState(user.getLogin(), TfaMethod.EMAIL, state); mailService.sendTfaVerificationCode(user, code); @@ -109,12 +109,17 @@ public void regenerateChallenge(User user) { throw new TooManyRequestsException("Challenge request too soon. Please wait " + state.getCooldownRemainingSeconds() + " seconds."); } - state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS)); + state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_EMAIL)); state.markChallengeRequested(); mailService.sendTfaVerificationCode(user, tfaService.generateCode(state.getSecret())); cache.storeState(user.getLogin(), TfaMethod.EMAIL, state); } + + @Override + public long expirationTimeSeconds() { + return Constants.EXPIRES_IN_SECONDS_EMAIL; + } } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java index fa1abd795..1e4b358a5 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import static com.park.utmstack.config.Constants.EXPIRES_IN_SECONDS; +import static com.park.utmstack.config.Constants.EXPIRES_IN_SECONDS_EMAIL; @Service public class EmailTotpService { @@ -38,7 +38,7 @@ public String generateCode(String secret) { try { Assert.hasText(secret, "Secret value is missing"); CodeGenerator codeGenerator = new DefaultCodeGenerator(); - return codeGenerator.generate(secret, Math.floorDiv(timeProvider.getTime(), EXPIRES_IN_SECONDS)); + return codeGenerator.generate(secret, Math.floorDiv(timeProvider.getTime(), EXPIRES_IN_SECONDS_EMAIL)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } @@ -57,7 +57,7 @@ public boolean validateCode(String secret, String code) { Assert.hasText(code, "Code value is missing"); DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); - verifier.setTimePeriod(EXPIRES_IN_SECONDS); + verifier.setTimePeriod(EXPIRES_IN_SECONDS_EMAIL); verifier.setAllowedTimePeriodDiscrepancy(1); return verifier.isValidCode(secret, code); diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java index ac00cfe06..d93ed9eeb 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java @@ -18,5 +18,7 @@ public interface TfaMethodService { void regenerateChallenge(User user); + long expirationTimeSeconds(); + } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java index 5e2c722f5..8fd6868f0 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java @@ -42,12 +42,14 @@ public void persistConfiguration(TfaMethod method) { selected.persistConfiguration(user); } - public void generateChallenge(User user) { + public long generateChallenge(User user) { TfaMethod method = TfaMethod.valueOf(user.getTfaMethod()); TfaMethodService selected = getMethodService(method); selected.generateChallenge(user); + + return selected.expirationTimeSeconds(); } public void regenerateChallenge(User user) { diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java index 8c21a2c22..06f458b7f 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java @@ -47,7 +47,7 @@ public TfaMethod getMethod() { @Loggable public TfaInitResponse initiateSetup(User user) { String secret = authenticator.createCredentials().getKey(); - long expiresAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS); + long expiresAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP * 10); TfaSetupState state = new TfaSetupState(secret, expiresAt); cache.storeState(user.getLogin(), TfaMethod.TOTP, state); @@ -57,7 +57,7 @@ public TfaInitResponse initiateSetup(User user) { String qrBase64 = generateQrBase64(uri); Delivery delivery = new Delivery(TfaMethod.TOTP, qrBase64); - return new TfaInitResponse("pending", delivery, Constants.EXPIRES_IN_SECONDS * 10); + return new TfaInitResponse("pending", delivery, Constants.EXPIRES_IN_SECONDS_TOTP * 10); } @Override @@ -92,7 +92,7 @@ public void persistConfiguration(User user) { public void generateChallenge(User user) { cache.clear(user.getLogin(), TfaMethod.TOTP); String secret = user.getTfaSecret(); - TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS)); + TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP)); cache.storeState(user.getLogin(), TfaMethod.TOTP, state); } @@ -107,7 +107,7 @@ public void regenerateChallenge(User user) { throw new TooManyRequestsException("Challenge request too soon. Please wait " + state.getCooldownRemainingSeconds() + " seconds."); } - state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS)); + state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP)); state.markChallengeRequested(); cache.storeState(user.getLogin(), TfaMethod.TOTP, state); @@ -126,6 +126,11 @@ private String generateQrBase64(String uri) { } } + @Override + public long expirationTimeSeconds() { + return Constants.EXPIRES_IN_SECONDS_TOTP; + } + } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java index 4e90eec61..0744540ef 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java @@ -94,9 +94,10 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM lo boolean isTfaSetup = isTfaEnabled && user.getTfaMethod() != null && !user.getTfaMethod().isEmpty() && !isAuth; Map args = logContextBuilder.buildArgs(request); + Long tfaExpiresInSeconds = 0L; if (isTfaSetup) { - tfaService.generateChallenge(user); + tfaExpiresInSeconds = tfaService.generateChallenge(user); args.put("tfaMethod", user.getTfaMethod()); applicationEventService.createEvent( @@ -118,6 +119,7 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM lo .success(true) .tfaConfigured(isTfaSetup) .forceTfa(!isAuth) + .tfaExpiresInSeconds(tfaExpiresInSeconds) .build(); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java index 94f6e36cb..99139e4d9 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java @@ -1,10 +1,13 @@ package com.park.utmstack.web.rest.tfa; import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.Authority; import com.park.utmstack.domain.User; import com.park.utmstack.domain.UtmConfigurationParameter; +import com.park.utmstack.security.jwt.TokenProvider; import com.park.utmstack.service.UserService; import com.park.utmstack.service.UtmConfigurationParameterService; +import com.park.utmstack.service.dto.jwt.LoginResponseDTO; import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; import com.park.utmstack.service.dto.tfa.init.TfaInitResponse; import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; @@ -16,12 +19,15 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; import java.util.List; +import java.util.stream.Collectors; import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; @@ -37,6 +43,7 @@ public class TfaEnrollmentResource { private final UserService userService; private final TfaService tfaService; private final UtmConfigurationParameterService utmConfigurationParameterService; + private final TokenProvider tokenProvider; @PostMapping @@ -69,8 +76,29 @@ public ResponseEntity enrollTfa(@RequestBody TfaEnrollRequest request) { tfaService.persistConfiguration(request.getMethod()); utmConfigurationParameterService.saveAllConfigParams(tfaParams); - tfaService.generateChallenge(user); - yield ResponseEntity.ok().build(); + List authorities = user.getAuthorities().stream() + .map(Authority::getName) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + org.springframework.security.core.userdetails.User principal = + new org.springframework.security.core.userdetails.User(user.getLogin(), "", authorities); + + UsernamePasswordAuthenticationToken fullAuth = + new UsernamePasswordAuthenticationToken(principal, "", authorities); + + + String fullToken = tokenProvider.createToken(fullAuth, false, true ); + + LoginResponseDTO response = LoginResponseDTO.builder() + .token(fullToken) + .method(user.getTfaMethod()) + .success(true) + .tfaConfigured(true) + .forceTfa(true) + .build(); + + yield ResponseEntity.ok(response); } default -> throw new InvalidTfaStageException("Invalid TFA stage: " + request.getStage()); }; diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java index b4c0ddb2b..03d612f7f 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java @@ -86,7 +86,7 @@ public ResponseEntity verifyTfa(@RequestBody TfaVerifyRequest } } - @GetMapping("/generate-challenge") + @GetMapping("/refresh") public ResponseEntity generateChallenge() { final String ctx = CLASSNAME + ".generateChallenge"; try { @@ -148,7 +148,7 @@ public ResponseEntity completeTfa(@RequestBody TfaSaveRequest request) { successType = ApplicationEventType.AUTH_SUCCESS, successMessage = "Login successfully completed" ) - @PostMapping("/verifyCode") + @PostMapping("/verify-code") public ResponseEntity verifyCode(@RequestBody String code, HttpServletRequest request) { final String ctx = CLASSNAME + ".verifyCode"; User user = userService.getCurrentUserLogin(); From 42b08332593e69f03b165c39d10263ce4b9b532b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Sat, 4 Oct 2025 14:43:55 -0500 Subject: [PATCH 08/13] feat(alert-echoes-timeline): rename component and integrate new timeline service --- .../alert-view/alert-view.component.html | 4 +- .../shared/alert-management-shared.module.ts | 4 +- .../alert-echoes-timeline.component.html} | 1 + .../alert-echoes-timeline.component.scss} | 2 +- .../alert-echoes-timeline.component.ts | 194 ++++++++++ .../alert-echoes-timeline.service.ts | 193 ++++++++++ .../utm-timeline/timeline.service.ts | 46 --- .../utm-timeline/utm-timeline.component.ts | 350 ------------------ frontend/src/app/shared/utm-shared.module.ts | 6 +- 9 files changed, 396 insertions(+), 404 deletions(-) rename frontend/src/app/data-management/alert-management/shared/components/{utm-timeline/utm-timeline.component.html => alert-echoes-timeline/alert-echoes-timeline.component.html} (95%) rename frontend/src/app/data-management/alert-management/shared/components/{utm-timeline/utm-timeline.component.scss => alert-echoes-timeline/alert-echoes-timeline.component.scss} (97%) create mode 100644 frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts create mode 100644 frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts delete mode 100644 frontend/src/app/data-management/alert-management/shared/components/utm-timeline/timeline.service.ts delete mode 100644 frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.ts diff --git a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html index 47bf33b6d..efe393837 100644 --- a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html +++ b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html @@ -240,12 +240,12 @@
- - + diff --git a/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts b/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts index 40a4cce6a..c98748668 100644 --- a/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts +++ b/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts @@ -61,7 +61,7 @@ import {RowToFiltersComponent} from './components/filters/row-to-filter/row-to-f import {StatusFilterComponent} from './components/filters/status-filter/status-filter.component'; import { AlertActionSelectComponent } from './components/alert-action-select/alert-action-select.component'; import { AlertChildColumnComponent } from './components/alert-child-column/alert-child-column.component'; -import {TimelineService} from "./components/utm-timeline/timeline.service"; +import {AlertEchoesTimelineService} from "./components/alert-echoes-timeline/alert-echoes-timeline.service"; @NgModule({ declarations: [ @@ -187,7 +187,7 @@ import {TimelineService} from "./components/utm-timeline/timeline.service"; InlineSVGModule, ], providers: [ - TimelineService + AlertEchoesTimelineService ], }) export class AlertManagementSharedModule { diff --git a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html similarity index 95% rename from frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.html rename to frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html index 8b275e016..be09b391e 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html @@ -2,6 +2,7 @@
diff --git a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss similarity index 97% rename from frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.scss rename to frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss index 9b971cac4..489d37791 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.scss +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss @@ -21,7 +21,7 @@ .timeline-chart { width: 100%; - height: 400px; + height: 500px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts new file mode 100644 index 000000000..250ac28ef --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts @@ -0,0 +1,194 @@ +import {HttpResponse} from '@angular/common/http'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; +import { + ALERT_PARENT_ID, + ALERT_STATUS_FIELD_AUTO, + ALERT_TAGS_FIELD, ALERT_TIMESTAMP_FIELD, FALSE_POSITIVE_OBJECT +} from '../../../../../shared/constants/alert/alert-field.constant'; +import {AUTOMATIC_REVIEW} from '../../../../../shared/constants/alert/alert-status.constant'; +import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; +import {DataNatureTypeEnum} from '../../../../../shared/enums/nature-data.enum'; +import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; +import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {ElasticFilterType} from '../../../../../shared/types/filter/elastic-filter.type'; +import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; +import {sanitizeFilters} from '../../../../../shared/util/elastic-filter.util'; +import {AlertEchoesTimelineService, TimelineGroup} from './alert-echoes-timeline.service'; + + +@Component({ + selector: 'app-alert-echoes-timeline', + templateUrl: './alert-echoes-timeline.component.html', + styleUrls: ['./alert-echoes-timeline.component.scss'] +}) +export class AlertEchoesTimelineComponent implements OnInit { + + @Input() alert: UtmAlertType; + @Input() page = 0; + @Input() pageSize = 100; + @Input() total = 0; + @Input() title = ''; + @Output() itemClick = new EventEmitter(); + + chartInstance: any; + + sortBy = ALERT_TIMESTAMP_FIELD + ',desc'; + alerts: UtmAlertType[] = []; + filters: ElasticFilterType[] = [ + {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, + {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, + ]; + loading = false; + chartOption: any = {}; + intervalMs = 60 * 1000; + groups: TimelineGroup[] = []; + readonly Math = Math; + + ngOnInit(): void { + this.filters.push({ + field: ALERT_PARENT_ID, + operator: ElasticOperatorsEnum.IS, + value: this.alert.id + }); + this.loadData(); + } + + constructor(private timelineService: AlertEchoesTimelineService, + private elasticDataService: ElasticDataService, + private utmToastService: UtmToastService, ) { + } + + onChartInit(ec: any) { + this.chartInstance = ec; + } + + refreshChart() { + if (this.chartInstance && this.chartOption) { + this.chartInstance.clear(); // limpia todo el canvas + this.chartInstance.setOption(this.chartOption, true); // redibuja + } + } + + buildChart() { + + const items = this.timelineService.buildTimelineFromAlerts(this.alerts); + this.groups = this.timelineService.generateTimelineGroups(this.alerts, this.intervalMs); + + const seriesData = []; + const cardHeight = 60; + const spacing = 10; + + + this.groups.forEach((group, index) => { + const timestamps = group.items.map(i => new Date(i.startDate).getTime()); + group.startTimestamp = Math.floor(timestamps.reduce((sum, t) => sum + t, 0) / timestamps.length); + + const rep = group.items[0] || ({} as any); // representative item + seriesData.push({ + value: [ + group.startTimestamp, // 0: timestamp (start of minute) + 0, // 1: y coordinate (not used) + rep.name || `Echoes`, // 2: representative name/title + new Date(group.startTimestamp).toISOString(), // 3: formatted minute + rep.iconUrl || 'assets/images/default-echo.png', // 4: icon url + group.items.length, // 5: count of echoes + index, + group.yOffset || 0 + ], + groupData: group.items, // full list for drill-down + }); + }); + + + const allTimestamps = items.map(i => new Date(i.startDate).getTime()); + const minTimestamp = Math.min(...allTimestamps); + const maxTimestamp = Math.max(...allTimestamps); + const padding = (maxTimestamp - minTimestamp) * 0.1; + + this.chartOption = { + title: {text: this.title, left: 'center', textStyle: {fontSize: 16, fontWeight: 'bold'}}, + tooltip: { + trigger: 'item', + formatter: (params: any) => + `Echoes: ${params.data.value[2]}
Minute: ${params.data.value[3]}
Total: ${params.data.value[5]}` + }, + grid: { + left: 0, + right: 0, + top: 0, + bottom: 20, + containLabel: true + }, + xAxis: { + type: 'time', + min: minTimestamp - padding, + max: maxTimestamp + padding, + axisLabel: {formatter: (val: number) => new Date(val).toLocaleTimeString()}, + splitLine: { + show: true, + lineStyle: { + type: 'dashed', + color: '#ccc', + width: 1 + } + } + }, + yAxis: { + type: 'value', + min: 0, + max: (cardHeight + spacing) * this.groups.length + 100, + show: false + }, + dataZoom: [ + {type: 'slider', xAxisIndex: 0, start: 0, end: 100}, + {type: 'inside', xAxisIndex: 0, zoomLock: false} + ], + series: [ + { + type: 'custom', + data: seriesData, + renderItem: (params: any, api: any) => this.timelineService.renderItem(params, api), + encode: {x: 0, y: 1} + } + ] + }; + } + + onChartClick(event: any) { + if (event.data && event.data.itemData) { + this.itemClick.emit(event.data.itemData); + } + } + + loadData() { + this.loading = true; + this.elasticDataService.search(this.page, this.pageSize, + 100000000, DataNatureTypeEnum.ALERT, + sanitizeFilters(this.filters), this.sortBy, true) + .subscribe( + (res: HttpResponse) => { + this.total = Number(res.headers.get('X-Total-Count')); + this.alerts = res.body; + this.loading = false; + this.buildChart(); + this.refreshChart(); + }, + (res: HttpResponse) => { + this.utmToastService.showError('Error', 'An error occurred while listing the alerts. Please try again later.'); + this.loading = false; + } + ); + } + + prevPage() { + this.page = this.page - 1; + this.loadData(); + } + + nextPage() { + this.page = this.page + 1; + this.loadData(); + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts new file mode 100644 index 000000000..a720ee6e4 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@angular/core'; +import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; + +export interface TimelineGroup { + startTimestamp: number; + items: TimelineItem[]; + yOffset?: number; +} + +const cardWidth = 240; +const cardHeight = 62; +const baseOffset = 80; +const spacing = 10; + + +@Injectable() +export class AlertEchoesTimelineService { + + renderItem(params: any, api: any) { + const ts = api.value(0); + const coord = api.coord([ts, 0]); + const chartWidth = params.coordSys.width; + api.getHeight(); + +// Calculate horizontal position (centered, but respecting canvas borders) + let x = coord[0] - cardWidth / 2; + x = Math.max(0, Math.min(x, chartWidth - cardWidth)); + + // Calculate vertical stacking offset + const level = api.value(7) || 0; // stack level + const levelOffset = level * (cardHeight + spacing); + let yCard = coord[1] - baseOffset - levelOffset - cardHeight; + + // Ensure card stays within the canvas + if (yCard < 0) { yCard = 0; } + + // Build children (card + text + line) + const children = [ + // Card background + { + type: 'rect', + shape: { x, y: yCard, width: cardWidth, height: cardHeight, r: 10 }, + style: { + fill: '#ffffff', + stroke: '#0277bd', + lineWidth: 1, + shadowBlur: 8, + shadowColor: 'rgba(0,0,0,0.2)', + cursor: 'pointer' + } + }, + // Icon + { + type: 'image', + style: { + image: api.value(4), + x: x + 5, + y: yCard + 5, + width: cardHeight - 10, + height: cardHeight - 10 + } + }, + // Title + { + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 5, + text: this.truncateText(api.value(2) || '', 150), + textAlign: 'left', + fill: '#000', + fontSize: 14, + fontWeight: 600, + width: cardWidth - (cardHeight - 2.5) - 25, + overflow: 'break', + ellipsis: '...' + } + }, + // Subtitle / date + { + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 25, + text: api.value(3), + textAlign: 'left', + fill: '#666', + fontSize: 12 + } + } + ]; + + // Total echoes + if (api.value(5) > 1) { + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + cardHeight - 18, + text: `Total: ${api.value(5)} echoes`, + textAlign: 'left', + fill: '#444', + fontSize: 12, + fontWeight: 'bold' + } + }); + } + + // Line connecting to timeline + children.push({ + type: 'line', + shape: { + x1: coord[0], + y1: yCard + cardHeight, + x2: coord[0], + y2: coord[1] + }, + style: { + stroke: '#0277bd', + lineWidth: 1.5 + } + }); + + return { type: 'group', children }; + } + + + private groupByInterval(items: TimelineItem[], intervalMs: number): TimelineGroup[] { + const sorted = [...items].sort( + (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + const groups: TimelineGroup[] = []; + let currentGroup: TimelineGroup = null; + + sorted.forEach(item => { + const ts = new Date(item.startDate).getTime(); + if (!currentGroup || ts > currentGroup.startTimestamp + intervalMs) { + currentGroup = { startTimestamp: ts, items: [] }; + groups.push(currentGroup); + } + currentGroup.items.push(item); + + if (ts > currentGroup.startTimestamp) { + currentGroup.startTimestamp = ts; + } + }); + + return groups; + } + + truncateText(text: string, maxWidth: number) { + const avgCharWidth = 7; + const maxChars = Math.floor(maxWidth / avgCharWidth); + return text.length > maxChars ? text.substring(0, maxChars - 3) + '...' : text; + } + + buildTimelineFromAlerts(alerts: UtmAlertType[]): TimelineItem[] { + return alerts.map(cha => ({ + startDate: cha['@timestamp'], + name: cha.name, + metadata: cha, + iconUrl: 'assets/icons/echoes/echoes_default.png' + })); + } + + private assignYOffsetToGroups(groups: TimelineGroup[]): TimelineGroup[] { + const FIVE_MINUTE = 5 * 60 * 1000; + const sorted = [...groups].sort((a, b) => a.startTimestamp - b.startTimestamp); + + for (let i = 0; i < sorted.length; i++) { + const group = sorted[i]; + group.yOffset = 0; // default base + + for (let j = 0; j < i; j++) { + const prev = sorted[j]; + const dx = group.startTimestamp - prev.startTimestamp; + if (dx < FIVE_MINUTE) { + group.yOffset = Math.max(group.yOffset, (prev.yOffset || 0) + 1); + } + } + } + + return sorted; + } + + generateTimelineGroups(alerts: UtmAlertType[], intervalMs: number): TimelineGroup[] { + const items = this.buildTimelineFromAlerts(alerts); + const groups = this.groupByInterval(items, intervalMs); + return this.assignYOffsetToGroups(groups); + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/timeline.service.ts b/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/timeline.service.ts deleted file mode 100644 index 66c158197..000000000 --- a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/timeline.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable } from '@angular/core'; -import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; - -export interface TimelineGroup { - startTimestamp: number; - items: TimelineItem[]; - yOffset?: number; -} - -@Injectable() -export class TimelineService { - - /** - * Groups timeline items by a fixed time interval (milliseconds) - */ - groupByInterval(items: TimelineItem[], intervalMs: number): TimelineGroup[] { - const sorted = [...items].sort( - (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() - ); - const groups: TimelineGroup[] = []; - let currentGroup: TimelineGroup = null; - - sorted.forEach(item => { - const ts = new Date(item.startDate).getTime(); - if (!currentGroup || ts > currentGroup.startTimestamp + intervalMs) { - currentGroup = { startTimestamp: ts, items: [] }; - groups.push(currentGroup); - } - currentGroup.items.push(item); - - if (ts > currentGroup.startTimestamp) { - currentGroup.startTimestamp = ts; - } - }); - - return groups; - } - - /** - * Simple pagination of timeline groups - */ - paginate(groups: TimelineGroup[], page: number, pageSize: number): TimelineGroup[] { - const start = page * pageSize; - return groups.slice(start, start + pageSize); - } -} diff --git a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.ts b/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.ts deleted file mode 100644 index 614e52dda..000000000 --- a/frontend/src/app/data-management/alert-management/shared/components/utm-timeline/utm-timeline.component.ts +++ /dev/null @@ -1,350 +0,0 @@ -import {group} from '@angular/animations'; -import {HttpResponse} from '@angular/common/http'; -import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; -import {text} from '@fortawesome/fontawesome-svg-core'; -import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; -import { - ALERT_PARENT_ID, - ALERT_STATUS_FIELD_AUTO, - ALERT_TAGS_FIELD, ALERT_TIMESTAMP_FIELD, FALSE_POSITIVE_OBJECT -} from '../../../../../shared/constants/alert/alert-field.constant'; -import {AUTOMATIC_REVIEW} from '../../../../../shared/constants/alert/alert-status.constant'; -import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; -import {DataNatureTypeEnum} from '../../../../../shared/enums/nature-data.enum'; -import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; -import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; -import {ElasticFilterType} from '../../../../../shared/types/filter/elastic-filter.type'; -import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; -import {sanitizeFilters} from '../../../../../shared/util/elastic-filter.util'; -import {TimelineGroup, TimelineService} from './timeline.service'; - - -@Component({ - selector: 'utm-timeline', - templateUrl: './utm-timeline.component.html', - styleUrls: ['./utm-timeline.component.scss'] -}) -export class UtmTimeLineComponent implements OnInit { - - @Input() alert: UtmAlertType; - @Input() page = 0; - @Input() pageSize = 100; - @Input() total = 0; - @Input() title = ''; - @Output() itemClick = new EventEmitter(); - - - sortBy = ALERT_TIMESTAMP_FIELD + ',desc'; - alerts: UtmAlertType[] = []; - filters: ElasticFilterType[] = [ - {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, - {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, - ]; - loading = false; - chartOption: any = {}; - intervalMs = 60 * 1000; - groups: TimelineGroup[] = []; - readonly Math = Math; - - ngOnInit(): void { - this.filters.push({ - field: ALERT_PARENT_ID, - operator: ElasticOperatorsEnum.IS, - value: this.alert.id - }); - this.loadData(); - } - - constructor(private timelineService: TimelineService, - private elasticDataService: ElasticDataService, - private utmToastService: UtmToastService,) { - } - - buildChart() { - const items = this.buildTimelineFromAlerts(); - const groups = this.timelineService.groupByInterval(items, this.intervalMs); - this.groups = this.assignYOffsetToGroups(groups); - /*console.log(this.groups.map(g => ({ ts: g.startTimestamp, yOffset: g.yOffset })));*/ - const seriesData = []; - - /*const cardWidth = 250; - const cardHeight = 60; - const spacing = 10;*/ - - const cardWidth = 250; - const cardHeight = 60; - const spacing = 10; - const baseOffset = 80; - - this.groups.forEach((group, index) => { - const timestamps = group.items.map(i => new Date(i.startDate).getTime()); - group.startTimestamp = Math.floor(timestamps.reduce((sum, t) => sum + t, 0) / timestamps.length); - - const rep = group.items[0] || ({} as any); // representative item - seriesData.push({ - value: [ - group.startTimestamp, // 0: timestamp (start of minute) - 0, // 1: y coordinate (not used) - rep.name || `Echoes`, // 2: representative name/title - new Date(group.startTimestamp).toISOString(), // 3: formatted minute - rep.iconUrl || 'assets/images/default-echo.png', // 4: icon url - group.items.length, // 5: count of echoes - index, - group.yOffset || 0 - ], - groupData: group.items, // full list for drill-down - }); - }); - - - const allTimestamps = items.map(i => new Date(i.startDate).getTime()); - const minTimestamp = Math.min(...allTimestamps); - const maxTimestamp = Math.max(...allTimestamps); - const padding = (maxTimestamp - minTimestamp) * 0.1; - - this.chartOption = { - title: {text: this.title, left: 'center', textStyle: {fontSize: 16, fontWeight: 'bold'}}, - tooltip: { - trigger: 'item', - formatter: (params: any) => - `Echoes: ${params.data.value[2]}
Minute: ${params.data.value[3]}
Total: ${params.data.value[5]}` - }, - grid: { - left: 0, - right: 0, - top: 0, - bottom: 20, - containLabel: true - }, - xAxis: { - type: 'time', - min: minTimestamp - padding, - max: maxTimestamp + padding, - axisLabel: {formatter: (val: number) => new Date(val).toLocaleTimeString()}, - splitLine: { - show: true, - lineStyle: { - type: 'dashed', - color: '#ccc', - width: 1 - } - } - }, - yAxis: { - type: 'value', - min: 0, - max: (cardHeight + spacing) * this.groups.length + 100, - show: false - }, - dataZoom: [ - {type: 'slider', xAxisIndex: 0, start: 0, end: 100}, - {type: 'inside', xAxisIndex: 0, zoomLock: false} - ], - series: [ - { - type: 'custom', - data: seriesData, - renderItem: (params: any, api: any) => { - /*const ts = api.value(0); - const coord = api.coord([ts, 0]); - const chartWidth = params.coordSys.width; - const offsetIndex = api.value(6) || 0; - const totalOffset = (cardHeight + spacing) * offsetIndex + 80; - const level = api.value(7) || 0; - const cardOffset = level * (cardHeight + spacing) + 80; - - let x = coord[0] - cardWidth / 2; - x = Math.max(0, Math.min(x, chartWidth - cardWidth));*/ - - const ts = api.value(0); - const coord = api.coord([ts, 0]); - const chartWidth = params.coordSys.width; - - // center x for card, clamped inside chart - let x = coord[0] - cardWidth / 2; - x = Math.max(0, Math.min(x, chartWidth - cardWidth)); - - const level = api.value(7) || 0; // stackLevel - const levelPx = level * (cardHeight + spacing); - const totalOffset = baseOffset + levelPx; - - return { - type: 'group', - children: this.buildChildren(api, coord, x, totalOffset, cardWidth, cardHeight) - }; - }, - encode: {x: 0, y: 1} - } - ] - }; - } - - onChartClick(event: any) { - if (event.data && event.data.itemData) { - this.itemClick.emit(event.data.itemData); - } - } - - truncateText(text: string, maxWidth: number) { - const avgCharWidth = 7; - const maxChars = Math.floor(maxWidth / avgCharWidth); - return text.length > maxChars ? text.substring(0, maxChars - 3) + '...' : text; - } - - loadData() { - this.loading = true; - this.elasticDataService.search(this.page, this.pageSize, - 100000000, DataNatureTypeEnum.ALERT, - sanitizeFilters(this.filters), this.sortBy, true) - .subscribe( - (res: HttpResponse) => { - this.total = Number(res.headers.get('X-Total-Count')); - this.alerts = res.body; - this.loading = false; - this.buildChart(); - }, - (res: HttpResponse) => { - this.utmToastService.showError('Error', 'An error occurred while listing the alerts. Please try again later.'); - this.loading = false; - } - ); - } - - prevPage() { - this.page = this.page - 1; - this.loadData(); - } - - nextPage() { - this.page = this.page + 1; - this.loadData(); - } - - buildTimelineFromAlerts(): TimelineItem[] { - return this.alerts.map(cha => ({ - startDate: cha['@timestamp'], - name: cha.name, - metadata: cha, - iconUrl: 'assets/icons/echoes/echoes_default.png' - })); - } - - private buildChildren(api: any, coord: number[], x: number, totalOffset: number, cardWidth: number, cardHeight: number): any[] { - const children: any[] = [ - // Background card - { - type: 'rect', - shape: { - x, - y: coord[1] - totalOffset - cardHeight, - width: cardWidth, - height: cardHeight, - r: 10 - }, - style: { - fill: '#ffffff', - stroke: '#0277bd', - lineWidth: 1, - shadowBlur: 8, - shadowColor: 'rgba(0,0,0,0.2)', - cursor: 'pointer' - } - }, - - // Icon - { - type: 'image', - style: { - image: api.value(4), - x: x + 5, - y: coord[1] - totalOffset - cardHeight + 5, - width: cardHeight - 10, - height: cardHeight - 10 - } - }, - - // Title - { - type: 'text', - style: { - x: x + (cardHeight - 2.5) + 15, - y: coord[1] - totalOffset - cardHeight + 5, - text: this.truncateText(api.value(2) || '', 150), - textAlign: 'left', - fill: '#000', - fontSize: 14, - fontWeight: 600, - width: cardWidth - (cardHeight - 2.5) - 25, - overflow: 'break', - ellipsis: '...' - } - }, - - // Formatted minute/date - { - type: 'text', - style: { - x: x + (cardHeight - 2.5) + 15, - y: coord[1] - totalOffset - cardHeight + 25, - text: api.value(3), - textAlign: 'left', - fill: '#666', - fontSize: 12 - } - } - ]; - - // Total echoes (solo si > 1) - if (api.value(5) > 1) { - children.push({ - type: 'text', - style: { - x: x + (cardHeight - 2.5) + 15, - y: coord[1] - totalOffset - cardHeight + 42, - text: `Total: ${api.value(5)} echoes`, - textAlign: 'left', - fill: '#444', - fontSize: 12, - fontWeight: 'bold' - } - }); - } - - // Connector line: conecta desde la base (coord[1]) hasta la tarjeta real (coord[1] - totalOffset) - children.push({ - type: 'line', - shape: { - x1: coord[0], - y1: coord[1] - totalOffset, - x2: coord[0], - y2: coord[1] - }, - style: { - stroke: '#0277bd', - lineWidth: 1.5 - } - }); - - return children; - } - - private assignYOffsetToGroups(groups: TimelineGroup[]): TimelineGroup[] { - const FIVE_MINUTE = 5 * 60 * 1000; - const sorted = [...groups].sort((a, b) => a.startTimestamp - b.startTimestamp); - - for (let i = 0; i < sorted.length; i++) { - const group = sorted[i]; - group.yOffset = 0; // default base - - for (let j = 0; j < i; j++) { - const prev = sorted[j]; - const dx = group.startTimestamp - prev.startTimestamp; - if (dx < FIVE_MINUTE) { - group.yOffset = Math.max(group.yOffset, (prev.yOffset || 0) + 1); - } - } - } - - return sorted; - } -} diff --git a/frontend/src/app/shared/utm-shared.module.ts b/frontend/src/app/shared/utm-shared.module.ts index 989b54961..287c78621 100644 --- a/frontend/src/app/shared/utm-shared.module.ts +++ b/frontend/src/app/shared/utm-shared.module.ts @@ -28,7 +28,7 @@ import {PasswordStrengthBarComponent} from './components/auth/password-strength/ import { TfaSetupComponent } from './components/auth/tfa-setup/tfa-setup.component'; import {TotpComponent} from './components/auth/totp/totp.component'; import {ContactUsComponent} from './components/contact-us/contact-us.component'; -import {UtmTimeLineComponent} from '../data-management/alert-management/shared/components/utm-timeline/utm-timeline.component'; +import {AlertEchoesTimelineComponent} from '../data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component'; import { EmailSettingNotificactionComponent } from './components/email-setting-notification/email-setting-notificaction.component'; @@ -270,7 +270,7 @@ import { NgxEchartsModule } from 'ngx-echarts'; HasAnyAuthorityDirective, NotFoundComponent, SidebarComponent, - UtmTimeLineComponent, + AlertEchoesTimelineComponent, PasswordResetInitComponent, PasswordResetFinishComponent, PasswordStrengthBarComponent, @@ -410,7 +410,7 @@ import { NgxEchartsModule } from 'ngx-echarts'; SidebarComponent, PasswordResetInitComponent, PasswordResetFinishComponent, - UtmTimeLineComponent, + AlertEchoesTimelineComponent, PasswordStrengthBarComponent, SortByComponent, UtmSpinnerComponent, From 67d04d857afe2959933152674677939ade63a4cf Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 09:54:39 -0500 Subject: [PATCH 09/13] feat(alert-echoes-timeline): improve timestamp handling and enhance tooltip formatting --- .../alert-view/alert-view.component.html | 2 +- .../alert-echoes-timeline.component.ts | 19 ++- .../alert-echoes-timeline.service.ts | 123 +++++++++--------- 3 files changed, 82 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html index efe393837..32b09f620 100644 --- a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html +++ b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html @@ -239,7 +239,7 @@
- + new Date(val).toLocaleTimeString()}, + min: minTimestamp - expand, + max: maxTimestamp + expand, + axisLabel: { + formatter: (val: number) => { + const d = new Date(val); + const year = d.getUTCFullYear(); + const month = (d.getUTCMonth() + 1).toString().padStart(2, '0'); + const day = d.getUTCDate().toString().padStart(2, '0'); + const hours = d.getUTCHours().toString().padStart(2, '0'); + const minutes = d.getUTCMinutes().toString().padStart(2, '0'); + const seconds = d.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }, + }, splitLine: { show: true, lineStyle: { diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts index a720ee6e4..a35546e89 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts @@ -19,77 +19,84 @@ export class AlertEchoesTimelineService { renderItem(params: any, api: any) { const ts = api.value(0); - const coord = api.coord([ts, 0]); + const coord = api.coord([ts, 0]) as number[]; const chartWidth = params.coordSys.width; api.getHeight(); -// Calculate horizontal position (centered, but respecting canvas borders) + // Horizontal position (centered, respecting canvas borders) let x = coord[0] - cardWidth / 2; x = Math.max(0, Math.min(x, chartWidth - cardWidth)); - // Calculate vertical stacking offset + // Vertical stacking offset const level = api.value(7) || 0; // stack level const levelOffset = level * (cardHeight + spacing); let yCard = coord[1] - baseOffset - levelOffset - cardHeight; - // Ensure card stays within the canvas + // Ensure card stays within canvas if (yCard < 0) { yCard = 0; } - // Build children (card + text + line) - const children = [ - // Card background - { - type: 'rect', - shape: { x, y: yCard, width: cardWidth, height: cardHeight, r: 10 }, - style: { - fill: '#ffffff', - stroke: '#0277bd', - lineWidth: 1, - shadowBlur: 8, - shadowColor: 'rgba(0,0,0,0.2)', - cursor: 'pointer' - } - }, - // Icon - { - type: 'image', - style: { - image: api.value(4), - x: x + 5, - y: yCard + 5, - width: cardHeight - 10, - height: cardHeight - 10 - } - }, - // Title - { - type: 'text', - style: { - x: x + (cardHeight - 2.5) + 15, - y: yCard + 5, - text: this.truncateText(api.value(2) || '', 150), - textAlign: 'left', - fill: '#000', - fontSize: 14, - fontWeight: 600, - width: cardWidth - (cardHeight - 2.5) - 25, - overflow: 'break', - ellipsis: '...' - } - }, - // Subtitle / date - { - type: 'text', - style: { - x: x + (cardHeight - 2.5) + 15, - y: yCard + 25, - text: api.value(3), - textAlign: 'left', - fill: '#666', - fontSize: 12 - } + // Generic type for children to satisfy TS + const children: { + type: string; + shape?: Record; + style?: Record; + }[] = []; + + // Card background + children.push({ + type: 'rect', + shape: { x, y: yCard, width: cardWidth, height: cardHeight, r: 10 }, + style: { + fill: '#ffffff', + stroke: '#0277bd', + lineWidth: 1, + shadowBlur: 8, + shadowColor: 'rgba(0,0,0,0.2)', + cursor: 'pointer' + } + }); + + // Icon + children.push({ + type: 'image', + style: { + image: api.value(4), + x: x + 5, + y: yCard + 5, + width: cardHeight - 10, + height: cardHeight - 10 + } + }); + + // Title + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 5, + text: this.truncateText(api.value(2) || '', 150), + textAlign: 'left', + fill: '#000', + fontSize: 14, + fontWeight: 600, + width: cardWidth - (cardHeight - 2.5) - 25, + overflow: 'break', + ellipsis: '...' } - ]; + }); + + // Subtitle / date + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 25, + text: api.value(3), + textAlign: 'left', + fill: '#666', + fontSize: 12 + } + }); // Total echoes if (api.value(5) > 1) { From 8ef92710bdbc66b70bdca2cd4232a3f61222c663 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 6 Oct 2025 10:08:04 -0500 Subject: [PATCH 10/13] feat(alert-echoes-timeline): improve timestamp handling and enhance tooltip formatting --- .../alert-echoes-timeline.component.ts | 8 +++++--- .../alert-view-detail/alert-view-detail.component.ts | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts index 0bb078fa0..a3d73ca80 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts @@ -85,6 +85,7 @@ export class AlertEchoesTimelineComponent implements OnInit { group.startTimestamp = Math.floor(timestamps.reduce((sum, t) => sum + t, 0) / timestamps.length); const rep = group.items[0] || ({} as any); // representative item + console.log('group', group, rep); seriesData.push({ value: [ group.startTimestamp, // 0: timestamp (start of minute) @@ -94,7 +95,8 @@ export class AlertEchoesTimelineComponent implements OnInit { rep.iconUrl || 'assets/images/default-echo.png', // 4: icon url group.items.length, // 5: count of echoes index, - group.yOffset || 0 + group.yOffset || 0, + rep.metadata ], groupData: group.items, // full list for drill-down }); @@ -169,8 +171,8 @@ export class AlertEchoesTimelineComponent implements OnInit { } onChartClick(event: any) { - if (event.data && event.data.itemData) { - this.itemClick.emit(event.data.itemData); + if (event.data && event.data.value) { + this.itemClick.emit(event.data.value[8] || {} as UtmAlertType); } } diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts index cab558a4f..c34e96eb6 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts @@ -16,8 +16,6 @@ import { ALERT_SEVERITY_FIELD_LABEL, ALERT_STATUS_FIELD, ALERT_TACTIC_FIELD, ALERT_TECHNIQUE_FIELD, ALERT_TIMESTAMP_FIELD } from '../../../../../shared/constants/alert/alert-field.constant'; -import {LOG_ANALYZER_TOTAL_ITEMS} from '../../../../../shared/constants/log-analyzer.constant'; -import {ITEMS_PER_PAGE} from '../../../../../shared/constants/pagination.constants'; import {IncidentOriginTypeEnum} from '../../../../../shared/enums/incident-response/incident-origin-type.enum'; import {AlertTags} from '../../../../../shared/types/alert/alert-tag.type'; import {AlertStatusEnum, UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; From 2123f531729842a18e5f2838d964f37111615a37 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 7 Oct 2025 10:56:36 -0500 Subject: [PATCH 11/13] feat(incident): add IncidentAlertConflictException and handle alert linking conflicts --- .../advice/GlobalExceptionHandler.java | 6 +++++ .../service/incident/UtmIncidentService.java | 22 +++++++++++++++++++ .../IncidentAlertConflictException.java | 7 ++++++ .../rest/incident/UtmIncidentResource.java | 12 ---------- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java index 8f81d7326..cada1ccb0 100644 --- a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import com.park.utmstack.security.TooMuchLoginAttemptsException; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.IncidentAlertConflictException; import com.park.utmstack.util.exceptions.TfaVerificationException; import com.park.utmstack.util.exceptions.TooManyRequestsException; import lombok.RequiredArgsConstructor; @@ -49,6 +50,11 @@ public ResponseEntity handleTooManyRequests(TooManyRequestsException e, HttpS return ResponseUtil.buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS, e.getMessage()); } + @ExceptionHandler(IncidentAlertConflictException.class) + public ResponseEntity handleConflict(IncidentAlertConflictException e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception e, HttpServletRequest request) { return ResponseUtil.buildInternalServerErrorResponse(e.getMessage()); diff --git a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java index b56146a8a..8c23e2c5c 100644 --- a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java +++ b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java @@ -17,10 +17,13 @@ import com.park.utmstack.service.dto.incident.NewIncidentDTO; import com.park.utmstack.service.dto.incident.RelatedIncidentAlertsDTO; import com.park.utmstack.service.incident.util.ResolveIncidentStatus; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.IncidentAlertConflictException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -140,6 +143,8 @@ public UtmIncident changeStatus(UtmIncident utmIncident) { public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { final String ctx = CLASSNAME + ".createIncident"; try { + validateAlertsNotAlreadyLinked(newIncidentDTO.getAlertList(), ctx); + UtmIncident utmIncident = new UtmIncident(); utmIncident.setIncidentName(newIncidentDTO.getIncidentName()); utmIncident.setIncidentDescription(newIncidentDTO.getIncidentDescription()); @@ -177,6 +182,8 @@ public UtmIncident addAlertsIncident(@Valid AddToIncidentDTO addToIncidentDTO) { final String ctx = CLASSNAME + ".addAlertsIncident"; try { log.debug("Request to add alert to UtmIncident : {}", addToIncidentDTO); + + validateAlertsNotAlreadyLinked(addToIncidentDTO.getAlertList(), ctx); UtmIncident utmIncident = utmIncidentRepository.findById(addToIncidentDTO.getIncidentId()).orElseThrow(() -> new RuntimeException(ctx + ": Incident not found")); saveRelatedAlerts(addToIncidentDTO.getAlertList(), utmIncident.getId()); String historyMessage = String.format("New %d alerts added to incident", addToIncidentDTO.getAlertList().size()); @@ -284,4 +291,19 @@ private void sendIncidentsEmail(List alertIds, UtmIncident utmIncident) eventService.createEvent(msg, ApplicationEventType.ERROR); } } + + private void validateAlertsNotAlreadyLinked(List alertList, String ctx) { + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .collect(Collectors.toList()); + + List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); + + if (!alertsFound.isEmpty()) { + String alertIdsList = String.join(", ", alertIds); + String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; + + throw new IncidentAlertConflictException(ctx + ": " + msg); + } + } } diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java new file mode 100644 index 000000000..c5aa47586 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class IncidentAlertConflictException extends RuntimeException { + public IncidentAlertConflictException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java index 9f10bf6a1..b6c43ff5a 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java @@ -143,19 +143,7 @@ public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody Ad if (CollectionUtils.isEmpty(addToIncidentDTO.getAlertList())) { throw new BadRequestAlertException("Add utmIncident cannot already have an empty related alerts", ENTITY_NAME, "alertList"); } - List alertIds = addToIncidentDTO.getAlertList().stream() - .map(RelatedIncidentAlertsDTO::getAlertId) - .collect(Collectors.toList()); - - List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); - if (!alertsFound.isEmpty()) { - String alertIdsList = String.join(", ", alertIds); - String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; - log.error(msg); - applicationEventService.createEvent(ctx + ": " + msg , ApplicationEventType.ERROR); - return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, utmIncidentAlertService.formatAlertMessage(alertsFound)); - } UtmIncident result = utmIncidentService.addAlertsIncident(addToIncidentDTO); return ResponseEntity.created(new URI("/api/utm-incidents/add-alerts" + result.getId())) .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString())) From 43ac06c20c553f676d91268c642a6bc18692d1be Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 7 Oct 2025 12:59:53 -0500 Subject: [PATCH 12/13] feat(incident): add NoAlertsProvidedException and enhance incident creation error handling --- .../advice/GlobalExceptionHandler.java | 6 + .../enums/ApplicationEventType.java | 10 +- .../service/impl/UtmAlertServiceImpl.java | 3 +- .../service/incident/UtmIncidentService.java | 36 ++++- .../exceptions/NoAlertsProvidedException.java | 7 + .../rest/incident/UtmIncidentResource.java | 149 +++++------------- 6 files changed, 89 insertions(+), 122 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java index cada1ccb0..937bdde86 100644 --- a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.exceptions.IncidentAlertConflictException; +import com.park.utmstack.util.exceptions.NoAlertsProvidedException; import com.park.utmstack.util.exceptions.TfaVerificationException; import com.park.utmstack.util.exceptions.TooManyRequestsException; import lombok.RequiredArgsConstructor; @@ -50,6 +51,11 @@ public ResponseEntity handleTooManyRequests(TooManyRequestsException e, HttpS return ResponseUtil.buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS, e.getMessage()); } + @ExceptionHandler({NoAlertsProvidedException.class}) + public ResponseEntity handleNoAlertsProvided(Exception e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()); + } + @ExceptionHandler(IncidentAlertConflictException.class) public ResponseEntity handleConflict(IncidentAlertConflictException e, HttpServletRequest request) { return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 1b5c0fa3f..91c5c8a19 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -31,8 +31,16 @@ public enum ApplicationEventType { CONFIG_GROUP_BULK_DELETE_SUCCESS, CONFIG_UPDATE_ATTEMPT, CONFIG_UPDATE_SUCCESS, + INCIDENT_CREATION_ATTEMPT, + INCIDENT_CREATED, + INCIDENT_CREATION_SUCCESS, + INCIDENT_ALERTS_ADDED, + INCIDENT_ALERT_ADD_ATTEMPT, + INCIDENT_ALERT_ADD_SUCCESS, + INCIDENT_UPDATE_ATTEMPT, + INCIDENT_UPDATE_SUCCESS, ERROR, WARNING, INFO, - INCIDENT_CREATION_ATTEMPT, INCIDENT_CREATION_SUCCESS, INCIDENT_ALERT_ADD_ATTEMPT, INCIDENT_ALERT_ADD_SUCCESS, INCIDENT_UPDATE_ATTEMPT, INCIDENT_UPDATE_SUCCESS, UNDEFINED + UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java b/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java index ed80a37f3..c3ef3f13a 100644 --- a/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java +++ b/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java @@ -169,7 +169,8 @@ public void updateStatus(List alertIds, int status, String statusObserva String alertsIds = String.join(",", alertIds); Map extra = Map.of( "alertIds", alertsIds, - "newStatus", status + "newStatus", status, + "source", "service" ); String attemptMsg = String.format("Attempt to update status to %1$s for alerts with ids: %2$s", diff --git a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java index 8c23e2c5c..a22a46c56 100644 --- a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java +++ b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java @@ -17,23 +17,19 @@ import com.park.utmstack.service.dto.incident.NewIncidentDTO; import com.park.utmstack.service.dto.incident.RelatedIncidentAlertsDTO; import com.park.utmstack.service.incident.util.ResolveIncidentStatus; -import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.enums.AlertStatus; import com.park.utmstack.util.exceptions.IncidentAlertConflictException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import javax.validation.Valid; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; /** @@ -79,7 +75,6 @@ public UtmIncidentService(UtmIncidentRepository utmIncidentRepository, */ public UtmIncident save(UtmIncident utmIncident) { final String ctx = ".save"; - log.debug("Request to save UtmIncident : {}", utmIncident); try { return utmIncidentRepository.save(utmIncident); } catch (Exception e) { @@ -143,6 +138,17 @@ public UtmIncident changeStatus(UtmIncident utmIncident) { public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { final String ctx = CLASSNAME + ".createIncident"; try { + List alertIds = newIncidentDTO.getAlertList(); + + String alertsIds = alertIds.stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.joining(",")); + Map extra = Map.of( + "alertIds", alertsIds, + "source", "service" + ); + String attemptMsg = String.format("Attempt to create incident with %d alerts", newIncidentDTO.getAlertList().size()); + + eventService.createEvent(attemptMsg, ApplicationEventType.INCIDENT_CREATION_ATTEMPT, extra); + validateAlertsNotAlreadyLinked(newIncidentDTO.getAlertList(), ctx); UtmIncident utmIncident = new UtmIncident(); @@ -164,6 +170,8 @@ public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { String historyMessage = String.format("Incident created with %d alerts", newIncidentDTO.getAlertList().size()); utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_CREATED, savedIncident.getId(), "Incident Created", historyMessage); + eventService.createEvent(historyMessage, ApplicationEventType.INCIDENT_CREATED, extra); + return savedIncident; } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); @@ -183,11 +191,24 @@ public UtmIncident addAlertsIncident(@Valid AddToIncidentDTO addToIncidentDTO) { try { log.debug("Request to add alert to UtmIncident : {}", addToIncidentDTO); + List alertIds = addToIncidentDTO.getAlertList(); + + String alertsIds = alertIds.stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.joining(",")); + Map extra = Map.of( + "alertIds", alertsIds, + "source", "service" + ); + String attemptMsg = String.format("Attempt to add %d alerts to incident %d", addToIncidentDTO.getAlertList().size(), addToIncidentDTO.getIncidentId()); + eventService.createEvent(attemptMsg, ApplicationEventType.INCIDENT_ALERT_ADD_ATTEMPT, extra); + validateAlertsNotAlreadyLinked(addToIncidentDTO.getAlertList(), ctx); UtmIncident utmIncident = utmIncidentRepository.findById(addToIncidentDTO.getIncidentId()).orElseThrow(() -> new RuntimeException(ctx + ": Incident not found")); saveRelatedAlerts(addToIncidentDTO.getAlertList(), utmIncident.getId()); String historyMessage = String.format("New %d alerts added to incident", addToIncidentDTO.getAlertList().size()); utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_ALERT_ADD, utmIncident.getId(), "New alerts added to incident", historyMessage); + + eventService.createEvent(historyMessage, ApplicationEventType.INCIDENT_ALERTS_ADDED, extra); + return utmIncident; } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); @@ -293,6 +314,7 @@ private void sendIncidentsEmail(List alertIds, UtmIncident utmIncident) } private void validateAlertsNotAlreadyLinked(List alertList, String ctx) { + List alertIds = alertList.stream() .map(RelatedIncidentAlertsDTO::getAlertId) .collect(Collectors.toList()); diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java new file mode 100644 index 000000000..9f8c7b22e --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class NoAlertsProvidedException extends RuntimeException { + public NoAlertsProvidedException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java index b6c43ff5a..f7b02d189 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java @@ -2,21 +2,18 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.incident.UtmIncident; -import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.incident.*; -import com.park.utmstack.service.incident.UtmIncidentAlertService; import com.park.utmstack.service.incident.UtmIncidentQueryService; import com.park.utmstack.service.incident.UtmIncidentService; -import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.NoAlertsProvidedException; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; @@ -27,35 +24,22 @@ import java.net.URISyntaxException; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; /** * REST controller for managing UtmIncident. */ @RestController +@RequiredArgsConstructor +@Slf4j @RequestMapping("/api") public class UtmIncidentResource { - private final String CLASS_NAME = "UtmIncidentResource"; - private final Logger log = LoggerFactory.getLogger(UtmIncidentResource.class); private static final String ENTITY_NAME = "utmIncident"; private final UtmIncidentService utmIncidentService; - private final UtmIncidentAlertService utmIncidentAlertService; - private final UtmIncidentQueryService utmIncidentQueryService; - private final ApplicationEventService applicationEventService; - - public UtmIncidentResource(UtmIncidentService utmIncidentService, - UtmIncidentAlertService utmIncidentAlertService, - UtmIncidentQueryService utmIncidentQueryService, - ApplicationEventService applicationEventService) { - this.utmIncidentService = utmIncidentService; - this.utmIncidentAlertService = utmIncidentAlertService; - this.utmIncidentQueryService = utmIncidentQueryService; - this.applicationEventService = applicationEventService; - } + /** * Creates a new incident based on the provided details. @@ -83,36 +67,13 @@ public UtmIncidentResource(UtmIncidentService utmIncidentService, ) public ResponseEntity createUtmIncident(@Valid @RequestBody NewIncidentDTO newIncidentDTO) { final String ctx = ".createUtmIncident"; - try { - if (CollectionUtils.isEmpty(newIncidentDTO.getAlertList())) { - String msg = ctx + ": A new incident has to have at least one alert related"; - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); - } - - List alertIds = newIncidentDTO.getAlertList().stream() - .map(RelatedIncidentAlertsDTO::getAlertId) - .collect(Collectors.toList()); - - List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); - - if (!alertsFound.isEmpty()) { - String alertIdsList = String.join(", ", alertIds); - String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; - log.error(msg); - applicationEventService.createEvent(ctx + ": " + msg , ApplicationEventType.ERROR); - return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, utmIncidentAlertService.formatAlertMessage(alertsFound)); - } - - - return ResponseEntity.ok(utmIncidentService.createIncident(newIncidentDTO)); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + + if (CollectionUtils.isEmpty(newIncidentDTO.getAlertList())) { + String msg = ctx + ": A new incident has to have at least one alert related"; + throw new NoAlertsProvidedException(ctx + ": " + msg); } + + return ResponseEntity.ok(utmIncidentService.createIncident(newIncidentDTO)); } /** @@ -137,23 +98,15 @@ public ResponseEntity createUtmIncident(@Valid @RequestBody NewInci successMessage = "Alerts added to incident successfully" ) public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody AddToIncidentDTO addToIncidentDTO) throws URISyntaxException { - final String ctx = ".addAlertsToUtmIncident"; - try { - log.debug("REST request to save UtmIncident : {}", addToIncidentDTO); - if (CollectionUtils.isEmpty(addToIncidentDTO.getAlertList())) { - throw new BadRequestAlertException("Add utmIncident cannot already have an empty related alerts", ENTITY_NAME, "alertList"); - } - - UtmIncident result = utmIncidentService.addAlertsIncident(addToIncidentDTO); - return ResponseEntity.created(new URI("/api/utm-incidents/add-alerts" + result.getId())) + + if (CollectionUtils.isEmpty(addToIncidentDTO.getAlertList())) { + throw new NoAlertsProvidedException("Add utmIncident cannot already have an empty related alerts"); + } + + UtmIncident result = utmIncidentService.addAlertsIncident(addToIncidentDTO); + return ResponseEntity.created(new URI("/api/utm-incidents/add-alerts" + result.getId())) .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString())) .body(result); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } } /** @@ -172,23 +125,16 @@ public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody Ad successType = ApplicationEventType.INCIDENT_UPDATE_SUCCESS, successMessage = "Incident status updated successfully" ) - public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmIncident utmIncident) throws URISyntaxException { - final String ctx = ".updateUtmIncident"; - try { - log.debug("REST request to update UtmIncident : {}", utmIncident); - if (utmIncident.getId() == null) { - throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); - } - UtmIncident result = utmIncidentService.changeStatus(utmIncident); - return ResponseEntity.ok() + public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmIncident utmIncident) { + + if (utmIncident.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + UtmIncident result = utmIncidentService.changeStatus(utmIncident); + + return ResponseEntity.ok() .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, utmIncident.getId().toString())) .body(result); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } } /** @@ -200,18 +146,10 @@ public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmInci */ @GetMapping("/utm-incidents") public ResponseEntity> getAllUtmIncidents(UtmIncidentCriteria criteria, Pageable pageable) { - final String ctx = ".getAllUtmIncidents"; - try { - log.debug("REST request to get UtmIncidents by criteria: {}", criteria); - Page page = utmIncidentQueryService.findByCriteria(criteria, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-incidents"); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + + Page page = utmIncidentQueryService.findByCriteria(criteria, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-incidents"); + return ResponseEntity.ok().headers(headers).body(page.getContent()); } /** @@ -221,15 +159,7 @@ public ResponseEntity> getAllUtmIncidents(UtmIncidentCriteria */ @GetMapping("/utm-incidents/users-assigned") public ResponseEntity> getAllUserAssigned() { - final String ctx = ".getAllUserAssigned"; - try { - return ResponseEntity.ok().body(utmIncidentQueryService.getAllUsersAssigned()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + return ResponseEntity.ok().body(utmIncidentQueryService.getAllUsersAssigned()); } /** @@ -240,16 +170,9 @@ public ResponseEntity> getAllUserAssigned() { */ @GetMapping("/utm-incidents/{id}") public ResponseEntity getUtmIncident(@PathVariable Long id) { - final String ctx = ".getUtmIncident"; - try { - log.debug("REST request to get UtmIncident : {}", id); - Optional utmIncident = utmIncidentService.findOne(id); - return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmIncident); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + + Optional utmIncident = utmIncidentService.findOne(id); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmIncident); + } } From 7188a2245fc64aa293e20bae59a935ee6cda31a8 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 7 Oct 2025 16:03:24 -0500 Subject: [PATCH 13/13] feat(audit): implement AuditableDTO interface and enhance incident-related DTOs for auditing --- .../aop/logging/impl/AuditAspect.java | 82 +++++++++++++++++++ .../aop/logging/impl/AuditEventAspect.java | 46 ----------- .../logging/impl/ControllerTracingAspect.java | 39 --------- .../aop/utils/AuditContextExtractor.java | 10 --- .../impl/JwtLoginAuditContextExtractor.java | 27 ------ .../service/dto/auditable/AuditableDTO.java | 7 ++ .../dto/incident/AddToIncidentDTO.java | 32 ++++---- .../service/dto/incident/NewIncidentDTO.java | 47 ++++------- .../service/incident/UtmIncidentService.java | 14 ---- .../park/utmstack/web/rest/vm/LoginVM.java | 40 +++++---- 10 files changed, 143 insertions(+), 201 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java delete mode 100644 backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditEventAspect.java delete mode 100644 backend/src/main/java/com/park/utmstack/aop/logging/impl/ControllerTracingAspect.java delete mode 100644 backend/src/main/java/com/park/utmstack/aop/utils/AuditContextExtractor.java delete mode 100644 backend/src/main/java/com/park/utmstack/aop/utils/impl/JwtLoginAuditContextExtractor.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java new file mode 100644 index 000000000..82cf76b58 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java @@ -0,0 +1,82 @@ +package com.park.utmstack.aop.logging.impl; + +import com.park.utmstack.aop.logging.AuditEvent; +import com.park.utmstack.aop.logging.NoLogException; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.loggin.LogContextBuilder; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.logstash.logback.argument.StructuredArguments; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class AuditAspect { + + private final ApplicationEventService applicationEventService; + private final LogContextBuilder logContextBuilder; + + @Around("@annotation(auditEvent)") + public Object logAuditEvent(ProceedingJoinPoint joinPoint, AuditEvent auditEvent) throws Throwable { + return handleAudit(joinPoint, auditEvent.attemptType(), auditEvent.successType(), + auditEvent.attemptMessage(), auditEvent.successMessage(), "controller"); + } + + private Object handleAudit(ProceedingJoinPoint joinPoint, + ApplicationEventType attemptType, + ApplicationEventType successType, + String attemptMessage, + String successMessage, + String layer) throws Throwable { + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String context = signature.getDeclaringType().getSimpleName() + "." + signature.getMethod().getName(); + MDC.put("context", context); + + Map extra = extractAuditData(joinPoint.getArgs()); + extra.put("layer", layer); + + try { + applicationEventService.createEvent(attemptMessage, attemptType, extra); + + Object result = joinPoint.proceed(); + + if (successType != ApplicationEventType.UNDEFINED) { + applicationEventService.createEvent(successMessage, successType, extra); + } + + return result; + + } catch (Exception e) { + if (!e.getClass().isAnnotationPresent(NoLogException.class)) { + String msg = String.format("%s: %s", context, e.getMessage()); + log.error(msg, e, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(e))); + } + + throw e; + } + } + + private Map extractAuditData(Object[] args) { + Map extra = new HashMap<>(); + for (Object arg : args) { + if (arg instanceof AuditableDTO auditable) { + extra.putAll(auditable.toAuditMap()); + } + } + return extra; + } +} + diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditEventAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditEventAspect.java deleted file mode 100644 index 8d822b98d..000000000 --- a/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditEventAspect.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.park.utmstack.aop.logging.impl; - -import com.park.utmstack.aop.logging.AuditEvent; -import com.park.utmstack.aop.utils.AuditContextExtractor; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; -import com.park.utmstack.loggin.LogContextBuilder; -import com.park.utmstack.service.application_events.ApplicationEventService; -import lombok.RequiredArgsConstructor; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -@Aspect -@Component -@RequiredArgsConstructor -public class AuditEventAspect { - - private final ApplicationEventService applicationEventService; - private final LogContextBuilder logContextBuilder; - private final List extractors; - - @Around("@annotation(auditEvent)") - public Object logAuditEvent(ProceedingJoinPoint joinPoint, AuditEvent auditEvent) throws Throwable { - Map args = logContextBuilder.buildArgs(); - for (AuditContextExtractor extractor : extractors) { - args.putAll(extractor.extract(joinPoint)); - } - - applicationEventService.createEvent(auditEvent.attemptMessage(), auditEvent.attemptType(), args); - - Object result = joinPoint.proceed(); - - if (!auditEvent.successType().equals(ApplicationEventType.UNDEFINED)) { - applicationEventService.createEvent(auditEvent.successMessage(), auditEvent.successType(), args); - } - - return result; - } - -} - diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/impl/ControllerTracingAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/ControllerTracingAspect.java deleted file mode 100644 index 71c18e452..000000000 --- a/backend/src/main/java/com/park/utmstack/aop/logging/impl/ControllerTracingAspect.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.park.utmstack.aop.logging.impl; - -import com.park.utmstack.aop.logging.NoLogException; -import com.park.utmstack.loggin.LogContextBuilder; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import net.logstash.logback.argument.StructuredArguments; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.slf4j.MDC; -import org.springframework.stereotype.Component; - -@Aspect -@Slf4j -@Component -@RequiredArgsConstructor -public class ControllerTracingAspect { - - private final LogContextBuilder logContextBuilder; - - @Around("within(@org.springframework.web.bind.annotation.RestController *)") - public Object enrichMDC(ProceedingJoinPoint joinPoint) throws Throwable { - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - String context = signature.getDeclaringType().getSimpleName() + "." + signature.getMethod().getName(); - MDC.put("context", context); - try { - return joinPoint.proceed(); - } catch (Exception e) { - if (!e.getClass().isAnnotationPresent(NoLogException.class)) { - String msg = String.format("%s: %s", context, e.getMessage()); - log.error(msg, e, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(e))); - } - throw e; - } - } -} - diff --git a/backend/src/main/java/com/park/utmstack/aop/utils/AuditContextExtractor.java b/backend/src/main/java/com/park/utmstack/aop/utils/AuditContextExtractor.java deleted file mode 100644 index 24a052eef..000000000 --- a/backend/src/main/java/com/park/utmstack/aop/utils/AuditContextExtractor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.park.utmstack.aop.utils; - -import org.aspectj.lang.ProceedingJoinPoint; - -import java.util.Map; - -public interface AuditContextExtractor { - Map extract(ProceedingJoinPoint joinPoint); -} - diff --git a/backend/src/main/java/com/park/utmstack/aop/utils/impl/JwtLoginAuditContextExtractor.java b/backend/src/main/java/com/park/utmstack/aop/utils/impl/JwtLoginAuditContextExtractor.java deleted file mode 100644 index 5de6d4d08..000000000 --- a/backend/src/main/java/com/park/utmstack/aop/utils/impl/JwtLoginAuditContextExtractor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.park.utmstack.aop.utils.impl; - -import com.park.utmstack.aop.utils.AuditContextExtractor; -import com.park.utmstack.web.rest.vm.LoginVM; -import org.aspectj.lang.ProceedingJoinPoint; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -@Component -public class JwtLoginAuditContextExtractor implements AuditContextExtractor { - - @Override - public Map extract(ProceedingJoinPoint joinPoint) { - Map context = new HashMap<>(); - - for (Object arg : joinPoint.getArgs()) { - if (arg instanceof LoginVM loginVM) { - context.put("loginAttempt", loginVM.getUsername()); - } - } - - return context; - } -} - diff --git a/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java new file mode 100644 index 000000000..60f04c0bd --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java @@ -0,0 +1,7 @@ +package com.park.utmstack.service.dto.auditable; + +import java.util.Map; + +public interface AuditableDTO { + Map toAuditMap(); +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java index 2e648f75d..154e58da1 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java @@ -1,9 +1,17 @@ package com.park.utmstack.service.dto.incident; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -public class AddToIncidentDTO { +@Setter +@Getter +public class AddToIncidentDTO implements AuditableDTO { @NotNull public Long incidentId; @NotNull @@ -12,19 +20,15 @@ public class AddToIncidentDTO { public AddToIncidentDTO() { } - public Long getIncidentId() { - return incidentId; - } - - public void setIncidentId(Long incidentId) { - this.incidentId = incidentId; - } - - public List getAlertList() { - return alertList; - } + @Override + public Map toAuditMap() { + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .toList(); - public void setAlertList(List alertList) { - this.alertList = alertList; + return Map.of( + "incidentId", incidentId, + "alertIds", alertIds + ); } } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java index 7b4c4b9ef..50d11ed7e 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java @@ -1,10 +1,17 @@ package com.park.utmstack.service.dto.incident; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.util.List; +import java.util.Map; -public class NewIncidentDTO { +@Setter +@Getter +public class NewIncidentDTO implements AuditableDTO { @NotNull @Pattern(regexp = "^[^\"]*$", message = "Double quotes are not allowed") public String incidentName; @@ -16,36 +23,16 @@ public class NewIncidentDTO { public NewIncidentDTO() { } - public String getIncidentName() { - return incidentName; - } - - public void setIncidentName(String incidentName) { - this.incidentName = incidentName; - } - - public String getIncidentDescription() { - return incidentDescription; - } - - public void setIncidentDescription(String incidentDescription) { - this.incidentDescription = incidentDescription; - } - - public String getIncidentAssignedTo() { - return incidentAssignedTo; - } - - public void setIncidentAssignedTo(String incidentAssignedTo) { - this.incidentAssignedTo = incidentAssignedTo; - } - - public List getAlertList() { - return alertList; - } + @Override + public Map toAuditMap() { + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .toList(); - public void setAlertList(List alertList) { - this.alertList = alertList; + return Map.of( + "incidentName", incidentName, + "alertIds", alertIds + ); } @Deprecated diff --git a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java index a22a46c56..f5dc323b6 100644 --- a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java +++ b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java @@ -17,7 +17,6 @@ import com.park.utmstack.service.dto.incident.NewIncidentDTO; import com.park.utmstack.service.dto.incident.RelatedIncidentAlertsDTO; import com.park.utmstack.service.incident.util.ResolveIncidentStatus; -import com.park.utmstack.util.enums.AlertStatus; import com.park.utmstack.util.exceptions.IncidentAlertConflictException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -138,17 +137,6 @@ public UtmIncident changeStatus(UtmIncident utmIncident) { public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { final String ctx = CLASSNAME + ".createIncident"; try { - List alertIds = newIncidentDTO.getAlertList(); - - String alertsIds = alertIds.stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.joining(",")); - Map extra = Map.of( - "alertIds", alertsIds, - "source", "service" - ); - String attemptMsg = String.format("Attempt to create incident with %d alerts", newIncidentDTO.getAlertList().size()); - - eventService.createEvent(attemptMsg, ApplicationEventType.INCIDENT_CREATION_ATTEMPT, extra); - validateAlertsNotAlreadyLinked(newIncidentDTO.getAlertList(), ctx); UtmIncident utmIncident = new UtmIncident(); @@ -170,8 +158,6 @@ public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { String historyMessage = String.format("Incident created with %d alerts", newIncidentDTO.getAlertList().size()); utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_CREATED, savedIncident.getId(), "Incident Created", historyMessage); - eventService.createEvent(historyMessage, ApplicationEventType.INCIDENT_CREATED, extra); - return savedIncident; } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java b/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java index 74c59fd49..bf41765a5 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java @@ -1,47 +1,36 @@ package com.park.utmstack.web.rest.vm; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import java.util.HashMap; +import java.util.Map; /** * View Model object for storing a user's credentials. */ -public class LoginVM { +@Setter +public class LoginVM implements AuditableDTO { + @Getter @NotNull @Size(min = 1, max = 50) private String username; + @Getter @NotNull @Size(min = ManagedUserVM.PASSWORD_MIN_LENGTH, max = ManagedUserVM.PASSWORD_MAX_LENGTH) private String password; private Boolean rememberMe; - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public Boolean isRememberMe() { return rememberMe; } - public void setRememberMe(Boolean rememberMe) { - this.rememberMe = rememberMe; - } - @Override public String toString() { return "LoginVM{" + @@ -49,4 +38,13 @@ public String toString() { ", rememberMe=" + rememberMe + '}'; } + + @Override + public Map toAuditMap() { + Map context = new HashMap<>(); + + context.put("loginAttempt", this.username); + + return context; + } }