diff --git a/README.md b/README.md index 419f3f3..5824113 100644 --- a/README.md +++ b/README.md @@ -268,14 +268,17 @@ adapter: ### Comportamiento según configuración -| Valor YAML | Header enviado por el cliente | Header ausente en request | Excepción sin `message-id` previo | -| -------------- | ----------------------------- | ------------------------------ | ------------------------------- | -| *(no declarada)* | Se propaga tal como viene | Se genera UUID automáticamente | Se genera UUID siempre | -| `"false"` | Se propaga tal como viene | No se genera UUID (`null`) | Se genera UUID siempre | -| `"true"` | Se propaga tal como viene | Se genera UUID automáticamente | Se genera UUID siempre | +| Valor YAML | Header enviado por el cliente | Header ausente en request | Excepción sin `message-id` previo | +| ------------------------------ | ----------------------------- | ------------------------------ | ------------------------------- | +| *(no declarada)* | Se propaga tal como viene | No se genera UUID (`null`) | Se genera UUID siempre | +| *(declarada sin valor / blanco)* | Se propaga tal como viene | No se genera UUID (`null`) | Se genera UUID siempre | +| `"false"` | Se propaga tal como viene | No se genera UUID (`null`) | Se genera UUID siempre | +| `"true"` | Se propaga tal como viene | Se genera UUID automáticamente | Se genera UUID siempre | > El header del cliente **siempre se propaga**, independientemente del valor configurado. La diferencia entre modos solo aplica cuando el cliente **no** envía el header. +> **Clave declarada sin valor:** si el microservicio consumidor escribe `enable_auto_register_message_id:` en su YAML sin asignarle un valor, Spring Boot la enlaza como cadena vacía (`""`), no como `null`. La librería trata ese caso igual que la propiedad no declarada: **no falla al arrancar** y el comportamiento por defecto es inactivo. + > Las **excepciones siempre tienen trazabilidad**: `resolveForException` genera UUID incondicionalmente cuando no hay un `message-id` previo, sin importar el valor de `enable_auto_register_message_id`. ### Normalización del header diff --git a/build.gradle b/build.gradle index ab2ad60..550b73e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,24 +1,24 @@ buildscript { ext { cleanArchitectureVersion = '4.0.4' - springBootVersion = '4.0.2' + springBootVersion = '4.0.6' sonarVersion = '6.0.1.5171' jacocoVersion = '0.8.12' pitestVersion = '1.15.0' lombokVersion = '1.18.38' junitVersion = '5.12.2' reactorVersion = '3.7.6' - fasterxmlJacksonCoreVersion = '2.21.1' - toolsJacksonCoreVersion = '3.1.0' + fasterxmlJacksonCoreVersion = '2.21.4' + toolsJacksonCoreVersion = '3.1.4' log4jVersion = '2.25.4' commonslang3= '3.18.0' - nettyVersion = '4.1.132.Final' - netty2Version = '4.2.11.Final' - logbackVersion = '1.5.25' - tomcatEmbedCoreVersion = "10.1.54" + nettyVersion = '4.1.135.Final' + netty2Version = '4.2.15.Final' + logbackVersion = '1.5.33' + tomcatEmbedCoreVersion = "10.1.56" assertjCoreVersion = "3.27.7" - springWebfluxVersion = "7.0.6" - springWebmvcVersion = "7.0.6" + springWebfluxVersion = "7.0.7" + springWebmvcVersion = "7.0.7" } } @@ -26,10 +26,9 @@ plugins { id 'co.com.bancolombia.cleanArchitecture' version "${cleanArchitectureVersion}" id 'org.springframework.boot' version "${springBootVersion}" apply false id 'info.solidsoft.pitest' version "${pitestVersion}" apply false - id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' - id 'org.owasp.dependencycheck' version '12.1.8' apply false id 'io.spring.dependency-management' version '1.1.7' id 'org.sonarqube' version "${sonarVersion}" + id 'org.owasp.dependencycheck' version '12.2.2' apply false id 'maven-publish' id 'jacoco' } @@ -42,28 +41,17 @@ sonar { def modules = subprojects.projectDir.collect { "${it.toString().replace(project.projectDir.toString() + "/", "")}" } properties { property "sonar.sourceEncoding", "UTF-8" - property "sonar.organization", "grupo-bancolombia" - property "sonar.host.url", "https://sonarcloud.io/" property "sonar.modules", "${modules.join(',')}" property "sonar.sources", "src,settings.gradle,main.gradle,build.gradle,${modules.collect { "${it}/build.gradle" }.join(',')}" property "sonar.exclusions", "**/test/**, **/ReactiveLogsConfiguration.java, **/ImperativeLogsConfiguration.java, **/middleware/**, " + "**/config/**" property "sonar.test", "src/test" property "sonar.java.binaries", "" - property "sonar.junit.reportsPath", "build/test-results/test" + property "sonar.junit.reportsPath", "" property "sonar.java.coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacocoMergedReport/jacocoMergedReport.xml" property "sonar.pitest.reportPaths", "build/reports/pitest/mutations.xml" } } -nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) - snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) - } - } -} - -apply from: './main.gradle' +apply from: './main.gradle' \ No newline at end of file diff --git a/dependencyMgmt.gradle b/dependencyMgmt.gradle index 9796d44..576031a 100644 --- a/dependencyMgmt.gradle +++ b/dependencyMgmt.gradle @@ -14,11 +14,43 @@ configurations.configureEach { } if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-http') { details.useVersion("${nettyVersion}") - details.because("CVE-2025-67735, CVE-2026-33870") + details.because("CVE-2025-67735, CVE-2026-33870, CVE-2026-50020") } if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-http2') { details.useVersion("${netty2Version}") - details.because("CVE-2026-33871") + details.because("CVE-2026-33871, CVE-2026-42587, CVE-2026-47244, CVE-2026-48043, CVE-2026-50560") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-compression') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-42583") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-dns') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-42579") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-http3') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-42582, CVE-2026-44892, CVE-2026-48748") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-handler-proxy') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-42578") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-transport-native-epoll') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-42577, CVE-2026-45536") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-codec-classes-quic') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-44894, CVE-2026-50009") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-handler') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-44249, CVE-2026-45416, CVE-2026-50010") + } + if (details.requested.group == 'io.netty' && details.requested.name == 'netty-resolver-dns') { + details.useVersion("${netty2Version}") + details.because("CVE-2026-45674, CVE-2026-47691, CVE-2026-45673") } if (details.requested.group == 'ch.qos.logback') { details.useVersion("${logbackVersion}") @@ -36,10 +68,18 @@ configurations.configureEach { details.useVersion("${fasterxmlJacksonCoreVersion}") details.because("GHSA-72hv-8253-57qq") } + if (details.requested.group == 'com.fasterxml.jackson.core' && details.requested.name == 'jackson-databind') { + details.useVersion("${fasterxmlJacksonCoreVersion}") + details.because("CVE-2026-54512, CVE-2026-54513, CVE-2026-54514, CVE-2026-54515, CVE-2026-54516, CVE-2026-54517, CVE-2026-54518") + } if (details.requested.group == 'tools.jackson.core' && details.requested.name == 'jackson-core') { details.useVersion("${toolsJacksonCoreVersion}") details.because("CVE-2026-29062, GHSA-72hv-8253-57qq") } + if (details.requested.group == 'tools.jackson.core' && details.requested.name == 'jackson-databind') { + details.useVersion("${toolsJacksonCoreVersion}") + details.because("CVE-2026-54512, CVE-2026-54513, CVE-2026-54514, CVE-2026-54515, CVE-2026-54516, CVE-2026-54517, CVE-2026-54518") + } if (details.requested.group == 'org.assertj' && details.requested.name == 'assertj-core') { details.useVersion("${assertjCoreVersion}") details.because("CVE-2026-24400") diff --git a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCase.java b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCase.java index 0e292d8..001d2e8 100644 --- a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCase.java +++ b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCase.java @@ -52,13 +52,14 @@ public String resolveFromHeaders(Set>> headers, S /** * Resuelve el messageId para una petición entrante a partir del valor raw del header. * */ public String resolveFromRequestEnvironment(String incomingHeaderValue) { - if (!Boolean.FALSE.equals(enabledFlag)) { + if (Boolean.TRUE.equals(enabledFlag)) { return Optional.ofNullable(incomingHeaderValue) .filter(Predicate.not(String::isBlank)) .orElseGet(() -> UUID.randomUUID().toString()); diff --git a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidator.java b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidator.java index a23cef5..9c3df87 100644 --- a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidator.java +++ b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidator.java @@ -7,15 +7,7 @@ import java.util.Set; -/** - * Validates that the raw value of {@code enable_auto_register_message_id} - * is strictly {@code "true"} or {@code "false"}. - * - *

