diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java
index 51e5f07187b..29f086d8784 100644
--- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java
+++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java
@@ -5,6 +5,7 @@
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES;
+import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_MULTICONFIG;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA;
@@ -18,6 +19,7 @@
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRINT;
+import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRACE_TAGGING_RULES;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT;
@@ -113,6 +115,8 @@ public AppSecConfigServiceImpl(
if (tracerConfig.isAppSecWafMetrics()) {
traceSegmentPostProcessors.add(statsReporter);
}
+ // Add trace tagging post processor for handling trace attributes
+ traceSegmentPostProcessors.add(new com.datadog.appsec.ddwaf.TraceTaggingPostProcessor());
}
private void subscribeConfigurationPoller() {
@@ -140,7 +144,9 @@ private void subscribeConfigurationPoller() {
| CAPABILITY_ENDPOINT_FINGERPRINT
| CAPABILITY_ASM_SESSION_FINGERPRINT
| CAPABILITY_ASM_NETWORK_FINGERPRINT
- | CAPABILITY_ASM_HEADER_FINGERPRINT;
+ | CAPABILITY_ASM_HEADER_FINGERPRINT
+ | CAPABILITY_ASM_DD_MULTICONFIG
+ | CAPABILITY_ASM_TRACE_TAGGING_RULES;
if (tracerConfig.isAppSecRaspEnabled()) {
capabilities |= CAPABILITY_ASM_RASP_SQLI;
capabilities |= CAPABILITY_ASM_RASP_SSRF;
@@ -490,7 +496,9 @@ public void close() {
| CAPABILITY_ENDPOINT_FINGERPRINT
| CAPABILITY_ASM_SESSION_FINGERPRINT
| CAPABILITY_ASM_NETWORK_FINGERPRINT
- | CAPABILITY_ASM_HEADER_FINGERPRINT);
+ | CAPABILITY_ASM_HEADER_FINGERPRINT
+ | CAPABILITY_ASM_DD_MULTICONFIG
+ | CAPABILITY_ASM_TRACE_TAGGING_RULES);
this.configurationPoller.removeListeners(Product.ASM_DD);
this.configurationPoller.removeListeners(Product.ASM_DATA);
this.configurationPoller.removeListeners(Product.ASM);
diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingPostProcessor.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingPostProcessor.java
new file mode 100644
index 00000000000..af7c8fb44d8
--- /dev/null
+++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingPostProcessor.java
@@ -0,0 +1,51 @@
+package com.datadog.appsec.ddwaf;
+
+import com.datadog.appsec.config.TraceSegmentPostProcessor;
+import com.datadog.appsec.gateway.AppSecRequestContext;
+import com.datadog.appsec.report.AppSecEvent;
+import datadog.trace.api.internal.TraceSegment;
+import java.util.Collection;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Post processor that serializes trace attributes from the AppSec request context to the trace
+ * segment during trace post-processing.
+ *
+ *
This processor handles the new trace tagging feature where WAF rules can specify attributes to
+ * be added to the trace segment.
+ */
+public class TraceTaggingPostProcessor implements TraceSegmentPostProcessor {
+ private static final Logger log = LoggerFactory.getLogger(TraceTaggingPostProcessor.class);
+
+ @Override
+ public void processTraceSegment(
+ TraceSegment segment, AppSecRequestContext ctx, Collection collectedEvents) {
+
+ Map traceAttributes = ctx.getTraceAttributes();
+ if (traceAttributes == null || traceAttributes.isEmpty()) {
+ return;
+ }
+
+ log.debug("Serializing {} trace attributes to trace segment", traceAttributes.size());
+
+ // Serialize each attribute to the trace segment
+ for (Map.Entry entry : traceAttributes.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+
+ if (key != null && !key.isEmpty() && value != null) {
+ try {
+ // Use setTagTop to add the attribute to the trace segment
+ segment.setTagTop(key, value);
+ log.debug("Added trace attribute: {} = {}", key, value);
+ } catch (Exception e) {
+ log.warn("Failed to serialize trace attribute {} = {}", key, value, e);
+ }
+ } else {
+ log.debug("Skipping invalid trace attribute: key='{}', value='{}'", key, value);
+ }
+ }
+ }
+}
diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingResultProcessor.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingResultProcessor.java
new file mode 100644
index 00000000000..548b1c7247e
--- /dev/null
+++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/TraceTaggingResultProcessor.java
@@ -0,0 +1,304 @@
+package com.datadog.appsec.ddwaf;
+
+import com.datadog.appsec.event.ChangeableFlow;
+import com.datadog.appsec.gateway.AppSecRequestContext;
+import com.datadog.appsec.gateway.GatewayContext;
+import com.datadog.appsec.gateway.RateLimiter;
+import com.datadog.appsec.report.AppSecEvent;
+import datadog.appsec.api.blocking.BlockingContentType;
+import datadog.trace.api.Config;
+import datadog.trace.api.ProductTraceSource;
+import datadog.trace.api.gateway.Flow;
+import datadog.trace.api.telemetry.WafMetricCollector;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import datadog.trace.util.stacktrace.StackTraceEvent;
+import datadog.trace.util.stacktrace.StackTraceFrame;
+import datadog.trace.util.stacktrace.StackUtils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Processor for handling trace tagging results from the WAF. This class processes the new trace
+ * tagging result structure that includes keep flags, attributes, and event generation control.
+ */
+public class TraceTaggingResultProcessor {
+ private static final Logger log = LoggerFactory.getLogger(TraceTaggingResultProcessor.class);
+ private static final String EXPLOIT_DETECTED_MSG = "Exploit detected";
+
+ private final RateLimiter rateLimiter;
+
+ public TraceTaggingResultProcessor(RateLimiter rateLimiter) {
+ this.rateLimiter = rateLimiter;
+ }
+
+ /**
+ * Process a trace tagging result from the WAF.
+ *
+ * @param result The trace tagging result to process
+ * @param flow The changeable flow for request modification
+ * @param reqCtx The request context
+ * @param gwCtx The gateway context
+ */
+ public void processResult(
+ WAFResultData.TraceTaggingResult result,
+ ChangeableFlow flow,
+ AppSecRequestContext reqCtx,
+ GatewayContext gwCtx) {
+
+ // Handle timeout
+ if (result.timeout) {
+ if (gwCtx.isRasp) {
+ reqCtx.increaseRaspTimeouts();
+ WafMetricCollector.get().raspTimeout(gwCtx.raspRuleType);
+ } else {
+ reqCtx.increaseWafTimeouts();
+ log.debug("Timeout calling the WAF");
+ }
+ return;
+ }
+
+ // Handle keep flag for sampling priority
+ if (result.keep) {
+ handleKeepFlag(reqCtx);
+ }
+
+ // Handle actions (blocking, redirects, stack generation, trace tagging)
+ if (result.actions != null && !result.actions.isEmpty()) {
+ handleActions(result.actions, flow, reqCtx, gwCtx);
+ }
+
+ // Handle events
+ if (result.events != null && !result.events.isEmpty()) {
+ handleEvents(result.events, reqCtx, gwCtx);
+ }
+
+ // Handle attributes for trace tagging
+ if (result.attributes != null && !result.attributes.isEmpty()) {
+ handleAttributes(result.attributes, reqCtx);
+ }
+
+ // Set blocking state if flow is blocking
+ if (flow.isBlocking()) {
+ if (gwCtx.isRasp) {
+ reqCtx.setRaspBlocked();
+ } else {
+ reqCtx.setWafBlocked();
+ }
+ }
+ }
+
+ /** Handle the keep flag by setting appropriate span tags for sampling priority. */
+ private void handleKeepFlag(AppSecRequestContext reqCtx) {
+ AgentSpan activeSpan = AgentTracer.get().activeSpan();
+ if (activeSpan != null) {
+ // Keep event related span, because it could be ignored in case of
+ // reduced datadog sampling rate.
+ activeSpan.getLocalRootSpan().setTag(Tags.ASM_KEEP, true);
+ // If APM is disabled, inform downstream services that the current
+ // distributed trace contains at least one ASM event and must inherit
+ // the given force-keep priority
+ activeSpan.getLocalRootSpan().setTag(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
+ } else {
+ // If active span is not available the ASM_KEEP tag will be set in the GatewayBridge
+ // when the request ends
+ log.debug("There is no active span available");
+ }
+ }
+
+ /** Handle actions from the WAF result. */
+ private void handleActions(
+ Map> actions,
+ ChangeableFlow flow,
+ AppSecRequestContext reqCtx,
+ GatewayContext gwCtx) {
+
+ for (Map.Entry> action : actions.entrySet()) {
+ String actionType = action.getKey();
+ Map actionParams = action.getValue();
+
+ if ("trace_tagging".equals(actionType)) {
+ handleTraceTaggingAction(actionParams, reqCtx);
+ } else {
+ WAFModule.ActionInfo actionInfo = new WAFModule.ActionInfo(actionType, actionParams);
+
+ if ("block_request".equals(actionInfo.type)) {
+ Flow.Action.RequestBlockingAction rba =
+ createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
+ flow.setAction(rba);
+ } else if ("redirect_request".equals(actionInfo.type)) {
+ Flow.Action.RequestBlockingAction rba =
+ createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
+ flow.setAction(rba);
+ } else if ("generate_stack".equals(actionInfo.type)) {
+ if (Config.get().isAppSecStackTraceEnabled()) {
+ String stackId = (String) actionInfo.parameters.get("stack_id");
+ StackTraceEvent stackTraceEvent = createExploitStackTraceEvent(stackId);
+ reqCtx.reportStackTrace(stackTraceEvent);
+ } else {
+ log.debug("Ignoring action with type generate_stack (stack traces disabled)");
+ }
+ } else {
+ log.info("Ignoring action with type {}", actionInfo.type);
+ if (!gwCtx.isRasp) {
+ reqCtx.setWafRequestBlockFailure();
+ }
+ }
+ }
+ }
+ }
+
+ /** Handle trace tagging action. */
+ private void handleTraceTaggingAction(
+ Map actionParams, AppSecRequestContext reqCtx) {
+ // Handle keep flag
+ Object keepObj = actionParams.get("keep");
+ if (keepObj instanceof Boolean && (Boolean) keepObj) {
+ handleKeepFlag(reqCtx);
+ }
+
+ // Handle attributes
+ Object attrsObj = actionParams.get("attributes");
+ if (attrsObj instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map attributes = (Map) attrsObj;
+ if (!attributes.isEmpty()) {
+ handleAttributes(attributes, reqCtx);
+ }
+ }
+ }
+
+ /** Handle events from the WAF result. */
+ private void handleEvents(
+ Collection events, AppSecRequestContext reqCtx, GatewayContext gwCtx) {
+
+ if (!events.isEmpty()) {
+ if (!reqCtx.isThrottled(rateLimiter)) {
+ AgentSpan activeSpan = AgentTracer.get().activeSpan();
+ if (activeSpan != null) {
+ log.debug("Setting force-keep tag on the current span");
+ // Keep event related span, because it could be ignored in case of
+ // reduced datadog sampling rate.
+ activeSpan.getLocalRootSpan().setTag(Tags.ASM_KEEP, true);
+ // If APM is disabled, inform downstream services that the current
+ // distributed trace contains at least one ASM event and must inherit
+ // the given force-keep priority
+ activeSpan
+ .getLocalRootSpan()
+ .setTag(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
+ } else {
+ // If active span is not available the ASM_KEEP tag will be set in the GatewayBridge
+ // when the request ends
+ log.debug("There is no active span available");
+ }
+
+ // Convert WAFResultData to AppSecEvent
+ Collection appSecEvents = convertToAppSecEvents(events);
+ reqCtx.reportEvents(appSecEvents);
+ } else {
+ log.debug("Rate limited WAF events");
+ if (!gwCtx.isRasp) {
+ reqCtx.setWafRateLimited();
+ }
+ }
+ }
+ }
+
+ /** Handle attributes for trace tagging. */
+ private void handleAttributes(Map attributes, AppSecRequestContext reqCtx) {
+ // Serialize attributes to the trace segment
+ // This will be handled by the trace segment post processor
+ reqCtx.setTraceAttributes(attributes);
+ }
+
+ /** Convert WAFResultData collection to AppSecEvent collection. */
+ private Collection convertToAppSecEvents(Collection wafResults) {
+ return wafResults.stream()
+ .map(this::buildEvent)
+ .filter(event -> event != null)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ /** Build an AppSecEvent from WAFResultData. */
+ private AppSecEvent buildEvent(WAFResultData wafResult) {
+ if (wafResult == null || wafResult.rule == null || wafResult.rule_matches == null) {
+ log.warn("WAF result is empty: {}", wafResult);
+ return null;
+ }
+
+ Long spanId = null;
+ AgentSpan agentSpan = AgentTracer.get().activeSpan();
+ if (agentSpan != null) {
+ spanId = agentSpan.getSpanId();
+ }
+
+ return new AppSecEvent.Builder()
+ .withRule(wafResult.rule)
+ .withRuleMatches(wafResult.rule_matches)
+ .withSpanId(spanId)
+ .withStackId(wafResult.stack_id)
+ .build();
+ }
+
+ /** Create a block request action. */
+ private Flow.Action.RequestBlockingAction createBlockRequestAction(
+ WAFModule.ActionInfo actionInfo, AppSecRequestContext reqCtx, boolean isRasp) {
+
+ Integer statusCode = (Integer) actionInfo.parameters.get("status_code");
+ if (statusCode == null) {
+ statusCode = 403;
+ }
+
+ String type = (String) actionInfo.parameters.get("type");
+ BlockingContentType blockingContentType = BlockingContentType.AUTO;
+ if ("json".equals(type)) {
+ blockingContentType = BlockingContentType.JSON;
+ } else if ("html".equals(type)) {
+ blockingContentType = BlockingContentType.HTML;
+ }
+
+ if (!isRasp) {
+ reqCtx.setWafBlocked();
+ }
+
+ return new Flow.Action.RequestBlockingAction(statusCode, blockingContentType);
+ }
+
+ /** Create a redirect request action. */
+ private Flow.Action.RequestBlockingAction createRedirectRequestAction(
+ WAFModule.ActionInfo actionInfo, AppSecRequestContext reqCtx, boolean isRasp) {
+
+ Integer statusCode = (Integer) actionInfo.parameters.get("status_code");
+ if (statusCode == null) {
+ statusCode = 303;
+ }
+
+ String location = (String) actionInfo.parameters.get("location");
+ if (location == null) {
+ location = "https://example.com/";
+ }
+
+ Map extraHeaders = Collections.singletonMap("Location", location);
+
+ if (!isRasp) {
+ reqCtx.setWafBlocked();
+ }
+
+ return new Flow.Action.RequestBlockingAction(
+ statusCode, BlockingContentType.AUTO, extraHeaders);
+ }
+
+ /** Create an exploit stack trace event. */
+ private StackTraceEvent createExploitStackTraceEvent(String stackId) {
+ if (stackId == null || stackId.isEmpty()) {
+ return null;
+ }
+ List result = StackUtils.generateUserCodeStackTrace();
+ return new StackTraceEvent(result, "java", stackId, EXPLOIT_DETECTED_MSG);
+ }
+}
diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java
index 609b2fdb64d..9d41b2f39b9 100644
--- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java
+++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java
@@ -86,11 +86,11 @@ public class WAFModule implements AppSecModule {
private String rulesetVersion;
private WafBuilder wafBuilder;
- private static class ActionInfo {
+ public static class ActionInfo {
final String type;
final Map parameters;
- private ActionInfo(String type, Map parameters) {
+ public ActionInfo(String type, Map parameters) {
this.type = type;
this.parameters = parameters;
}
@@ -142,6 +142,7 @@ static void createLimitsObject() {
Config.get().isAppSecWafMetrics(); // could be static if not for tests
private final AtomicReference ctxAndAddresses = new AtomicReference<>();
private final RateLimiter rateLimiter;
+ private final TraceTaggingResultProcessor traceTaggingResultProcessor;
public WAFModule() {
this(null);
@@ -149,6 +150,7 @@ public WAFModule() {
public WAFModule(Monitoring monitoring) {
this.rateLimiter = getRateLimiter(monitoring);
+ this.traceTaggingResultProcessor = new TraceTaggingResultProcessor(rateLimiter);
}
@Override
@@ -361,85 +363,94 @@ public void onDataAvailable(
StandardizedLogging.inAppWafReturn(log, resultWithData);
- if (resultWithData.result != Waf.Result.OK) {
- if (log.isDebugEnabled()) {
- log.warn("WAF signalled result {}: {}", resultWithData.result, resultWithData.data);
- }
+ // Check if this is a new trace tagging result structure
+ if (isTraceTaggingResult(resultWithData)) {
+ // Handle new trace tagging result structure
+ WAFResultData.TraceTaggingResult traceTaggingResult =
+ convertToTraceTaggingResult(resultWithData);
+ traceTaggingResultProcessor.processResult(traceTaggingResult, flow, reqCtx, gwCtx);
+ } else {
+ // Handle legacy result structure
+ if (resultWithData.result != Waf.Result.OK) {
+ if (log.isDebugEnabled()) {
+ log.warn("WAF signalled result {}: {}", resultWithData.result, resultWithData.data);
+ }
- if (gwCtx.isRasp) {
- reqCtx.setRaspMatched(true);
- WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
- }
+ if (gwCtx.isRasp) {
+ reqCtx.setRaspMatched(true);
+ WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
+ }
- for (Map.Entry> action : resultWithData.actions.entrySet()) {
- String actionType = action.getKey();
- Map actionParams = action.getValue();
-
- ActionInfo actionInfo = new ActionInfo(actionType, actionParams);
-
- if ("block_request".equals(actionInfo.type)) {
- Flow.Action.RequestBlockingAction rba =
- createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
- flow.setAction(rba);
- } else if ("redirect_request".equals(actionInfo.type)) {
- Flow.Action.RequestBlockingAction rba =
- createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
- flow.setAction(rba);
- } else if ("generate_stack".equals(actionInfo.type)) {
- if (Config.get().isAppSecStackTraceEnabled()) {
- String stackId = (String) actionInfo.parameters.get("stack_id");
- StackTraceEvent stackTraceEvent = createExploitStackTraceEvent(stackId);
- reqCtx.reportStackTrace(stackTraceEvent);
+ for (Map.Entry> action : resultWithData.actions.entrySet()) {
+ String actionType = action.getKey();
+ Map actionParams = action.getValue();
+
+ ActionInfo actionInfo = new ActionInfo(actionType, actionParams);
+
+ if ("block_request".equals(actionInfo.type)) {
+ Flow.Action.RequestBlockingAction rba =
+ createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
+ flow.setAction(rba);
+ } else if ("redirect_request".equals(actionInfo.type)) {
+ Flow.Action.RequestBlockingAction rba =
+ createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
+ flow.setAction(rba);
+ } else if ("generate_stack".equals(actionInfo.type)) {
+ if (Config.get().isAppSecStackTraceEnabled()) {
+ String stackId = (String) actionInfo.parameters.get("stack_id");
+ StackTraceEvent stackTraceEvent = createExploitStackTraceEvent(stackId);
+ reqCtx.reportStackTrace(stackTraceEvent);
+ } else {
+ log.debug("Ignoring action with type generate_stack (disabled by config)");
+ }
} else {
- log.debug("Ignoring action with type generate_stack (disabled by config)");
- }
- } else {
- log.info("Ignoring action with type {}", actionInfo.type);
- if (!gwCtx.isRasp) {
- reqCtx.setWafRequestBlockFailure();
+ log.info("Ignoring action with type {}", actionInfo.type);
+ if (!gwCtx.isRasp) {
+ reqCtx.setWafRequestBlockFailure();
+ }
}
}
- }
- Collection events = buildEvents(resultWithData);
-
- if (!events.isEmpty()) {
- if (!reqCtx.isThrottled(rateLimiter)) {
- AgentSpan activeSpan = AgentTracer.get().activeSpan();
- if (activeSpan != null) {
- log.debug("Setting force-keep tag on the current span");
- // Keep event related span, because it could be ignored in case of
- // reduced datadog sampling rate.
- activeSpan.getLocalRootSpan().setTag(Tags.ASM_KEEP, true);
- // If APM is disabled, inform downstream services that the current
- // distributed trace contains at least one ASM event and must inherit
- // the given force-keep priority
- activeSpan
- .getLocalRootSpan()
- .setTag(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
+ Collection events = buildEvents(resultWithData);
+
+ if (!events.isEmpty()) {
+ if (!reqCtx.isThrottled(rateLimiter)) {
+ AgentSpan activeSpan = AgentTracer.get().activeSpan();
+ if (activeSpan != null) {
+ log.debug("Setting force-keep tag on the current span");
+ // Keep event related span, because it could be ignored in case of
+ // reduced datadog sampling rate.
+ activeSpan.getLocalRootSpan().setTag(Tags.ASM_KEEP, true);
+ // If APM is disabled, inform downstream services that the current
+ // distributed trace contains at least one ASM event and must inherit
+ // the given force-keep priority
+ activeSpan
+ .getLocalRootSpan()
+ .setTag(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
+ } else {
+ // If active span is not available the ASM_KEEP tag will be set in the GatewayBridge
+ // when the request ends
+ log.debug("There is no active span available");
+ }
+ reqCtx.reportEvents(events);
} else {
- // If active span is not available the ASM_KEEP tag will be set in the GatewayBridge
- // when the request ends
- log.debug("There is no active span available");
+ log.debug("Rate limited WAF events");
+ if (!gwCtx.isRasp) {
+ reqCtx.setWafRateLimited();
+ }
}
- reqCtx.reportEvents(events);
- } else {
- log.debug("Rate limited WAF events");
+ }
+
+ if (flow.isBlocking()) {
if (!gwCtx.isRasp) {
- reqCtx.setWafRateLimited();
+ reqCtx.setWafBlocked();
}
}
}
- if (flow.isBlocking()) {
- if (!gwCtx.isRasp) {
- reqCtx.setWafBlocked();
- }
+ if (resultWithData.derivatives != null) {
+ reqCtx.reportDerivatives(resultWithData.derivatives);
}
}
-
- if (resultWithData.derivatives != null) {
- reqCtx.reportDerivatives(resultWithData.derivatives);
- }
}
private Flow.Action.RequestBlockingAction createBlockRequestAction(
@@ -556,6 +567,86 @@ private Waf.ResultWithData runWafTransient(
new DataBundleMapWrapper(ctxAndAddr.addressesOfInterest, newData), LIMITS, metrics);
}
+ /**
+ * Check if the result is a new trace tagging result structure. For now, we'll use a simple
+ * heuristic based on the presence of trace tagging actions.
+ */
+ private boolean isTraceTaggingResult(Waf.ResultWithData resultWithData) {
+ // Check if the result has trace tagging actions
+ if (resultWithData.actions != null) {
+ for (String actionType : resultWithData.actions.keySet()) {
+ if ("trace_tagging".equals(actionType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Convert the legacy ResultWithData to the new TraceTaggingResult structure. */
+ private WAFResultData.TraceTaggingResult convertToTraceTaggingResult(
+ Waf.ResultWithData resultWithData) {
+ WAFResultData.TraceTaggingResult result = new WAFResultData.TraceTaggingResult();
+
+ // Map the fields from the legacy structure to the new structure
+ result.timeout = false; // Timeout is handled separately in the calling code
+ result.keep = extractKeepFlag(resultWithData);
+ result.duration = 0L; // Duration is not available in current structure
+ result.events = buildWafResultData(resultWithData);
+ result.actions = resultWithData.actions;
+ result.attributes = extractTraceAttributes(resultWithData);
+
+ return result;
+ }
+
+ /** Extract keep flag from trace tagging actions. */
+ private boolean extractKeepFlag(Waf.ResultWithData resultWithData) {
+ if (resultWithData.actions != null) {
+ Map traceTaggingAction = resultWithData.actions.get("trace_tagging");
+ if (traceTaggingAction != null) {
+ Object keepObj = traceTaggingAction.get("keep");
+ if (keepObj instanceof Boolean) {
+ return (Boolean) keepObj;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Extract trace attributes from the WAF result. */
+ private Map extractTraceAttributes(Waf.ResultWithData resultWithData) {
+ Map attributes = new HashMap<>();
+
+ // Extract attributes from trace tagging actions
+ if (resultWithData.actions != null) {
+ Map traceTaggingAction = resultWithData.actions.get("trace_tagging");
+ if (traceTaggingAction != null) {
+ Object attrs = traceTaggingAction.get("attributes");
+ if (attrs instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map attrMap = (Map) attrs;
+ attributes.putAll(attrMap);
+ }
+ }
+ }
+
+ return attributes;
+ }
+
+ private List buildWafResultData(Waf.ResultWithData actionWithData) {
+ List listResults;
+ try {
+ listResults = RES_JSON_ADAPTER.fromJson(actionWithData.data);
+ } catch (IOException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+
+ if (listResults != null && !listResults.isEmpty()) {
+ return listResults;
+ }
+ return emptyList();
+ }
+
private Collection buildEvents(Waf.ResultWithData actionWithData) {
Collection listResults;
try {
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..756167cc24b 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
@@ -1,13 +1,23 @@
package com.datadog.appsec.ddwaf;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class WAFResultData {
Rule rule;
List rule_matches;
String stack_id;
+ // Forbidden addresses that contain sensitive data and must not be allowed
+ private static final Set FORBIDDEN_ADDRESSES = new HashSet<>();
+
+ static {
+ FORBIDDEN_ADDRESSES.add("usr.session_id");
+ FORBIDDEN_ADDRESSES.add("server.request.cookies");
+ }
+
public static class RuleMatch {
String operator;
String operator_value;
@@ -18,6 +28,158 @@ public static class Rule {
public String id; // expose for log message
String name;
Map tags;
+ Output output; // optional output field for trace tagging support
+
+ /**
+ * Check if events should be generated for this rule. Backwards compatibility: if the output is
+ * null or keep is null, we default to true.
+ *
+ * @return true if events should be generated, false otherwise
+ */
+ public boolean shouldGenerateEvents() {
+ return output == null || output.event == null || output.event;
+ }
+
+ /**
+ * Check if the trace should be kept for this rule. Backwards compatibility: if the output is
+ * null or keep is null, we default to true.
+ *
+ * @return true if the trace should be kept, false otherwise
+ */
+ public boolean shouldKeepTrace() {
+ return output == null || output.keep == null || output.keep;
+ }
+
+ /**
+ * Get the attributes to be added to the trace.
+ *
+ * @return the attributes map, or null if no attributes
+ */
+ public Map getAttributes() {
+ return output != null ? output.attributes : null;
+ }
+ }
+
+ /** Represents the optional "output" field in a rule for trace tagging support. */
+ public static class Output {
+ private final Boolean keep;
+ private final Boolean event;
+ private final Map attributes;
+
+ public Output(Boolean keep, Boolean event, Map attributes) {
+ this.keep = keep;
+ this.event = event;
+ this.attributes = attributes;
+ }
+
+ /** Whether to keep the trace (set sampling priority). */
+ public Boolean getKeep() {
+ return keep;
+ }
+
+ /** Whether to generate events. */
+ public Boolean getEvent() {
+ return event;
+ }
+
+ /** Get the attributes to be added to the trace. */
+ public Map getAttributes() {
+ return attributes;
+ }
+ }
+
+ /**
+ * Represents an attribute value that can be either a literal value or extracted from request
+ * data.
+ */
+ public static class AttributeValue {
+ private final Object literalValue;
+ private final String address;
+ private final List keyPath;
+ private final List transformers;
+
+ /** Create a literal attribute value. */
+ public static AttributeValue literal(Object value) {
+ if (value != null && !isScalar(value)) {
+ throw new IllegalArgumentException(
+ "Literal values must be scalar (string, number, boolean)");
+ }
+ return new AttributeValue(value, null, null, null);
+ }
+
+ /** Create an attribute value extracted from request data. */
+ public static AttributeValue fromRequestData(
+ String address, List keyPath, List transformers) {
+ if (address == null || address.trim().isEmpty()) {
+ throw new IllegalArgumentException("Address cannot be null or empty");
+ }
+
+ // Check for forbidden addresses
+ if (FORBIDDEN_ADDRESSES.contains(address)) {
+ throw new IllegalArgumentException(
+ "Address '" + address + "' is forbidden as it contains sensitive data");
+ }
+
+ return new AttributeValue(null, address, keyPath, transformers);
+ }
+
+ private AttributeValue(
+ Object literalValue, String address, List keyPath, List transformers) {
+ this.literalValue = literalValue;
+ this.address = address;
+ this.keyPath = keyPath;
+ this.transformers = transformers;
+ }
+
+ /** Check if this is a literal value. */
+ public boolean isLiteral() {
+ return address == null;
+ }
+
+ /** Get the literal value (only valid if isLiteral() returns true). */
+ public Object getLiteralValue() {
+ return literalValue;
+ }
+
+ /** Get the address for request data extraction (only valid if isLiteral() returns false). */
+ public String getAddress() {
+ return address;
+ }
+
+ /** Get the key path for request data extraction (only valid if isLiteral() returns false). */
+ public List getKeyPath() {
+ return keyPath;
+ }
+
+ /**
+ * Get the transformers for request data extraction (only valid if isLiteral() returns false).
+ */
+ public List getTransformers() {
+ return transformers;
+ }
+
+ /** Check if a value is scalar (string, number, boolean). */
+ private static boolean isScalar(Object value) {
+ return value instanceof String
+ || value instanceof Number
+ || value instanceof Boolean
+ || value instanceof Character;
+ }
+
+ @Override
+ public String toString() {
+ if (isLiteral()) {
+ return "AttributeValue{literal=" + literalValue + "}";
+ } else {
+ return "AttributeValue{address='"
+ + address
+ + "', keyPath="
+ + keyPath
+ + ", transformers="
+ + transformers
+ + "}";
+ }
+ }
}
public static class Parameter extends MatchInfo {
@@ -32,4 +194,27 @@ public static class MatchInfo {
List