Skip to content

Commit

Permalink
Report stack trace in iast
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyasShabi committed Dec 23, 2024
1 parent 98ceacf commit f6edc6e
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ class CookieAnalyzer extends Analyzer {
return super._checkOCE(context, value)
}

_getLocation (value) {
_getLocation (value, callSiteList) {
if (!value) {
return super._getLocation()
return super._getLocation(value, callSiteList)
}

if (value.location) {
return value.location
}
const location = super._getLocation(value)
const location = super._getLocation(value, callSiteList)
value.location = location
return location
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const { storage } = require('../../../../../datadog-core')
const { getFirstNonDDPathAndLine } = require('../path-line')
const { addVulnerability } = require('../vulnerability-reporter')
const { addVulnerability, getVulnerabilityCallSiteList } = require('../vulnerability-reporter')
const { getIastContext } = require('../iast-context')
const overheadController = require('../overhead-controller')
const { SinkIastPlugin } = require('../iast-plugin')
Expand All @@ -28,12 +28,14 @@ class Analyzer extends SinkIastPlugin {
}

_reportEvidence (value, context, evidence) {
const location = this._getLocation(value)
const callSiteList = getVulnerabilityCallSiteList()
const location = this._getLocation(value, callSiteList)

if (!this._isExcluded(location)) {
const locationSourceMap = this._replaceLocationFromSourceMap(location)
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
const vulnerability = this._createVulnerability(this._type, evidence, spanId, locationSourceMap)
addVulnerability(context, vulnerability)
addVulnerability(context, vulnerability, callSiteList)
}
}

Expand All @@ -49,8 +51,8 @@ class Analyzer extends SinkIastPlugin {
return { value }
}

_getLocation () {
return getFirstNonDDPathAndLine(this._getExcludedPaths())
_getLocation (value, callSiteList) {
return getFirstNonDDPathAndLine(callSiteList, this._getExcludedPaths())
}

_replaceLocationFromSourceMap (location) {
Expand Down
12 changes: 12 additions & 0 deletions packages/dd-trace/src/appsec/iast/iast-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ function getIastContext (store, topContext) {
return iastContext
}

function getIastStackTraceId (iastContext) {
if (!iastContext) return 0

if (!iastContext.iastStackTraceId) {
iastContext.iastStackTraceId = 0
}

iastContext.iastStackTraceId += 1
return iastContext.iastStackTraceId
}

/* TODO Fix storage problem when the close event is called without
finish event to remove `topContext` references
We have to save the context in two places, because
Expand Down Expand Up @@ -51,6 +62,7 @@ module.exports = {
getIastContext,
saveIastContext,
cleanIastContext,
getIastStackTraceId,
IAST_CONTEXT_KEY,
IAST_TRANSACTION_ID
}
5 changes: 2 additions & 3 deletions packages/dd-trace/src/appsec/iast/path-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const path = require('path')
const process = require('process')
const { calculateDDBasePath } = require('../../util')
const { getCallSiteList } = require('../stack_trace')
const pathLine = {
getFirstNonDDPathAndLine,
getNodeModulesPaths,
Expand Down Expand Up @@ -73,8 +72,8 @@ function isExcluded (callsite, externallyExcludedPaths) {
return false
}

function getFirstNonDDPathAndLine (externallyExcludedPaths) {
return getFirstNonDDPathAndLineFromCallsites(getCallSiteList(), externallyExcludedPaths)
function getFirstNonDDPathAndLine (callSiteList, externallyExcludedPaths) {
return getFirstNonDDPathAndLineFromCallsites(callSiteList, externallyExcludedPaths)
}

function getNodeModulesPaths (...paths) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class VulnerabilityFormatter {
const formattedVulnerability = {
type: vulnerability.type,
hash: vulnerability.hash,
stackId: vulnerability.stackId,
evidence: this.formatEvidence(vulnerability.type, vulnerability.evidence, sourcesIndexes, sources),
location: {
spanId: vulnerability.location.spanId
Expand Down
28 changes: 26 additions & 2 deletions packages/dd-trace/src/appsec/iast/vulnerability-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
const standalone = require('../standalone')
const { SAMPLING_MECHANISM_APPSEC } = require('../../constants')
const { keepTrace } = require('../../priority_sampler')
const { reportStackTrace, getCallSiteList, STACK_TRACE_NAMESPACES } = require('../stack_trace')
const { getIastStackTraceId } = require('./iast-context')

const VULNERABILITIES_KEY = 'vulnerabilities'
const VULNERABILITY_HASHES_MAX_SIZE = 1000
Expand All @@ -15,8 +17,10 @@ const RESET_VULNERABILITY_CACHE_INTERVAL = 60 * 60 * 1000 // 1 hour
let tracer
let resetVulnerabilityCacheTimer
let deduplicationEnabled = true
let stackTraceMaxDepth
let maxStackTraces

function addVulnerability (iastContext, vulnerability) {
function addVulnerability (iastContext, vulnerability, callSiteList) {
if (vulnerability?.evidence && vulnerability?.type && vulnerability?.location) {
if (deduplicationEnabled && isDuplicatedVulnerability(vulnerability)) return

Expand All @@ -41,6 +45,18 @@ function addVulnerability (iastContext, vulnerability) {
keepTrace(span, SAMPLING_MECHANISM_APPSEC)
standalone.sample(span)

const stackId = getIastStackTraceId(iastContext)
vulnerability.stackId = stackId

reportStackTrace(
iastContext?.rootSpan,
stackId,
stackTraceMaxDepth,
maxStackTraces,
callSiteList,
STACK_TRACE_NAMESPACES.IAST
)

if (iastContext?.rootSpan) {
iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
iastContext[VULNERABILITIES_KEY].push(vulnerability)
Expand Down Expand Up @@ -91,11 +107,18 @@ function stopClearCacheTimer () {
}

function isDuplicatedVulnerability (vulnerability) {
return VULNERABILITY_HASHES.get(`${vulnerability.type}${vulnerability.hash}`)
return VULNERABILITY_HASHES.has(`${vulnerability.type}${vulnerability.hash}`)
}

function getVulnerabilityCallSiteList () {
return getCallSiteList(stackTraceMaxDepth)
}

function start (config, _tracer) {
deduplicationEnabled = config.iast.deduplicationEnabled
stackTraceMaxDepth = config.appsec.stackTrace.maxDepth
maxStackTraces = config.appsec.stackTrace.maxStackTraces

vulnerabilitiesFormatter.setRedactVulnerabilities(
config.iast.redactionEnabled,
config.iast.redactionNamePattern,
Expand All @@ -114,6 +137,7 @@ function stop () {
module.exports = {
addVulnerability,
sendVulnerabilities,
getVulnerabilityCallSiteList,
clearCache,
start,
stop
Expand Down
12 changes: 9 additions & 3 deletions packages/dd-trace/src/appsec/rasp/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const web = require('../../plugins/util/web')
const { reportStackTrace } = require('../stack_trace')
const { getCallSiteList, reportStackTrace } = require('../stack_trace')
const { getBlockingAction } = require('../blocking')
const log = require('../../log')

Expand Down Expand Up @@ -30,13 +30,19 @@ class DatadogRaspAbortError extends Error {

function handleResult (actions, req, res, abortController, config) {
const generateStackTraceAction = actions?.generate_stack

const { maxDepth, maxStackTraces } = config.appsec.stackTrace

const callSiteList = getCallSiteList(maxDepth)

if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
const rootSpan = web.root(req)
reportStackTrace(
rootSpan,
generateStackTraceAction.stack_id,
config.appsec.stackTrace.maxDepth,
config.appsec.stackTrace.maxStackTraces
maxDepth,
maxStackTraces,
callSiteList
)
}

Expand Down
31 changes: 20 additions & 11 deletions packages/dd-trace/src/appsec/stack_trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ const ddBasePath = calculateDDBasePath(__dirname)

const LIBRARY_FRAMES_BUFFER = 20

const STACK_TRACE_NAMESPACES = {
RASP: 'exploit',
IAST: 'vulnerability'
}

function getCallSiteList (maxDepth = 100) {
if (maxDepth < 1) {
maxDepth = Infinity
}

const previousPrepareStackTrace = Error.prepareStackTrace
const previousStackTraceLimit = Error.stackTraceLimit
let callsiteList
Error.stackTraceLimit = maxDepth
// Since some frames will be discarded because they come from tracer codebase, a buffer is added
// to the limit in order to get as close as `maxDepth` number of frames.
Error.stackTraceLimit = maxDepth + LIBRARY_FRAMES_BUFFER

try {
Error.prepareStackTrace = function (_, callsites) {
Expand Down Expand Up @@ -52,14 +63,11 @@ function getFramesForMetaStruct (callSiteList, maxDepth = 32) {
return indexedFrames
}

function reportStackTrace (rootSpan, stackId, maxDepth, maxStackTraces, callSiteListGetter = getCallSiteList) {
function reportStackTrace (
rootSpan, stackId, maxDepth, maxStackTraces, callSiteList, namespace = STACK_TRACE_NAMESPACES.RASP) {
if (!rootSpan) return

if (maxStackTraces < 1 || (rootSpan.meta_struct?.['_dd.stack']?.exploit?.length ?? 0) < maxStackTraces) {
// Since some frames will be discarded because they come from tracer codebase, a buffer is added
// to the limit in order to get as close as `maxDepth` number of frames.
if (maxDepth < 1) maxDepth = Infinity
const callSiteList = callSiteListGetter(maxDepth + LIBRARY_FRAMES_BUFFER)
if (maxStackTraces < 1 || (rootSpan.meta_struct?.['_dd.stack']?.[namespace]?.length ?? 0) < maxStackTraces) {
if (!Array.isArray(callSiteList)) return

if (!rootSpan.meta_struct) {
Expand All @@ -70,13 +78,13 @@ function reportStackTrace (rootSpan, stackId, maxDepth, maxStackTraces, callSite
rootSpan.meta_struct['_dd.stack'] = {}
}

if (!rootSpan.meta_struct['_dd.stack'].exploit) {
rootSpan.meta_struct['_dd.stack'].exploit = []
if (!rootSpan.meta_struct['_dd.stack'][namespace]) {
rootSpan.meta_struct['_dd.stack'][namespace] = []
}

const frames = getFramesForMetaStruct(callSiteList, maxDepth)

rootSpan.meta_struct['_dd.stack'].exploit.push({
rootSpan.meta_struct['_dd.stack'][namespace].push({
id: stackId,
language: 'nodejs',
frames
Expand All @@ -86,5 +94,6 @@ function reportStackTrace (rootSpan, stackId, maxDepth, maxStackTraces, callSite

module.exports = {
getCallSiteList,
reportStackTrace
reportStackTrace,
STACK_TRACE_NAMESPACES
}
16 changes: 16 additions & 0 deletions packages/dd-trace/test/appsec/iast/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const { assert } = require('chai')

const agent = require('../../plugins/agent')
const axios = require('axios')
const iast = require('../../../src/appsec/iast')
const Config = require('../../../src/config')
const vulnerabilityReporter = require('../../../src/appsec/iast/vulnerability-reporter')

function getWebSpan (traces) {
for (const trace of traces) {
for (const span of trace) {
if (span.type === 'web') {
return span
}
}
}
throw new Error('web span not found')
}

function testInRequest (app, tests) {
let http
let listener
Expand Down Expand Up @@ -161,6 +173,10 @@ function checkVulnerabilityInRequest (vulnerability, occurrencesAndLocation, cb,
.use(traces => {
expect(traces[0][0].metrics['_dd.iast.enabled']).to.be.equal(1)
expect(traces[0][0].meta).to.have.property('_dd.iast.json')

const span = getWebSpan(traces)
assert.property(span.meta_struct, '_dd.stack')

const vulnerabilitiesTrace = JSON.parse(traces[0][0].meta['_dd.iast.json'])
expect(vulnerabilitiesTrace).to.not.be.null
const vulnerabilitiesCount = new Map()
Expand Down

0 comments on commit f6edc6e

Please sign in to comment.