Spring Boot silently converts values like {@code "yes"}, {@code "on"}, - * {@code "1"} to {@code Boolean.TRUE} when the property type is {@code Boolean}. - * Using {@code String} as the property type and calling this validator at startup - * produces a clear error, as close as possible to a compile-time check. - */ + public final class MessageIdMngValidator { static final String MESSAGE_ID_PREFIX = "adapter.ecs.logs.message-id"; @@ -30,40 +22,26 @@ public final class MessageIdMngValidator { private static final String ERROR_VALUE_MSG = "Invalid value '%s' for property '%s'. " + - "Accepted values are: 'true' or 'false'. " + - "Please review your application.yaml configuration."; + "Accepted values are: 'true' or 'false'. " + + "Please review your application.yaml configuration."; private static final String ERROR_KEY_MSG = "Invalid or unknown property '%s' under the 'adapter.ecs.logs.message-id' namespace. " + - "The only accepted property is " + - "'adapter.ecs.logs.message-id.enable_auto_register_message_id'. " + - "Check for typos in the property key (e.g. 'message-id-' instead of 'message-id'). " + - "Please review your application.yaml configuration."; + "The only accepted property is " + + "'adapter.ecs.logs.message-id.enable_auto_register_message_id'. " + + "Check for typos in the property key (e.g. 'message-id-' instead of 'message-id'). " + + "Please review your application.yaml configuration."; private MessageIdMngValidator() {} - /** - * Validates {@code rawValue} against the allowed domain {"true", "false"}. - * - * @param rawValue the raw string read from the YAML property - * @param propertyKey the canonical property key (for the error message) - * @throws BeanInitializationException if the value is non-null and not accepted - */ + public static void validate(String rawValue, String propertyKey) { - if (rawValue != null && !VALID_VALUES.contains(rawValue.toLowerCase())) { + if (rawValue != null && !rawValue.isBlank() && !VALID_VALUES.contains(rawValue.toLowerCase())) { throw new BeanInitializationException( String.format(ERROR_VALUE_MSG, rawValue, propertyKey)); } } - /** - * Scans all enumerable property sources for keys that start with the - * {@code adapter.ecs.logs.message-id} namespace but are not the recognized - * canonical key. This catches prefix typos such as {@code message-id-:}. - * - * @param environment the application environment - * @throws BeanInitializationException if an unknown key is found - */ public static void validatePropertyKeys(Environment environment) { if (!(environment instanceof ConfigurableEnvironment ce)) return; for (var source : ce.getPropertySources()) { diff --git a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/domain/MessageIdRequestProperties.java b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/domain/MessageIdRequestProperties.java index b9a28eb..8a010a9 100644 --- a/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/domain/MessageIdRequestProperties.java +++ b/ecs-core/src/main/java/co/com/bancolombia/ecs/infra/config/managementid/domain/MessageIdRequestProperties.java @@ -7,25 +7,6 @@ import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; -/** - * Configuration properties for the message-id feature. - * - *

The field type is intentionally {@code String} (not {@code Boolean}) to capture - * the raw YAML value before Spring Boot's relaxed binding can silently convert - * ambiguous values like {@code "yes"}, {@code "on"}, or {@code "1"} to {@code Boolean.TRUE}. - * {@link MessageIdMngValidator} enforces that only {@code "true"} or {@code "false"} are accepted. - * - *

Accepted YAML key: {@code enable_auto_register_message_id} (snake_case canonical form). - * Spring's relaxed binding also accepts kebab-case for backwards compatibility. - * - *

Semantics of the parsed {@link #enabled} value: - *

- */ @ConfigurationProperties(prefix = "adapter.ecs.logs.message-id") public class MessageIdRequestProperties implements InitializingBean, EnvironmentAware { @@ -35,7 +16,6 @@ public class MessageIdRequestProperties implements InitializingBean, Environment private final String enableAutoRegisterMessageId; private Environment environment; - /** Validated and parsed value; available after {@link #afterPropertiesSet()}. */ @Getter private Boolean enabled; @@ -52,7 +32,7 @@ public void setEnvironment(Environment environment) { public void afterPropertiesSet() { MessageIdMngValidator.validatePropertyKeys(environment); MessageIdMngValidator.validate(enableAutoRegisterMessageId, PROPERTY_KEY); - this.enabled = enableAutoRegisterMessageId == null + this.enabled = (enableAutoRegisterMessageId == null || enableAutoRegisterMessageId.isBlank()) ? null : Boolean.parseBoolean(enableAutoRegisterMessageId.toLowerCase()); } diff --git a/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCaseTest.java b/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCaseTest.java index 47df5b6..a0264f9 100644 --- a/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCaseTest.java +++ b/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngUseCaseTest.java @@ -61,9 +61,9 @@ void shouldGenerateUuidForExceptionWhenFlagIsNull() { @Test - void shouldGenerateUuidWhenFlagIsNullAndNoHeader() { + void shouldReturnNullWhenFlagIsNullAndNoHeader() { MessageIdMngUseCase useCaseNullFlag = new MessageIdMngUseCase(new MessageIdRequestProperties(null)); - assertNotNull(useCaseNullFlag.resolveFromRequestEnvironment(null)); + assertNull(useCaseNullFlag.resolveFromRequestEnvironment(null)); } @Test @@ -72,11 +72,9 @@ void shouldPropagateHeaderWhenFlagIsNullAndHeaderPresent() { assertEquals("from-client", useCaseNullFlag.resolveFromRequestEnvironment("from-client")); } - // ── resolveFromRequest con flag=false ───────────────────────────────────── @Test void shouldReturnNullWhenFlagIsFalseAndNoHeader() { - // flag=false: no genera UUID, solo propaga si el cliente lo envía MessageIdMngUseCase useCaseFalse = new MessageIdMngUseCase(propsDisabled()); assertNull(useCaseFalse.resolveFromRequestEnvironment(null)); } @@ -86,4 +84,27 @@ void shouldPropagateHeaderWhenFlagIsFalseAndHeaderPresent() { MessageIdMngUseCase useCaseFalse = new MessageIdMngUseCase(propsDisabled()); assertEquals("from-client", useCaseFalse.resolveFromRequestEnvironment("from-client")); } + + @Test + void shouldNotThrowWhenValueIsBlank() { + MessageIdRequestProperties props = new MessageIdRequestProperties(" "); + assertDoesNotThrow(props::afterPropertiesSet); + assertNull(props.getEnabled()); + } + + @Test + void shouldReturnNullWhenFlagIsBlankAndNoHeader() { + MessageIdRequestProperties props = new MessageIdRequestProperties(""); + props.afterPropertiesSet(); + MessageIdMngUseCase useCaseBlankFlag = new MessageIdMngUseCase(props); + assertNull(useCaseBlankFlag.resolveFromRequestEnvironment(null)); + } + + @Test + void shouldPropagateHeaderWhenFlagIsBlankAndHeaderPresent() { + MessageIdRequestProperties props = new MessageIdRequestProperties(""); + props.afterPropertiesSet(); + MessageIdMngUseCase useCaseBlankFlag = new MessageIdMngUseCase(props); + assertEquals("from-client", useCaseBlankFlag.resolveFromRequestEnvironment("from-client")); + } } diff --git a/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidatorTest.java b/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidatorTest.java index fae77b7..04a19f5 100644 --- a/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidatorTest.java +++ b/ecs-core/src/test/java/co/com/bancolombia/ecs/infra/config/managementid/application/MessageIdMngValidatorTest.java @@ -14,7 +14,6 @@ class MessageIdMngValidatorTest { private static final String PROPERTY_KEY = "adapter.ecs.logs.message-id.enable_auto_register_message_id"; - // ── Valores inválidos ───────────────────────────────────────────────────── @ParameterizedTest @ValueSource(strings = {"1", "0", "yes", "no", "on", "off", "maybe", "2", "si", "verdadero"}) @@ -29,7 +28,6 @@ void shouldThrowWhenValueIsNotStrictlyTrueOrFalse(String invalidValue) { "El mensaje de error debe indicar los valores aceptados"); } - // ── Valores válidos ─────────────────────────────────────────────────────── @ParameterizedTest @ValueSource(strings = {"true", "false", "TRUE", "FALSE", "True", "False"}) @@ -42,10 +40,18 @@ void shouldAcceptTrueOrFalseInAnyCase(String validValue) { @Test void shouldAcceptNullValue() { - // null = propiedad no configurada → feature desactivada, no debe lanzar excepción assertDoesNotThrow( () -> MessageIdMngValidator.validate(null, PROPERTY_KEY), "null debe ser aceptado (feature desactivada)" ); } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\t"}) + void shouldAcceptBlankValue(String blankValue) { + assertDoesNotThrow( + () -> MessageIdMngValidator.validate(blankValue, PROPERTY_KEY), + "Un valor en blanco debe ser aceptado igual que la propiedad no declarada" + ); + } } diff --git a/examples/ecs_logs_reactive_test/build.gradle b/examples/ecs_logs_reactive_test/build.gradle index cd2678f..f3c0377 100644 --- a/examples/ecs_logs_reactive_test/build.gradle +++ b/examples/ecs_logs_reactive_test/build.gradle @@ -1,13 +1,13 @@ buildscript { - ext { - cleanArchitectureVersion = '3.26.1' - springBootVersion = '3.5.4' - sonarVersion = '6.3.1.5724' - jacocoVersion = '0.8.13' - pitestVersion = '1.19.0-rc.1' + ext { + cleanArchitectureVersion = '3.26.1' + springBootVersion = '3.5.15' + sonarVersion = '6.3.1.5724' + jacocoVersion = '0.8.13' + pitestVersion = '1.19.0-rc.1' lombokVersion = '1.18.38' - ecsversion = '1.2.1' - } + ecsversion = '2.1.0' + } } plugins {