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 faec447c2c4..f4473aff6f3 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 @@ -77,6 +77,9 @@ public class AppSecRequestContext implements DataBundle, Closeable { public static final Set RESPONSE_HEADERS_ALLOW_LIST = new TreeSet<>( Arrays.asList("content-length", "content-type", "content-encoding", "content-language")); + public static final int DD_WAF_RUN_INTERNAL_ERROR = -3; + public static final int DD_WAF_RUN_INVALID_OBJECT_ERROR = -2; + public static final int DD_WAF_RUN_INVALID_ARGUMENT_ERROR = -1; static { REQUEST_HEADERS_ALLOW_LIST.addAll(DEFAULT_REQUEST_HEADERS_ALLOW_LIST); @@ -124,6 +127,9 @@ public class AppSecRequestContext implements DataBundle, Closeable { private volatile boolean blocked; private volatile int wafTimeouts; private volatile int raspTimeouts; + private volatile int raspInternalErrors; + private volatile int raspInvalidObjectErrors; + private volatile int raspInvalidArgumentErrors; // keep a reference to the last published usr.id private volatile String userId; @@ -139,6 +145,18 @@ public class AppSecRequestContext implements DataBundle, Closeable { private static final AtomicIntegerFieldUpdater RASP_TIMEOUTS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspTimeouts"); + private static final AtomicIntegerFieldUpdater + RASP_INTERNAL_ERRORS_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspInternalErrors"); + private static final AtomicIntegerFieldUpdater + RASP_INVALID_OBJECT_ERRORS_UPDATER = + AtomicIntegerFieldUpdater.newUpdater( + AppSecRequestContext.class, "raspInvalidObjectErrors"); + private static final AtomicIntegerFieldUpdater + RASP_INVALID_ARGUMENT_ERRORS_UPDATER = + AtomicIntegerFieldUpdater.newUpdater( + AppSecRequestContext.class, "raspInvalidArgumentErrors"); + // to be called by the Event Dispatcher public void addAll(DataBundle newData) { for (Map.Entry, Object> entry : newData) { @@ -188,6 +206,22 @@ public void increaseRaspTimeouts() { RASP_TIMEOUTS_UPDATER.incrementAndGet(this); } + public void increaseRaspErrorCode(int code) { + switch (code) { + case DD_WAF_RUN_INTERNAL_ERROR: + RASP_INTERNAL_ERRORS_UPDATER.incrementAndGet(this); + break; + case DD_WAF_RUN_INVALID_OBJECT_ERROR: + RASP_INVALID_OBJECT_ERRORS_UPDATER.incrementAndGet(this); + break; + case DD_WAF_RUN_INVALID_ARGUMENT_ERROR: + RASP_INVALID_ARGUMENT_ERRORS_UPDATER.incrementAndGet(this); + break; + default: + break; + } + } + public int getWafTimeouts() { return wafTimeouts; } @@ -196,6 +230,19 @@ public int getRaspTimeouts() { return raspTimeouts; } + public int getRaspError(int code) { + switch (code) { + case DD_WAF_RUN_INTERNAL_ERROR: + return raspInternalErrors; + case DD_WAF_RUN_INVALID_OBJECT_ERROR: + return raspInvalidObjectErrors; + case DD_WAF_RUN_INVALID_ARGUMENT_ERROR: + return raspInvalidArgumentErrors; + default: + return 0; + } + } + public Additive getOrCreateAdditive(PowerwafContext ctx, boolean createMetrics, boolean isRasp) { if (createMetrics) { diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java index 35562832c89..233ba236756 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java @@ -47,6 +47,7 @@ import io.sqreen.powerwaf.exception.AbstractPowerwafException; import io.sqreen.powerwaf.exception.InvalidRuleSetException; import io.sqreen.powerwaf.exception.TimeoutPowerwafException; +import io.sqreen.powerwaf.exception.UnclassifiedPowerwafException; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; @@ -440,11 +441,21 @@ public void onDataAvailable( log.debug(LogCollector.EXCLUDE_TELEMETRY, "Timeout calling the WAF", tpe); } return; - } catch (AbstractPowerwafException e) { + } catch (UnclassifiedPowerwafException e) { if (!reqCtx.isAdditiveClosed()) { log.error("Error calling WAF", e); } return; + } catch (AbstractPowerwafException e) { + if (gwCtx.isRasp) { + reqCtx.increaseRaspErrorCode(e.code); + WafMetricCollector.get().raspErrorCode(gwCtx.raspRuleType, e.code); + } else { + // TODO APPSEC-56703 + // reqCtx.increaseWafErrorCode(e.code); + // WafMetricCollector.get().wafErrorCode(e.code); + } + return; } finally { if (log.isDebugEnabled()) { long elapsed = System.currentTimeMillis() - start; 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 9552feba11d..bec280f1f20 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 @@ -290,4 +290,17 @@ class AppSecRequestContextSpecification extends DDSpecification { then: ctx.getRaspTimeouts() == 2 } + + def "test increase and get RaspErrors"() { + when: + ctx.increaseRaspErrorCode(AppSecRequestContext.DD_WAF_RUN_INTERNAL_ERROR) + ctx.increaseRaspErrorCode(AppSecRequestContext.DD_WAF_RUN_INTERNAL_ERROR) + ctx.increaseRaspErrorCode(AppSecRequestContext.DD_WAF_RUN_INVALID_OBJECT_ERROR) + + then: + ctx.getRaspError(AppSecRequestContext.DD_WAF_RUN_INTERNAL_ERROR) == 2 + ctx.getRaspError(AppSecRequestContext.DD_WAF_RUN_INVALID_OBJECT_ERROR) == 1 + ctx.getRaspError(AppSecRequestContext.DD_WAF_RUN_INVALID_ARGUMENT_ERROR) == 0 + ctx.getRaspError(0) == 0 + } } diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java index 9266b74691c..b0e19dc9264 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/WafMetricCollector.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; @@ -13,6 +15,8 @@ public class WafMetricCollector implements MetricCollector { public static WafMetricCollector INSTANCE = new WafMetricCollector(); + private static final int ABSTRACT_POWERWAF_EXCEPTION_NUMBER = + 3; // only 3 error codes are possible for now in AbstractPowerwafException public static WafMetricCollector get() { return WafMetricCollector.INSTANCE; @@ -40,6 +44,15 @@ private WafMetricCollector() { new AtomicLongArray(RuleType.getNumValues()); private static final AtomicLongArray raspTimeoutCounter = new AtomicLongArray(RuleType.getNumValues()); + private static final ConcurrentMap raspErrorCodeCounter = + new ConcurrentSkipListMap<>(); + + static { + for (int i = -1 * ABSTRACT_POWERWAF_EXCEPTION_NUMBER; i < 0; i++) { + raspErrorCodeCounter.put(i, new AtomicLongArray(RuleType.getNumValues())); + } + } + private static final AtomicLongArray missingUserLoginQueue = new AtomicLongArray(LoginFramework.getNumValues() * LoginEvent.getNumValues()); private static final AtomicLongArray missingUserIdQueue = @@ -104,6 +117,10 @@ public void raspTimeout(final RuleType ruleType) { raspTimeoutCounter.incrementAndGet(ruleType.ordinal()); } + public void raspErrorCode(final RuleType ruleType, final int ddwafRunErrorCode) { + raspErrorCodeCounter.get(ddwafRunErrorCode).incrementAndGet(ruleType.ordinal()); + } + public void missingUserLogin(final LoginFramework framework, final LoginEvent eventType) { missingUserLoginQueue.incrementAndGet( framework.ordinal() * LoginEvent.getNumValues() + eventType.ordinal()); @@ -216,6 +233,19 @@ public void prepareMetrics() { } } + // RASP rule type for each possible error code + for (int i = -1 * ABSTRACT_POWERWAF_EXCEPTION_NUMBER; i < 0; i++) { + for (RuleType ruleType : RuleType.values()) { + long counter = raspErrorCodeCounter.get(i).getAndSet(ruleType.ordinal(), 0); + if (counter > 0) { + if (!rawMetricsQueue.offer( + new RaspError(counter, ruleType, WafMetricCollector.wafVersion, i))) { + return; + } + } + } + } + // Missing user login for (LoginFramework framework : LoginFramework.values()) { for (LoginEvent event : LoginEvent.values()) { @@ -367,6 +397,31 @@ public RaspTimeout(final long counter, final RuleType ruleType, final String waf } } + public static class RaspError extends WafMetric { + public RaspError( + final long counter, + final RuleType ruleType, + final String wafVersion, + final Integer ddwafRunError) { + super( + "rasp.error", + counter, + ruleType.variant != null + ? new String[] { + "rule_type:" + ruleType.type, + "rule_variant:" + ruleType.variant, + "waf_version:" + wafVersion, + "event_rules_version:" + rulesVersion, + "waf_error:" + ddwafRunError + } + : new String[] { + "rule_type:" + ruleType.type, + "waf_version:" + wafVersion, + "waf_error:" + ddwafRunError + }); + } + } + public static class AtomicRequestCounter { private final AtomicLong atomicLong = new AtomicLong(); diff --git a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy index 5f532b3ed8f..bb83fd9387b 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/telemetry/WafMetricCollectorTest.groovy @@ -8,6 +8,9 @@ import java.util.concurrent.TimeUnit class WafMetricCollectorTest extends DDSpecification { + public static final int DD_WAF_RUN_INTERNAL_ERROR = -3 + public static final int DD_WAF_RUN_INVALID_OBJECT_ERROR = -2 + def "no metrics - drain empty list"() { when: WafMetricCollector.get().prepareMetrics() @@ -32,6 +35,8 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().raspRuleMatch(RuleType.SQL_INJECTION) WafMetricCollector.get().raspRuleEval(RuleType.SQL_INJECTION) WafMetricCollector.get().raspTimeout(RuleType.SQL_INJECTION) + WafMetricCollector.get().raspErrorCode(RuleType.SHELL_INJECTION, DD_WAF_RUN_INTERNAL_ERROR) + WafMetricCollector.get().raspErrorCode(RuleType.SQL_INJECTION, DD_WAF_RUN_INVALID_OBJECT_ERROR) WafMetricCollector.get().prepareMetrics() @@ -131,6 +136,31 @@ class WafMetricCollectorTest extends DDSpecification { raspTimeout.namespace == 'appsec' raspTimeout.metricName == 'rasp.timeout' raspTimeout.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() + + def raspInvalidCode = (WafMetricCollector.RaspError)metrics[10] + raspInvalidCode.type == 'count' + raspInvalidCode.value == 1 + raspInvalidCode.namespace == 'appsec' + raspInvalidCode.metricName == 'rasp.error' + raspInvalidCode.tags.toSet() == [ + 'waf_version:waf_ver1', + 'rule_type:command_injection', + 'rule_variant:shell', + 'event_rules_version:rules.3', + 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR + ].toSet() + + def raspInvalidObjectCode = (WafMetricCollector.RaspError)metrics[11] + raspInvalidObjectCode.type == 'count' + raspInvalidObjectCode.value == 1 + raspInvalidObjectCode.namespace == 'appsec' + raspInvalidObjectCode.metricName == 'rasp.error' + raspInvalidObjectCode.tags.toSet() == [ + 'rule_type:sql_injection', + 'waf_version:waf_ver1', + 'waf_error:' + DD_WAF_RUN_INVALID_OBJECT_ERROR + ] + .toSet() } def "overflowing WafMetricCollector does not crash"() { @@ -304,6 +334,7 @@ class WafMetricCollectorTest extends DDSpecification { WafMetricCollector.get().raspRuleMatch(ruleType) WafMetricCollector.get().raspRuleEval(ruleType) WafMetricCollector.get().raspTimeout(ruleType) + WafMetricCollector.get().raspErrorCode(ruleType, DD_WAF_RUN_INTERNAL_ERROR) WafMetricCollector.get().prepareMetrics() then: @@ -345,6 +376,19 @@ class WafMetricCollectorTest extends DDSpecification { 'event_rules_version:rules.1' ].toSet() + def raspInvalidCode = (WafMetricCollector.RaspError)metrics[4] + raspInvalidCode.type == 'count' + raspInvalidCode.value == 1 + raspInvalidCode.namespace == 'appsec' + raspInvalidCode.metricName == 'rasp.error' + raspInvalidCode.tags.toSet() == [ + 'waf_version:waf_ver1', + 'rule_type:command_injection', + 'rule_variant:' + ruleType.variant, + 'event_rules_version:rules.1', + 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR + ].toSet() + where: ruleType << [RuleType.COMMAND_INJECTION, RuleType.SHELL_INJECTION] }