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

Collect request headers on user event #4385

Merged
merged 12 commits into from
Jun 27, 2024
52 changes: 37 additions & 15 deletions packages/dd-trace/src/appsec/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@ const REQUEST_HEADERS_MAP = mapHeaderAndTags([
'via',
...contentHeaderList,
'host',
'user-agent',
'accept',
'accept-encoding',
'accept-language'
], 'http.request.headers.')

const IDENTIFICATION_HEADERS_MAP = mapHeaderAndTags([
const identificationHeaders = [
'x-amzn-trace-id',
'cloudfront-viewer-ja3-fingerprint',
'cf-ray',
Expand All @@ -47,6 +45,14 @@ const IDENTIFICATION_HEADERS_MAP = mapHeaderAndTags([
'x-sigsci-requestid',
'x-sigsci-tags',
'akamai-user-risk'
]

// these request headers are always collected - it breaks the expected spec orders
uurien marked this conversation as resolved.
Show resolved Hide resolved
const MANDATORY_REQUEST_HEADERS_MAP = mapHeaderAndTags([
uurien marked this conversation as resolved.
Show resolved Hide resolved
'content-type',
'user-agent',
'accept',
...identificationHeaders
simon-id marked this conversation as resolved.
Show resolved Hide resolved
], 'http.request.headers.')

const RESPONSE_HEADERS_MAP = mapHeaderAndTags(contentHeaderList, 'http.response.headers.')
Expand Down Expand Up @@ -112,9 +118,9 @@ function reportAttack (attackData) {

const currentTags = rootSpan.context()._tags

const newTags = filterHeaders(req.headers, REQUEST_HEADERS_MAP)

newTags['appsec.event'] = 'true'
const newTags = {
'appsec.event': 'true'
}

if (limiter.isAllowed()) {
newTags['manual.keep'] = 'true' // TODO: figure out how to keep appsec traces with sampling revamp
Expand All @@ -134,11 +140,6 @@ function reportAttack (attackData) {
newTags['_dd.appsec.json'] = '{"triggers":' + attackData + '}'
}

const ua = newTags['http.request.headers.user-agent']
if (ua) {
newTags['http.useragent'] = ua
}

newTags['network.client.ip'] = req.socket.remoteAddress

rootSpan.addTags(newTags)
Expand Down Expand Up @@ -183,19 +184,40 @@ function finishRequest (req, res) {
incrementWafRequestsMetric(req)

// collect some headers even when no attack is detected
rootSpan.addTags(filterHeaders(req.headers, IDENTIFICATION_HEADERS_MAP))
const mandatoryTags = filterHeaders(req.headers, MANDATORY_REQUEST_HEADERS_MAP)
const ua = mandatoryTags['http.request.headers.user-agent']
if (ua) {
simon-id marked this conversation as resolved.
Show resolved Hide resolved
mandatoryTags['http.useragent'] = ua
}
rootSpan.addTags(mandatoryTags)

if (!rootSpan.context()._tags['appsec.event']) return
const tags = rootSpan.context()._tags
if (!shouldTrackHeaders(tags)) return
simon-id marked this conversation as resolved.
Show resolved Hide resolved

const newTags = filterHeaders(res.getHeaders(), RESPONSE_HEADERS_MAP)
Object.assign(newTags, filterHeaders(req.headers, REQUEST_HEADERS_MAP))

if (req.route && typeof req.route.path === 'string') {
const appsecEvent = tags['appsec.event'] === 'true'
if (req.route && typeof req.route.path === 'string' && appsecEvent) {
uurien marked this conversation as resolved.
Show resolved Hide resolved
newTags['http.endpoint'] = req.route.path
}
uurien marked this conversation as resolved.
Show resolved Hide resolved

rootSpan.addTags(newTags)
}

function shouldTrackHeaders (tags) {
uurien marked this conversation as resolved.
Show resolved Hide resolved
if (tags['appsec.event'] === 'true') {
return true
}

for (const tagName of Object.keys(tags)) {
if (tagName.startsWith('appsec.events.')) {
return true
}
}

return false
}

function setRateLimit (rateLimit) {
limiter = new Limiter(rateLimit)
}
Expand Down
151 changes: 139 additions & 12 deletions packages/dd-trace/test/appsec/reporter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,6 @@ describe('reporter', () => {
'manual.keep': 'true',
'_dd.origin': 'appsec',
'_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]}]}',
'http.request.headers.host': 'localhost',
'http.request.headers.user-agent': 'arachni',
'http.useragent': 'arachni',
'network.client.ip': '8.8.8.8'
})
})
Expand Down Expand Up @@ -246,12 +243,9 @@ describe('reporter', () => {
expect(web.root).to.have.been.calledOnceWith(req)

expect(span.addTags).to.have.been.calledOnceWithExactly({
'http.request.headers.host': 'localhost',
'http.request.headers.user-agent': 'arachni',
'appsec.event': 'true',
'manual.keep': 'true',
'_dd.appsec.json': '{"triggers":[]}',
'http.useragent': 'arachni',
'network.client.ip': '8.8.8.8'
})
})
Expand All @@ -264,13 +258,10 @@ describe('reporter', () => {
expect(web.root).to.have.been.calledOnceWith(req)

expect(span.addTags).to.have.been.calledOnceWithExactly({
'http.request.headers.host': 'localhost',
'http.request.headers.user-agent': 'arachni',
'appsec.event': 'true',
'manual.keep': 'true',
'_dd.origin': 'appsec',
'_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]},{"rule":{}},{"rule":{},"rule_matches":[{}]}]}',
'http.useragent': 'arachni',
'network.client.ip': '8.8.8.8'
})
})
Expand Down Expand Up @@ -323,6 +314,33 @@ describe('reporter', () => {
describe('finishRequest', () => {
let wafContext

const requestHeadersToTrackOnEvent = [
'x-forwarded-for',
'x-real-ip',
'true-client-ip',
'x-client-ip',
'x-forwarded',
'forwarded-for',
'x-cluster-client-ip',
'fastly-client-ip',
'cf-connecting-ip',
'cf-connecting-ipv6',
'forwarded',
'via',
'content-length',
'content-encoding',
'content-language',
'host',
'accept-encoding',
'accept-language'
]
const requestHeadersAndValuesToTrackOnEvent = {}
const expectedRequestTagsToTrackOnEvent = {}
requestHeadersToTrackOnEvent.forEach((header, index) => {
requestHeadersAndValuesToTrackOnEvent[header] = `val-${index}`
expectedRequestTagsToTrackOnEvent[`http.request.headers.${header}`] = `val-${index}`
})

beforeEach(() => {
wafContext = {
dispose: sinon.stub()
Expand Down Expand Up @@ -356,7 +374,7 @@ describe('reporter', () => {
expect(Reporter.metricsQueue).to.be.empty
})

it('should only add identification headers when no attack was previously found', () => {
it('should only add mandatory headers when no attack or event was previously found', () => {
const req = {
headers: {
'not-included': 'hello',
Expand All @@ -367,7 +385,10 @@ describe('reporter', () => {
'x-appgw-trace-id': 'e',
'x-sigsci-requestid': 'f',
'x-sigsci-tags': 'g',
'akamai-user-risk': 'h'
'akamai-user-risk': 'h',
'content-type': 'i',
accept: 'j',
'user-agent': 'k'
}
}

Expand All @@ -381,7 +402,11 @@ describe('reporter', () => {
'http.request.headers.x-appgw-trace-id': 'e',
'http.request.headers.x-sigsci-requestid': 'f',
'http.request.headers.x-sigsci-tags': 'g',
'http.request.headers.akamai-user-risk': 'h'
'http.request.headers.akamai-user-risk': 'h',
'http.request.headers.content-type': 'i',
'http.request.headers.accept': 'j',
'http.request.headers.user-agent': 'k',
'http.useragent': 'k'
})
})

Expand Down Expand Up @@ -442,6 +467,108 @@ describe('reporter', () => {
})
})

it('should add http request data inside request span when appsec.event is true', () => {
const req = {
headers: {
'user-agent': 'arachni',
...requestHeadersAndValuesToTrackOnEvent
}
}
const res = {
getHeaders: () => {
return {}
}
}
span.context()._tags['appsec.event'] = 'true'

Reporter.finishRequest(req, res)

expect(span.addTags).to.have.been.calledWithExactly({
'http.request.headers.user-agent': 'arachni',
'http.useragent': 'arachni'
})

expect(span.addTags).to.have.been.calledWithExactly(expectedRequestTagsToTrackOnEvent)
})

it('should add http request data inside request span when user login success is tracked', () => {
const req = {
headers: {
'user-agent': 'arachni',
...requestHeadersAndValuesToTrackOnEvent
}
}
const res = {
getHeaders: () => {
return {}
}
}

span.context()
._tags['appsec.events.users.login.success.track'] = 'true'

Reporter.finishRequest(req, res)

expect(span.addTags).to.have.been.calledWithExactly({
'http.request.headers.user-agent': 'arachni',
'http.useragent': 'arachni'
})

expect(span.addTags).to.have.been.calledWithExactly(expectedRequestTagsToTrackOnEvent)
})

it('should add http request data inside request span when user login failure is tracked', () => {
const req = {
headers: {
'user-agent': 'arachni',
...requestHeadersAndValuesToTrackOnEvent
}
}
const res = {
getHeaders: () => {
return {}
}
}

span.context()
._tags['appsec.events.users.login.failure.track'] = 'true'

Reporter.finishRequest(req, res)

expect(span.addTags).to.have.been.calledWithExactly({
'http.request.headers.user-agent': 'arachni',
'http.useragent': 'arachni'
})

expect(span.addTags).to.have.been.calledWithExactly(expectedRequestTagsToTrackOnEvent)
})

it('should add http request data inside request span when user custom event is tracked', () => {
const req = {
headers: {
'user-agent': 'arachni',
...requestHeadersAndValuesToTrackOnEvent
}
}
const res = {
getHeaders: () => {
return {}
}
}

span.context()
._tags['appsec.events.custon.event.track'] = 'true'

Reporter.finishRequest(req, res)

expect(span.addTags).to.have.been.calledWithExactly({
'http.request.headers.user-agent': 'arachni',
'http.useragent': 'arachni'
})

expect(span.addTags).to.have.been.calledWithExactly(expectedRequestTagsToTrackOnEvent)
})

it('should call incrementWafRequestsMetric', () => {
const req = {}
const res = {}
Expand Down
Loading