Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic userID tracking and blocking #4670

Draft
wants to merge 100 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
81f6ab5
instrument passport
simon-id Sep 11, 2024
023828c
lint
simon-id Sep 11, 2024
98090fb
update config
simon-id Sep 11, 2024
5cc7274
add new RC capability
simon-id Sep 11, 2024
3343878
Merge branch 'master' into new_user_collection
simon-id Sep 11, 2024
4e127de
add RC handler
simon-id Sep 12, 2024
b4daf8f
fix require path
simon-id Sep 12, 2024
5f85b8e
add setCollectionMode()
simon-id Sep 12, 2024
1b074ad
use setCollectionMode() in appsec index
simon-id Sep 12, 2024
cbc8cf3
DRY up passport strategies instrumentation
simon-id Sep 17, 2024
729241d
simplify passport strategies instrumentations
simon-id Sep 17, 2024
6ffc21f
simplify instrumentation
simon-id Sep 17, 2024
4433baa
note for later
simon-id Sep 17, 2024
cf39858
add blocking to login
simon-id Sep 17, 2024
2da5bef
Merge branch 'master' into new_user_collection
simon-id Sep 17, 2024
d8bed47
add abortController in listener
simon-id Sep 17, 2024
5105006
cleanup
simon-id Oct 20, 2024
65c680b
update typings
simon-id Oct 20, 2024
23f965d
cleanup
simon-id Oct 20, 2024
b5b336a
RC config update
simon-id Oct 20, 2024
53a7791
push everything
simon-id Oct 30, 2024
2b7d2e6
simplify passport strategies instrumentation
simon-id Oct 30, 2024
24c2828
fixes
simon-id Oct 30, 2024
cdb89f6
simplify code
simon-id Oct 30, 2024
32edfae
cleanup
simon-id Oct 30, 2024
50b3605
revert some changes
simon-id Oct 30, 2024
b5aa0d4
delete passport.js
simon-id Oct 30, 2024
1f30a3c
Update packages/datadog-instrumentations/src/passport-utils.js
simon-id Oct 30, 2024
9fee2bc
update verify subscriber
simon-id Oct 30, 2024
be3115a
rollback config changes
simon-id Oct 30, 2024
38b4419
fix config
simon-id Oct 30, 2024
40c9df1
add blocking for passport strategies
simon-id Oct 30, 2024
e34bb72
update typings and docs
simon-id Oct 30, 2024
d255702
update appsec index
simon-id Oct 30, 2024
b6d4584
update RC
simon-id Oct 30, 2024
75782f9
Merge branch 'master' into new_user_collection
simon-id Oct 30, 2024
4ff8379
push some stuff
simon-id Oct 30, 2024
be473e6
push some stuff
simon-id Oct 30, 2024
4a566a8
cleanup
simon-id Oct 30, 2024
882bc8b
cleanup
simon-id Oct 30, 2024
d4345ef
add new usr.login waf address
simon-id Oct 31, 2024
be4a06c
commit some stuff
simon-id Oct 31, 2024
f129890
cleanup
simon-id Oct 31, 2024
58897c8
Merge branch 'new_user_collection' into automatic_userid_blocking
simon-id Oct 31, 2024
cceac23
cleanup
simon-id Oct 31, 2024
654d260
add comment for later
simon-id Oct 31, 2024
066da87
add some ideas
simon-id Oct 31, 2024
da9b270
cleanup
simon-id Nov 1, 2024
01c8240
aaaaa
simon-id Nov 1, 2024
cc86e3a
pass abort controller for blocking
simon-id Nov 1, 2024
69d12a9
push some notes
simon-id Nov 14, 2024
ce604c1
change config default
simon-id Nov 15, 2024
56c30e8
handle duplicate RC confs for auto_user_instrum.mode
simon-id Nov 15, 2024
1d3773a
refactor sdk/track_event.js to only be used by the SDK
simon-id Nov 15, 2024
a9f5c01
Merge branch 'new_user_collection' into automatic_userid_blocking
simon-id Nov 15, 2024
a72695f
push some stuff
simon-id Nov 19, 2024
91a59db
remove some comments
simon-id Nov 19, 2024
faea3aa
pass login to WAF in SDK login success event
simon-id Nov 19, 2024
fe70c84
add comments
simon-id Nov 19, 2024
e32f78e
add framework name to passport strategy instrum
simon-id Nov 20, 2024
806ab23
add framework name and waf handleResults() to onPassportVerify()
simon-id Nov 20, 2024
69be031
Merge branch 'master' into new_user_collection
simon-id Nov 20, 2024
1e1bb47
finally commit trackLogin()
simon-id Nov 21, 2024
8eac055
fix tag override condition
simon-id Nov 21, 2024
a444cfe
add telemetry function
simon-id Nov 21, 2024
187b42f
Merge branch 'master' into new_user_collection
simon-id Nov 21, 2024
72a4a8b
Merge branch 'master' into new_user_collection
simon-id Nov 22, 2024
9fa94f6
move user_tracking into a file instead of a subfolder
simon-id Nov 22, 2024
d51e434
Merge branch 'master' into new_user_collection
simon-id Nov 22, 2024
8b22248
remove changes
simon-id Nov 22, 2024
25abd50
temp revert changes
simon-id Nov 22, 2024
52a0601
temp revert changes
simon-id Nov 22, 2024
b72ce34
Merge branch 'new_user_collection' into automatic_userid_blocking
simon-id Nov 22, 2024
5a10fc0
fix conflict
simon-id Nov 22, 2024
9869c4b
wrong file
simon-id Nov 22, 2024
76e2e65
add some stuff
simon-id Nov 22, 2024
80ae693
push some stuff
simon-id Nov 25, 2024
7e3374d
Use addresses enum instead of hardcoded business logic events
simon-id Nov 25, 2024
c099a2a
Update default value of collection mode in typings
simon-id Nov 25, 2024
96cb722
Add string check in getUserId()
simon-id Nov 25, 2024
0a347e8
Merge branch 'master' into new_user_collection
simon-id Nov 26, 2024
85ad6f2
fix existing RC tests
simon-id Nov 26, 2024
70dcbab
add tests for RC collection mode
simon-id Nov 26, 2024
ff72271
fix env var ordering
simon-id Nov 26, 2024
7338bd6
cleanup track_event.spec.js
simon-id Nov 29, 2024
b71ed06
do not export or test trackEvent()
simon-id Nov 29, 2024
166bc23
update test, cleanup, and add missing coverage
simon-id Nov 29, 2024
a93b79a
change ordering of code to match tests
simon-id Nov 29, 2024
a83c4fb
delete passport-utils tests because it's useless
simon-id Nov 29, 2024
6cf7ef2
fix test to correctly use passReqToCallback
simon-id Nov 29, 2024
29c0d62
update passport-local tests
simon-id Nov 29, 2024
a6211b2
Merge branch 'new_user_collection' into automatic_userid_blocking
simon-id Dec 2, 2024
afafb8a
update tests for passpot-http
simon-id Dec 2, 2024
956cb9b
allow empty login strings
simon-id Dec 2, 2024
62ae42b
update TS tests
simon-id Dec 2, 2024
375f688
update telemetry tests
simon-id Dec 2, 2024
79a7ff5
Merge branch 'new_user_collection' into automatic_userid_blocking
simon-id Dec 2, 2024
b47e44a
Merge branch 'master' into automatic_userid_blocking
simon-id Dec 17, 2024
5a0ee2e
push stuff
simon-id Dec 26, 2024
775bf9a
remove session id support
simon-id Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,12 +655,24 @@ declare namespace tracer {
*/
eventTracking?: {
/**
* Controls the automated user event tracking mode. Possible values are disabled, safe and extended.
* On safe mode, any detected Personally Identifiable Information (PII) about the user will be redacted from the event.
* On extended mode, no redaction will take place.
* @default 'safe'
* Controls the automated user tracking mode for user IDs and logins collections. Possible values:
* * 'anonymous': will hash user IDs and user logins before collecting them
* * 'anon': alias for 'anonymous'
* * 'safe': deprecated alias for 'anonymous'
*
* * 'identification': will collect user IDs and logins without redaction
* * 'ident': alias for 'identification'
* * 'extended': deprecated alias for 'identification'
*
* * 'disabled': will not collect user IDs and logins
*
* Unknown values will be considered as 'disabled'
* @default 'ident'
*/
mode?: 'safe' | 'extended' | 'disabled'
mode?:
'anonymous' | 'anon' | 'safe' |
'identification' | 'ident' | 'extended' |
'disabled'
},
/**
* Configuration for Api Security
Expand Down
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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'),
Expand Down
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/http/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ addHook({ name: httpNames }, http => {
shimmer.wrap(http.Server.prototype, 'emit', wrapEmit)
shimmer.wrap(http.ServerResponse.prototype, 'writeHead', wrapWriteHead)
shimmer.wrap(http.ServerResponse.prototype, 'write', wrapWrite)
http.ServerResponse.prototype._originalEnd = http.ServerResponse.prototype.end
shimmer.wrap(http.ServerResponse.prototype, 'end', wrapEnd)
shimmer.wrap(http.ServerResponse.prototype, 'setHeader', wrapSetHeader)
shimmer.wrap(http.ServerResponse.prototype, 'removeHeader', wrapAppendOrRemoveHeader)
Expand Down
16 changes: 2 additions & 14 deletions packages/datadog-instrumentations/src/passport-http.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const { addHook } = require('./helpers/instrument')
const { wrapVerify } = require('./passport-utils')
const { strategyHook } = require('./passport-utils')

addHook({
name: 'passport-http',
file: 'lib/passport-http/strategies/basic.js',
versions: ['>=0.3.0']
}, BasicStrategy => {
return shimmer.wrapFunction(BasicStrategy, BasicStrategy => function () {
const type = 'http'

if (typeof arguments[0] === 'function') {
arguments[0] = wrapVerify(arguments[0], false, type)
} else {
arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type)
}
return BasicStrategy.apply(this, arguments)
})
})
}, strategyHook)
16 changes: 2 additions & 14 deletions packages/datadog-instrumentations/src/passport-local.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const { addHook } = require('./helpers/instrument')
const { wrapVerify } = require('./passport-utils')
const { strategyHook } = require('./passport-utils')

addHook({
name: 'passport-local',
file: 'lib/strategy.js',
versions: ['>=1.0.0']
}, Strategy => {
return shimmer.wrapFunction(Strategy, Strategy => function () {
const type = 'local'

if (typeof arguments[0] === 'function') {
arguments[0] = wrapVerify(arguments[0], false, type)
} else {
arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type)
}
return Strategy.apply(this, arguments)
})
})
}, strategyHook)
62 changes: 43 additions & 19 deletions packages/datadog-instrumentations/src/passport-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,57 @@ const { channel } = require('./helpers/instrument')

const passportVerifyChannel = channel('datadog:passport:verify:finish')

function wrapVerifiedAndPublish (username, password, verified, type) {
if (!passportVerifyChannel.hasSubscribers) {
return verified
}
function wrapVerifiedAndPublish (framework, username, verified) {
return shimmer.wrapFunction(verified, function wrapVerify (verified) {
return function wrappedVerified (err, user) {
// if there is an error, it's neither an auth success nor a failure
if (!err) {
const abortController = new AbortController()

passportVerifyChannel.publish({ framework, login: username, user, success: !!user, abortController })

if (abortController.signal.aborted) return
}

// eslint-disable-next-line n/handle-callback-err
return shimmer.wrapFunction(verified, verified => function (err, user, info) {
const credentials = { type, username }
passportVerifyChannel.publish({ credentials, user })
return verified.apply(this, arguments)
return verified.apply(this, arguments)
}
})
}

function wrapVerify (verify, passReq, type) {
if (passReq) {
return function (req, username, password, verified) {
arguments[3] = wrapVerifiedAndPublish(username, password, verified, type)
return verify.apply(this, arguments)
function wrapVerify (verify) {
return function wrappedVerify (req, username, password, verified) {
if (passportVerifyChannel.hasSubscribers) {
const framework = `passport-${this.name}`

// replace the callback with our own wrapper to get the result
if (this._passReqToCallback) {
arguments[3] = wrapVerifiedAndPublish(framework, arguments[1], arguments[3])
} else {
arguments[2] = wrapVerifiedAndPublish(framework, arguments[0], arguments[2])
}
}
} else {
return function (username, password, verified) {
arguments[2] = wrapVerifiedAndPublish(username, password, verified, type)
return verify.apply(this, arguments)

return verify.apply(this, arguments)
}
}

function wrapStrategy (Strategy) {
return function wrappedStrategy () {
// verify function can be either the first or second argument
if (typeof arguments[0] === 'function') {
arguments[0] = wrapVerify(arguments[0])
} else {
arguments[1] = wrapVerify(arguments[1])
}

return Strategy.apply(this, arguments)
}
}

function strategyHook (Strategy) {
return shimmer.wrapFunction(Strategy, wrapStrategy)
}

module.exports = {
wrapVerify
strategyHook
}
87 changes: 87 additions & 0 deletions packages/datadog-instrumentations/src/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const { channel, 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
*/

const onPassportDeserializeUserChannel = channel('datadog:passport:deserializeUser:finish')

function wrapDone (req, 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 ?

Check failure on line 28 in packages/datadog-instrumentations/src/passport.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 127. Maximum allowed is 120
// what if session IDs are using rolling sessions or always changing or something idk ?
onPassportDeserializeUserChannel.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)
}
}


