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..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 @@ -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 } @@ -89,15 +98,17 @@ class AppSecEventTrackerSpecification extends DDSpecification { 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('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) 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) } def 'test track login failure event (SDK)'() { @@ -105,17 +116,19 @@ class AppSecEventTrackerSpecification extends DDSpecification { GlobalTracer.getEventTracker().trackLoginFailureEvent(USER_ID, 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.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_ID) >> NoopFlow.INSTANCE 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE 0 * _ + + assertAppSecSdkEvent(LOGIN_FAILURE, V1) } def 'test track custom event (SDK)'() { @@ -123,7 +136,63 @@ 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('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) + 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 +206,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 +260,91 @@ 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, ['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 - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE - } + 1 * loginEvent.apply(_ as RequestContext, SIGN_UP, expectedUserLogin) >> 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, ['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 - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE - } + 1 * loginEvent.apply(_ as RequestContext, LOGIN_SUCCESS, expectedUserLogin) >> 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 - if (mode == SDK) { - 1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE - } + 1 * loginEvent.apply(_ as RequestContext, LOGIN_FAILURE, expectedUserLogin) >> 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 +354,7 @@ class AppSecEventTrackerSpecification extends DDSpecification { 0 * _ where: - mode << UserIdCollectionMode.values() + mode << [IDENTIFICATION, ANONYMIZATION, DISABLED] } def "test onUserNotFound (#mode)"() { @@ -320,17 +363,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 +426,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 +440,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 +449,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 +460,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 +486,25 @@ 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() + } + final expectedTags = ["event_type:${event.getTag()}".toString(), "sdk_version:${version.getTag()}".toString()] + 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.size() == 2 + assert metric.tags.containsAll(expectedTags) + } + private static class ActionFlow implements Flow { private Action action 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']) 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..25a2f376bd4 --- /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(String login, 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..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 @@ -4,13 +4,18 @@ 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.AUTO; +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,80 +29,112 @@ 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; 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(V1, 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(V1, LOGIN_FAILURE_EVENT, SDK, userId, userId, 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(V2, 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(V2, 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; + @SuppressWarnings("deprecation") + @Override + public final void trackCustomEvent(String eventName, Map metadata) { + if (eventName == null || eventName.isEmpty()) { + throw new IllegalArgumentException("eventName is null or empty"); } - final AgentTracer.TracerAPI tracer = tracer(); - if (tracer == null) { - return; + WafMetricCollector.get().appSecSdkEvent(CUSTOM, V2); + if (handleLoginEvent(V2, eventName, SDK, null, null, null, metadata)) { + throw new BlockingException("Blocked request (for custom event)"); } - final TraceSegment segment = tracer.getTraceSegment(); - if (segment == null) { - return; + } + + @Override + public void trackUserEvent(final String userId, final Map metadata) { + if (userId == null || userId.isEmpty()) { + throw new IllegalArgumentException("userId 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); + if (handleUser(SDK, userId, metadata)) { + throw new BlockingException("Blocked request (for user)"); } } @@ -107,197 +144,169 @@ public void onUserEvent(final UserIdCollectionMode mode, final String userId) { 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(AUTO, 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 Map metadata) { + onSignupEvent(mode, login, null, metadata); + } + + public void onSignupEvent( + final UserIdCollectionMode mode, + final String login, + final String userId, + final Map metadata) { + if (handleLoginEvent(AUTO, 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 Map metadata) { + onLoginSuccessEvent(mode, login, null, metadata); + } + + public void onLoginSuccessEvent( + final UserIdCollectionMode mode, + final String login, + final String user, + final Map metadata) { + if (handleLoginEvent(AUTO, 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(AUTO, 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 LoginVersion version, + 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 (finalLogin != null) { + segment.setTagTop("_dd.appsec.usr.login", finalLogin); + } + segment.setTagTop("_dd.appsec.events." + eventName + ".auto.mode", mode.fullName(), true); } - if (isNewLoginEvent(mode, segment, LOGIN_FAILURE_TAG)) { - segment.setTagTop("appsec.events.users.login.failure.usr.login", finalUserId); + final LoginEvent event = EVENT_MAPPING.get(eventName); + 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.users.login.failure", metadata); + segment.setTagTop("appsec.events." + eventName, metadata, true); } if (exists != null) { - segment.setTagTop("appsec.events.users.login.failure.usr.exists", exists); + segment.setTagTop("appsec.events." + eventName + ".usr.exists", exists, true); } - segment.setTagTop("appsec.events.users.login.failure.track", true); + segment.setTagTop("appsec.events." + eventName + ".track", true, 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)); - } - } - } - 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); + if (finalLogin != null && event != null) { + block = + dispatch(tracer, EVENTS.loginEvent(), (ctx, cb) -> cb.apply(ctx, event, finalLogin)); + } } - if (isNewLoginEvent(mode, segment, eventName)) { - if (metadata != null && !metadata.isEmpty()) { - segment.setTagTop("appsec.events." + eventName, metadata, true); + if (userId != null) { + 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)); } - segment.setTagTop("appsec.events." + eventName + ".track", true, true); - segment.setTagTop(Tags.ASM_KEEP, true); - segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM); + block |= blockUser; } + return block; } private boolean isNewLoginEvent( @@ -312,36 +321,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 +367,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 +393,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..d91fd519cca --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/telemetry/LoginVersion.java @@ -0,0 +1,23 @@ +package datadog.trace.api.telemetry; + +public enum LoginVersion { + V1("v1"), + V2("v2"), + AUTO(null); + + 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..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 @@ -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("sdk.event", counter, "event_type:" + event, "sdk_version:" + version); + } + } + public static class WafRequestsRawMetric extends WafMetric { public WafRequestsRawMetric( final long counter, 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'] + } }