diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index 8647601dde5..d030901fa22 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation project(':internal-api') implementation project(':communication') implementation project(':telemetry') - implementation group: 'io.sqreen', name: 'libsqreen', version: '15.0.0' + implementation group: 'io.sqreen', name: 'libsqreen', version: '15.0.2' implementation libs.moshi testImplementation libs.bytebuddy 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 4d6eb78f864..eaafc29a4b6 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; @@ -37,8 +39,8 @@ import com.datadog.ddwaf.exception.InvalidRuleSetException; import com.datadog.ddwaf.exception.UnclassifiedWafException; import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; -import com.squareup.moshi.Types; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import datadog.remoteconfig.ConfigurationEndListener; import datadog.remoteconfig.ConfigurationPoller; import datadog.remoteconfig.PollingRateHinter; @@ -61,6 +63,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -92,15 +95,12 @@ public class AppSecConfigServiceImpl implements AppSecConfigService { new WAFInitializationResultReporter(); private final WAFStatsReporter statsReporter = new WAFStatsReporter(); - private static final JsonAdapter> ADAPTER = - new Moshi.Builder() - .build() - .adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); + private static final JsonAdapter ADAPTER = new SafeMapAdapter(); private boolean hasUserWafConfig; private boolean defaultConfigActivated; private final Set usedDDWafConfigKeys = new HashSet<>(); - private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG"; + private final String DEFAULT_WAF_CONFIG_RULE = "ASM_DD/default"; private String currentRuleVersion; private List modulesToUpdateVersionIn; @@ -131,6 +131,7 @@ private void subscribeConfigurationPoller() { long capabilities = CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -142,7 +143,8 @@ private void subscribeConfigurationPoller() { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT; + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES; if (tracerConfig.isAppSecRaspEnabled()) { capabilities |= CAPABILITY_ASM_RASP_SQLI; capabilities |= CAPABILITY_ASM_RASP_SSRF; @@ -185,7 +187,8 @@ public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollin } } else { Map contentMap = - ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); + (Map) + ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content)))); try { handleWafUpdateResultReport(configKey.toString(), contentMap); } catch (AppSecModule.AppSecModuleActivationException e) { @@ -211,7 +214,7 @@ private class AppSecConfigChangesDDListener extends AppSecConfigChangesListener public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter) throws IOException { if (defaultConfigActivated) { // if we get any config, remove the default one - log.debug("Removing default config"); + log.debug("Removing default config ASM_DD/default"); try { wafBuilder.removeConfig(DEFAULT_WAF_CONFIG_RULE); } catch (UnclassifiedWafException e) { @@ -425,7 +428,8 @@ private static Map loadDefaultWafConfig() throws IOException { throw new IOException("Resource " + DEFAULT_CONFIG_LOCATION + " not found"); } - Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); + Map ret = + (Map) ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, ""); if (log.isInfoEnabled()) { @@ -442,7 +446,8 @@ private static Map loadUserWafConfig(Config tracerConfig) throws return null; } try (InputStream is = new FileInputStream(filename)) { - Map ret = ADAPTER.fromJson(Okio.buffer(Okio.source(is))); + Map ret = + (Map) ADAPTER.fromJson(Okio.buffer(Okio.source(is))); StandardizedLogging._initialConfigSourceAndLibddwafVersion(log, filename); if (log.isInfoEnabled()) { @@ -471,6 +476,7 @@ public void close() { this.configurationPoller.removeCapabilities( CAPABILITY_ASM_ACTIVATION | CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -488,7 +494,8 @@ public void close() { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT); + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES); this.configurationPoller.removeListeners(Product.ASM_DD); this.configurationPoller.removeListeners(Product.ASM_DATA); this.configurationPoller.removeListeners(Product.ASM); @@ -558,4 +565,59 @@ private static WafConfig createWafConfig(Config config) { } return wafConfig; } + + private static class SafeMapAdapter extends JsonAdapter { + @Override + public Object fromJson(JsonReader reader) throws IOException { + switch (reader.peek()) { + case BEGIN_OBJECT: + Map map = new LinkedHashMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + map.put(reader.nextName(), fromJson(reader)); + } + reader.endObject(); + return map; + + case BEGIN_ARRAY: + List list = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + list.add(fromJson(reader)); + } + reader.endArray(); + return list; + + case STRING: + return reader.nextString(); + case NUMBER: + String numberStr = reader.nextString(); + try { + if (numberStr.contains(".")) { + return Double.parseDouble(numberStr); + } else { + return Long.parseLong(numberStr); + } + } catch (NumberFormatException e) { + // Fallback to string if parsing fails + return numberStr; + } + + case BOOLEAN: + return reader.nextBoolean(); + + case NULL: + reader.nextNull(); + return null; + + default: + throw new IllegalStateException("Unexpected token: " + reader.peek()); + } + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + throw new UnsupportedOperationException("Serialization not supported"); + } + } } 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 325c1313263..a200f9f7158 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 @@ -223,6 +223,11 @@ private void initOrUpdateWafHandle(AppSecModuleConfigurer.Reconfiguration reconf reconf.reloadSubscriptions(); } + /** + * Creates a rate limiter for AppSec events. The rate limiter accounts for when libddwaf returns + * keep with a value of true, rather than when events are present, as specified in the technical + * specification. + */ private static RateLimiter getRateLimiter(Monitoring monitoring) { if (monitoring == null) { return null; @@ -401,12 +406,13 @@ public void onDataAvailable( } } Collection events = buildEvents(resultWithData); + boolean isThrottled = reqCtx.isThrottled(rateLimiter); - if (!events.isEmpty()) { - if (!reqCtx.isThrottled(rateLimiter)) { + if (resultWithData.keep) { + if (!isThrottled) { AgentSpan activeSpan = AgentTracer.get().activeSpan(); if (activeSpan != null) { - log.debug("Setting force-keep tag on the current span"); + log.debug("Setting force-keep tag and manual 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); @@ -417,11 +423,9 @@ public void onDataAvailable( .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 + // If active span is not available then we need to set manual keep in GatewayBridge log.debug("There is no active span available"); } - reqCtx.reportEvents(events); } else { log.debug("Rate limited WAF events"); if (!gwCtx.isRasp) { @@ -429,6 +433,9 @@ public void onDataAvailable( } } } + if (resultWithData.events && !events.isEmpty() && !isThrottled) { + reqCtx.reportEvents(events); + } if (flow.isBlocking()) { if (!gwCtx.isRasp) { @@ -437,8 +444,8 @@ public void onDataAvailable( } } - if (resultWithData.derivatives != null) { - reqCtx.reportDerivatives(resultWithData.derivatives); + if (resultWithData.attributes != null && !resultWithData.attributes.isEmpty()) { + reqCtx.reportDerivatives(resultWithData.attributes); } } @@ -559,6 +566,10 @@ private Waf.ResultWithData runWafTransient( private Collection buildEvents(Waf.ResultWithData actionWithData) { Collection listResults; try { + if (actionWithData.data == null || actionWithData.data.isEmpty()) { + log.debug("WAF returned no data"); + return emptyList(); + } listResults = RES_JSON_ADAPTER.fromJson(actionWithData.data); } catch (IOException e) { throw new UndeclaredThrowableException(e); diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java index 376b0448591..9a9eab02511 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java @@ -108,7 +108,7 @@ public class AppSecRequestContext implements DataBundle, Closeable { private boolean responseBodyPublished; private boolean respDataPublished; private boolean pathParamsPublished; - private volatile Map derivatives; + private volatile Map derivatives; private final AtomicBoolean rateLimited = new AtomicBoolean(false); private volatile boolean throttled; @@ -645,24 +645,258 @@ List getStackTraces() { return stackTraces; } - public void reportDerivatives(Map data) { + /** + * Attempts to parse a string value as a number. Returns the parsed number if successful, null + * otherwise. Tries to parse as integer first, then as double if it contains a decimal point. + */ + private static Number convertToNumericAttribute(String value) { + if (value == null || value.isEmpty()) { + return null; + } + try { + // Check if it contains a decimal point to determine if it's a double + if (value.contains(".")) { + return Double.parseDouble(value); + } else { + // Try to parse as integer first + return Long.parseLong(value); + } + } catch (NumberFormatException e) { + return null; + } + } + + public void reportDerivatives(Map data) { log.debug("Reporting derivatives: {}", data); if (data == null || data.isEmpty()) return; + // Store raw derivatives if (derivatives == null) { - derivatives = data; - } else { - derivatives.putAll(data); + derivatives = new HashMap<>(); + } + + // Process each attribute according to the specification + for (Map.Entry entry : data.entrySet()) { + String attributeKey = entry.getKey(); + Object attributeConfig = entry.getValue(); + + if (attributeConfig instanceof Map) { + @SuppressWarnings("unchecked") + Map config = (Map) attributeConfig; + + // Check if it's a literal value schema + if (config.containsKey("value")) { + Object literalValue = config.get("value"); + if (literalValue != null) { + // Preserve the original type - don't convert to string + derivatives.put(attributeKey, literalValue); + log.debug( + "Added literal attribute: {} = {} (type: {})", + attributeKey, + literalValue, + literalValue.getClass().getSimpleName()); + } + } + // Check if it's a request data schema + else if (config.containsKey("address")) { + String address = (String) config.get("address"); + @SuppressWarnings("unchecked") + List keyPath = (List) config.get("key_path"); + @SuppressWarnings("unchecked") + List transformers = (List) config.get("transformers"); + + Object extractedValue = extractValueFromRequestData(address, keyPath, transformers); + if (extractedValue != null) { + // For extracted values, convert to string as they come from request data + derivatives.put(attributeKey, extractedValue.toString()); + log.debug("Added extracted attribute: {} = {}", attributeKey, extractedValue); + } + } + } else { + // Handle plain string/numeric values + derivatives.put(attributeKey, attributeConfig); + log.debug("Added direct attribute: {} = {}", attributeKey, attributeConfig); + } + } + } + + /** + * Extracts a value from request data based on address, key path, and transformers. + * + * @param address The address to extract from (e.g., "server.request.headers") + * @param keyPath Optional key path to navigate the data structure + * @param transformers Optional list of transformers to apply + * @return The extracted value, or null if not found + */ + private Object extractValueFromRequestData( + String address, List keyPath, List transformers) { + // Get the data from the address + Object data = getDataForAddress(address); + if (data == null) { + log.debug("No data found for address: {}", address); + return null; + } + + // Navigate through the key path + Object currentValue = data; + if (keyPath != null && !keyPath.isEmpty()) { + currentValue = navigateKeyPath(currentValue, keyPath); + if (currentValue == null) { + log.debug("Could not navigate key path {} for address {}", keyPath, address); + return null; + } + } + + // Apply transformers if specified + if (transformers != null && !transformers.isEmpty()) { + currentValue = applyTransformers(currentValue, transformers); + } + + return currentValue; + } + + /** Gets data for a specific address from the request context. */ + private Object getDataForAddress(String address) { + // Map common addresses to our data structures + switch (address) { + case "server.request.headers": + return requestHeaders; + case "server.response.headers": + return responseHeaders; + case "server.request.cookies": + return collectedCookies; + case "server.request.uri.raw": + return savedRawURI; + case "server.request.method": + return method; + case "server.request.scheme": + return scheme; + case "server.request.route": + return route; + case "server.response.status": + return responseStatus; + case "server.request.body": + return getStoredRequestBody(); + case "usr.id": + return userId; + case "usr.login": + return userLogin; + case "usr.session_id": + return sessionId; + default: + log.debug("Unknown address: {}", address); + return null; + } + } + + /** Navigates through a data structure using a key path. */ + private Object navigateKeyPath(Object data, List keyPath) { + Object current = data; + + for (String key : keyPath) { + if (current instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) current; + current = map.get(key); + } else if (current instanceof List) { + try { + int index = Integer.parseInt(key); + @SuppressWarnings("unchecked") + List list = (List) current; + if (index >= 0 && index < list.size()) { + current = list.get(index); + } else { + return null; + } + } catch (NumberFormatException e) { + log.debug("Invalid list index: {}", key); + return null; + } + } else { + log.debug("Cannot navigate key {} in data type: {}", key, current.getClass()); + return null; + } + + if (current == null) { + return null; + } + } + + return current; + } + + /** Applies transformers to a value. */ + private Object applyTransformers(Object value, List transformers) { + Object current = value; + + for (String transformer : transformers) { + switch (transformer) { + case "lowercase": + if (current instanceof String) { + current = ((String) current).toLowerCase(); + } + break; + case "uppercase": + if (current instanceof String) { + current = ((String) current).toUpperCase(); + } + break; + case "trim": + if (current instanceof String) { + current = ((String) current).trim(); + } + break; + case "length": + if (current instanceof String) { + current = ((String) current).length(); + } else if (current instanceof List) { + current = ((List) current).size(); + } else if (current instanceof Map) { + current = ((Map) current).size(); + } + break; + default: + log.debug("Unknown transformer: {}", transformer); + break; + } } + + return current; } public boolean commitDerivatives(TraceSegment traceSegment) { log.debug("Committing derivatives: {} for {}", derivatives, traceSegment); - if (traceSegment == null || derivatives == null) { + if (traceSegment == null) { return false; } - derivatives.forEach(traceSegment::setTagTop); - log.debug("Committed derivatives: {} for {}", derivatives, traceSegment); + + // Process and commit derivatives directly + if (derivatives != null && !derivatives.isEmpty()) { + for (Map.Entry entry : derivatives.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // Handle different value types + if (value instanceof Number) { + traceSegment.setTagTop(key, (Number) value); + } else if (value instanceof String) { + // Try to parse as numeric, otherwise use as string + Number parsedNumber = convertToNumericAttribute((String) value); + if (parsedNumber != null) { + traceSegment.setTagTop(key, parsedNumber); + } else { + traceSegment.setTagTop(key, value); + } + } else if (value instanceof Boolean) { + traceSegment.setTagTop(key, value); + } else { + // Convert other types to string + traceSegment.setTagTop(key, value.toString()); + } + } + } + + // Clear all attribute maps derivatives = null; return true; } diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index c055f709a74..1728aec7545 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -21,6 +21,7 @@ import com.datadog.appsec.report.AppSecEvent; import com.datadog.appsec.report.AppSecEventWrapper; import datadog.trace.api.Config; +import datadog.trace.api.DDTags; import datadog.trace.api.ProductTraceSource; import datadog.trace.api.gateway.Events; import datadog.trace.api.gateway.Flow; @@ -738,6 +739,8 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { // AppSec report metric and events for web span only if (traceSeg != null) { + // Set keep to manual in case that root span was not available when events are detected + traceSeg.setTagTop(DDTags.MANUAL_KEEP, true); traceSeg.setTagTop("_dd.appsec.enabled", 1); traceSeg.setTagTop("_dd.runtime_family", "jvm"); @@ -749,8 +752,6 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { // If detected any events - mark span at appsec.event if (!collectedEvents.isEmpty()) { - // Set asm keep in case that root span was not available when events are detected - traceSeg.setTagTop(Tags.ASM_KEEP, true); traceSeg.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); traceSeg.setTagTop("appsec.event", true); traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress()); diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index 07fcae0da20..98d14c0123e 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -40,6 +40,8 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRI 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 +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_MULTICONFIG +import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRACE_TAGGING_RULES import static datadog.remoteconfig.PollingHinterNoop.NOOP import static datadog.trace.api.UserIdCollectionMode.ANONYMIZATION import static datadog.trace.api.UserIdCollectionMode.DISABLED @@ -257,6 +259,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -272,7 +275,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES) 0 * _._ when: @@ -408,6 +412,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -423,7 +428,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES) 0 * _._ when: @@ -503,6 +509,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { then: 1 * poller.removeCapabilities(CAPABILITY_ASM_ACTIVATION | CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -520,7 +527,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES) 4 * poller.removeListeners(_) 1 * poller.removeConfigurationEndListener(_) 1 * poller.stop() @@ -583,6 +591,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.addConfigurationEndListener(_) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -599,7 +608,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES) 0 * _._ cleanup: @@ -651,6 +661,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION) 1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE) 1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES + | CAPABILITY_ASM_DD_MULTICONFIG | CAPABILITY_ASM_IP_BLOCKING | CAPABILITY_ASM_EXCLUSIONS | CAPABILITY_ASM_EXCLUSION_DATA @@ -666,7 +677,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { | CAPABILITY_ENDPOINT_FINGERPRINT | CAPABILITY_ASM_SESSION_FINGERPRINT | CAPABILITY_ASM_NETWORK_FINGERPRINT - | CAPABILITY_ASM_HEADER_FINGERPRINT) + | CAPABILITY_ASM_HEADER_FINGERPRINT + | CAPABILITY_ASM_TRACE_TAGGING_RULES) 0 * _._ when: 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 7cddcd62523..80771efde75 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 @@ -1326,7 +1326,7 @@ class WAFModuleSpecification extends DDSpecification { void 'bad ResultWithData - empty list'() { def waf = new WAFModule() - Waf.ResultWithData rwd = new Waf.ResultWithData(null, "[]", null, null) + Waf.ResultWithData rwd = new Waf.ResultWithData(null, "[]", null, null, false, 0, false) Collection ret when: @@ -1338,7 +1338,7 @@ class WAFModuleSpecification extends DDSpecification { void 'bad ResultWithData - empty object'() { def waf = new WAFModule() - Waf.ResultWithData rwd = new Waf.ResultWithData(null, "[{}]", null, null) + Waf.ResultWithData rwd = new Waf.ResultWithData(null, "[{}]", null, null, false, 0, false) Collection ret when: @@ -1695,4 +1695,209 @@ class WAFModuleSpecification extends DDSpecification { throw new IllegalStateException("Unhandled WafErrorCode: $code") } } + + void 'test rules_compat with output attributes'() { + setup: + def rulesConfig = [ + version: '2.1', + metadata: [ + rules_version: '1.2.7' + ], + rules: [ + [ + id: 'arachni_rule', + name: 'Arachni', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^Arachni\\/v' + ], + operator: 'match_regex' + ] + ], + transformers: [], + on_match: ['block'] + ] + ], + rules_compat: [ + [ + id: 'rc-000-001', + name: 'Rules Compat Test: Attributes, No Keep, No Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^RulesCompat\\/v1' + ], + operator: 'match_regex' + ] + ], + output: [ + event: false, + keep: false, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 123456789 + ], + '_dd.appsec.trace.agent': [ + value: 'RulesCompat/v1' + ] + ] + ], + on_match: [] + ], + [ + id: 'rc-000-002', + name: 'Rules Compat Test: Attributes, Keep, No Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^RulesCompat\\/v2' + ], + operator: 'match_regex' + ] + ], + output: [ + event: false, + keep: true, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 987654321 + ], + '_dd.appsec.trace.agent': [ + value: 'RulesCompat/v2' + ] + ] + ], + on_match: [] + ], + [ + id: 'rc-000-003', + name: 'Rules Compat Test: Attributes, Keep, Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^RulesCompat\\/v3' + ], + operator: 'match_regex' + ] + ], + output: [ + event: true, + keep: true, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 555666777 + ], + '_dd.appsec.trace.agent': [ + value: 'RulesCompat/v3' + ] + ] + ], + on_match: [] + ] + ] + ] + + when: + initialRuleAddWithMap(rulesConfig) + wafModule.applyConfig(reconf) + + then: + 1 * wafMetricCollector.wafInit(Waf.LIB_VERSION, _, true) + 1 * wafMetricCollector.wafUpdates(_, true) + 1 * reconf.reloadSubscriptions() + 0 * _ + + when: 'test rules_compat rule with attributes, no keep and no event' + def bundle1 = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': 'RulesCompat/v1'])) + def flow1 = new ChangeableFlow() + dataListener.onDataAvailable(flow1, ctx, bundle1, gwCtx) + ctx.closeWafContext() + + then: + 1 * ctx.getOrCreateWafContext(_, true, false) + 2 * ctx.getWafMetrics() >> metrics + 1 * ctx.isWafContextClosed() >> false + 1 * ctx.closeWafContext() + 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v1', '_dd.appsec.trace.integer': 123456789]) + 1 * ctx.isThrottled(null) + 0 * ctx._(*_) + !flow1.blocking + + when: 'test rules_compat rule with attributes, keep and no event' + def bundle2 = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': 'RulesCompat/v2'])) + def flow2 = new ChangeableFlow() + dataListener.onDataAvailable(flow2, ctx, bundle2, gwCtx) + ctx.closeWafContext() + + then: + 1 * ctx.getOrCreateWafContext(_, true, false) + 2 * ctx.getWafMetrics() >> metrics + 1 * ctx.isWafContextClosed() >> false + 1 * ctx.closeWafContext() + 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v2', '_dd.appsec.trace.integer': 987654321]) + 1 * ctx.isThrottled(null) + 0 * ctx._(*_) + !flow2.blocking + + when: 'test rules_compat rule with attributes, keep and event' + def bundle3 = MapDataBundle.of(KnownAddresses.HEADERS_NO_COOKIES, + new CaseInsensitiveMap>(['user-agent': 'RulesCompat/v3'])) + def flow3 = new ChangeableFlow() + dataListener.onDataAvailable(flow3, ctx, bundle3, gwCtx) + ctx.closeWafContext() + + then: + 1 * ctx.getOrCreateWafContext(_, true, false) + 2 * ctx.getWafMetrics() >> metrics + 1 * ctx.isWafContextClosed() >> false + 1 * ctx.closeWafContext() + 1 * ctx.reportDerivatives(['_dd.appsec.trace.agent':'RulesCompat/v3', '_dd.appsec.trace.integer': 555666777]) + 1 * ctx.reportEvents(_ as Collection) + 1 * ctx.isThrottled(null) + 0 * ctx._(*_) + !flow3.blocking + } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy index 0b3b2c91bd7..00074d3776b 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/AppSecRequestContextSpecification.groovy @@ -312,4 +312,125 @@ class AppSecRequestContextSpecification extends DDSpecification { cleanup: TestLogCollector.disable() } + void 'test that processed attributes are cleared on close'() { + setup: + def derivatives = [ + 'numeric': '42', + 'string': 'value' + ] + + when: + ctx.reportDerivatives(derivatives) + ctx.close() + + then: + ctx.getDerivativeKeys().isEmpty() + } + + def "test attribute handling with literal values and request data extraction"() { + given: + def context = new AppSecRequestContext() + context.setMethod("POST") + context.setScheme("https") + context.setRawURI("/api/test") + context.setRoute("/api/{param}") + context.setResponseStatus(200) + context.addRequestHeader("user-agent", "TestAgent/1.0") + context.addRequestHeader("content-type", "application/json") + + // Test data for attributes + def attributes = [ + "_dd.appsec.s.res.headers": [ + "value": "literal-header-value" + ], + "_dd.appsec.s.res.method": [ + "address": "server.request.method" + ], + "_dd.appsec.s.res.scheme": [ + "address": "server.request.scheme" + ], + "_dd.appsec.s.res.uri": [ + "address": "server.request.uri.raw" + ], + "_dd.appsec.s.res.route": [ + "address": "server.request.route" + ], + "_dd.appsec.s.res.status": [ + "address": "server.response.status" + ], + "_dd.appsec.s.res.user_agent": [ + "address": "server.request.headers", + "key_path": ["user-agent"] + ], + "_dd.appsec.s.res.content_type": [ + "address": "server.request.headers", + "key_path": ["content-type"] + ], + "_dd.appsec.s.res.user_agent_lower": [ + "address": "server.request.headers", + "key_path": ["user-agent"], + "transformers": ["lowercase"] + ], + "_dd.appsec.s.res.content_type_upper": [ + "address": "server.request.headers", + "key_path": ["content-type"], + "transformers": ["uppercase"] + ] + ] + + when: + context.reportDerivatives(attributes) + def keys = context.getDerivativeKeys() + + then: + keys.size() == 10 + keys.contains("_dd.appsec.s.res.headers") + keys.contains("_dd.appsec.s.res.method") + keys.contains("_dd.appsec.s.res.scheme") + keys.contains("_dd.appsec.s.res.uri") + keys.contains("_dd.appsec.s.res.route") + keys.contains("_dd.appsec.s.res.status") + keys.contains("_dd.appsec.s.res.user_agent") + keys.contains("_dd.appsec.s.res.content_type") + keys.contains("_dd.appsec.s.res.user_agent_lower") + keys.contains("_dd.appsec.s.res.content_type_upper") + } + + def "test attribute handling with unknown address"() { + given: + def context = new AppSecRequestContext() + + def attributes = [ + "_dd.appsec.s.res.unknown": [ + "address": "server.request.unknown" + ] + ] + + when: + context.reportDerivatives(attributes) + def keys = context.getDerivativeKeys() + + then: + keys.size() == 0 // No attributes should be added for unknown addresses + } + + def "test attribute handling with invalid key path"() { + given: + def context = new AppSecRequestContext() + context.addRequestHeader("user-agent", "TestAgent/1.0") + + def attributes = [ + "_dd.appsec.s.res.invalid": [ + "address": "server.request.headers", + "key_path": ["non-existent-header"] + ] + ] + + when: + context.reportDerivatives(attributes) + def keys = context.getDerivativeKeys() + + then: + keys.size() == 0 // No attributes should be added for invalid key paths + } } diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy new file mode 100644 index 00000000000..6d5fb584c5d --- /dev/null +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/TraceTaggingSmokeTest.groovy @@ -0,0 +1,333 @@ +package datadog.smoketest.appsec + +import groovy.json.JsonSlurper +import okhttp3.Request +import spock.lang.Shared + +class TraceTaggingSmokeTest extends AbstractAppSecServerSmokeTest { + + @Override + def logLevel() { + 'DEBUG' + } + + @Shared + String buildDir = new File(System.getProperty("datadog.smoketest.builddir")).absolutePath + @Shared + String customRulesPath = "${buildDir}/appsec_custom_rules.json" + + def prepareCustomRules() { + // Create a custom rules file with rules_compat section + def rulesContent = [ + version: '2.1', + metadata: [ + rules_version: '1.2.7' + ], + rules: [ + [ + id: 'arachni_rule', + name: 'Arachni', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^Arachni\\/v' + ], + operator: 'match_regex' + ] + ], + transformers: [], + on_match: ['block'] + ] + ], + rules_compat: [ + [ + id: 'ttr-000-001', + name: 'Trace Tagging Rule: Attributes, No Keep, No Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v1' + ], + operator: 'match_regex' + ] + ], + output: [ + event: false, + keep: false, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 662607015 + ], + '_dd.appsec.trace.agent': [ + value: 'TraceTagging/v1' + ] + ] + ], + on_match: [] + ], + [ + id: 'ttr-000-002', + name: 'Trace Tagging Rule: Attributes, Keep, No Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v2' + ], + operator: 'match_regex' + ] + ], + output: [ + event: false, + keep: true, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 602214076 + ], + '_dd.appsec.trace.agent': [ + value: 'TraceTagging/v2' + ] + ] + ], + on_match: [] + ], + [ + id: 'ttr-000-003', + name: 'Trace Tagging Rule: Attributes, Keep, Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v3' + ], + operator: 'match_regex' + ] + ], + output: [ + event: true, + keep: true, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 299792458 + ], + '_dd.appsec.trace.agent': [ + value: 'TraceTagging/v3' + ] + ] + ], + on_match: [] + ], + [ + id: 'ttr-000-004', + name: 'Trace Tagging Rule: Attributes, No Keep, Event', + tags: [ + type: 'security_scanner', + category: 'attack_attempt' + ], + conditions: [ + [ + parameters: [ + inputs: [ + [ + address: 'server.request.headers.no_cookies', + key_path: ['user-agent'] + ] + ], + regex: '^TraceTagging\\/v4' + ], + operator: 'match_regex' + ] + ], + output: [ + event: true, + keep: false, + attributes: [ + '_dd.appsec.trace.integer': [ + value: 1729 + ], + '_dd.appsec.trace.agent': [ + value: 'TraceTagging/v4' + ] + ] + ], + on_match: [] + ] + ] + ] + + // Write the custom rules to file + def gen = new groovy.json.JsonGenerator.Options().build() + new File(customRulesPath).withWriter { writer -> + writer.write(gen.toJson(rulesContent)) + } + + // Add a new property pointing to the new ruleset + defaultAppSecProperties += "-Ddd.appsec.rules=${customRulesPath}" as String + } + + @Override + ProcessBuilder createProcessBuilder() { + // We run this here to ensure it runs before starting the process. Child setupSpec runs after parent setupSpec, + // so it is not a valid location. + prepareCustomRules() + + String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot.shadowJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(defaultAppSecProperties) + command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"]) + + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + def "test trace tagging rule with attributes, no keep and no event"() { + when: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "TraceTagging/v1") + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount(1) + + then: + responseBodyStr == "Sup AppSec Dawg" + response.code() == 200 + rootSpans.size() == 1 + + def rootSpan = rootSpans[0] + assert rootSpan.meta['_dd.appsec.trace.agent'] != null, "Missing _dd.appsec.trace.agent from span's meta" + assert rootSpan.metrics['_dd.appsec.trace.integer'] != null, "Missing _dd.appsec.trace.integer from span's metrics" + + assert rootSpan.meta['_dd.appsec.trace.agent'].startsWith("TraceTagging/v1") + assert rootSpan.metrics['_dd.appsec.trace.integer'] == 662607015 + } + + def "test trace tagging rule with attributes, keep and no event"() { + when: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "TraceTagging/v2") + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount(1) + + then: + responseBodyStr == "Sup AppSec Dawg" + response.code() == 200 + rootSpans.size() == 1 + + def rootSpan = rootSpans[0] + assert rootSpan.meta['_dd.appsec.trace.agent'] != null, "Missing _dd.appsec.trace.agent from span's meta" + assert rootSpan.metrics['_dd.appsec.trace.integer'] != null, "Missing _dd.appsec.trace.integer from span's metrics" + + assert rootSpan.meta['_dd.appsec.trace.agent'].startsWith("TraceTagging/v2") + assert rootSpan.metrics['_dd.appsec.trace.integer'] == 602214076 + assert rootSpan.metrics.get('_sampling_priority_v1') == 2 // USER_KEEP + } + + def "test trace tagging rule with attributes, keep and event"() { + when: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "TraceTagging/v3") + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount(1) + + then: + responseBodyStr == "Sup AppSec Dawg" + response.code() == 200 + rootSpans.size() == 1 + + def rootSpan = rootSpans[0] + assert rootSpan.meta['_dd.appsec.trace.agent'] != null, "Missing _dd.appsec.trace.agent from span's meta" + assert rootSpan.metrics['_dd.appsec.trace.integer'] != null, "Missing _dd.appsec.trace.integer from span's metrics" + + assert rootSpan.meta['_dd.appsec.trace.agent'].startsWith("TraceTagging/v3") + assert rootSpan.metrics['_dd.appsec.trace.integer'] == 299792458 + assert rootSpan.metrics.get('_sampling_priority_v1') == 2 // USER_KEEP + + // Check for WAF attack event + assert rootSpan.meta['_dd.appsec.json'] != null, "Missing WAF attack event" + def appsecJson = new JsonSlurper().parseText(rootSpan.meta['_dd.appsec.json']) + assert appsecJson.triggers != null, "Missing triggers in WAF attack event" + } + + def "test trace tagging rule with attributes, no keep and event"() { + when: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "TraceTagging/v4") + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount(1) + + then: + responseBodyStr == "Sup AppSec Dawg" + response.code() == 200 + rootSpans.size() == 1 + + def rootSpan = rootSpans[0] + assert rootSpan.meta['_dd.appsec.trace.agent'] != null, "Missing _dd.appsec.trace.agent from span's meta" + assert rootSpan.metrics['_dd.appsec.trace.integer'] != null, "Missing _dd.appsec.trace.integer from span's metrics" + + assert rootSpan.meta['_dd.appsec.trace.agent'].startsWith("TraceTagging/v4") + assert rootSpan.metrics['_dd.appsec.trace.integer'] == 1729 + assert rootSpan.metrics.get('_sampling_priority_v1') < 2 // Should NOT be USER_KEEP + + // Check for WAF attack event + assert rootSpan.meta['_dd.appsec.json'] != null, "Missing WAF attack event" + def appsecJson = new JsonSlurper().parseText(rootSpan.meta['_dd.appsec.json']) + assert appsecJson.triggers != null, "Missing triggers in WAF attack event" + } + +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 482d1efe09e..60edd892706 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -515,7 +515,11 @@ public void forceKeep(byte samplingMechanism) { private void forceKeepThisSpan(byte samplingMechanism) { // if the user really wants to keep this trace chunk, we will let them, // even if the old sampling priority and mechanism have already propagated - if (SAMPLING_PRIORITY_UPDATER.getAndSet(this, PrioritySampling.USER_KEEP) + if (samplingMechanism == SamplingMechanism.MANUAL + && SAMPLING_PRIORITY_UPDATER.getAndSet(this, PrioritySampling.SAMPLER_KEEP) + == PrioritySampling.UNSET) { + propagationTags.updateTraceSamplingPriority(PrioritySampling.SAMPLER_KEEP, samplingMechanism); + } else if (SAMPLING_PRIORITY_UPDATER.getAndSet(this, PrioritySampling.USER_KEEP) == PrioritySampling.UNSET) { propagationTags.updateTraceSamplingPriority(PrioritySampling.USER_KEEP, samplingMechanism); } diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index b8e1124a1b2..6c237d84163 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -42,4 +42,6 @@ public interface Capabilities { long CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY = 1L << 39; long CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN = 1L << 40; long CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING = 1L << 41; + long CAPABILITY_ASM_DD_MULTICONFIG = 1L << 42; + long CAPABILITY_ASM_TRACE_TAGGING_RULES = 1L << 43; }