Check failure on line 53 in packages/datadog-instrumentations/src/passport.js

View workflow job for this annotation

GitHub Actions / lint

More than 1 blank line not allowed
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options.session can be used to know if it's login or session or not ?


return function wrappedMiddleware (req, res, next) {
return middleware(req, res, function wrappedNext (err) {

Check failure on line 73 in packages/datadog-instrumentations/src/passport.js

View workflow job for this annotation

GitHub Actions / lint

Expected error to be handled
console.log('NEW', req.user)

Check failure on line 74 in packages/datadog-instrumentations/src/passport.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
if (req.user?.name === 'bitch') {

Check failure on line 75 in packages/datadog-instrumentations/src/passport.js

View workflow job for this annotation

GitHub Actions / lint

Block must not be padded by blank lines

return block(req, res, getRootSpan(global._ddtrace))
}

return next.apply(this, arguments)
})
}
}
})

return Authenticator
})
3 changes: 3 additions & 0 deletions packages/dd-trace/src/appsec/addresses.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

// TODO: reorder all this, it's a mess
module.exports = {
HTTP_INCOMING_BODY: 'server.request.body',
HTTP_INCOMING_QUERY: 'server.request.query',
Expand All @@ -20,6 +21,8 @@ module.exports = {
HTTP_CLIENT_IP: 'http.client_ip',

USER_ID: 'usr.id',
USER_LOGIN: 'usr.login',

WAF_CONTEXT_PROCESSOR: 'waf.context.processor',

HTTP_OUTGOING_URL: 'server.io.net.url',
Expand Down
2 changes: 1 addition & 1 deletion packages/dd-trace/src/appsec/blocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB
res.removeHeader(headerName)
}

res.writeHead(statusCode, headers).end(body)
res.writeHead(statusCode, headers)._originalEnd(body)

responseBlockedSet.add(res)

Expand Down
1 change: 1 addition & 0 deletions packages/dd-trace/src/appsec/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
apolloServerCoreChannel: dc.tracingChannel('datadog:apollo-server-core:request'),
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
passportUser: dc.channel('datadog:passport:deserializeUser:finish'),
passportVerify: dc.channel('datadog:passport:verify:finish'),
queryParser: dc.channel('datadog:query:read:finish'),
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
Expand Down
29 changes: 22 additions & 7 deletions packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
multerParser,
incomingHttpRequestStart,
incomingHttpRequestEnd,
passportUser,
passportVerify,
queryParser,
nextBodyParsed,
Expand All @@ -27,7 +28,7 @@
const { extractIp } = require('../plugins/util/ip_extractor')
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking')
const { passportTrackEvent } = require('./passport')
const UserTracking = require('./user_tracking')
const { storage } = require('../../../datadog-core')
const graphql = require('./graphql')
const rasp = require('./rasp')
Expand Down Expand Up @@ -58,11 +59,15 @@

apiSecuritySampler.configure(_config.appsec)

UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false)

