Skip to content

Commit

Permalink
Automated user events (#3205)
Browse files Browse the repository at this point in the history
* Add passport local and http instrumentations.
* Add automated event tracking in appsec..

---------

Co-authored-by: Carles Capell <107924659+CarlesDD@users.noreply.github.com>
Co-authored-by: simon-id <simon.id@datadoghq.com>
  • Loading branch information
3 people authored and tlhunter committed Jun 30, 2023
1 parent df112b2 commit ca31bd6
Show file tree
Hide file tree
Showing 19 changed files with 972 additions and 39 deletions.
5 changes: 4 additions & 1 deletion docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ tracer.init({
obfuscatorKeyRegex: '.*',
obfuscatorValueRegex: '.*',
blockedTemplateHtml: './blocked.html',
blockedTemplateJson: './blocked.json'
blockedTemplateJson: './blocked.json',
eventTracking: {
mode: 'safe'
}
}
});

Expand Down
13 changes: 13 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,19 @@ export declare interface TracerOptions {
* Specifies a path to a custom blocking template json file.
*/
blockedTemplateJson?: string,

/**
* Controls the automated user event tracking configuration
*/
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'
*/
mode?: 'safe' | 'extended' | 'disabled'
}
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ module.exports = {
'oracledb': () => require('../oracledb'),
'openai': () => require('../openai'),
'paperplane': () => require('../paperplane'),
'passport-http': () => require('../passport-http'),
'passport-local': () => require('../passport-local'),
'pg': () => require('../pg'),
'pino': () => require('../pino'),
'pino-pretty': () => require('../pino'),
Expand Down
22 changes: 22 additions & 0 deletions packages/datadog-instrumentations/src/passport-http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'

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

addHook({
name: 'passport-http',
file: 'lib/passport-http/strategies/basic.js',
versions: ['>=0.3.0']
}, BasicStrategy => {
return shimmer.wrap(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)
})
})
22 changes: 22 additions & 0 deletions packages/datadog-instrumentations/src/passport-local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'

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

addHook({
name: 'passport-local',
file: 'lib/strategy.js',
versions: ['>=1.0.0']
}, Strategy => {
return shimmer.wrap(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)
})
})
36 changes: 36 additions & 0 deletions packages/datadog-instrumentations/src/passport-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const { channel } = require('./helpers/instrument')

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

function wrapVerifiedAndPublish (username, password, verified, type) {
if (!passportVerifyChannel.hasSubscribers) {
return verified
}

return shimmer.wrap(verified, function (err, user, info) {
const credentials = { type, username }
passportVerifyChannel.publish({ credentials, user })
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)
}
} else {
return function (username, password, verified) {
arguments[2] = wrapVerifiedAndPublish(username, password, verified, type)
return verify.apply(this, arguments)
}
}
}

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

const agent = require('../../dd-trace/test/plugins/agent')
const getPort = require('get-port')
const axios = require('axios')
const dc = require('../../diagnostics_channel')

withVersions('passport-http', 'passport-http', version => {
describe('passport-http instrumentation', () => {
const passportVerifyChannel = dc.channel('datadog:passport:verify:finish')
let port, server, subscriberStub

before(() => {
return agent.load(['express', 'passport', 'passport-http'], { client: false })
})
before((done) => {
const express = require('../../../versions/express').get()
const passport = require('../../../versions/passport').get()
const BasicStrategy = require(`../../../versions/passport-http@${version}`).get().BasicStrategy
const app = express()

passport.use(new BasicStrategy((username, password, done) => {
const users = [{
_id: 1,
username: 'test',
password: '1234',
email: 'testuser@ddog.com'
}]

const user = users.find(user => (user.username === username) && (user.password === password))

if (!user) {
return done(null, false)
} else {
return done(null, user)
}
}
))

app.use(passport.initialize())
app.use(express.json())

app.get('/',
passport.authenticate('basic', {
successRedirect: '/grant',
failureRedirect: '/deny',
passReqToCallback: false,
session: false
})
)

app.post('/req',
passport.authenticate('basic', {
successRedirect: '/grant',
failureRedirect: '/deny',
passReqToCallback: true,
session: false
})
)

app.get('/grant', (req, res) => {
res.send('Granted')
})

app.get('/deny', (req, res) => {
res.send('Denied')
})

passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) {
subscriberStub(arguments[0])
})

getPort().then(newPort => {
port = newPort
server = app.listen(port, () => {
done()
})
})
})
beforeEach(() => {
subscriberStub = sinon.stub()
})

after(() => {
server.close()
return agent.close({ ritmReset: false })
})

it('should call subscriber with proper arguments on success', async () => {
const res = await axios.get(`http://localhost:${port}/`, {
headers: {
// test:1234
'Authorization': 'Basic dGVzdDoxMjM0'
}
})

expect(res.status).to.equal(200)
expect(res.data).to.equal('Granted')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'http', username: 'test' },
user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }
}
)
})

