Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
116 changes: 46 additions & 70 deletions integration-tests/appsec/graphql.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const assert = require('node:assert/strict')
const path = require('path')
const { inspect } = require('node:util')
const axios = require('axios')

const {
FakeAgent,
Expand Down Expand Up @@ -38,6 +37,19 @@ describe('graphql', () => {
await agent.stop()
})

/**
* @param {object|object[]} body
*/
async function request (body) {
const response = await fetch(`${proc.url}/graphql`, {
method: 'post',
headers: { 'Content-type': 'application/json' },
body: JSON.stringify(body),
})
await response.arrayBuffer()
return response
}

it('should not report any attack', async () => {
const agentPromise = agent.assertMessageReceived(({ headers, payload }) => {
assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`)
Expand All @@ -54,20 +66,14 @@ describe('graphql', () => {
assert.ok(!('_dd.appsec.json' in payload[1][0].meta))
})

await axios({
url: `${proc.url}/graphql`,
method: 'post',
headers: {
'Content-type': 'application/json',
},
data: {
query: 'query getSingleImage($imageId: Int!) { image(imageId: $imageId) { title owner category url }}',
variables: {
imageId: 1,
},
operationName: 'getSingleImage',
const response = await request({
query: 'query getSingleImage($imageId: Int!) { image(imageId: $imageId) { title owner category url }}',
variables: {
imageId: 1,
},
operationName: 'getSingleImage',
})
assert.strictEqual(response.status, 200)

return agentPromise
})
Expand Down Expand Up @@ -123,20 +129,14 @@ describe('graphql', () => {
assert.deepStrictEqual(JSON.parse(payload[1][0].meta['_dd.appsec.json']), result)
})

await axios({
url: `${proc.url}/graphql`,
method: 'post',
headers: {
'Content-type': 'application/json',
},
data: {
query: 'query getImagesByCategory($category: String) { images(category: $category) { title owner url }}',
variables: {
category: 'testattack',
},
operationName: 'getImagesByCategory',
const response = await request({
query: 'query getImagesByCategory($category: String) { images(category: $category) { title owner url }}',
variables: {
category: 'testattack',
},
operationName: 'getImagesByCategory',
})
assert.strictEqual(response.status, 200)

return agentPromise
})
Expand All @@ -153,26 +153,14 @@ describe('graphql', () => {
assert.strictEqual(payload[1][0].meta['appsec.event'], 'true')
})

await assert.rejects(
axios({
url: `${proc.url}/graphql`,
method: 'post',
headers: {
'Content-type': 'application/json',
},
data: {
query: 'query getImagesByCategory($category: String) { images(category: $category) { title owner url }}',
variables: {
category: 'blockattack',
},
operationName: 'getImagesByCategory',
},
}),
(err) => {
assert.strictEqual(err.response.status, 403)
return true
}
)
const response = await request({
query: 'query getImagesByCategory($category: String) { images(category: $category) { title owner url }}',
variables: {
category: 'blockattack',
},
operationName: 'getImagesByCategory',
})
assert.strictEqual(response.status, 403)

return agentPromise
})
Expand All @@ -189,31 +177,19 @@ describe('graphql', () => {
assert.strictEqual(payload[1][0].meta['appsec.event'], 'true')
})

await assert.rejects(
axios({
url: `${proc.url}/graphql`,
method: 'post',
headers: {
'Content-type': 'application/json',
},
data: [
{
query: 'query getSingleImage($imageId: Int!) { image(imageId: $imageId) { title }}',
variables: { imageId: 1 },
operationName: 'getSingleImage',
},
{
query: 'query getImagesByCategory($category: String) { images(category: $category) { title }}',
variables: { category: 'blockattack' },
operationName: 'getImagesByCategory',
},
],
}),
(err) => {
assert.strictEqual(err.response.status, 403)
return true
}
)
const response = await request([
{
query: 'query getSingleImage($imageId: Int!) { image(imageId: $imageId) { title }}',
variables: { imageId: 1 },
operationName: 'getSingleImage',
},
{
query: 'query getImagesByCategory($category: String) { images(category: $category) { title }}',
variables: { category: 'blockattack' },
operationName: 'getImagesByCategory',
},
])
assert.strictEqual(response.status, 403)

return agentPromise
})
Expand Down
49 changes: 29 additions & 20 deletions integration-tests/appsec/headers-collection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
const assert = require('node:assert/strict')
const path = require('path')
const { inspect } = require('node:util')
const Axios = require('axios')

const {
sandboxCwd,
useSandbox,
Expand All @@ -13,8 +11,19 @@ const {
stopProc,
} = require('../helpers')

/**
* @param {string} baseUrl
* @param {string} url
* @param {object} [options]
*/
async function request (baseUrl, url, options) {
const response = await fetch(new URL(url, baseUrl), options)
await response.arrayBuffer()
return response
}

describe('AppSec headers collection - Express', () => {
let axios, cwd, appFile, agent, proc
let cwd, appFile, agent, proc

useSandbox(['express'])

Expand All @@ -40,7 +49,6 @@ describe('AppSec headers collection - Express', () => {
}

proc = await spawnProc(appFile, { cwd, env, execArgv: [] })
axios = Axios.create({ baseURL: proc.url })
})

afterEach(async () => {
Expand Down Expand Up @@ -81,10 +89,10 @@ describe('AppSec headers collection - Express', () => {
startServer('appsec/data-collection/index.js')

it('should collect event headers when a WAF event is triggered', async () => {
const expectedRequestHeaders = ['user-agent', 'accept', 'host', 'accept-encoding']
const expectedRequestHeaders = ['user-agent', 'accept', 'host', 'accept-encoding', 'accept-language']
const expectedResponseHeaders = ['content-type', 'content-language']

await axios.get('/', { headers: { 'User-Agent': 'Arachni/v1' } })
await request(proc.url, '/', { headers: { 'User-Agent': 'Arachni/v1' } })
await assertHeadersReported(expectedRequestHeaders, expectedResponseHeaders)
})
})
Expand All @@ -93,7 +101,9 @@ describe('AppSec headers collection - Express', () => {
startServer('appsec/data-collection/index.js', { extendedDataCollection: true })

it('should collect extended headers when a WAF event is triggered', async () => {
const expectedRequestHeaders = ['user-agent', 'accept', 'host', 'accept-encoding', 'connection']
const expectedRequestHeaders = [
'user-agent', 'accept', 'host', 'accept-encoding', 'accept-language', 'sec-fetch-mode', 'connection',
]

// DD_APPSEC_MAX_COLLECTED_HEADERS is set to 25, so it is expected to collect
// 22 x-datadog-res-XX headers + x-powered-by, content-type and content-language, for a total of 25.
Expand All @@ -104,7 +114,7 @@ describe('AppSec headers collection - Express', () => {
'content-language',
]

await axios.get('/', { headers: { 'User-Agent': 'Arachni/v1' } })
await request(proc.url, '/', { headers: { 'User-Agent': 'Arachni/v1' } })
await assertHeadersReported(expectedRequestHeaders, expectedResponseHeaders)
})
})
Expand All @@ -113,26 +123,26 @@ describe('AppSec headers collection - Express', () => {
startServer('appsec/response-headers/express.js')

it('should always collect content-type and content-length response headers when AppSec is enabled', async () => {
const response = await axios.get('/', { headers: { 'User-Agent': 'Mozilla/5.0' } })
const response = await request(proc.url, '/', { headers: { 'User-Agent': 'Mozilla/5.0' } })

assert.equal(response.status, 200)
assert.ok(response.headers['content-type'])
assert.ok(response.headers['content-length'])
assert.ok(response.headers.get('content-type'))
assert.ok(response.headers.get('content-length'))

await agent.assertMessageReceived(({ payload }) => {
const span = payload[0]?.find(s => s.type === 'web')
if (!span) throw new Error('web-type span not yet received')

assert.equal(span.meta['http.response.headers.content-type'], response.headers['content-type'])
assert.equal(span.meta['http.response.headers.content-length'], response.headers['content-length'])
assert.equal(span.meta['http.response.headers.content-type'], response.headers.get('content-type'))
assert.equal(span.meta['http.response.headers.content-length'], response.headers.get('content-length'))
assert.equal(span.meta['appsec.event'], undefined)
})
})
})
})

describe('AppSec headers collection - Fastify', () => {
let axios, cwd, appFile, agent, proc
let cwd, appFile, agent, proc

useSandbox(['fastify'])

Expand All @@ -151,7 +161,6 @@ describe('AppSec headers collection - Fastify', () => {
},
execArgv: [],
})
axios = Axios.create({ baseURL: proc.url })
})

afterEach(async () => {
Expand All @@ -161,18 +170,18 @@ describe('AppSec headers collection - Fastify', () => {

describe('No security event', () => {
it('should always emit content-type and content-length response headers when AppSec is enabled', async () => {
const response = await axios.get('/', { headers: { 'User-Agent': 'Mozilla/5.0' } })
const response = await request(proc.url, '/', { headers: { 'User-Agent': 'Mozilla/5.0' } })

assert.equal(response.status, 200)
assert.ok(response.headers['content-type'])
assert.ok(response.headers['content-length'])
assert.ok(response.headers.get('content-type'))
assert.ok(response.headers.get('content-length'))

await agent.assertMessageReceived(({ payload }) => {
const span = payload[0]?.find(s => s.type === 'web')
if (!span) throw new Error('web-type span not yet received')

assert.equal(span.meta['http.response.headers.content-type'], response.headers['content-type'])
assert.equal(span.meta['http.response.headers.content-length'], response.headers['content-length'])
assert.equal(span.meta['http.response.headers.content-type'], response.headers.get('content-type'))
assert.equal(span.meta['http.response.headers.content-length'], response.headers.get('content-length'))
assert.equal(span.meta['appsec.event'], undefined)
})
})
Expand Down
29 changes: 19 additions & 10 deletions integration-tests/appsec/iast-esbuild.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const childProcess = require('child_process')
const fs = require('fs')
const path = require('path')
const { promisify, inspect } = require('util')
const Axios = require('axios')
const msgpack = require('@msgpack/msgpack')

const { sandboxCwd, useSandbox, FakeAgent, spawnProc, stopProc } = require('../helpers')
Expand Down Expand Up @@ -108,7 +107,7 @@ describe('esbuild support for IAST', () => {
DD_IAST_REQUEST_SAMPLING: '100',
},
})
contextVars.axios = Axios.create({ baseURL: contextVars.proc.url })
contextVars.url = contextVars.proc.url
})

afterEach(async () => {
Expand All @@ -118,8 +117,18 @@ describe('esbuild support for IAST', () => {
}
}

/**
* @param {string} baseUrl
* @param {string} url
*/
async function request (baseUrl, url) {
const response = await fetch(new URL(url, baseUrl))
assert.strictEqual(response.status, 200)
await response.arrayBuffer()
}

describe('cjs', () => {
const context = { proc: null, agent: null, axios: null, applicationDir: null, bundledApplicationDir: null }
const context = { proc: null, agent: null, url: '', applicationDir: null, bundledApplicationDir: null }

before(async function () {
this.timeout(120_000)
Expand All @@ -136,7 +145,7 @@ describe('esbuild support for IAST', () => {
startServer('iast-enabled-with-sm.js', true)

it('should detect vulnerability with correct location', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')

const expectedPath = path.join('iast', 'index.js')
const expectedLine = 9
Expand All @@ -149,7 +158,7 @@ describe('esbuild support for IAST', () => {
startServer('iast-enabled-with-no-sm.js', true)

it('should detect vulnerability with first callsite location', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')

const expectedPath = path.join('build', 'iast-enabled-with-no-sm.js')

Expand All @@ -162,14 +171,14 @@ describe('esbuild support for IAST', () => {
startServer('iast-disabled.js', false)

it('should not detect any vulnerability', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')
await assertNoVulnerability(context.agent)
})
})
})

describe('esm', () => {
const context = { proc: null, agent: null, axios: null, applicationDir: null, bundledApplicationDir: null }
const context = { proc: null, agent: null, url: '', applicationDir: null, bundledApplicationDir: null }

before(async () => {
const setup = await setupApplication('iast-esbuild-esm')
Expand All @@ -184,7 +193,7 @@ describe('esbuild support for IAST', () => {
startServer('iast-enabled-with-sm.mjs', true)

it('should detect vulnerability with correct location', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')

const expectedPath = path.join('iast', 'index.mjs')
const expectedLine = 7
Expand All @@ -197,7 +206,7 @@ describe('esbuild support for IAST', () => {
startServer('iast-enabled-with-no-sm.mjs', true)

it('should detect vulnerability with first callsite location', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')

const expectedPath = path.join('build', 'iast-enabled-with-no-sm.mjs')

Expand All @@ -210,7 +219,7 @@ describe('esbuild support for IAST', () => {
startServer('iast-disabled.mjs', false)

it('should not detect any vulnerability', async () => {
await context.axios.get('/iast/cmdi-vulnerable?args=-la')
await request(context.url, '/iast/cmdi-vulnerable?args=-la')
await assertNoVulnerability(context.agent)
})
})
Expand Down
Loading
Loading