From 0ee0477f73907f1412502d673d1cd21986d9338a Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 31 Jul 2026 10:31:48 +0200 Subject: [PATCH] Bump io.sqreen:libsqreen to 17.5.0 (libddwaf 2.0.1) - Fix a stale WAFModuleSpecification assertion: Waf.ResultWithData.events now correctly reflects the "events" array (libddwaf-java#206), so attributes-only rules_compat matches no longer call reportEvents([]). - Fix key_path numeric array indices being serialized as "0.0" instead of "0" via a custom Moshi Double adapter in AppSecEventWrapper, since libddwaf 2.x emits array indices as JSON numbers instead of strings. --- dd-java-agent/appsec/build.gradle | 2 +- .../datadog/appsec/ddwaf/WAFResultData.java | 23 ++++++ .../appsec/report/AppSecEventWrapper.java | 28 ++++++- .../ddwaf/WAFModuleSpecification.groovy | 6 +- .../report/AppSecEventWrapperTest.groovy | 42 ---------- .../appsec/report/AppSecEventWrapperTest.java | 77 +++++++++++++++++++ 6 files changed, 132 insertions(+), 46 deletions(-) delete mode 100644 dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/report/AppSecEventWrapperTest.groovy create mode 100644 dd-java-agent/appsec/src/test/java/com/datadog/appsec/report/AppSecEventWrapperTest.java diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 3f448a05158..dc166a54fdf 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation project(':communication') implementation project(':products:metrics:metrics-api') implementation project(':telemetry') - implementation group: 'io.sqreen', name: 'libsqreen', version: '17.4.0' + implementation group: 'io.sqreen', name: 'libsqreen', version: '17.5.0' implementation libs.moshi compileOnly project(':dd-java-agent:agent-bootstrap') diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFResultData.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFResultData.java index 051afd9b62d..c3b8f750dba 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFResultData.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFResultData.java @@ -12,12 +12,24 @@ public static class RuleMatch { String operator; String operator_value; List parameters; + + public RuleMatch(String operator, String operator_value, List parameters) { + this.operator = operator; + this.operator_value = operator_value; + this.parameters = parameters; + } } public static class Rule { public String id; // expose for log message String name; Map tags; + + public Rule(String id, String name, Map tags) { + this.id = id; + this.name = name; + this.tags = tags; + } } public static class Parameter extends MatchInfo { @@ -25,11 +37,22 @@ public static class Parameter extends MatchInfo { MatchInfo params; MatchInfo db_type; List highlight; + + public Parameter(String address, List key_path, String value, List highlight) { + super(address, key_path, value); + this.highlight = highlight; + } } public static class MatchInfo { String address; List key_path; String value; + + public MatchInfo(String address, List key_path, String value) { + this.address = address; + this.key_path = key_path; + this.value = value; + } } } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/report/AppSecEventWrapper.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/report/AppSecEventWrapper.java index dcbff552463..a1ed1cb55ae 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/report/AppSecEventWrapper.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/report/AppSecEventWrapper.java @@ -1,14 +1,40 @@ package com.datadog.appsec.report; import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; +import java.io.IOException; import java.util.Collection; import java.util.Objects; public class AppSecEventWrapper { private static final JsonAdapter ADAPTER = - new Moshi.Builder().build().adapter(AppSecEventWrapper.class); + new Moshi.Builder() + .add(Double.class, new IntegralDoubleJsonAdapter()) + .build() + .adapter(AppSecEventWrapper.class); + + // Writes whole-number Doubles (e.g. key_path array indices) without a trailing ".0". + + private static final class IntegralDoubleJsonAdapter extends JsonAdapter { + @Override + public Double fromJson(JsonReader reader) throws IOException { + return reader.nextDouble(); + } + + @Override + public void toJson(JsonWriter writer, Double value) throws IOException { + if (value == null) { + writer.nullValue(); + } else if (!value.isInfinite() && !value.isNaN() && value == Math.rint(value)) { + writer.value(value.longValue()); + } else { + writer.value(value); + } + } + } private final Collection triggers; private String json; diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy index 93ea2f56bf5..1c3bb0b32ff 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy @@ -1918,7 +1918,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.closeWafContext() 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v1', '_dd.appsec.trace.integer': 123456789]) 1 * ctx.isThrottled(null) - 1 * ctx.reportEvents([]) + // libddwaf 2.0.1: ResultWithData.events now reflects the real "events" array, so attributes-only matches don't call reportEvents(). + 0 * ctx.reportEvents(_) 0 * ctx._(*_) !flow1.blocking @@ -1937,7 +1938,8 @@ class WAFModuleSpecification extends DDSpecification { 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v2', '_dd.appsec.trace.integer': 987654321]) 1 * ctx.isThrottled(null) 1 * ctx.setManuallyKept(true) - 1 * ctx.reportEvents([]) + // See comment on the previous scenario: event:false means no entry in the "events" array. + 0 * ctx.reportEvents(_) 0 * ctx._(*_) !flow2.blocking diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/report/AppSecEventWrapperTest.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/report/AppSecEventWrapperTest.groovy deleted file mode 100644 index e2f080fd4e7..00000000000 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/report/AppSecEventWrapperTest.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package com.datadog.appsec.report - -import com.datadog.appsec.ddwaf.WAFResultData.Rule -import com.datadog.appsec.ddwaf.WAFResultData.RuleMatch -import com.datadog.appsec.ddwaf.WAFResultData.Parameter -import datadog.trace.test.util.DDSpecification - -class AppSecEventWrapperTest extends DDSpecification { - - void 'validate json serialization for AppSecEvent report'() { - setup: - def event = new AppSecEvent.Builder() - .withRule( - new Rule( - id: 'rule_id', - name: 'rule_name', - tags: [tag: 'value']) - ) - .withRuleMatches([ - new RuleMatch( - operator: 'rule_match_operator', - operator_value: 'rule_match_operator_value', - parameters: [ - new Parameter( - address: 'parameter_address', - highlight: ['parameter_highlight'], - key_path: ['parameter_key_path'], - value: 'parameter_value') - ]) - ]) - .build() - - def expectedJson = '{"triggers":[{"rule":{"id":"rule_id","name":"rule_name","tags":{"tag":"value"}},"rule_matches":[{"operator":"rule_match_operator","operator_value":"rule_match_operator_value","parameters":[{"address":"parameter_address","highlight":["parameter_highlight"],"key_path":["parameter_key_path"],"value":"parameter_value"}]}]}]}' - - when: - def wrapper = new AppSecEventWrapper([event]) - def json = wrapper.toString() - - then: - json == expectedJson - } -} diff --git a/dd-java-agent/appsec/src/test/java/com/datadog/appsec/report/AppSecEventWrapperTest.java b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/report/AppSecEventWrapperTest.java new file mode 100644 index 00000000000..1eb6f9031d7 --- /dev/null +++ b/dd-java-agent/appsec/src/test/java/com/datadog/appsec/report/AppSecEventWrapperTest.java @@ -0,0 +1,77 @@ +package com.datadog.appsec.report; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.datadog.appsec.ddwaf.WAFResultData.Parameter; +import com.datadog.appsec.ddwaf.WAFResultData.Rule; +import com.datadog.appsec.ddwaf.WAFResultData.RuleMatch; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class AppSecEventWrapperTest { + + @Test + void validateJsonSerializationForAppSecEvent() { + Parameter parameter = + new Parameter( + "parameter_address", + singletonList("parameter_key_path"), + "parameter_value", + singletonList("parameter_highlight")); + RuleMatch ruleMatch = + new RuleMatch("rule_match_operator", "rule_match_operator_value", singletonList(parameter)); + AppSecEvent event = + new AppSecEvent.Builder() + .withRule(new Rule("rule_id", "rule_name", singletonMap("tag", "value"))) + .withRuleMatches(singletonList(ruleMatch)) + .build(); + + String json = new AppSecEventWrapper(singletonList(event)).toString(); + + String expectedJson = + "{\"triggers\":[{\"rule\":{\"id\":\"rule_id\",\"name\":\"rule_name\",\"tags\":{\"tag\":\"value\"}}," + + "\"rule_matches\":[{\"operator\":\"rule_match_operator\",\"operator_value\":\"rule_match_operator_value\"," + + "\"parameters\":[{\"address\":\"parameter_address\",\"highlight\":[\"parameter_highlight\"]," + + "\"key_path\":[\"parameter_key_path\"],\"value\":\"parameter_value\"}]}]}]}"; + assertEquals(expectedJson, json); + } + + // libddwaf 2.x emits array indices in key_path as numbers (e.g. key_path: [0], not ["0"]); + // this guards that a whole-number Double round-trips as "0", not "0.0". + @Test + void validateJsonSerializationForNumericKeyPath() { + Parameter parameter = + new Parameter("server.request.body", Arrays.asList("items", 0.0, "name"), "value", null); + RuleMatch ruleMatch = new RuleMatch("operator", "operator_value", singletonList(parameter)); + AppSecEvent event = + new AppSecEvent.Builder() + .withRule(new Rule("rule_id", "rule_name", singletonMap("tag", "value"))) + .withRuleMatches(singletonList(ruleMatch)) + .build(); + + String json = new AppSecEventWrapper(singletonList(event)).toString(); + + assertTrue(json.contains("\"key_path\":[\"items\",0,\"name\"]"), "actual json: " + json); + } + + // Non-whole-number Doubles must keep Moshi's default formatting - only whole numbers get the + // integer-style rewrite. + @Test + void validateJsonSerializationForNonIntegralKeyPath() { + Parameter parameter = + new Parameter("server.request.body", Arrays.asList("items", 1.5, "name"), "value", null); + RuleMatch ruleMatch = new RuleMatch("operator", "operator_value", singletonList(parameter)); + AppSecEvent event = + new AppSecEvent.Builder() + .withRule(new Rule("rule_id", "rule_name", singletonMap("tag", "value"))) + .withRuleMatches(singletonList(ruleMatch)) + .build(); + + String json = new AppSecEventWrapper(singletonList(event)).toString(); + + assertTrue(json.contains("\"key_path\":[\"items\",1.5,\"name\"]"), "actual json: " + json); + } +}