dd-trace
Version:
Datadog APM tracing client for JavaScript
223 lines (186 loc) • 6.32 kB
JavaScript
'use strict'
const { channel } = require('dc-polyfill')
const Plugin = require('../../plugins/plugin')
const { storage } = require('../../../../datadog-core')
const instrumentations = require('../../../../datadog-instrumentations/src/helpers/instrumentations')
const log = require('../../log')
const iastTelemetry = require('./telemetry')
const { getInstrumentedMetric, getExecutedMetric, TagKey, EXECUTED_SOURCE, formatTags } =
require('./telemetry/iast-metric')
const { getIastContext } = require('./iast-context')
/**
* Used by vulnerability sources and sinks to subscribe diagnostic channel events
* and indicate what kind of metrics the subscription provides
* - moduleName is used identify when a module is loaded and
* to increment the INSTRUMENTED_[SINK|SOURCE] metric when it occurs
* - channelName is the channel used by the hook to publish execution events
* - tag indicates the name of the metric: taint-tracking/source-types for Sources and analyzers type for Sinks
* - tagKey can be only SOURCE_TYPE (Source) or VULNERABILITY_TYPE (Sink)
*/
class IastPluginSubscription {
constructor (moduleName, channelName, tagValues, tagKey = TagKey.VULNERABILITY_TYPE) {
this.moduleName = moduleName
this.channelName = channelName
tagValues = Array.isArray(tagValues) ? tagValues : [tagValues]
this.tags = formatTags(tagValues, tagKey)
this.executedMetric = getExecutedMetric(tagKey)
this.instrumentedMetric = getInstrumentedMetric(tagKey)
this.moduleInstrumented = false
}
increaseInstrumented () {
if (!this.moduleInstrumented) {
this.moduleInstrumented = true
this.tags.forEach(tag => this.instrumentedMetric.inc(undefined, tag))
}
}
increaseExecuted (iastContext) {
this.tags.forEach(tag => this.executedMetric.inc(iastContext, tag))
}
matchesModuleInstrumented (name) {
// Remove node: prefix if present
if (name.startsWith('node:')) {
name = name.slice(5)
}
// https module is a special case because it's events are published as http
name = name === 'https' ? 'http' : name
return this.moduleName === name
}
}
class IastPlugin extends Plugin {
constructor () {
super()
this.configured = false
this.pluginSubs = []
}
_getTelemetryHandler (iastSub) {
return () => {
const iastContext = getIastContext(storage('legacy').getStore())
iastSub.increaseExecuted(iastContext)
}
}
_execHandlerAndIncMetric ({ handler, metric, tags, iastContext = getIastContext(storage('legacy').getStore()) }) {
try {
const result = handler()
if (iastTelemetry.isEnabled()) {
if (Array.isArray(tags)) {
tags.forEach(tag => metric.inc(iastContext, tag))
} else {
metric.inc(iastContext, tags)
}
}
return result
} catch (e) {
log.error('[ASM] Error executing handler or increasing metrics', e)
}
}
addSub (iastSub, handler) {
if (typeof iastSub === 'string') {
super.addSub(iastSub, handler)
} else {
iastSub = this._getAndRegisterSubscription(iastSub)
if (iastSub) {
super.addSub(iastSub.channelName, handler)
if (iastTelemetry.isEnabled()) {
super.addSub(iastSub.channelName, this._getTelemetryHandler(iastSub))
}
}
}
}
enable (iastConfig) {
this.iastConfig = iastConfig
this.configure(true)
}
disable () {
this.configure(false)
}
onConfigure () {}
configure (config) {
if (typeof config !== 'object') {
config = { enabled: config }
}
if (config.enabled && !this.configured) {
this.onConfigure()
this.configured = true
}
if (iastTelemetry.isEnabled()) {
if (config.enabled) {
this.enableTelemetry()
} else {
this.disableTelemetry()
}
}
super.configure(config)
}
_getAndRegisterSubscription ({ moduleName, channelName, tag, tagKey }) {
if (!moduleName) {
if (!channelName) return
let firstSep = channelName.indexOf(':')
if (firstSep === -1) {
moduleName = channelName
} else {
if (channelName.startsWith('tracing:')) {
firstSep = channelName.indexOf(':', 'tracing:'.length + 1)
}
const lastSep = channelName.indexOf(':', firstSep + 1)
moduleName = channelName.slice(firstSep + 1, lastSep === -1 ? channelName.length : lastSep)
}
}
const iastSub = new IastPluginSubscription(moduleName, channelName, tag, tagKey)
this.pluginSubs.push(iastSub)
return iastSub
}
enableTelemetry () {
if (this.onInstrumentationLoadedListener) return
this.onInstrumentationLoadedListener = ({ name }) => this._onInstrumentationLoaded(name)
const loadChannel = channel('dd-trace:instrumentation:load')
loadChannel.subscribe(this.onInstrumentationLoadedListener)
// check for already instrumented modules
for (const name in instrumentations) {
this._onInstrumentationLoaded(name)
}
}
disableTelemetry () {
if (!this.onInstrumentationLoadedListener) return
const loadChannel = channel('dd-trace:instrumentation:load')
if (loadChannel.hasSubscribers) {
loadChannel.unsubscribe(this.onInstrumentationLoadedListener)
}
this.onInstrumentationLoadedListener = null
}
_onInstrumentationLoaded (name) {
this.pluginSubs
.filter(sub => sub.matchesModuleInstrumented(name))
.forEach(sub => sub.increaseInstrumented())
}
}
class SourceIastPlugin extends IastPlugin {
addSub (iastPluginSub, handler) {
return super.addSub({ tagKey: TagKey.SOURCE_TYPE, ...iastPluginSub }, handler)
}
addInstrumentedSource (moduleName, tag) {
this._getAndRegisterSubscription({
moduleName,
tag,
tagKey: TagKey.SOURCE_TYPE
})
}
execSource (sourceHandlerInfo) {
this._execHandlerAndIncMetric({
metric: EXECUTED_SOURCE,
...sourceHandlerInfo
})
}
}
class SinkIastPlugin extends IastPlugin {
addSub (iastPluginSub, handler) {
return super.addSub({ tagKey: TagKey.VULNERABILITY_TYPE, ...iastPluginSub }, handler)
}
addNotSinkSub (iastPluginSub, handler) {
return super.addSub(iastPluginSub, handler)
}
}
module.exports = {
SourceIastPlugin,
SinkIastPlugin,
IastPlugin
}