diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index dbcd55a0b8..314fae9d20 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -95,6 +95,7 @@ module.exports = { oracledb: () => require('../oracledb'), openai: () => require('../openai'), paperplane: () => require('../paperplane'), + passport: () => require('../passport'), 'passport-http': () => require('../passport-http'), 'passport-local': () => require('../passport-local'), pg: () => require('../pg'), diff --git a/packages/datadog-instrumentations/src/passport.js b/packages/datadog-instrumentations/src/passport.js new file mode 100644 index 0000000000..6429bb67b0 --- /dev/null +++ b/packages/datadog-instrumentations/src/passport.js @@ -0,0 +1,85 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') + +/* TODO: test with: +passport-jwt JWTs + can be used both for login events, or as a session, that complicates things it think + maybe instrument this lib directly, and ofc only send the events after it was verified +@nestjs/passport +pasport-local +passport-oauth2 +passport-google-oauth20 +passport-custom +passport-http +passport-http-bearer +koa-passport +*/ + +function wrapDone (done) { + // eslint-disable-next-line n/handle-callback-err + return function wrappedDone (err, user) { + if (user) { + const abortController = new AbortController() + + // express-session middleware sets req.sessionID, it's required to use passport sessions anyway so might as well use it ? + // what if session IDs are using rolling sessions or always changing or something idk ? + channel.publish({ req, user, sessionId: req.sessionID, abortController }) + + if (abortController.signal.aborted) return + } + + return done.apply(this, arguments) + } +} + +function wrapDeserializeUser (deserializeUser) { + return function wrappedDeserializeUser (fn, req, done) { + if (typeof req === 'function') { + done = req + // req = storage.getStore().get('req') + arguments[1] = wrapDone(done) + } else { + arguments[2] = wrapDone(done) + } + + return deserializeUser.apply(this, arguments) + } +} + + +const { block } = require('../../dd-trace/src/appsec/blocking') +const { getRootSpan } = require('../../dd-trace/src/appsec/sdk/utils') + +addHook({ + name: 'passport', + file: 'lib/authenticator.js', + versions: ['>=0.3.0'] // TODO +}, Authenticator => { + shimmer.wrap(Authenticator.prototype, 'deserializeUser', wrapDeserializeUser) + + shimmer.wrap(Authenticator.prototype, 'authenticate', function wrapAuthenticate (authenticate) { + return function wrappedAuthenticate (name) { + const middleware = authenticate.apply(this, arguments) + + const strategy = this._strategy(name) + + strategy._verify + + return function wrappedMiddleware (req, res, next) { + return middleware(req, res, function wrappedNext (err) { + console.log('NEW', req.user) + if (req.user?.name === 'bitch') { + + return block(req, res, getRootSpan(global._ddtrace)) + } + + return next.apply(this, arguments) + }) + } + } + }) + + return Authenticator +}) diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index 02f9f00837..e78f1f42a3 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -180,6 +180,18 @@ function incomingHttpEndTranslator ({ req, res }) { Reporter.finishRequest(req, res) } +function onPassportDeserializeUser ({ req, user, sessionId, abordController }) { + UserTracking.trackUser(user, abortController) + + if (sessionId && typeof sessionId === 'string') { + const results = waf.run({ + persistent: { + 'usr.session_id': sessionId + } + }) + } +} + function onPassportVerify ({ login, user, success }) { const store = storage.getStore() const rootSpan = store?.req && web.root(store.req) diff --git a/packages/dd-trace/src/appsec/sdk/set_user.js b/packages/dd-trace/src/appsec/sdk/set_user.js index 07a075bfea..d1293878d7 100644 --- a/packages/dd-trace/src/appsec/sdk/set_user.js +++ b/packages/dd-trace/src/appsec/sdk/set_user.js @@ -2,6 +2,7 @@ const { getRootSpan } = require('./utils') const log = require('../../log') +const waf = require('../waf') function setUserTags (user, rootSpan) { for (const k of Object.keys(user)) { @@ -23,16 +24,40 @@ function setUser (tracer, user) { // must get user ID with USER_ID_FIELDS - // _dd.appsec.user.collection_mode: collectionMode // sdk/ident/anon + setTags({ + 'usr.id': userId, + '_dd.appsec.user.collection_mode': 'sdk/ident/anon' + }) + + /* + When the user monitoring SDK is available and in use, the following applies: + The usr.id must be set to the value provided through the user monitoring SDK. + The span tag _dd.appsec.user.collection_mode must be set to sdk. + This effectively means that the automated user ID collection mechanism must not overwrite the aforementioned span tags, while the user monitoring SDK must overwrite them if present. + */ + - setUserTags(user, rootSpan) /* - User IDs generated through the SDK must now be provided to libddwaf as a persistent addresses. - If the user monitoring SDK has already resulted in a call to libddwaf before any automated instrumentation or collection method has been executed, no extra call should be made. - If the automated instrumentation or collection method has resulted in a call to libddwaf before the user monitoring SDK has been executed, a second call must be performed with the user ID obtained through the SDK. + When a user provides their own session ID through the use of the SDK using the session_id key: + If libddwaf hasn’t already been called with the usr.session_id address, it should be called with the provided session_id and further calls through the automated collection method should be inhibited. + If libddwaf has already been called with the usr.session_id address, it should be called again. */ + if (user.session_id && typeof user.session_id === 'string') { + persistent['usr.session_id'] = user.session_id + } + + + setUserTags(user, rootSpan) + + // If the automated instrumentation or collection method has resulted in a call to libddwaf before the user monitoring SDK has been executed, a second call must be performed with the user ID obtained through the SDK. // will the second call trigger tho ? make some edge case tests + const results = waf.run({ + persistent: { + [USER_ID]: userId + } + }) + } module.exports = { diff --git a/packages/dd-trace/src/appsec/user_tracking/index.js b/packages/dd-trace/src/appsec/user_tracking/index.js index f4588a6dd9..52b96475b1 100644 --- a/packages/dd-trace/src/appsec/user_tracking/index.js +++ b/packages/dd-trace/src/appsec/user_tracking/index.js @@ -127,6 +127,31 @@ function passportTrackEvent (credentials, passportUser, rootSpan) { } } + +function trackUser (user, abortController) { + if (!collectionMode || collectionMode === 'disabled') return + + const userId = getUserId(user) + + if (!userId) { + + } + + // don't override tags if already set by "sdk" but still add the _dd.appsec.usr.id + rootSpan.addTags({ + 'usr.id': userId, + '_dd.appsec.usr.id': userId, // always AND only send when automated + '_dd.appsec.user.collection_mode': collectionMode // short or long form ? + }) + + // If the user monitoring SDK has already resulted in a call to libddwaf before any automated instrumentation or collection method has been executed, no extra call should be made. + const results = waf.run({ + persistent: { + [USER_ID]: userID + } + }) +} + module.exports = { setCollectionMode, trackLogin