From f4b762c9155e712f368688def114cd79e4a89b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 25 Mar 2025 14:45:40 +0100 Subject: [PATCH 1/7] Update to ATO V2 --- .../datadog/appsec/gateway/GatewayBridge.java | 16 +- .../AppSecEventTrackerSpecification.groovy | 254 ++++++++----- .../SpringSecurityUserEventDecorator.java | 7 +- dd-trace-api/build.gradle | 1 + .../appsec/api/login/EventTrackerService.java | 26 ++ .../appsec/api/login/EventTrackerV2.java | 52 +++ .../java/datadog/trace/api/EventTracker.java | 7 + .../java/datadog/trace/api/GlobalTracer.java | 2 + .../trace/api/appsec/AppSecEventTracker.java | 351 ++++++++---------- .../trace/api/telemetry/LoginEvent.java | 3 +- .../trace/api/telemetry/LoginVersion.java | 25 ++ .../api/telemetry/WafMetricCollector.java | 28 ++ 12 files changed, 485 insertions(+), 287 deletions(-) create mode 100644 dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java create mode 100644 dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerV2.java create mode 100644 internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java 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 95831f90c53..8e5030912db 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 @@ -68,9 +68,13 @@ public class GatewayBridge { /** User tracking tags that will force the collection of request headers */ private static final String[] USER_TRACKING_TAGS = { - "appsec.events.users.login.success.track", "appsec.events.users.login.failure.track" + "appsec.events.users.login.success.track", + "appsec.events.users.login.failure.track", + "appsec.events.users.signup.track" }; + private static final String USER_COLLECTION_MODE_TAG = "_dd.appsec.user.collection_mode"; + private static final Map> EVENT_MAPPINGS = new EnumMap<>(LoginEvent.class); static { @@ -708,7 +712,7 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) { StackUtils.addStacktraceEventsToMetaStruct(ctx_, METASTRUCT_EXPLOIT, stackTraces); } - } else if (hasUserTrackingEvent(traceSeg)) { + } else if (hasUserInfo(traceSeg)) { // Report all collected request headers on user tracking event writeRequestHeaders(traceSeg, REQUEST_HEADERS_ALLOW_LIST, ctx.getRequestHeaders()); } else { @@ -803,6 +807,10 @@ public void stop() { subscriptionService.reset(); } + private static boolean hasUserInfo(final TraceSegment traceSegment) { + return hasUserTrackingEvent(traceSegment) || hasUserCollectionEvent(traceSegment); + } + private static boolean hasUserTrackingEvent(final TraceSegment traceSeg) { for (String tagName : USER_TRACKING_TAGS) { final Object value = traceSeg.getTagTop(tagName); @@ -813,6 +821,10 @@ private static boolean hasUserTrackingEvent(final TraceSegment traceSeg) { return false; } + private static boolean hasUserCollectionEvent(final TraceSegment traceSeg) { + return traceSeg.getTagTop(USER_COLLECTION_MODE_TAG) != null; + } + private static void writeRequestHeaders( final TraceSegment traceSeg, final Set allowed, diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy index 79021938cb5..4a63e4139cf 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy @@ -3,6 +3,7 @@ package com.datadog.appsec.user import com.datadog.appsec.gateway.NoopFlow import datadog.appsec.api.blocking.BlockingContentType import datadog.appsec.api.blocking.BlockingException +import datadog.appsec.api.login.EventTrackerV2 import datadog.appsec.api.user.User import datadog.trace.api.EventTracker import datadog.trace.api.GlobalTracer @@ -16,6 +17,8 @@ import datadog.trace.api.gateway.RequestContext import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.api.internal.TraceSegment import datadog.trace.api.telemetry.LoginEvent +import datadog.trace.api.telemetry.LoginVersion +import datadog.trace.api.telemetry.WafMetricCollector import datadog.trace.bootstrap.ActiveSubsystems import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI @@ -29,15 +32,20 @@ import static datadog.trace.api.UserIdCollectionMode.DISABLED import static datadog.trace.api.UserIdCollectionMode.IDENTIFICATION import static datadog.trace.api.UserIdCollectionMode.SDK import static datadog.trace.api.gateway.Events.EVENTS +import static datadog.trace.api.telemetry.LoginEvent.CUSTOM import static datadog.trace.api.telemetry.LoginEvent.LOGIN_FAILURE import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS import static datadog.trace.api.telemetry.LoginEvent.SIGN_UP import static datadog.appsec.api.user.User.setUser +import static datadog.trace.api.telemetry.LoginVersion.V1 +import static datadog.trace.api.telemetry.LoginVersion.V2 class AppSecEventTrackerSpecification extends DDSpecification { - private static final String USER_ID = 'user' - private static final String ANONYMIZED_USER_ID = 'anon_04f8996da763b7a969b1028ee3007569' + private static final String USER_LOGIN = 'user' + private static final String ANONYMIZED_USER_LOGIN = 'anon_04f8996da763b7a969b1028ee3007569' + private static final String USER_ID = '1' + private static final String ANONYMIZED_USER_ID = 'anon_6b86b273ff34fce19d6b804eff5a3f57' @Shared private static boolean appSecActiveBefore = ActiveSubsystems.APPSEC_ACTIVE @@ -75,6 +83,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { } GlobalTracer.setEventTracker(tracker) User.setUserService(tracker) + EventTrackerV2.setEventTrackerService(tracker) ActiveSubsystems.APPSEC_ACTIVE = true } @@ -85,37 +94,41 @@ class AppSecEventTrackerSpecification extends DDSpecification { def 'test track login success event (SDK)'() { when: - GlobalTracer.getEventTracker().trackLoginSuccessEvent(USER_ID, ['key1': 'value1', 'key2': 'value2']) + GlobalTracer.getEventTracker().trackLoginSuccessEvent(USER_LOGIN, ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('usr.id', USER_ID) - 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_ID) - 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1':'value1', 'key2':'value2']) - 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true) - 1 * traceSegment.setTagTop('asm.keep', true) - 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_ID) >> NoopFlow.INSTANCE - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 1 * traceSegment.setTagTop('usr.id', USER_LOGIN) + 1 * traceSegment.setTagTop('usr', ['key1': 'value1', 'key2': 'value2']) + 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', 'sdk') + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true, true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true, true) + 2 * traceSegment.setTagTop('asm.keep', true) + 2 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) + 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_LOGIN) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE 0 * _ + + assertAppSecSdkEvent(LOGIN_SUCCESS, V1) } def 'test track login failure event (SDK)'() { when: - GlobalTracer.getEventTracker().trackLoginFailureEvent(USER_ID, true, ['key1': 'value1', 'key2': 'value2']) + GlobalTracer.getEventTracker().trackLoginFailureEvent(USER_LOGIN, true, ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.id', USER_ID) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', USER_ID) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1':'value1', 'key2':'value2']) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true, true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, USER_ID) >> NoopFlow.INSTANCE - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, USER_LOGIN) >> NoopFlow.INSTANCE 0 * _ + + assertAppSecSdkEvent(LOGIN_FAILURE, V1) } def 'test track custom event (SDK)'() { @@ -123,7 +136,61 @@ class AppSecEventTrackerSpecification extends DDSpecification { GlobalTracer.getEventTracker().trackCustomEvent('myevent', ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('appsec.events.myevent', ['key1':'value1', 'key2':'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.myevent', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.myevent.track', true, true) + 1 * traceSegment.setTagTop('_dd.appsec.events.myevent.sdk', true, true) + 1 * traceSegment.setTagTop('asm.keep', true) + 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) + 0 * _ + + assertAppSecSdkEvent(CUSTOM, V2) + } + + def 'test track login success event V2 (SDK)'() { + when: + EventTrackerV2.trackUserLoginSuccess(USER_LOGIN, USER_ID, ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('usr.id', USER_ID) + 1 * traceSegment.setTagTop('usr', ['key1': 'value1', 'key2': 'value2']) + 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', 'sdk') + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true, true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true, true) + 2 * traceSegment.setTagTop('asm.keep', true) + 2 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) + 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_LOGIN) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 0 * _ + + assertAppSecSdkEvent(LOGIN_SUCCESS, V2) + } + + def 'test track login failure event V2 (SDK)'() { + when: + EventTrackerV2.trackUserLoginFailure(USER_LOGIN, true, ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true, true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true, true) + 1 * traceSegment.setTagTop('asm.keep', true) + 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) + 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, USER_LOGIN) >> NoopFlow.INSTANCE + 0 * _ + + assertAppSecSdkEvent(LOGIN_FAILURE, V2) + } + + def 'test track custom event V2 (SDK)'() { + when: + EventTrackerV2.trackCustomEvent('myevent', ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('appsec.events.myevent', ['key1': 'value1', 'key2': 'value2'], true) 1 * traceSegment.setTagTop('appsec.events.myevent.track', true, true) 1 * traceSegment.setTagTop('_dd.appsec.events.myevent.sdk', true, true) 1 * traceSegment.setTagTop('asm.keep', true) @@ -137,7 +204,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { then: 1 * traceSegment.setTagTop('usr.id', USER_ID) - 1 * traceSegment.setTagTop('usr', ['key1':'value1', 'key2':'value2']) + 1 * traceSegment.setTagTop('usr', ['key1': 'value1', 'key2': 'value2']) 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', SDK.fullName()) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) @@ -191,117 +258,100 @@ class AppSecEventTrackerSpecification extends DDSpecification { def "test onSignup (#mode)"() { setup: - final expectedUserId = mode == ANONYMIZATION ? ANONYMIZED_USER_ID: USER_ID + final expectedUserLogin = mode == ANONYMIZATION ? ANONYMIZED_USER_LOGIN : USER_LOGIN when: - tracker.onSignupEvent(mode, USER_ID, ['key1': 'value1', 'key2': 'value2']) + tracker.onSignupEvent(mode, USER_LOGIN, null, ['key1': 'value1', 'key2': 'value2']) then: if (mode != DISABLED) { - if (mode == SDK) { - 1 * traceSegment.setTagTop('appsec.events.users.signup.usr.id', USER_ID) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.signup.sdk', true) - } else { - 1 * traceSegment.getTagTop('_dd.appsec.events.users.signup.sdk') >> null // no SDK event before - 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.signup.auto.mode', mode.fullName()) - } - 1 * traceSegment.setTagTop('appsec.events.users.signup.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('appsec.events.users.signup', ['key1':'value1', 'key2':'value2']) - 1 * traceSegment.setTagTop('appsec.events.users.signup.track', true) + 1 * traceSegment.getTagTop('_dd.appsec.events.users.signup.sdk') >> null // no SDK event before + 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserLogin) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.signup.auto.mode', mode.fullName(), true) + 1 * traceSegment.setTagTop('appsec.events.users.signup.usr.login', expectedUserLogin, true) + 1 * traceSegment.setTagTop('appsec.events.users.signup', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.signup.track', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, SIGN_UP, expectedUserId) >> NoopFlow.INSTANCE + 1 * loginEvent.apply(_ as RequestContext, SIGN_UP, expectedUserLogin) >> NoopFlow.INSTANCE if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE } } 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test onLoginSuccess (#mode)"() { setup: - final expectedUserId = mode == ANONYMIZATION ? ANONYMIZED_USER_ID: USER_ID + final expectedUserLogin = mode == ANONYMIZATION ? ANONYMIZED_USER_LOGIN : USER_LOGIN when: - tracker.onLoginSuccessEvent(mode, USER_ID, ['key1': 'value1', 'key2': 'value2']) + tracker.onLoginSuccessEvent(mode, USER_LOGIN, null, ['key1': 'value1', 'key2': 'value2']) then: if (mode != DISABLED) { - if (mode == SDK) { - 1 * traceSegment.setTagTop('usr.id', USER_ID) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true) - } else { - 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.success.sdk') >> null // no SDK event before - 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.auto.mode', mode.fullName()) - } - 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1':'value1', 'key2':'value2']) - 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true) + 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.success.sdk') >> null // no SDK event before + 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserLogin) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.auto.mode', mode.fullName(), true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', expectedUserLogin, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, expectedUserId) >> NoopFlow.INSTANCE + 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, expectedUserLogin) >> NoopFlow.INSTANCE if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE } } 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test onLoginFailed (#mode)"() { setup: - final expectedUserId = mode == ANONYMIZATION ? ANONYMIZED_USER_ID: USER_ID + final expectedUserLogin = mode == ANONYMIZATION ? ANONYMIZED_USER_LOGIN : USER_LOGIN when: - tracker.onLoginFailureEvent(mode, USER_ID, true, ['key1': 'value1', 'key2': 'value2']) + tracker.onLoginFailureEvent(mode, USER_LOGIN, true, ['key1': 'value1', 'key2': 'value2']) then: if (mode != DISABLED) { - if (mode == SDK) { - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.id', USER_ID) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true) - } else { - 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> null // no SDK event before - 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', mode.fullName()) - } - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', expectedUserId) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1':'value1', 'key2':'value2']) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true) - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true) + 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> null // no SDK event before + 1 * traceSegment.setTagTop('_dd.appsec.usr.login', expectedUserLogin) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', mode.fullName(), true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', expectedUserLogin, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1': 'value1', 'key2': 'value2'], true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, expectedUserId) >> NoopFlow.INSTANCE + 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, expectedUserLogin) >> NoopFlow.INSTANCE if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE } } 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test onUserEvent (#mode)"() { setup: - final expectedUserId = mode == ANONYMIZATION ? ANONYMIZED_USER_ID: USER_ID + final expectedUserId = mode == ANONYMIZATION ? ANONYMIZED_USER_ID : USER_ID when: - tracker.onUserEvent(mode, USER_ID) + tracker.onUserEvent(mode, USER_ID, [:]) then: if (mode != DISABLED) { - if (mode != SDK) { - 1 * traceSegment.setTagTop('_dd.appsec.usr.id', expectedUserId) - 1 * traceSegment.getTagTop('_dd.appsec.user.collection_mode') >> null // no user event before - } + 1 * traceSegment.setTagTop('_dd.appsec.usr.id', expectedUserId) + 1 * traceSegment.getTagTop('_dd.appsec.user.collection_mode') >> null // no user event before 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', mode.fullName()) 1 * traceSegment.setTagTop('usr.id', expectedUserId) 1 * traceSegment.setTagTop('asm.keep', true) @@ -311,7 +361,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test onUserNotFound (#mode)"() { @@ -320,17 +370,17 @@ class AppSecEventTrackerSpecification extends DDSpecification { then: if (mode != DISABLED) { - if (mode != SDK) { - 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> null // no SDK event before - } - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', false) + 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> null // no SDK event before + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', mode.fullName(), true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', false, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) } 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test isEnabled (appsec = #appsec, tracking = #trackingMode, collection = #collectionMode)"() { @@ -383,13 +433,13 @@ class AppSecEventTrackerSpecification extends DDSpecification { true | 'anon' | 'disabled' | true } - void 'test blocking on a userId'() { + void 'test blocking on a login'() { setup: final action = new Flow.Action.RequestBlockingAction(403, BlockingContentType.AUTO) - loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_ID) >> new ActionFlow(action: action) + loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_LOGIN) >> new ActionFlow(action: action) when: - tracker.onLoginSuccessEvent(SDK, USER_ID, ['key1': 'value1', 'key2': 'value2']) + tracker.onLoginSuccessEvent(SDK, USER_LOGIN, USER_ID, ['key1': 'value1', 'key2': 'value2']) then: thrown(BlockingException) @@ -397,7 +447,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { void 'should not fail on null callback'() { when: - tracker.onUserEvent(IDENTIFICATION, 'test-user') + tracker.onUserEvent(IDENTIFICATION, 'test-user', [:]) then: noExceptionThrown() @@ -406,7 +456,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { void 'test onUserEvent (automated login events should not overwrite SDK)'() { when: - tracker.onUserEvent(IDENTIFICATION, USER_ID) + tracker.onUserEvent(IDENTIFICATION, USER_ID, [:]) then: 'SDK data remains untouched' 1 * traceSegment.getTagTop('_dd.appsec.user.collection_mode') >> SDK.fullName() @@ -417,23 +467,23 @@ class AppSecEventTrackerSpecification extends DDSpecification { void 'test onLoginSuccess (automated login events should not overwrite SDK)'() { when: - tracker.onLoginSuccessEvent(IDENTIFICATION, USER_ID, [:]) + tracker.onLoginSuccessEvent(IDENTIFICATION, USER_LOGIN, null, [:]) then: 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.success.sdk') >> true - 1 * traceSegment.setTagTop('_dd.appsec.usr.login', USER_ID) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.auto.mode', IDENTIFICATION.fullName()) + 1 * traceSegment.setTagTop('_dd.appsec.usr.login', USER_LOGIN) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.auto.mode', IDENTIFICATION.fullName(), true) 0 * _ } void 'test onLoginFailure (automated login events should not overwrite SDK)'() { when: - tracker.onLoginFailureEvent(IDENTIFICATION, USER_ID, null, [:]) + tracker.onLoginFailureEvent(IDENTIFICATION, USER_LOGIN, null, [:]) then: 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> true - 1 * traceSegment.setTagTop('_dd.appsec.usr.login', USER_ID) - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', IDENTIFICATION.fullName()) + 1 * traceSegment.setTagTop('_dd.appsec.usr.login', USER_LOGIN) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', IDENTIFICATION.fullName(), true) 0 * _ } @@ -443,9 +493,23 @@ class AppSecEventTrackerSpecification extends DDSpecification { then: 1 * traceSegment.getTagTop('_dd.appsec.events.users.login.failure.sdk') >> true + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.auto.mode', IDENTIFICATION.fullName(), true) 0 * _ } + private static void assertAppSecSdkEvent(final LoginEvent event, final LoginVersion version) { + final metrics = WafMetricCollector.get().with { + prepareMetrics() + drain() + } + assert metrics.size() == 1 + final metric = metrics.find { it.metricName == 'appsec.sdk.event'} + assert metric != null + assert metric.value == 1 + assert metric.tags.find { "event_type:${event.getTag()}" } != null + assert metric.tags.find { "sdk_version:${version.getTag()}" } != null + } + private static class ActionFlow implements Flow { private Action action diff --git a/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java b/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java index d7285e7f976..90264f78e30 100644 --- a/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java +++ b/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java @@ -2,6 +2,7 @@ import static datadog.trace.api.UserIdCollectionMode.IDENTIFICATION; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; +import static java.util.Collections.emptyMap; import datadog.trace.api.EventTracker; import datadog.trace.api.GlobalTracer; @@ -69,7 +70,7 @@ public void onSignup(UserDetails user, Throwable throwable) { user.getAuthorities().stream().map(Object::toString).collect(Collectors.joining(","))); } - tracker.onSignupEvent(mode, username, metadata); + tracker.onSignupEvent(mode, username, null, metadata); } public void onLogin(Authentication authentication, Throwable throwable, Authentication result) { @@ -105,7 +106,7 @@ public void onLogin(Authentication authentication, Throwable throwable, Authenti metadata.put("accountNonLocked", String.valueOf(user.isAccountNonLocked())); metadata.put("credentialsNonExpired", String.valueOf(user.isCredentialsNonExpired())); } - tracker.onLoginSuccessEvent(mode, username, metadata); + tracker.onLoginSuccessEvent(mode, username, null, metadata); } else if (throwable != null) { if (missingUsername(username, LoginEvent.LOGIN_FAILURE)) { return; @@ -143,7 +144,7 @@ public void onUser(final Authentication authentication) { if (missingUserId(username)) { return; } - tracker.onUserEvent(mode, username); + tracker.onUserEvent(mode, username, emptyMap()); } private static boolean shouldSkipAuthentication(final Authentication authentication) { diff --git a/dd-trace-api/build.gradle b/dd-trace-api/build.gradle index e6cb5e7409a..c4b00313208 100644 --- a/dd-trace-api/build.gradle +++ b/dd-trace-api/build.gradle @@ -38,6 +38,7 @@ excludedClassesCoverage += [ 'datadog.trace.api.experimental.DataStreamsContextCarrier.NoOp', 'datadog.appsec.api.blocking.*', 'datadog.appsec.api.user.*', + 'datadog.appsec.api.login.*', // Default fallback methods to not break legacy API 'datadog.trace.context.TraceScope', 'datadog.trace.context.NoopTraceScope.NoopContinuation', diff --git a/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java new file mode 100644 index 00000000000..f42bd1e9824 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java @@ -0,0 +1,26 @@ +package datadog.appsec.api.login; + +import java.util.Map; + +public interface EventTrackerService { + + EventTrackerService NO_OP = + new EventTrackerService() { + @Override + public void trackUserLoginSuccess( + final String login, final String userId, final Map metadata) {} + + @Override + public void trackUserLoginFailure( + final String login, final boolean exists, final Map metadata) {} + + @Override + public void trackCustomEvent(final String eventName, final Map metadata) {} + }; + + void trackUserLoginSuccess(final String login, final String userId, Map metadata); + + void trackUserLoginFailure(String login, boolean exists, Map metadata); + + void trackCustomEvent(String eventName, Map metadata); +} diff --git a/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerV2.java b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerV2.java new file mode 100644 index 00000000000..2bc31a14159 --- /dev/null +++ b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerV2.java @@ -0,0 +1,52 @@ +package datadog.appsec.api.login; + +import java.util.Map; + +public class EventTrackerV2 { + + private static volatile EventTrackerService SERVICE = EventTrackerService.NO_OP; + + /** + * Controls the implementation for service. The AppSec subsystem calls this method on startup. + * This can be called explicitly for e.g. testing purposes. + * + * @param service the implementation for the user service. + */ + public static void setEventTrackerService(final EventTrackerService service) { + SERVICE = service; + } + + /** + * Tracks a successful user login event. + * + * @param login the non-null login data (e.g., username or email) used for authentication. + * @param userId an optional user ID string; can be null. + * @param metadata optional metadata for the login event; can be null. + */ + public static void trackUserLoginSuccess( + final String login, final String userId, Map metadata) { + SERVICE.trackUserLoginSuccess(login, userId, metadata); + } + + /** + * Tracks a failed user login event. + * + * @param login the non-null login data (e.g., username or email) used for authentication. + * @param exists indicates whether the provided login identifier corresponds to an existing user. + * @param metadata optional metadata for the login event; can be null. + */ + public static void trackUserLoginFailure( + String login, boolean exists, Map metadata) { + SERVICE.trackUserLoginFailure(login, exists, metadata); + } + + /** + * Method for tracking custom events. + * + * @param eventName name of the custom event + * @param metadata custom metadata data represented as key/value map + */ + public static void trackCustomEvent(String eventName, Map metadata) { + SERVICE.trackCustomEvent(eventName, metadata); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/EventTracker.java b/dd-trace-api/src/main/java/datadog/trace/api/EventTracker.java index f9a273a7c11..b52632e5482 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/EventTracker.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/EventTracker.java @@ -1,7 +1,10 @@ package datadog.trace.api; +import datadog.appsec.api.login.EventTrackerV2; import java.util.Map; +/** This class has been deprecated in favor of {@link EventTrackerV2} */ +@Deprecated public class EventTracker { public static final EventTracker NO_EVENT_TRACKER = new EventTracker(); @@ -12,7 +15,9 @@ public class EventTracker { * * @param userId user id used for login * @param metadata custom metadata data represented as key/value map + * @deprecated use {@link EventTrackerV2#trackUserLoginSuccess(String, String, Map)} */ + @Deprecated public void trackLoginSuccessEvent(String userId, Map metadata) {} /** @@ -22,6 +27,7 @@ public void trackLoginSuccessEvent(String userId, Map metadata) * @param userId user id used for login * @param exists flag indicates if provided userId exists * @param metadata custom metadata data represented as key/value map + * @deprecated use {@link EventTrackerV2#trackUserLoginFailure(String, boolean, Map)} */ public void trackLoginFailureEvent(String userId, boolean exists, Map metadata) {} @@ -31,6 +37,7 @@ public void trackLoginFailureEvent(String userId, boolean exists, Map metadata) {} } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/GlobalTracer.java b/dd-trace-api/src/main/java/datadog/trace/api/GlobalTracer.java index 7fae3855476..f91cafb79ef 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/GlobalTracer.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/GlobalTracer.java @@ -85,6 +85,8 @@ public static Tracer get() { return provider; } + /** @deprecated use static methods in {@link EventTrackerV2} directly */ + @Deprecated public static EventTracker getEventTracker() { return eventTracker; } diff --git a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java index 8f969503d43..12995d0a15c 100644 --- a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java +++ b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java @@ -4,13 +4,16 @@ import static datadog.trace.api.UserIdCollectionMode.DISABLED; import static datadog.trace.api.UserIdCollectionMode.SDK; import static datadog.trace.api.gateway.Events.EVENTS; +import static datadog.trace.api.telemetry.LoginEvent.CUSTOM; import static datadog.trace.api.telemetry.LoginEvent.LOGIN_FAILURE; import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS; -import static datadog.trace.api.telemetry.LoginEvent.SIGN_UP; +import static datadog.trace.api.telemetry.LoginVersion.V1; +import static datadog.trace.api.telemetry.LoginVersion.V2; import static datadog.trace.util.Strings.toHexString; -import static java.util.Collections.emptyMap; import datadog.appsec.api.blocking.BlockingException; +import datadog.appsec.api.login.EventTrackerService; +import datadog.appsec.api.login.EventTrackerV2; import datadog.appsec.api.user.User; import datadog.appsec.api.user.UserService; import datadog.trace.api.EventTracker; @@ -24,280 +27,255 @@ import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.internal.TraceSegment; +import datadog.trace.api.telemetry.LoginEvent; +import datadog.trace.api.telemetry.WafMetricCollector; import datadog.trace.bootstrap.ActiveSubsystems; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.bootstrap.instrumentation.api.Tags; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Base64; +import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; -public class AppSecEventTracker extends EventTracker implements UserService { +public class AppSecEventTracker extends EventTracker implements UserService, EventTrackerService { private static final int HASH_SIZE_BYTES = 16; // 128 bits private static final String ANON_PREFIX = "anon_"; - private static final String LOGIN_SUCCESS_TAG = "users.login.success"; - private static final String LOGIN_FAILURE_TAG = "users.login.failure"; - private static final String SIGNUP_TAG = "users.signup"; + private static final Map EVENT_MAPPING; + + private static final String LOGIN_SUCCESS_EVENT = "users.login.success"; + private static final String LOGIN_FAILURE_EVENT = "users.login.failure"; + private static final String SIGNUP_EVENT = "users.signup"; + + static { + EVENT_MAPPING = new HashMap<>(); + EVENT_MAPPING.put(LOGIN_SUCCESS_EVENT, LOGIN_SUCCESS); + EVENT_MAPPING.put(LOGIN_FAILURE_EVENT, LoginEvent.LOGIN_FAILURE); + EVENT_MAPPING.put(SIGNUP_EVENT, LoginEvent.SIGN_UP); + } + + private static final String COLLECTION_MODE = "_dd.appsec.user.collection_mode"; public static void install() { final AppSecEventTracker tracker = new AppSecEventTracker(); GlobalTracer.setEventTracker(tracker); + EventTrackerV2.setEventTrackerService(tracker); User.setUserService(tracker); } @Override public final void trackLoginSuccessEvent(String userId, Map metadata) { if (userId == null || userId.isEmpty()) { - throw new IllegalArgumentException("UserId is null or empty"); + throw new IllegalArgumentException("userId is null or empty"); + } + WafMetricCollector.get().appSecSdkEvent(LOGIN_SUCCESS, V1); + if (handleLoginEvent(LOGIN_SUCCESS_EVENT, SDK, userId, userId, null, metadata)) { + throw new BlockingException("Blocked request (for login success)"); } - onLoginSuccessEvent(SDK, userId, metadata); } @Override public final void trackLoginFailureEvent( String userId, boolean exists, Map metadata) { if (userId == null || userId.isEmpty()) { - throw new IllegalArgumentException("UserId is null or empty"); + throw new IllegalArgumentException("userId is null or empty"); + } + WafMetricCollector.get().appSecSdkEvent(LOGIN_FAILURE, V1); + if (handleLoginEvent(LOGIN_FAILURE_EVENT, SDK, userId, null, exists, metadata)) { + throw new BlockingException("Blocked request (for login failure)"); } - onLoginFailureEvent(SDK, userId, exists, metadata); } @Override - public final void trackCustomEvent(String eventName, Map metadata) { - if (eventName == null || eventName.isEmpty()) { - throw new IllegalArgumentException("EventName is null or empty"); + public void trackUserLoginSuccess( + final String login, final String userId, final Map metadata) { + if (login == null || login.isEmpty()) { + throw new IllegalArgumentException("login is null or empty"); + } + WafMetricCollector.get().appSecSdkEvent(LOGIN_SUCCESS, V2); + if (handleLoginEvent(LOGIN_SUCCESS_EVENT, SDK, login, userId, null, metadata)) { + throw new BlockingException("Blocked request (for login success)"); } - onCustomEvent(SDK, eventName, metadata); } @Override - public void trackUserEvent(final String userId, final Map metadata) { - if (userId == null || userId.isEmpty()) { - throw new IllegalArgumentException("UserId is null or empty"); + public void trackUserLoginFailure( + final String login, final boolean exists, final Map metadata) { + if (login == null || login.isEmpty()) { + throw new IllegalArgumentException("login is null or empty"); + } + WafMetricCollector.get().appSecSdkEvent(LOGIN_FAILURE, V2); + if (handleLoginEvent(LOGIN_FAILURE_EVENT, SDK, login, null, exists, metadata)) { + throw new BlockingException("Blocked request (for login failure)"); } - onUserEvent(SDK, userId, metadata); } - public void onUserNotFound(final UserIdCollectionMode mode) { - if (!isEnabled(mode)) { - return; - } - final AgentTracer.TracerAPI tracer = tracer(); - if (tracer == null) { - return; - } - final TraceSegment segment = tracer.getTraceSegment(); - if (segment == null) { - return; + @SuppressWarnings("deprecation") + @Override + public final void trackCustomEvent(String eventName, Map metadata) { + if (eventName == null || eventName.isEmpty()) { + throw new IllegalArgumentException("eventName is null or empty"); } - if (isNewLoginEvent(mode, segment, LOGIN_FAILURE_TAG)) { - segment.setTagTop("appsec.events.users.login.failure.usr.exists", false); - segment.setTagTop(Tags.ASM_KEEP, true); - segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); + WafMetricCollector.get().appSecSdkEvent(CUSTOM, V2); + if (handleLoginEvent(eventName, SDK, null, null, null, metadata)) { + throw new BlockingException("Blocked request (for custom event)"); } } - public void onUserEvent(final UserIdCollectionMode mode, final String userId) { - onUserEvent(mode, userId, emptyMap()); + @Override + public void trackUserEvent(final String userId, final Map metadata) { + if (userId == null || userId.isEmpty()) { + throw new IllegalArgumentException("userId is null or empty"); + } + if (handleUser(SDK, userId, metadata)) { + throw new BlockingException("Blocked request (for user)"); + } } public void onUserEvent( final UserIdCollectionMode mode, final String userId, final Map metadata) { - if (!isEnabled(mode)) { - return; - } - final AgentTracer.TracerAPI tracer = tracer(); - if (tracer == null) { - return; - } - final TraceSegment segment = tracer.getTraceSegment(); - if (segment == null) { - return; - } - final String finalUserId = anonymizeUser(mode, userId); - if (finalUserId == null) { - return; - } - if (mode != SDK) { - segment.setTagTop("_dd.appsec.usr.id", finalUserId); + if (handleUser(mode, userId, metadata)) { + throw new BlockingException("Blocked request (for user)"); } - if (isNewUser(mode, segment)) { - segment.setTagTop("usr.id", finalUserId); - if (metadata != null && !metadata.isEmpty()) { - segment.setTagTop("usr", metadata); - } - segment.setTagTop("_dd.appsec.user.collection_mode", mode.fullName()); - segment.setTagTop(Tags.ASM_KEEP, true); - segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - dispatch(tracer, EVENTS.user(), (ctx, cb) -> cb.apply(ctx, finalUserId)); + } + + public void onUserNotFound(final UserIdCollectionMode mode) { + if (handleLoginEvent(LOGIN_FAILURE_EVENT, mode, null, null, false, null)) { + throw new BlockingException("Blocked request (for user not found)"); } } public void onSignupEvent( - final UserIdCollectionMode mode, final String userId, final Map metadata) { - if (!isEnabled(mode)) { - return; - } - final AgentTracer.TracerAPI tracer = tracer(); - if (tracer == null) { - return; - } - final TraceSegment segment = tracer.getTraceSegment(); - if (segment == null) { - return; - } - final String finalUserId = anonymizeUser(mode, userId); - if (finalUserId == null) { - return; + final UserIdCollectionMode mode, + final String login, + final String userId, + final Map metadata) { + if (handleLoginEvent(SIGNUP_EVENT, mode, login, userId, null, metadata)) { + throw new BlockingException("Blocked request (for signup)"); } + } - if (mode == SDK) { - // TODO update SDK separating usr.login / usr.id - segment.setTagTop("appsec.events.users.signup.usr.id", finalUserId); - segment.setTagTop("_dd.appsec.events.users.signup.sdk", true); - } else { - segment.setTagTop("_dd.appsec.usr.login", finalUserId); - segment.setTagTop("_dd.appsec.events.users.signup.auto.mode", mode.fullName()); + public void onLoginSuccessEvent( + final UserIdCollectionMode mode, + final String login, + final String user, + final Map metadata) { + if (handleLoginEvent(LOGIN_SUCCESS_EVENT, mode, login, user, null, metadata)) { + throw new BlockingException("Blocked request (for login success)"); } - if (isNewLoginEvent(mode, segment, SIGNUP_TAG)) { - segment.setTagTop("appsec.events.users.signup.usr.login", finalUserId); - if (metadata != null && !metadata.isEmpty()) { - segment.setTagTop("appsec.events.users.signup", metadata); - } - segment.setTagTop("appsec.events.users.signup.track", true); - segment.setTagTop(Tags.ASM_KEEP, true); - segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - dispatch( - tracer, - EVENTS.loginEvent(), - (ctx, callback) -> callback.apply(ctx, SIGN_UP, finalUserId)); - if (mode == SDK) { - dispatch(tracer, EVENTS.user(), (ctx, callback) -> callback.apply(ctx, finalUserId)); - } + } + + public void onLoginFailureEvent( + final UserIdCollectionMode mode, + final String login, + final Boolean exists, + final Map metadata) { + if (handleLoginEvent(LOGIN_FAILURE_EVENT, mode, login, null, exists, metadata)) { + throw new BlockingException("Blocked request (for login failure)"); } } - public void onLoginSuccessEvent( + /** + * Takes care of the logged-in user and returns {@code true} if execution must be halted due to a + * blocking action + */ + private boolean handleUser( final UserIdCollectionMode mode, final String userId, final Map metadata) { if (!isEnabled(mode)) { - return; + return false; } final AgentTracer.TracerAPI tracer = tracer(); if (tracer == null) { - return; + return false; } final TraceSegment segment = tracer.getTraceSegment(); if (segment == null) { - return; + return false; } - final String finalUserId = anonymizeUser(mode, userId); + final String finalUserId = anonymize(mode, userId); if (finalUserId == null) { - return; + return false; // could not anonymize the user } - if (mode == SDK) { - // TODO update SDK separating usr.login / usr.id - segment.setTagTop("usr.id", finalUserId); - segment.setTagTop("_dd.appsec.events.users.login.success.sdk", true); - } else { - segment.setTagTop("_dd.appsec.usr.login", finalUserId); - segment.setTagTop("_dd.appsec.events.users.login.success.auto.mode", mode.fullName()); + if (mode != SDK) { + segment.setTagTop("_dd.appsec.usr.id", finalUserId); } - if (isNewLoginEvent(mode, segment, LOGIN_SUCCESS_TAG)) { - segment.setTagTop("appsec.events.users.login.success.usr.login", finalUserId); + if (isNewUser(mode, segment)) { + segment.setTagTop("usr.id", finalUserId); if (metadata != null && !metadata.isEmpty()) { - segment.setTagTop("appsec.events.users.login.success", metadata); + segment.setTagTop("usr", metadata); } - segment.setTagTop("appsec.events.users.login.success.track", true); + segment.setTagTop(COLLECTION_MODE, mode.fullName()); segment.setTagTop(Tags.ASM_KEEP, true); segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - dispatch( - tracer, - EVENTS.loginEvent(), - (ctx, callback) -> callback.apply(ctx, LOGIN_SUCCESS, finalUserId)); - if (mode == SDK) { - dispatch(tracer, EVENTS.user(), (ctx, callback) -> callback.apply(ctx, finalUserId)); - } + return dispatch(tracer, EVENTS.user(), (ctx, cb) -> cb.apply(ctx, finalUserId)); } + return false; } - public void onLoginFailureEvent( + /** + * Takes care of a login event and returns {@code true} if execution must be halted due to a + * blocking action + */ + private boolean handleLoginEvent( + final String eventName, final UserIdCollectionMode mode, + final String login, final String userId, final Boolean exists, final Map metadata) { if (!isEnabled(mode)) { - return; + return false; } final AgentTracer.TracerAPI tracer = tracer(); if (tracer == null) { - return; + return false; } final TraceSegment segment = tracer.getTraceSegment(); if (segment == null) { - return; + return false; } - final String finalUserId = anonymizeUser(mode, userId); - if (finalUserId == null) { - return; + boolean block = false; + final String finalLogin = anonymize(mode, login); + if (finalLogin == null && login != null) { + return false; // could not anonymize the login } - if (mode == SDK) { - // TODO update SDK separating usr.login / usr.id - segment.setTagTop("appsec.events.users.login.failure.usr.id", finalUserId); - segment.setTagTop("_dd.appsec.events.users.login.failure.sdk", true); + segment.setTagTop("_dd.appsec.events." + eventName + ".sdk", true, true); } else { - segment.setTagTop("_dd.appsec.usr.login", finalUserId); - segment.setTagTop("_dd.appsec.events.users.login.failure.auto.mode", mode.fullName()); - } - if (isNewLoginEvent(mode, segment, LOGIN_FAILURE_TAG)) { - segment.setTagTop("appsec.events.users.login.failure.usr.login", finalUserId); - if (metadata != null && !metadata.isEmpty()) { - segment.setTagTop("appsec.events.users.login.failure", metadata); - } - if (exists != null) { - segment.setTagTop("appsec.events.users.login.failure.usr.exists", exists); - } - segment.setTagTop("appsec.events.users.login.failure.track", true); - segment.setTagTop(Tags.ASM_KEEP, true); - segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - dispatch( - tracer, - EVENTS.loginEvent(), - (ctx, callback) -> callback.apply(ctx, LOGIN_FAILURE, finalUserId)); - if (mode == SDK) { - dispatch(tracer, EVENTS.user(), (ctx, callback) -> callback.apply(ctx, finalUserId)); + if (finalLogin != null) { + segment.setTagTop("_dd.appsec.usr.login", finalLogin); } - } - } - - public void onCustomEvent( - final UserIdCollectionMode mode, final String eventName, final Map metadata) { - if (!isEnabled(mode)) { - return; - } - final AgentTracer.TracerAPI tracer = tracer(); - if (tracer == null) { - return; - } - final TraceSegment segment = tracer.getTraceSegment(); - if (segment == null) { - return; - } - if (mode == SDK) { - segment.setTagTop("_dd.appsec.events." + eventName + ".sdk", true, true); + segment.setTagTop("_dd.appsec.events." + eventName + ".auto.mode", mode.fullName(), true); } if (isNewLoginEvent(mode, segment, eventName)) { + if (finalLogin != null) { + segment.setTagTop("appsec.events." + eventName + ".usr.login", finalLogin, true); + } if (metadata != null && !metadata.isEmpty()) { segment.setTagTop("appsec.events." + eventName, metadata, true); } + if (exists != null) { + segment.setTagTop("appsec.events." + eventName + ".usr.exists", exists, true); + } segment.setTagTop("appsec.events." + eventName + ".track", true, true); segment.setTagTop(Tags.ASM_KEEP, true); segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); + + final LoginEvent event = EVENT_MAPPING.get(eventName); + if (finalLogin != null && event != null) { + block = + dispatch(tracer, EVENTS.loginEvent(), (ctx, cb) -> cb.apply(ctx, event, finalLogin)); + } + } + if (userId != null) { + // call setUser + final boolean blockUser = handleUser(mode, userId, metadata); + block |= blockUser; } + return block; } private boolean isNewLoginEvent( @@ -312,36 +290,40 @@ private boolean isNewUser(final UserIdCollectionMode mode, final TraceSegment se if (mode == SDK) { return true; } - final Object value = segment.getTagTop("_dd.appsec.user.collection_mode"); + final Object value = segment.getTagTop(COLLECTION_MODE); return value == null || !"sdk".equalsIgnoreCase(value.toString()); } - private void dispatch( + /** + * Dispatch the selected event and return {@code true} if the execution must be halted due to a + * block action + */ + private boolean dispatch( final AgentTracer.TracerAPI tracer, final EventType event, final BiFunction> consumer) { if (tracer == null) { - return; + return false; } final CallbackProvider cbp = tracer.getCallbackProvider(RequestContextSlot.APPSEC); if (cbp == null) { - return; + return false; } final AgentSpan span = tracer.activeSpan(); if (span == null) { - return; + return false; } final RequestContext ctx = span.getRequestContext(); if (ctx == null) { - return; + return false; } final T callback = cbp.getCallback(event); if (callback == null) { - return; + return false; } final Flow flow = consumer.apply(ctx, callback); if (flow == null) { - return; + return false; } final Flow.Action action = flow.getAction(); if (action instanceof Flow.Action.RequestBlockingAction) { @@ -354,11 +336,12 @@ private void dispatch( rba.getBlockingContentType(), rba.getExtraHeaders()); } - throw new BlockingException("Blocked request (for user)"); + return true; } + return false; } - protected static String anonymizeUser(final UserIdCollectionMode mode, final String userId) { + protected static String anonymize(final UserIdCollectionMode mode, final String userId) { if (mode != ANONYMIZATION || userId == null) { return userId; } @@ -379,10 +362,6 @@ protected static String anonymizeUser(final UserIdCollectionMode mode, final Str return ANON_PREFIX + toHexString(hash); } - protected static String encodeBase64(final String userId) { - return Base64.getEncoder().encodeToString(userId.getBytes()); - } - protected boolean isEnabled(final UserIdCollectionMode mode) { return mode == SDK || (ActiveSubsystems.APPSEC_ACTIVE && mode != DISABLED); } diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/LoginEvent.java b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginEvent.java index 550d21ad4b5..c7aeeb91db9 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/LoginEvent.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginEvent.java @@ -3,7 +3,8 @@ public enum LoginEvent { LOGIN_SUCCESS("login_success"), LOGIN_FAILURE("login_failure"), - SIGN_UP("signup"); + SIGN_UP("signup"), + CUSTOM("custom"); private static final int numValues = LoginEvent.values().length; diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java new file mode 100644 index 00000000000..4161ba55e0d --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java @@ -0,0 +1,25 @@ +package datadog.trace.api.telemetry; + +public enum LoginVersion { + + /** Login events generated via V1 of ATO */ + V1("v1"), + /** Login event generated via V2 of ATO */ + V2("v2"); + + private final String tag; + + LoginVersion(final String tag) { + this.tag = tag; + } + + public String getTag() { + return tag; + } + + private static final int numValues = LoginVersion.values().length; + + public static int getNumValues() { + return numValues; + } +} 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 91505599200..33cdd73b71e 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 @@ -69,6 +69,8 @@ private WafMetricCollector() { new AtomicLongArray(LoginFramework.getNumValues() * LoginEvent.getNumValues()); private static final AtomicLongArray missingUserIdQueue = new AtomicLongArray(LoginFramework.getNumValues()); + private static final AtomicLongArray appSecSdkEventQueue = + new AtomicLongArray(LoginEvent.getNumValues() * LoginVersion.getNumValues()); /** WAF version that will be initialized with wafInit and reused for all metrics. */ private static String wafVersion = ""; @@ -166,6 +168,11 @@ public void missingUserId(final LoginFramework framework) { missingUserIdQueue.incrementAndGet(framework.ordinal()); } + public void appSecSdkEvent(final LoginEvent event, final LoginVersion version) { + final int index = event.ordinal() * LoginVersion.getNumValues() + version.ordinal(); + appSecSdkEventQueue.incrementAndGet(index); + } + @Override public Collection drain() { if (!rawMetricsQueue.isEmpty()) { @@ -354,6 +361,20 @@ public void prepareMetrics() { } } + // ATO login events + for (LoginEvent event : LoginEvent.values()) { + for (LoginVersion version : LoginVersion.values()) { + final int ordinal = event.ordinal() * LoginVersion.getNumValues() + version.ordinal(); + long counter = appSecSdkEventQueue.getAndSet(ordinal, 0); + if (counter > 0) { + if (!rawMetricsQueue.offer( + new AppSecSdkEvent(counter, event.getTag(), version.getTag()))) { + return; + } + } + } + } + // RASP rule skipped per rule type for after-request reason for (RuleType ruleType : RuleType.values()) { long counter = raspRuleSkippedCounter.getAndSet(ruleType.ordinal(), 0); @@ -424,6 +445,13 @@ public MissingUserIdMetric(long counter, String framework) { } } + public static class AppSecSdkEvent extends WafMetric { + + public AppSecSdkEvent(long counter, String event, final String version) { + super("appsec.sdk.event", counter, "event_type:" + event, "sdk_version:" + version); + } + } + public static class WafRequestsRawMetric extends WafMetric { public WafRequestsRawMetric( final long counter, From c8869f6e9511efe8237bc0c9bf24fb9499c40efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Wed, 26 Mar 2025 10:17:09 +0100 Subject: [PATCH 2/7] Fix metrics and tests --- .../AppSecEventTrackerSpecification.groovy | 35 +++++++++------- .../trace/api/appsec/AppSecEventTracker.java | 40 +++++++++++++------ .../trace/api/telemetry/LoginVersion.java | 6 +-- .../api/telemetry/WafMetricCollector.java | 2 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy index 4a63e4139cf..e9f20c18440 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy @@ -94,20 +94,18 @@ class AppSecEventTrackerSpecification extends DDSpecification { def 'test track login success event (SDK)'() { when: - GlobalTracer.getEventTracker().trackLoginSuccessEvent(USER_LOGIN, ['key1': 'value1', 'key2': 'value2']) + GlobalTracer.getEventTracker().trackLoginSuccessEvent(USER_ID, ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('usr.id', USER_LOGIN) - 1 * traceSegment.setTagTop('usr', ['key1': 'value1', 'key2': 'value2']) - 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', 'sdk') - 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('usr.id', USER_ID) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_ID, true) 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1': 'value1', 'key2': 'value2'], true) 1 * traceSegment.setTagTop('appsec.events.users.login.success.track', true, true) 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true, true) - 2 * traceSegment.setTagTop('asm.keep', true) - 2 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_LOGIN) >> NoopFlow.INSTANCE - 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE + 1 * traceSegment.setTagTop('asm.keep', true) + 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) + 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, USER_ID) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE 0 * _ assertAppSecSdkEvent(LOGIN_SUCCESS, V1) @@ -115,17 +113,19 @@ class AppSecEventTrackerSpecification extends DDSpecification { def 'test track login failure event (SDK)'() { when: - GlobalTracer.getEventTracker().trackLoginFailureEvent(USER_LOGIN, true, ['key1': 'value1', 'key2': 'value2']) + GlobalTracer.getEventTracker().trackLoginFailureEvent(USER_ID, true, ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', USER_LOGIN, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.id', USER_ID, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.login', USER_ID, true) 1 * traceSegment.setTagTop('appsec.events.users.login.failure.usr.exists', true, true) 1 * traceSegment.setTagTop('appsec.events.users.login.failure', ['key1': 'value1', 'key2': 'value2'], true) 1 * traceSegment.setTagTop('appsec.events.users.login.failure.track', true, true) 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true, true) 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) - 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, USER_LOGIN) >> NoopFlow.INSTANCE + 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, USER_ID) >> NoopFlow.INSTANCE + 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE 0 * _ assertAppSecSdkEvent(LOGIN_FAILURE, V1) @@ -153,6 +153,8 @@ class AppSecEventTrackerSpecification extends DDSpecification { then: 1 * traceSegment.setTagTop('usr.id', USER_ID) 1 * traceSegment.setTagTop('usr', ['key1': 'value1', 'key2': 'value2']) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.id', USER_ID, true) + 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr', ['key1': 'value1', 'key2': 'value2'], true) 1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', 'sdk') 1 * traceSegment.setTagTop('appsec.events.users.login.success.usr.login', USER_LOGIN, true) 1 * traceSegment.setTagTop('appsec.events.users.login.success', ['key1': 'value1', 'key2': 'value2'], true) @@ -502,12 +504,15 @@ class AppSecEventTrackerSpecification extends DDSpecification { prepareMetrics() drain() } + final expectedTags = ["event_type:${event.getTag()}".toString(), "sdk_version:${version.getTag()}".toString()] assert metrics.size() == 1 - final metric = metrics.find { it.metricName == 'appsec.sdk.event'} + final metric = metrics.find { it.metricName == 'sdk.event'} assert metric != null + assert metric.namespace == 'appsec' + assert metric.type == 'count' assert metric.value == 1 - assert metric.tags.find { "event_type:${event.getTag()}" } != null - assert metric.tags.find { "sdk_version:${version.getTag()}" } != null + assert metric.tags.size() == 2 + assert metric.tags.containsAll(expectedTags) } private static class ActionFlow implements Flow { diff --git a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java index 12995d0a15c..88490162d78 100644 --- a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java +++ b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java @@ -7,6 +7,7 @@ import static datadog.trace.api.telemetry.LoginEvent.CUSTOM; import static datadog.trace.api.telemetry.LoginEvent.LOGIN_FAILURE; import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS; +import static datadog.trace.api.telemetry.LoginVersion.AUTO; import static datadog.trace.api.telemetry.LoginVersion.V1; import static datadog.trace.api.telemetry.LoginVersion.V2; import static datadog.trace.util.Strings.toHexString; @@ -28,6 +29,7 @@ import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.api.internal.TraceSegment; import datadog.trace.api.telemetry.LoginEvent; +import datadog.trace.api.telemetry.LoginVersion; import datadog.trace.api.telemetry.WafMetricCollector; import datadog.trace.bootstrap.ActiveSubsystems; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; @@ -72,7 +74,7 @@ public final void trackLoginSuccessEvent(String userId, Map meta throw new IllegalArgumentException("userId is null or empty"); } WafMetricCollector.get().appSecSdkEvent(LOGIN_SUCCESS, V1); - if (handleLoginEvent(LOGIN_SUCCESS_EVENT, SDK, userId, userId, null, metadata)) { + if (handleLoginEvent(V1, LOGIN_SUCCESS_EVENT, SDK, userId, userId, null, metadata)) { throw new BlockingException("Blocked request (for login success)"); } } @@ -84,7 +86,7 @@ public final void trackLoginFailureEvent( throw new IllegalArgumentException("userId is null or empty"); } WafMetricCollector.get().appSecSdkEvent(LOGIN_FAILURE, V1); - if (handleLoginEvent(LOGIN_FAILURE_EVENT, SDK, userId, null, exists, metadata)) { + if (handleLoginEvent(V1, LOGIN_FAILURE_EVENT, SDK, userId, userId, exists, metadata)) { throw new BlockingException("Blocked request (for login failure)"); } } @@ -96,7 +98,7 @@ public void trackUserLoginSuccess( throw new IllegalArgumentException("login is null or empty"); } WafMetricCollector.get().appSecSdkEvent(LOGIN_SUCCESS, V2); - if (handleLoginEvent(LOGIN_SUCCESS_EVENT, SDK, login, userId, null, metadata)) { + if (handleLoginEvent(V2, LOGIN_SUCCESS_EVENT, SDK, login, userId, null, metadata)) { throw new BlockingException("Blocked request (for login success)"); } } @@ -108,7 +110,7 @@ public void trackUserLoginFailure( throw new IllegalArgumentException("login is null or empty"); } WafMetricCollector.get().appSecSdkEvent(LOGIN_FAILURE, V2); - if (handleLoginEvent(LOGIN_FAILURE_EVENT, SDK, login, null, exists, metadata)) { + if (handleLoginEvent(V2, LOGIN_FAILURE_EVENT, SDK, login, null, exists, metadata)) { throw new BlockingException("Blocked request (for login failure)"); } } @@ -120,7 +122,7 @@ public final void trackCustomEvent(String eventName, Map metadat throw new IllegalArgumentException("eventName is null or empty"); } WafMetricCollector.get().appSecSdkEvent(CUSTOM, V2); - if (handleLoginEvent(eventName, SDK, null, null, null, metadata)) { + if (handleLoginEvent(V2, eventName, SDK, null, null, null, metadata)) { throw new BlockingException("Blocked request (for custom event)"); } } @@ -143,7 +145,7 @@ public void onUserEvent( } public void onUserNotFound(final UserIdCollectionMode mode) { - if (handleLoginEvent(LOGIN_FAILURE_EVENT, mode, null, null, false, null)) { + if (handleLoginEvent(AUTO, LOGIN_FAILURE_EVENT, mode, null, null, false, null)) { throw new BlockingException("Blocked request (for user not found)"); } } @@ -153,7 +155,7 @@ public void onSignupEvent( final String login, final String userId, final Map metadata) { - if (handleLoginEvent(SIGNUP_EVENT, mode, login, userId, null, metadata)) { + if (handleLoginEvent(AUTO, SIGNUP_EVENT, mode, login, userId, null, metadata)) { throw new BlockingException("Blocked request (for signup)"); } } @@ -163,7 +165,7 @@ public void onLoginSuccessEvent( final String login, final String user, final Map metadata) { - if (handleLoginEvent(LOGIN_SUCCESS_EVENT, mode, login, user, null, metadata)) { + if (handleLoginEvent(AUTO, LOGIN_SUCCESS_EVENT, mode, login, user, null, metadata)) { throw new BlockingException("Blocked request (for login success)"); } } @@ -173,7 +175,7 @@ public void onLoginFailureEvent( final String login, final Boolean exists, final Map metadata) { - if (handleLoginEvent(LOGIN_FAILURE_EVENT, mode, login, null, exists, metadata)) { + if (handleLoginEvent(AUTO, LOGIN_FAILURE_EVENT, mode, login, null, exists, metadata)) { throw new BlockingException("Blocked request (for login failure)"); } } @@ -220,6 +222,7 @@ private boolean handleUser( * blocking action */ private boolean handleLoginEvent( + final LoginVersion version, final String eventName, final UserIdCollectionMode mode, final String login, @@ -250,6 +253,7 @@ private boolean handleLoginEvent( } segment.setTagTop("_dd.appsec.events." + eventName + ".auto.mode", mode.fullName(), true); } + final LoginEvent event = EVENT_MAPPING.get(eventName); if (isNewLoginEvent(mode, segment, eventName)) { if (finalLogin != null) { segment.setTagTop("appsec.events." + eventName + ".usr.login", finalLogin, true); @@ -264,15 +268,27 @@ private boolean handleLoginEvent( segment.setTagTop(Tags.ASM_KEEP, true); segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); - final LoginEvent event = EVENT_MAPPING.get(eventName); if (finalLogin != null && event != null) { block = dispatch(tracer, EVENTS.loginEvent(), (ctx, cb) -> cb.apply(ctx, event, finalLogin)); } } if (userId != null) { - // call setUser - final boolean blockUser = handleUser(mode, userId, metadata); + boolean blockUser; + if (version == V2) { + segment.setTagTop("appsec.events." + eventName + ".usr.id", userId, true); + if (metadata != null && !metadata.isEmpty()) { + segment.setTagTop("appsec.events." + eventName + ".usr", metadata, true); + } + blockUser = handleUser(mode, userId, metadata); + } else { + if (event == LOGIN_SUCCESS) { + segment.setTagTop("usr.id", userId); + } else { + segment.setTagTop("appsec.events." + eventName + ".usr.id", userId, true); + } + blockUser = dispatch(tracer, EVENTS.user(), (ctx, cb) -> cb.apply(ctx, userId)); + } block |= blockUser; } return block; diff --git a/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java index 4161ba55e0d..d91fd519cca 100644 --- a/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java @@ -1,11 +1,9 @@ package datadog.trace.api.telemetry; public enum LoginVersion { - - /** Login events generated via V1 of ATO */ V1("v1"), - /** Login event generated via V2 of ATO */ - V2("v2"); + V2("v2"), + AUTO(null); private final String tag; 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 33cdd73b71e..996470015cf 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 @@ -448,7 +448,7 @@ public MissingUserIdMetric(long counter, String framework) { public static class AppSecSdkEvent extends WafMetric { public AppSecSdkEvent(long counter, String event, final String version) { - super("appsec.sdk.event", counter, "event_type:" + event, "sdk_version:" + version); + super("sdk.event", counter, "event_type:" + event, "sdk_version:" + version); } } From 33592d6ba4ced3e7b7e89c2e346859860029149d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 27 Mar 2025 09:26:31 +0100 Subject: [PATCH 3/7] Fix code quality --- .../main/java/datadog/appsec/api/login/EventTrackerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java index f42bd1e9824..25a2f376bd4 100644 --- a/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java +++ b/dd-trace-api/src/main/java/datadog/appsec/api/login/EventTrackerService.java @@ -18,7 +18,7 @@ public void trackUserLoginFailure( public void trackCustomEvent(final String eventName, final Map metadata) {} }; - void trackUserLoginSuccess(final String login, final String userId, Map metadata); + void trackUserLoginSuccess(String login, String userId, Map metadata); void trackUserLoginFailure(String login, boolean exists, Map metadata); From 79c35f192f8153f6c1a391f11855fe28dc0d6618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 27 Mar 2025 09:49:22 +0100 Subject: [PATCH 4/7] Add extra methods for code clarity --- .../SpringSecurityUserEventDecorator.java | 7 +++---- .../trace/api/appsec/AppSecEventTracker.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java b/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java index 90264f78e30..d7285e7f976 100644 --- a/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java +++ b/dd-java-agent/instrumentation/spring-security-5/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java @@ -2,7 +2,6 @@ import static datadog.trace.api.UserIdCollectionMode.IDENTIFICATION; import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; -import static java.util.Collections.emptyMap; import datadog.trace.api.EventTracker; import datadog.trace.api.GlobalTracer; @@ -70,7 +69,7 @@ public void onSignup(UserDetails user, Throwable throwable) { user.getAuthorities().stream().map(Object::toString).collect(Collectors.joining(","))); } - tracker.onSignupEvent(mode, username, null, metadata); + tracker.onSignupEvent(mode, username, metadata); } public void onLogin(Authentication authentication, Throwable throwable, Authentication result) { @@ -106,7 +105,7 @@ public void onLogin(Authentication authentication, Throwable throwable, Authenti metadata.put("accountNonLocked", String.valueOf(user.isAccountNonLocked())); metadata.put("credentialsNonExpired", String.valueOf(user.isCredentialsNonExpired())); } - tracker.onLoginSuccessEvent(mode, username, null, metadata); + tracker.onLoginSuccessEvent(mode, username, metadata); } else if (throwable != null) { if (missingUsername(username, LoginEvent.LOGIN_FAILURE)) { return; @@ -144,7 +143,7 @@ public void onUser(final Authentication authentication) { if (missingUserId(username)) { return; } - tracker.onUserEvent(mode, username, emptyMap()); + tracker.onUserEvent(mode, username); } private static boolean shouldSkipAuthentication(final Authentication authentication) { diff --git a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java index 88490162d78..8bf939caccb 100644 --- a/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java +++ b/internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java @@ -11,6 +11,7 @@ import static datadog.trace.api.telemetry.LoginVersion.V1; import static datadog.trace.api.telemetry.LoginVersion.V2; import static datadog.trace.util.Strings.toHexString; +import static java.util.Collections.emptyMap; import datadog.appsec.api.blocking.BlockingException; import datadog.appsec.api.login.EventTrackerService; @@ -137,6 +138,10 @@ public void trackUserEvent(final String userId, final Map metada } } + public void onUserEvent(final UserIdCollectionMode mode, final String userId) { + onUserEvent(mode, userId, emptyMap()); + } + public void onUserEvent( final UserIdCollectionMode mode, final String userId, final Map metadata) { if (handleUser(mode, userId, metadata)) { @@ -150,6 +155,11 @@ public void onUserNotFound(final UserIdCollectionMode mode) { } } + public void onSignupEvent( + final UserIdCollectionMode mode, final String login, final Map metadata) { + onSignupEvent(mode, login, null, metadata); + } + public void onSignupEvent( final UserIdCollectionMode mode, final String login, @@ -160,6 +170,11 @@ public void onSignupEvent( } } + public void onLoginSuccessEvent( + final UserIdCollectionMode mode, final String login, final Map metadata) { + onLoginSuccessEvent(mode, login, null, metadata); + } + public void onLoginSuccessEvent( final UserIdCollectionMode mode, final String login, From db8097be2f896082d52075842896b5443d54cceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 27 Mar 2025 09:53:30 +0100 Subject: [PATCH 5/7] Remove extra SDK checks in auto login event tests --- .../user/AppSecEventTrackerSpecification.groovy | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy index e9f20c18440..3b656fac5fa 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy @@ -263,7 +263,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { final expectedUserLogin = mode == ANONYMIZATION ? ANONYMIZED_USER_LOGIN : USER_LOGIN when: - tracker.onSignupEvent(mode, USER_LOGIN, null, ['key1': 'value1', 'key2': 'value2']) + tracker.onSignupEvent(mode, USER_LOGIN, ['key1': 'value1', 'key2': 'value2']) then: if (mode != DISABLED) { @@ -276,9 +276,6 @@ class AppSecEventTrackerSpecification extends DDSpecification { 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) 1 * loginEvent.apply(_ as RequestContext, SIGN_UP, expectedUserLogin) >> NoopFlow.INSTANCE - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE - } } 0 * _ @@ -291,7 +288,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { final expectedUserLogin = mode == ANONYMIZATION ? ANONYMIZED_USER_LOGIN : USER_LOGIN when: - tracker.onLoginSuccessEvent(mode, USER_LOGIN, null, ['key1': 'value1', 'key2': 'value2']) + tracker.onLoginSuccessEvent(mode, USER_LOGIN, ['key1': 'value1', 'key2': 'value2']) then: if (mode != DISABLED) { @@ -304,9 +301,6 @@ class AppSecEventTrackerSpecification extends DDSpecification { 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, expectedUserLogin) >> NoopFlow.INSTANCE - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE - } } 0 * _ @@ -333,9 +327,6 @@ class AppSecEventTrackerSpecification extends DDSpecification { 1 * traceSegment.setTagTop('asm.keep', true) 1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM) 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, expectedUserLogin) >> NoopFlow.INSTANCE - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_LOGIN) >> NoopFlow.INSTANCE - } } 0 * _ From 5c6ed51a5d0b0a1943a10297dc4ec4b9bb27d7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 27 Mar 2025 10:07:14 +0100 Subject: [PATCH 6/7] Add tests for the waf metrics --- .../telemetry/WafMetricCollectorTest.groovy | 100 ++++++++++++------ 1 file changed, 65 insertions(+), 35 deletions(-) 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 cba2f0c63ea..2125ba3ace5 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 @@ -6,6 +6,11 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import static datadog.trace.api.telemetry.LoginEvent.LOGIN_FAILURE +import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS +import static datadog.trace.api.telemetry.LoginVersion.V1 +import static datadog.trace.api.telemetry.LoginVersion.V2 + class WafMetricCollectorTest extends DDSpecification { public static final int DD_WAF_RUN_INTERNAL_ERROR = -3 @@ -50,28 +55,28 @@ class WafMetricCollectorTest extends DDSpecification { then: def metrics = WafMetricCollector.get().drain() - def initMetric = (WafMetricCollector.WafInitRawMetric)metrics[0] + def initMetric = (WafMetricCollector.WafInitRawMetric) metrics[0] initMetric.type == 'count' initMetric.value == 1 initMetric.namespace == 'appsec' initMetric.metricName == 'waf.init' initMetric.tags.toSet() == ['waf_version:waf_ver1', 'event_rules_version:rules.1', 'success:true'].toSet() - def updateMetric1 = (WafMetricCollector.WafUpdatesRawMetric)metrics[1] + def updateMetric1 = (WafMetricCollector.WafUpdatesRawMetric) metrics[1] updateMetric1.type == 'count' updateMetric1.value == 1 updateMetric1.namespace == 'appsec' updateMetric1.metricName == 'waf.updates' updateMetric1.tags.toSet() == ['waf_version:waf_ver1', 'event_rules_version:rules.2', 'success:true'].toSet() - def updateMetric2 = (WafMetricCollector.WafUpdatesRawMetric)metrics[2] + def updateMetric2 = (WafMetricCollector.WafUpdatesRawMetric) metrics[2] updateMetric2.type == 'count' updateMetric2.value == 2 updateMetric2.namespace == 'appsec' updateMetric2.metricName == 'waf.updates' updateMetric2.tags.toSet() == ['waf_version:waf_ver1', 'event_rules_version:rules.3', 'success:false'].toSet() - def requestMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[3] + def requestMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[3] requestMetric.namespace == 'appsec' requestMetric.metricName == 'waf.requests' requestMetric.type == 'count' @@ -88,7 +93,7 @@ class WafMetricCollectorTest extends DDSpecification { 'input_truncated:true', ].toSet() - def requestTriggeredMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[4] + def requestTriggeredMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[4] requestTriggeredMetric.namespace == 'appsec' requestTriggeredMetric.metricName == 'waf.requests' requestTriggeredMetric.value == 1 @@ -105,7 +110,7 @@ class WafMetricCollectorTest extends DDSpecification { ].toSet() - def requestBlockedMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[5] + def requestBlockedMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[5] requestBlockedMetric.namespace == 'appsec' requestBlockedMetric.metricName == 'waf.requests' requestBlockedMetric.type == 'count' @@ -122,7 +127,7 @@ class WafMetricCollectorTest extends DDSpecification { 'input_truncated:true', ].toSet() - def requestTimeoutMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[6] + def requestTimeoutMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[6] requestTimeoutMetric.namespace == 'appsec' requestTimeoutMetric.metricName == 'waf.requests' requestTimeoutMetric.type == 'count' @@ -139,7 +144,7 @@ class WafMetricCollectorTest extends DDSpecification { 'input_truncated:true', ].toSet() - def requestWafErrorMetric = (WafMetricCollector.WafRequestsRawMetric)metrics[7] + def requestWafErrorMetric = (WafMetricCollector.WafRequestsRawMetric) metrics[7] requestWafErrorMetric.namespace == 'appsec' requestWafErrorMetric.metricName == 'waf.requests' requestWafErrorMetric.type == 'count' @@ -156,28 +161,28 @@ class WafMetricCollectorTest extends DDSpecification { 'input_truncated:true', ].toSet() - def raspRuleEvalSqli = (WafMetricCollector.RaspRuleEval)metrics[8] + def raspRuleEvalSqli = (WafMetricCollector.RaspRuleEval) metrics[8] raspRuleEvalSqli.type == 'count' raspRuleEvalSqli.value == 3 raspRuleEvalSqli.namespace == 'appsec' raspRuleEvalSqli.metricName == 'rasp.rule.eval' raspRuleEvalSqli.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspRuleMatch = (WafMetricCollector.RaspRuleMatch)metrics[9] + def raspRuleMatch = (WafMetricCollector.RaspRuleMatch) metrics[9] raspRuleMatch.type == 'count' raspRuleMatch.value == 1 raspRuleMatch.namespace == 'appsec' raspRuleMatch.metricName == 'rasp.rule.match' raspRuleMatch.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspTimeout = (WafMetricCollector.RaspTimeout)metrics[10] + def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[10] raspTimeout.type == 'count' raspTimeout.value == 1 raspTimeout.namespace == 'appsec' raspTimeout.metricName == 'rasp.timeout' raspTimeout.tags.toSet() == ['rule_type:sql_injection', 'waf_version:waf_ver1'].toSet() - def raspInvalidCode = (WafMetricCollector.RaspError)metrics[11] + def raspInvalidCode = (WafMetricCollector.RaspError) metrics[11] raspInvalidCode.type == 'count' raspInvalidCode.value == 1 raspInvalidCode.namespace == 'appsec' @@ -190,7 +195,7 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def wafInvalidCode = (WafMetricCollector.WafError)metrics[12] + def wafInvalidCode = (WafMetricCollector.WafError) metrics[12] wafInvalidCode.type == 'count' wafInvalidCode.value == 1 wafInvalidCode.namespace == 'appsec' @@ -200,10 +205,10 @@ class WafMetricCollectorTest extends DDSpecification { 'rule_type:command_injection', 'rule_variant:shell', 'event_rules_version:rules.3', - 'waf_error:' +DD_WAF_RUN_INTERNAL_ERROR + 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def raspInvalidObjectCode = (WafMetricCollector.RaspError)metrics[13] + def raspInvalidObjectCode = (WafMetricCollector.RaspError) metrics[13] raspInvalidObjectCode.type == 'count' raspInvalidObjectCode.value == 1 raspInvalidObjectCode.namespace == 'appsec' @@ -215,7 +220,7 @@ class WafMetricCollectorTest extends DDSpecification { ] .toSet() - def wafInvalidObjectCode = (WafMetricCollector.WafError)metrics[14] + def wafInvalidObjectCode = (WafMetricCollector.WafError) metrics[14] wafInvalidObjectCode.type == 'count' wafInvalidObjectCode.value == 1 wafInvalidObjectCode.namespace == 'appsec' @@ -223,10 +228,10 @@ class WafMetricCollectorTest extends DDSpecification { wafInvalidObjectCode.tags.toSet() == [ 'rule_type:sql_injection', 'waf_version:waf_ver1', - 'waf_error:'+DD_WAF_RUN_INVALID_OBJECT_ERROR + 'waf_error:' + DD_WAF_RUN_INVALID_OBJECT_ERROR ].toSet() - def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped)metrics[15] + def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped) metrics[15] raspRuleSkipped.type == 'count' raspRuleSkipped.value == 1 raspRuleSkipped.namespace == 'appsec' @@ -240,7 +245,7 @@ class WafMetricCollectorTest extends DDSpecification { def collector = WafMetricCollector.get() when: - (0..limit*2).each { + (0..limit * 2).each { collector.wafInit("foo", "bar", true) } @@ -249,7 +254,7 @@ class WafMetricCollectorTest extends DDSpecification { collector.drain().size() == limit when: - (0..limit*2).each { + (0..limit * 2).each { collector.wafUpdates("bar", true) } @@ -258,7 +263,7 @@ class WafMetricCollectorTest extends DDSpecification { collector.drain().size() == limit when: - (0..limit*2).each { + (0..limit * 2).each { collector.wafRequest() collector.prepareMetrics() } @@ -268,7 +273,7 @@ class WafMetricCollectorTest extends DDSpecification { collector.drain().size() == limit when: - (0..limit*2).each { + (0..limit * 2).each { collector.wafRequestTriggered() collector.prepareMetrics() } @@ -278,7 +283,7 @@ class WafMetricCollectorTest extends DDSpecification { collector.drain().size() == limit when: - (0..limit*2).each { + (0..limit * 2).each { collector.wafRequestBlocked() collector.prepareMetrics() } @@ -304,7 +309,7 @@ class WafMetricCollectorTest extends DDSpecification { when: (1..loginSuccessCount).each { executors.submit { - action.call(LoginFramework.SPRING_SECURITY, LoginEvent.LOGIN_SUCCESS) + action.call(LoginFramework.SPRING_SECURITY, LOGIN_SUCCESS) } } (1..loginFailureCount).each { @@ -339,7 +344,7 @@ class WafMetricCollectorTest extends DDSpecification { } assert tags["framework"] == LoginFramework.SPRING_SECURITY.getTag() switch (tags["event_type"]) { - case LoginEvent.LOGIN_SUCCESS.getTag(): + case LOGIN_SUCCESS.getTag(): assert metric.value == loginSuccessCount break case LoginEvent.LOGIN_FAILURE.getTag(): @@ -413,43 +418,43 @@ class WafMetricCollectorTest extends DDSpecification { then: def metrics = WafMetricCollector.get().drain() - def raspRuleEval = (WafMetricCollector.RaspRuleEval)metrics[1] + def raspRuleEval = (WafMetricCollector.RaspRuleEval) metrics[1] raspRuleEval.type == 'count' raspRuleEval.value == 3 raspRuleEval.namespace == 'appsec' raspRuleEval.metricName == 'rasp.rule.eval' raspRuleEval.tags.toSet() == [ 'rule_type:command_injection', - 'rule_variant:'+ruleType.variant, + 'rule_variant:' + ruleType.variant, 'waf_version:waf_ver1', 'event_rules_version:rules.1' ].toSet() - def raspRuleMatch = (WafMetricCollector.RaspRuleMatch)metrics[2] + def raspRuleMatch = (WafMetricCollector.RaspRuleMatch) metrics[2] raspRuleMatch.type == 'count' raspRuleMatch.value == 1 raspRuleMatch.namespace == 'appsec' raspRuleMatch.metricName == 'rasp.rule.match' raspRuleMatch.tags.toSet() == [ 'rule_type:command_injection', - 'rule_variant:'+ruleType.variant, + 'rule_variant:' + ruleType.variant, 'waf_version:waf_ver1', 'event_rules_version:rules.1' ].toSet() - def raspTimeout = (WafMetricCollector.RaspTimeout)metrics[3] + def raspTimeout = (WafMetricCollector.RaspTimeout) metrics[3] raspTimeout.type == 'count' raspTimeout.value == 1 raspTimeout.namespace == 'appsec' raspTimeout.metricName == 'rasp.timeout' raspTimeout.tags.toSet() == [ 'rule_type:command_injection', - 'rule_variant:'+ruleType.variant, + 'rule_variant:' + ruleType.variant, 'waf_version:waf_ver1', 'event_rules_version:rules.1' ].toSet() - def raspInvalidCode = (WafMetricCollector.RaspError)metrics[4] + def raspInvalidCode = (WafMetricCollector.RaspError) metrics[4] raspInvalidCode.type == 'count' raspInvalidCode.value == 1 raspInvalidCode.namespace == 'appsec' @@ -462,7 +467,7 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def wafInvalidCode = (WafMetricCollector.WafError)metrics[5] + def wafInvalidCode = (WafMetricCollector.WafError) metrics[5] wafInvalidCode.type == 'count' wafInvalidCode.value == 1 wafInvalidCode.namespace == 'appsec' @@ -475,18 +480,43 @@ class WafMetricCollectorTest extends DDSpecification { 'waf_error:' + DD_WAF_RUN_INTERNAL_ERROR ].toSet() - def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped)metrics[6] + def raspRuleSkipped = (WafMetricCollector.AfterRequestRaspRuleSkipped) metrics[6] raspRuleSkipped.type == 'count' raspRuleSkipped.value == 1 raspRuleSkipped.namespace == 'appsec' raspRuleSkipped.metricName == 'rasp.rule.skipped' raspRuleSkipped.tags.toSet() == [ 'rule_type:command_injection', - 'rule_variant:'+ruleType.variant, + 'rule_variant:' + ruleType.variant, 'reason:after-request', ].toSet() where: ruleType << [RuleType.COMMAND_INJECTION, RuleType.SHELL_INJECTION] } + + void 'test login event metrics'() { + when: + WafMetricCollector.get().appSecSdkEvent(LOGIN_SUCCESS, V1) + WafMetricCollector.get().appSecSdkEvent(LOGIN_FAILURE, V2) + + then: + WafMetricCollector.get().prepareMetrics() + final metrics = WafMetricCollector.get().drain() + final sdkEvents = metrics.findAll { it.metricName == 'sdk.event' } + + final loginSuccess = sdkEvents[0] + loginSuccess.type == 'count' + loginSuccess.value == 1 + loginSuccess.namespace == 'appsec' + loginSuccess.metricName == 'sdk.event' + loginSuccess.tags == ['event_type:login_success', 'sdk_version:v1'] + + final loginFailure = sdkEvents[1] + loginFailure.type == 'count' + loginFailure.value == 1 + loginFailure.namespace == 'appsec' + loginFailure.metricName == 'sdk.event' + loginFailure.tags == ['event_type:login_failure', 'sdk_version:v2'] + } } From 7dbc70c41dc1e25794990894545634195a16632c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 27 Mar 2025 11:06:55 +0100 Subject: [PATCH 7/7] Fix tests --- .../AppSecEventTrackerSpecification.groovy | 1 - ...ventTrackerAppSecDisabledForkedTest.groovy | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy index 3b656fac5fa..97106fad055 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy @@ -496,7 +496,6 @@ class AppSecEventTrackerSpecification extends DDSpecification { drain() } final expectedTags = ["event_type:${event.getTag()}".toString(), "sdk_version:${version.getTag()}".toString()] - assert metrics.size() == 1 final metric = metrics.find { it.metricName == 'sdk.event'} assert metric != null assert metric.namespace == 'appsec' diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/EventTrackerAppSecDisabledForkedTest.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/EventTrackerAppSecDisabledForkedTest.groovy index 9fd1ee9e10f..6ea611d93e0 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/EventTrackerAppSecDisabledForkedTest.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/EventTrackerAppSecDisabledForkedTest.groovy @@ -1,5 +1,6 @@ package com.datadog.appsec.user +import datadog.appsec.api.login.EventTrackerV2 import datadog.appsec.api.user.User import datadog.trace.api.GlobalTracer import datadog.trace.api.UserIdCollectionMode @@ -26,6 +27,7 @@ class EventTrackerAppSecDisabledForkedTest extends DDSpecification { void setup() { tracker = new AppSecEventTracker() GlobalTracer.setEventTracker(tracker) + EventTrackerV2.setEventTrackerService(tracker) User.setUserService(tracker) traceSegment = Mock(TraceSegment) final tracer = Stub(AgentTracer.TracerAPI) { @@ -40,7 +42,7 @@ class EventTrackerAppSecDisabledForkedTest extends DDSpecification { GlobalTracer.getEventTracker().trackLoginSuccessEvent('user', ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true, true) } void 'test track login failure event (SDK)'() { @@ -48,7 +50,7 @@ class EventTrackerAppSecDisabledForkedTest extends DDSpecification { GlobalTracer.getEventTracker().trackLoginFailureEvent('user', true, ['key1': 'value1', 'key2': 'value2']) then: - 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true) + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true, true) } void 'test track custom event (SDK)'() { @@ -59,6 +61,30 @@ class EventTrackerAppSecDisabledForkedTest extends DDSpecification { 1 * traceSegment.setTagTop('_dd.appsec.events.myevent.sdk', true, true) } + void 'test track login success event V2 (SDK)'() { + when: + EventTrackerV2.trackUserLoginSuccess('user', 'id', ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.success.sdk', true, true) + } + + void 'test track login failure event V2 (SDK)'() { + when: + EventTrackerV2.trackUserLoginFailure('user', true, ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('_dd.appsec.events.users.login.failure.sdk', true, true) + } + + void 'test track custom event V2 (SDK)'() { + when: + EventTrackerV2.trackCustomEvent('myevent', ['key1': 'value1', 'key2': 'value2']) + + then: + 1 * traceSegment.setTagTop('_dd.appsec.events.myevent.sdk', true, true) + } + void 'test onSignup'() { when: tracker.onSignupEvent(IDENTIFICATION, 'user', ['key1': 'value1', 'key2': 'value2'])