it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => {
const res = await axios.get(`http://localhost:${port}/`, {
headers: {
// test:1234
'Authorization': 'Basic dGVzdDoxMjM0'
}
})

expect(res.status).to.equal(200)
expect(res.data).to.equal('Granted')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'http', username: 'test' },
user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }
}
)
})

it('should call subscriber with proper arguments on failure', async () => {
const res = await axios.get(`http://localhost:${port}/`, {
headers: {
// test:1
'Authorization': 'Basic dGVzdDox'
}
})

expect(res.status).to.equal(200)
expect(res.data).to.equal('Denied')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'http', username: 'test' },
user: false
}
)
})
})
})
129 changes: 129 additions & 0 deletions packages/datadog-instrumentations/test/passport-local.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use strict'

const agent = require('../../dd-trace/test/plugins/agent')
const getPort = require('get-port')
const axios = require('axios')
const dc = require('../../diagnostics_channel')

withVersions('passport-local', 'passport-local', version => {
describe('passport-local instrumentation', () => {
const passportVerifyChannel = dc.channel('datadog:passport:verify:finish')
let port, server, subscriberStub

before(() => {
return agent.load(['express', 'passport', 'passport-local'], { client: false })
})
before((done) => {
const express = require('../../../versions/express').get()
const passport = require(`../../../versions/passport`).get()
const LocalStrategy = require(`../../../versions/passport-local@${version}`).get().Strategy
const app = express()

passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password' },
(username, password, done) => {
const users = [{
_id: 1,
username: 'test',
password: '1234',
email: 'testuser@ddog.com'
}]

const user = users.find(user => (user.username === username) && (user.password === password))

if (!user) {
return done(null, false)
} else {
return done(null, user)
}
}
))

app.use(passport.initialize())
app.use(express.json())

app.post('/',
passport.authenticate('local', {
successRedirect: '/grant',
failureRedirect: '/deny',
passReqToCallback: false,
session: false
})
)

app.post('/req',
passport.authenticate('local', {
successRedirect: '/grant',
failureRedirect: '/deny',
passReqToCallback: true,
session: false
})
)

app.get('/grant', (req, res) => {
res.send('Granted')
})

app.get('/deny', (req, res) => {
res.send('Denied')
})

passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) {
subscriberStub(arguments[0])
})

getPort().then(newPort => {
port = newPort
server = app.listen(port, () => {
done()
})
})
})
beforeEach(() => {
subscriberStub = sinon.stub()
})

after(() => {
server.close()
return agent.close({ ritmReset: false })
})

it('should call subscriber with proper arguments on success', async () => {
const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1234' })

expect(res.status).to.equal(200)
expect(res.data).to.equal('Granted')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'local', username: 'test' },
user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }
}
)
})

it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => {
const res = await axios.post(`http://localhost:${port}/req`, { username: 'test', password: '1234' })

expect(res.status).to.equal(200)
expect(res.data).to.equal('Granted')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'local', username: 'test' },
user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }
}
)
})

it('should call subscriber with proper arguments on failure', async () => {
const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1' })

expect(res.status).to.equal(200)
expect(res.data).to.equal('Denied')
expect(subscriberStub).to.be.calledOnceWithExactly(
{
credentials: { type: 'local', username: 'test' },
user: false
}
)
})
})
})
Loading

0 comments on commit ca31bd6

Please sign in to comment.