bodyParser.subscribe(onRequestBodyParsed)
multerParser.subscribe(onRequestBodyParsed)
cookieParser.subscribe(onRequestCookieParser)
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
passportUser.subscribe(onPassportDeserializeUser)
passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled
queryParser.subscribe(onRequestQueryParsed)
nextBodyParsed.subscribe(onRequestBodyParsed)
nextQueryParsed.subscribe(onRequestQueryParsed)
Expand All @@ -71,10 +76,6 @@
responseWriteHead.subscribe(onResponseWriteHead)
responseSetHeader.subscribe(onResponseSetHeader)

if (_config.appsec.eventTracking.enabled) {
passportVerify.subscribe(onPassportVerify)
}

isEnabled = true
config = _config
} catch (err) {
Expand Down Expand Up @@ -180,7 +181,19 @@
Reporter.finishRequest(req, res)
}

function onPassportVerify ({ credentials, user }) {
function onPassportDeserializeUser ({ req, user, sessionId, abordController }) {
UserTracking.trackUser(user)

if (sessionId && typeof sessionId === 'string') {
const results = waf.run({

Check failure on line 188 in packages/dd-trace/src/appsec/index.js

View workflow job for this annotation

GitHub Actions / lint

'results' is assigned a value but never used
persistent: {
'usr.session_id': sessionId
}
})
}
}

function onPassportVerify ({ framework, login, user, success, abortController }) {
const store = storage.getStore()
const rootSpan = store?.req && web.root(store.req)

Expand All @@ -189,7 +202,9 @@
return
}

passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
const results = UserTracking.trackLogin(framework, login, user, success, rootSpan)

handleResults(results, store.req, store.req.res, rootSpan, abortController)
}

function onRequestQueryParsed ({ req, res, query, abortController }) {
Expand Down
Loading
Loading