dd-trace
Version:
Datadog APM tracing client for JavaScript
191 lines (150 loc) • 5.36 kB
JavaScript
const crypto = require('crypto')
const log = require('../log')
const telemetry = require('./telemetry')
const addresses = require('./addresses')
const { keepTrace } = require('../priority_sampler')
const waf = require('./waf')
const { ASM } = require('../standalone/product')
// the RFC doesn't include '_id', but it's common in MongoDB
const USER_ID_FIELDS = ['id', '_id', 'email', 'username', 'login', 'user']
let collectionMode
function setCollectionMode (mode, overwrite = true) {
// don't overwrite if already set, only used in appsec/index.js to not overwrite RC values
if (!overwrite && collectionMode) return
/* eslint-disable no-fallthrough */
switch (mode) {
case 'safe':
log.warn('[ASM] Using deprecated value "safe" in config.appsec.eventTracking.mode')
case 'anon':
case 'anonymization':
collectionMode = 'anonymization'
break
case 'extended':
log.warn('[ASM] Using deprecated value "extended" in config.appsec.eventTracking.mode')
case 'ident':
case 'identification':
collectionMode = 'identification'
break
default:
collectionMode = 'disabled'
}
/* eslint-enable no-fallthrough */
}
function obfuscateIfNeeded (str) {
return collectionMode === 'anonymization'
// get first 16 bytes of sha256 hash in lowercase hex
? 'anon_' + crypto.createHash('sha256').update(str).digest().toString('hex', 0, 16).toLowerCase()
: str
}
// TODO: should we find other ways to get the user ID ?
function getUserId (user) {
if (!user) return
// should we iterate on user keys instead to be case insensitive ?
// but if we iterate over user then we're missing the inherited props ?
for (const field of USER_ID_FIELDS) {
let id = user[field]
// try to find a field that can be stringified
if (id && typeof id.toString === 'function') {
id = id.toString()
if (typeof id !== 'string' || id.startsWith('[object ')) {
// probably not a usable ID ?
continue
}
return obfuscateIfNeeded(id)
}
}
}
function trackLogin (framework, login, user, success, rootSpan) {
if (!collectionMode || collectionMode === 'disabled') return
if (typeof login !== 'string') {
log.error('[ASM] Invalid login provided to AppSec trackLogin')
telemetry.incrementMissingUserLoginMetric(framework, success ? 'login_success' : 'login_failure')
// note:
// if we start supporting using userId if login is missing, we need to only give up if both are missing, and
// implement 'appsec.instrum.user_auth.missing_user_id' telemetry too
return
}
login = obfuscateIfNeeded(login)
const userId = getUserId(user)
let newTags
const persistent = {
[addresses.USER_LOGIN]: login
}
const currentTags = rootSpan.context()._tags
const isSdkCalled = currentTags[`_dd.appsec.events.users.login.${success ? 'success' : 'failure'}.sdk`] === 'true'
// used to not overwrite tags set by SDK
function shouldSetTag (tag) {
return !(isSdkCalled && currentTags[tag])
}
if (success) {
newTags = {
'appsec.events.users.login.success.track': 'true',
'_dd.appsec.events.users.login.success.auto.mode': collectionMode,
'_dd.appsec.usr.login': login
}
if (shouldSetTag('appsec.events.users.login.success.usr.login')) {
newTags['appsec.events.users.login.success.usr.login'] = login
}
if (userId) {
newTags['_dd.appsec.usr.id'] = userId
if (shouldSetTag('usr.id')) {
newTags['usr.id'] = userId
persistent[addresses.USER_ID] = userId
}
}
persistent[addresses.LOGIN_SUCCESS] = null
} else {
newTags = {
'appsec.events.users.login.failure.track': 'true',
'_dd.appsec.events.users.login.failure.auto.mode': collectionMode,
'_dd.appsec.usr.login': login
}
if (shouldSetTag('appsec.events.users.login.failure.usr.login')) {
newTags['appsec.events.users.login.failure.usr.login'] = login
}
if (userId) {
newTags['_dd.appsec.usr.id'] = userId
if (shouldSetTag('appsec.events.users.login.failure.usr.id')) {
newTags['appsec.events.users.login.failure.usr.id'] = userId
}
}
/* TODO: if one day we have this info
if (exists != null && shouldSetTag('appsec.events.users.login.failure.usr.exists')) {
newTags['appsec.events.users.login.failure.usr.exists'] = exists
}
*/
persistent[addresses.LOGIN_FAILURE] = null
}
keepTrace(rootSpan, ASM)
rootSpan.addTags(newTags)
return waf.run({ persistent })
}
function trackUser (user, rootSpan) {
if (!collectionMode || collectionMode === 'disabled') return
const userId = getUserId(user)
if (!userId) {
log.error('[ASM] No valid user ID found in AppSec trackUser')
telemetry.incrementMissingUserIdMetric('passport', 'authenticated_request')
return
}
rootSpan.setTag('_dd.appsec.usr.id', userId)
const isSdkCalled = rootSpan.context()._tags['_dd.appsec.user.collection_mode'] === 'sdk'
// do not override SDK
if (!isSdkCalled) {
rootSpan.addTags({
'usr.id': userId,
'_dd.appsec.user.collection_mode': collectionMode
})
return waf.run({
persistent: {
[addresses.USER_ID]: userId
}
})
}
}
module.exports = {
setCollectionMode,
trackLogin,
trackUser
}