Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 12 additions & 24 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
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"
}
}

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'
}
Expand All @@ -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'
44 changes: 42 additions & 2 deletions dependencyMgmt.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ public String resolveFromHeaders(Set<Map.Entry<String, List<String>>> headers, S
/**
* Resuelve el messageId para una petición entrante a partir del valor raw del header.
* <ul>
* <li>{@code true} – genera UUID si el header está ausente o en blanco.</li>
* <li>{@code true/null} – genera UUID si el header está ausente o en blanco.</li>
* <li>{@code false} – devuelve el header tal como llega; {@code null} si no hay header.</li>
* <li>{@code true} – genera UUID si el header está ausente o en blanco.</li>
* <li>{@code false/null} – devuelve el header tal como llega; {@code null} si no hay header.
* {@code null} cubre tanto la propiedad no registrada como un
* valor en blanco declarado en el YAML: ambos son inactivos por defecto.</li>
* </ul>
*/
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}.
*
* <p>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";
Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,6 @@
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;

/**
* Configuration properties for the message-id feature.
*
* <p>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.
*
* <p>Accepted YAML key: {@code enable_auto_register_message_id} (snake_case canonical form).
* Spring's relaxed binding also accepts kebab-case for backwards compatibility.
*
* <p>Semantics of the parsed {@link #enabled} value:
* <ul>
* <li>{@code null} – feature disabled; library behaves as before</li>
* <li>{@code false} – generates a UUID only when a BusinessException has no messageId
* (exception traceability)</li>
* <li>{@code true} – generates a UUID on every request that lacks a message-id header</li>
* </ul>
*/
@ConfigurationProperties(prefix = "adapter.ecs.logs.message-id")
public class MessageIdRequestProperties implements InitializingBean, EnvironmentAware {

Expand All @@ -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;

Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
Expand All @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand All @@ -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"})
Expand All @@ -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"
);
}
}
Loading
Loading