Skip to content

Commit

Permalink
Report stack trace in iast (#5055)
Browse files Browse the repository at this point in the history
* Report stack trace in iast

* fix stack trace tests

* fix names

* call site frames

* fix path-line tests

* use frames instead of call list

* fix hardcoded-analyzers tests

* clear tests

* get original locations only if we can add vulnerability

* add iast stacktrace variable

* vulnerability reporter unit test with stack trace

* maintain stack trace limit per request

* dont report stack trace if we reach max by request

* add use strict to utils test file
  • Loading branch information
IlyasShabi authored Jan 23, 2025
1 parent c28765a commit 30efc06
Show file tree
Hide file tree
Showing 22 changed files with 678 additions and 326 deletions.
5 changes: 4 additions & 1 deletion docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ tracer.init({
redactionEnabled: true,
redactionNamePattern: 'password',
redactionValuePattern: 'bearer',
telemetryVerbosity: 'OFF'
telemetryVerbosity: 'OFF',
stackTrace: {
enabled: true
}
}
});

Expand Down
12 changes: 11 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,17 @@ declare namespace tracer {
/**
* Specifies the verbosity of the sent telemetry. Default 'INFORMATION'
*/
telemetryVerbosity?: string
telemetryVerbosity?: string,

/**
* Configuration for stack trace reporting
*/
stackTrace?: {
/** Whether to enable stack trace reporting.
* @default true
*/
enabled?: boolean,
}
}

export namespace llmobs {
Expand Down
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, callSiteFrames) {
if (!value) {
return super._getLocation()
return super._getLocation(value, callSiteFrames)
}

if (value.location) {
return value.location
}
const location = super._getLocation(value)
const location = super._getLocation(value, callSiteFrames)
value.location = location
return location
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use strict'

const { storage } = require('../../../../../datadog-core')
const { getFirstNonDDPathAndLine } = require('../path-line')
const { addVulnerability } = require('../vulnerability-reporter')
const { getIastContext } = require('../iast-context')
const { getNonDDCallSiteFrames } = require('../path-line')
const { getIastContext, getIastStackTraceId } = require('../iast-context')
const overheadController = require('../overhead-controller')
const { SinkIastPlugin } = require('../iast-plugin')
const { getOriginalPathAndLineFromSourceMap } = require('../taint-tracking/rewriter')
const {
addVulnerability,
getVulnerabilityCallSiteFrames,
replaceCallSiteFromSourceMap
} = require('../vulnerability-reporter')

class Analyzer extends SinkIastPlugin {
constructor (type) {
Expand All @@ -28,12 +31,24 @@ class Analyzer extends SinkIastPlugin {
}

_reportEvidence (value, context, evidence) {
const location = this._getLocation(value)
const callSiteFrames = getVulnerabilityCallSiteFrames()
const nonDDCallSiteFrames = getNonDDCallSiteFrames(callSiteFrames, this._getExcludedPaths())

const location = this._getLocation(value, nonDDCallSiteFrames)

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

addVulnerability(context, vulnerability, nonDDCallSiteFrames)
}
}

Expand All @@ -49,24 +64,25 @@ class Analyzer extends SinkIastPlugin {
return { value }
}

_getLocation () {
return getFirstNonDDPathAndLine(this._getExcludedPaths())
_getLocation (value, callSiteFrames) {
return callSiteFrames[0]
}

_replaceLocationFromSourceMap (location) {
if (location) {
const { path, line, column } = getOriginalPathAndLineFromSourceMap(location)
if (path) {
location.path = path
}
if (line) {
location.line = line
}
if (column) {
location.column = column
}
_getOriginalLocation (location) {
const locationFromSourceMap = replaceCallSiteFromSourceMap(location)
const originalLocation = {}

if (locationFromSourceMap?.path) {
originalLocation.path = locationFromSourceMap.path
}
if (locationFromSourceMap?.line) {
originalLocation.line = locationFromSourceMap.line
}
return location
if (locationFromSourceMap?.column) {
originalLocation.column = locationFromSourceMap.column
}

return originalLocation
}

_getExcludedPaths () {}
Expand Down Expand Up @@ -102,12 +118,13 @@ class Analyzer extends SinkIastPlugin {
return overheadController.hasQuota(overheadController.OPERATIONS.REPORT_VULNERABILITY, context)
}

_createVulnerability (type, evidence, spanId, location) {
_createVulnerability (type, evidence, spanId, location, stackId) {
if (type && evidence) {
const _spanId = spanId || 0
return {
type,
evidence,
stackId,
location: {
spanId: _spanId,
...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.stackTraceId) {
iastContext.stackTraceId = 0
}

iastContext.stackTraceId += 1
return iastContext.stackTraceId
}

/* 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
}
42 changes: 19 additions & 23 deletions packages/dd-trace/src/appsec/iast/path-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
const path = require('path')
const process = require('process')
const { calculateDDBasePath } = require('../../util')
const { getCallSiteList } = require('../stack_trace')
const pathLine = {
getFirstNonDDPathAndLine,
getNodeModulesPaths,
getRelativePath,
getFirstNonDDPathAndLineFromCallsites, // Exported only for test purposes
getNonDDCallSiteFrames,
calculateDDBasePath, // Exported only for test purposes
ddBasePath: calculateDDBasePath(__dirname) // Only for test purposes
}
Expand All @@ -25,31 +23,33 @@ const EXCLUDED_PATH_PREFIXES = [
'async_hooks'
]

function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPaths) {
if (callsites) {
for (let i = 0; i < callsites.length; i++) {
const callsite = callsites[i]
const filepath = callsite.getFileName()
if (!isExcluded(callsite, externallyExcludedPaths) && filepath.indexOf(pathLine.ddBasePath) === -1) {
return {
path: getRelativePath(filepath),
line: callsite.getLineNumber(),
column: callsite.getColumnNumber(),
isInternal: !path.isAbsolute(filepath)
}
}
function getNonDDCallSiteFrames (callSiteFrames, externallyExcludedPaths) {
if (!callSiteFrames) {
return []
}

const result = []

for (const callsite of callSiteFrames) {
const filepath = callsite.file
if (!isExcluded(callsite, externallyExcludedPaths) && filepath.indexOf(pathLine.ddBasePath) === -1) {
callsite.path = getRelativePath(filepath)
callsite.isInternal = !path.isAbsolute(filepath)

result.push(callsite)
}
}
return null

return result
}

function getRelativePath (filepath) {
return path.relative(process.cwd(), filepath)
}

function isExcluded (callsite, externallyExcludedPaths) {
if (callsite.isNative()) return true
const filename = callsite.getFileName()
if (callsite.isNative) return true
const filename = callsite.file
if (!filename) {
return true
}
Expand All @@ -73,10 +73,6 @@ function isExcluded (callsite, externallyExcludedPaths) {
return false
}

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

function getNodeModulesPaths (...paths) {
const nodeModulesPaths = []

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
Loading

0 comments on commit 30efc06

Please sign in to comment.