Skip to content

Commit

Permalink
Add _dd.p.appsec tag on standalone ASM events
Browse files Browse the repository at this point in the history
  • Loading branch information
iunanua committed May 20, 2024
1 parent 957760a commit 5d6210d
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 30 deletions.
8 changes: 8 additions & 0 deletions packages/dd-trace/src/appsec/iast/vulnerability-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { MANUAL_KEEP } = require('../../../../../ext/tags')
const LRU = require('lru-cache')
const vulnerabilitiesFormatter = require('./vulnerabilities-formatter')
const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
const { APPSEC_PROPAGATION_KEY } = require('../../constants')

const VULNERABILITIES_KEY = 'vulnerabilities'
const VULNERABILITY_HASHES_MAX_SIZE = 1000
Expand All @@ -13,6 +14,7 @@ const RESET_VULNERABILITY_CACHE_INTERVAL = 60 * 60 * 1000 // 1 hour
let tracer
let resetVulnerabilityCacheTimer
let deduplicationEnabled = true
let standaloneEnabled = false

function addVulnerability (iastContext, vulnerability) {
if (vulnerability && vulnerability.evidence && vulnerability.type &&
Expand Down Expand Up @@ -56,6 +58,11 @@ function sendVulnerabilities (vulnerabilities, rootSpan) {
// TODO: Store this outside of the span and set the tag in the exporter.
tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend)
tags[MANUAL_KEEP] = 'true'

if (standaloneEnabled) {
tags[APPSEC_PROPAGATION_KEY] = '1' // string goes to meta and number goes to metrics
}

span.addTags(tags)
if (!rootSpan) span.finish()
}
Expand Down Expand Up @@ -95,6 +102,7 @@ function deduplicateVulnerabilities (vulnerabilities) {

function start (config, _tracer) {
deduplicationEnabled = config.iast.deduplicationEnabled
standaloneEnabled = !!config.appsec?.standalone?.enabled
vulnerabilitiesFormatter.setRedactVulnerabilities(
config.iast.redactionEnabled,
config.iast.redactionNamePattern,
Expand Down
2 changes: 1 addition & 1 deletion packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function enable (_config) {

remoteConfig.enableWafUpdate(_config.appsec)

Reporter.setRateLimit(_config.appsec.rateLimit)
Reporter.configure(_config.appsec)

apiSecuritySampler.configure(_config.appsec)

Expand Down
20 changes: 17 additions & 3 deletions packages/dd-trace/src/appsec/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ const {
} = require('./telemetry')
const zlib = require('zlib')
const { MANUAL_KEEP } = require('../../../../ext/tags')
const { APPSEC_PROPAGATION_KEY } = require('../constants')

// default limiter, configurable with setRateLimit()
let limiter = new Limiter(100)

let standaloneEnabled = false

const metricsQueue = new Map()

// following header lists are ordered in the same way the spec orders them, it doesn't matter but it's easier to compare
Expand Down Expand Up @@ -89,6 +92,9 @@ function reportWafInit (wafVersion, rulesVersion, diagnosticsRules = {}) {
}

metricsQueue.set(MANUAL_KEEP, 'true')
if (standaloneEnabled) {
metricsQueue.set(APPSEC_PROPAGATION_KEY, '1')
}

incrementWafInitMetric(wafVersion, rulesVersion)
}
Expand Down Expand Up @@ -117,8 +123,11 @@ function reportAttack (attackData) {

newTags['appsec.event'] = 'true'

if (limiter.isAllowed()) {
if (standaloneEnabled) {
newTags[MANUAL_KEEP] = 'true' // TODO: figure out how to keep appsec traces with sampling revamp
newTags[APPSEC_PROPAGATION_KEY] = '1'
} else if (limiter.isAllowed()) {
newTags[MANUAL_KEEP] = 'true'
}

// TODO: maybe add this to format.js later (to take decision as late as possible)
Expand Down Expand Up @@ -168,7 +177,6 @@ function finishRequest (req, res) {

if (metricsQueue.size) {
rootSpan.addTags(Object.fromEntries(metricsQueue))

metricsQueue.clear()
}

Expand Down Expand Up @@ -201,6 +209,11 @@ function setRateLimit (rateLimit) {
limiter = new Limiter(rateLimit)
}

function configure (config) {
setRateLimit(config.rateLimit)
standaloneEnabled = !!config.standalone?.enabled
}

module.exports = {
metricsQueue,
filterHeaders,
Expand All @@ -212,5 +225,6 @@ module.exports = {
reportSchemas,
finishRequest,
setRateLimit,
mapHeaderAndTags
mapHeaderAndTags,
configure
}
7 changes: 4 additions & 3 deletions packages/dd-trace/src/appsec/sdk/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ const { setUser } = require('./set_user')
class AppsecSdk {
constructor (tracer, config) {
this._tracer = tracer
this.standaloneEnabled = !!config.appsec.standalone?.enabled
if (config) {
setTemplates(config)
}
}

trackUserLoginSuccessEvent (user, metadata) {
return trackUserLoginSuccessEvent(this._tracer, user, metadata)
return trackUserLoginSuccessEvent(this._tracer, user, metadata, this.standaloneEnabled)
}

trackUserLoginFailureEvent (userId, exists, metadata) {
return trackUserLoginFailureEvent(this._tracer, userId, exists, metadata)
return trackUserLoginFailureEvent(this._tracer, userId, exists, metadata, this.standaloneEnabled)
}

trackCustomEvent (eventName, metadata) {
return trackCustomEvent(this._tracer, eventName, metadata)
return trackCustomEvent(this._tracer, eventName, metadata, this.standaloneEnabled)
}

isUserBlocked (user) {
Expand Down
20 changes: 13 additions & 7 deletions packages/dd-trace/src/appsec/sdk/track_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ const log = require('../../log')
const { getRootSpan } = require('./utils')
const { MANUAL_KEEP } = require('../../../../../ext/tags')
const { setUserTags } = require('./set_user')
const { APPSEC_PROPAGATION_KEY } = require('../../constants')

function trackUserLoginSuccessEvent (tracer, user, metadata) {
function trackUserLoginSuccessEvent (tracer, user, metadata, standaloneEnabled) {
// TODO: better user check here and in _setUser() ?
if (!user || !user.id) {
log.warn('Invalid user provided to trackUserLoginSuccessEvent')
Expand All @@ -20,10 +21,10 @@ function trackUserLoginSuccessEvent (tracer, user, metadata) {

setUserTags(user, rootSpan)

trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan, 'sdk')
trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan, 'sdk', standaloneEnabled)
}

function trackUserLoginFailureEvent (tracer, userId, exists, metadata) {
function trackUserLoginFailureEvent (tracer, userId, exists, metadata, standaloneEnabled) {
if (!userId || typeof userId !== 'string') {
log.warn('Invalid userId provided to trackUserLoginFailureEvent')
return
Expand All @@ -35,19 +36,20 @@ function trackUserLoginFailureEvent (tracer, userId, exists, metadata) {
...metadata
}

trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer), 'sdk')
trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer), 'sdk',
standaloneEnabled)
}

function trackCustomEvent (tracer, eventName, metadata) {
function trackCustomEvent (tracer, eventName, metadata, standaloneEnabled) {
if (!eventName || typeof eventName !== 'string') {
log.warn('Invalid eventName provided to trackCustomEvent')
return
}

trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer), 'sdk')
trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer), 'sdk', standaloneEnabled)
}

function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) {
function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode, standaloneEnabled) {
if (!rootSpan) {
log.warn(`Root span not available in ${sdkMethodName}`)
return
Expand All @@ -58,6 +60,10 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) {
[MANUAL_KEEP]: 'true'
}

if (standaloneEnabled) {
tags[APPSEC_PROPAGATION_KEY] = '1'
}

if (mode === 'sdk') {
tags[`_dd.appsec.events.${eventName}.sdk`] = 'true'
}
Expand Down
15 changes: 13 additions & 2 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ class Config {
this._setValue(defaults, 'appsec.rateLimit', 100)
this._setValue(defaults, 'appsec.rules', undefined)
this._setValue(defaults, 'appsec.sca.enabled', null)
this._setValue(defaults, 'appsec.standalone.enabled', undefined)
this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs
this._setValue(defaults, 'clientIpEnabled', false)
this._setValue(defaults, 'clientIpHeader', null)
Expand Down Expand Up @@ -524,7 +525,6 @@ class Config {
const {
AWS_LAMBDA_FUNCTION_NAME,
DD_AGENT_HOST,
DD_APM_TRACING_ENABLED,
DD_APPSEC_ENABLED,
DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML,
DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON,
Expand All @@ -539,6 +539,7 @@ class Config {
DD_DOGSTATSD_HOSTNAME,
DD_DOGSTATSD_PORT,
DD_ENV,
DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED,
DD_EXPERIMENTAL_PROFILING_ENABLED,
JEST_WORKER_ID,
DD_IAST_DEDUPLICATION_ENABLED,
Expand Down Expand Up @@ -612,7 +613,6 @@ class Config {
tagger.add(tags, DD_TRACE_TAGS)
tagger.add(tags, DD_TRACE_GLOBAL_TAGS)

this._setBoolean(env, 'apmTracingEnabled', DD_APM_TRACING_ENABLED)
this._setValue(env, 'appsec.blockedTemplateHtml', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML))
this._setValue(env, 'appsec.blockedTemplateJson', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON))
this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
Expand All @@ -622,6 +622,7 @@ class Config {
this._setString(env, 'appsec.rules', DD_APPSEC_RULES)
// DD_APPSEC_SCA_ENABLED is never used locally, but only sent to the backend
this._setBoolean(env, 'appsec.sca.enabled', DD_APPSEC_SCA_ENABLED)
this._setBoolean(env, 'appsec.standalone.enabled', DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED)
this._setValue(env, 'appsec.wafTimeout', maybeInt(DD_APPSEC_WAF_TIMEOUT))
this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
Expand Down Expand Up @@ -728,6 +729,7 @@ class Config {
this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex)
this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec.rateLimit))
this._setString(opts, 'appsec.rules', options.appsec.rules)
this._setBoolean(opts, 'appsec.standalone.enabled', options.experimental?.appsec?.standalone?.enabled)
this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout))
this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
this._setString(opts, 'clientIpHeader', options.clientIpHeader)
Expand Down Expand Up @@ -849,6 +851,14 @@ class Config {
return spanComputePeerService
}

_isAppsecStandaloneEnabled () {
return isTrue(coalesce(
this.options.experimental?.appsec?.standalone?.enabled,
process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED,
false
))
}

_isCiVisibilityGitUploadEnabled () {
return coalesce(
process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED,
Expand Down Expand Up @@ -904,6 +914,7 @@ class Config {
calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled()))
this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService())
this._setBoolean(calc, 'stats.enabled', this._isTraceStatsComputationEnabled())
this._setBoolean(calc, 'apmTracingEnabled', !this._isAppsecStandaloneEnabled())
}

_applyRemote (options) {
Expand Down
5 changes: 3 additions & 2 deletions packages/dd-trace/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ module.exports = {
SPAN_SAMPLING_MECHANISM: '_dd.span_sampling.mechanism',
SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate',
SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second',
APM_TRACING_ENABLED_KEY: '_dd.apm.enabled',
DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent',
DECISION_MAKER_KEY: '_dd.p.dm',
PROCESS_ID: 'process_id',
Expand All @@ -31,5 +30,7 @@ module.exports = {
PEER_SERVICE_SOURCE_KEY: '_dd.peer.service.source',
PEER_SERVICE_REMAP_KEY: '_dd.peer.service.remapped_from',
SCI_REPOSITORY_URL: '_dd.git.repository_url',
SCI_COMMIT_SHA: '_dd.git.commit.sha'
SCI_COMMIT_SHA: '_dd.git.commit.sha',
APM_TRACING_ENABLED_KEY: '_dd.apm.enabled',
APPSEC_PROPAGATION_KEY: '_dd.p.appsec'
}
52 changes: 52 additions & 0 deletions packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ describe('vulnerability-reporter', () => {
start({
iast: {
deduplicationEnabled: true
},
appsec: {
standalone: {
enabled: null
}
}
}, fakeTracer)
})
Expand Down Expand Up @@ -138,6 +143,11 @@ describe('vulnerability-reporter', () => {
start({
iast: {
deduplicationEnabled: true
},
appsec: {
standalone: {
enabled: null
}
}
})
})
Expand Down Expand Up @@ -362,6 +372,11 @@ describe('vulnerability-reporter', () => {
start({
iast: {
deduplicationEnabled: false
},
appsec: {
standalone: {
enabled: null
}
}
})
const iastContext = { rootSpan: span }
Expand All @@ -380,6 +395,43 @@ describe('vulnerability-reporter', () => {
})
})

describe('sendVulnerabilities appsec standalone', () => {
let span

beforeEach(() => {
span = {
addTags: sinon.stub()
}
start({
iast: {
deduplicationEnabled: true
},
appsec: {
standalone: {
enabled: true
}
}
})
})
afterEach(() => {
sinon.restore()
stop()
})

it('should add _dd.p.appsec span tag', () => {
const iastContext = { rootSpan: span }
addVulnerability(iastContext,
vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 999))
sendVulnerabilities(iastContext.vulnerabilities, span)
expect(span.addTags).to.have.been.calledOnceWithExactly({
'manual.keep': 'true',
'_dd.p.appsec': '1',
'_dd.iast.json': '{"sources":[],"vulnerabilities":[{"type":"INSECURE_HASHING","hash":3254801297,' +
'"evidence":{"value":"sha1"},"location":{"spanId":999}}]}'
})
})
})

describe('clearCache', () => {
let span
let originalSetInterval
Expand Down
4 changes: 2 additions & 2 deletions packages/dd-trace/test/appsec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('AppSec Index', () => {
sinon.stub(fs, 'readFileSync').returns(JSON.stringify(RULES))
sinon.stub(waf, 'init').callThrough()
sinon.stub(RuleManager, 'loadRules')
sinon.stub(Reporter, 'setRateLimit')
sinon.stub(Reporter, 'configure')
sinon.stub(incomingHttpRequestStart, 'subscribe')
sinon.stub(incomingHttpRequestEnd, 'subscribe')
})
Expand All @@ -121,7 +121,7 @@ describe('AppSec Index', () => {

expect(blocking.setTemplates).to.have.been.calledOnceWithExactly(config)
expect(RuleManager.loadRules).to.have.been.calledOnceWithExactly(config.appsec)
expect(Reporter.setRateLimit).to.have.been.calledOnceWithExactly(42)
expect(Reporter.configure).to.have.been.calledOnceWithExactly(config.appsec)
expect(incomingHttpRequestStart.subscribe)
.to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator)
expect(incomingHttpRequestEnd.subscribe).to.have.been.calledOnceWithExactly(AppSec.incomingHttpEndTranslator)
Expand Down
Loading

0 comments on commit 5d6210d

Please sign in to comment.