UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

402 lines (317 loc) 12.1 kB
'use strict' const log = require('../log') const RuleManager = require('./rule_manager') const remoteConfig = require('../remote_config') const { bodyParser, cookieParser, multerParser, fastifyBodyParser, fastifyCookieParser, incomingHttpRequestStart, incomingHttpRequestEnd, passportVerify, passportUser, expressSession, queryParser, nextBodyParsed, nextQueryParsed, expressProcessParams, fastifyQueryParams, responseBody, responseWriteHead, responseSetHeader, routerParam, fastifyResponseChannel, fastifyPathParams } = require('./channels') const waf = require('./waf') const addresses = require('./addresses') const Reporter = require('./reporter') const appsecTelemetry = require('./telemetry') const apiSecuritySampler = require('./api_security_sampler') const web = require('../plugins/util/web') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking') const UserTracking = require('./user_tracking') const { storage } = require('../../../datadog-core') const graphql = require('./graphql') const rasp = require('./rasp') const { isInServerlessEnvironment } = require('../serverless') const responseAnalyzedSet = new WeakSet() const storedResponseHeaders = new WeakMap() let isEnabled = false let config function enable (_config) { if (isEnabled) return try { appsecTelemetry.enable(_config) graphql.enable() if (_config.appsec.rasp.enabled) { rasp.enable(_config) } setTemplates(_config) RuleManager.loadRules(_config.appsec) remoteConfig.enableWafUpdate(_config.appsec) Reporter.init(_config.appsec) apiSecuritySampler.configure(_config) UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false) bodyParser.subscribe(onRequestBodyParsed) multerParser.subscribe(onRequestBodyParsed) cookieParser.subscribe(onRequestCookieParser) incomingHttpRequestStart.subscribe(incomingHttpStartTranslator) incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator) passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled passportUser.subscribe(onPassportDeserializeUser) expressSession.subscribe(onExpressSession) queryParser.subscribe(onRequestQueryParsed) nextBodyParsed.subscribe(onRequestBodyParsed) nextQueryParsed.subscribe(onRequestQueryParsed) expressProcessParams.subscribe(onRequestProcessParams) fastifyBodyParser.subscribe(onRequestBodyParsed) fastifyQueryParams.subscribe(onRequestQueryParsed) fastifyCookieParser.subscribe(onRequestCookieParser) fastifyPathParams.subscribe(onRequestProcessParams) routerParam.subscribe(onRequestProcessParams) responseBody.subscribe(onResponseBody) fastifyResponseChannel.subscribe(onResponseBody) responseWriteHead.subscribe(onResponseWriteHead) responseSetHeader.subscribe(onResponseSetHeader) isEnabled = true config = _config } catch (err) { if (!isInServerlessEnvironment()) { log.error('[ASM] Unable to start AppSec', err) } disable() } } function onRequestBodyParsed ({ req, res, body, abortController }) { if (body === undefined || body === null) return if (!req) { const store = storage('legacy').getStore() req = store?.req } const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ persistent: { [addresses.HTTP_INCOMING_BODY]: body } }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function onRequestCookieParser ({ req, res, abortController, cookies }) { if (!cookies || typeof cookies !== 'object') return const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ persistent: { [addresses.HTTP_INCOMING_COOKIES]: cookies } }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function incomingHttpStartTranslator ({ req, res, abortController }) { const rootSpan = web.root(req) if (!rootSpan) return const clientIp = extractIp(config, req) rootSpan.addTags({ '_dd.appsec.enabled': 1, '_dd.runtime_family': 'nodejs', [HTTP_CLIENT_IP]: clientIp }) const requestHeaders = { ...req.headers } delete requestHeaders.cookie const persistent = { [addresses.HTTP_INCOMING_URL]: req.url, [addresses.HTTP_INCOMING_HEADERS]: requestHeaders, [addresses.HTTP_INCOMING_METHOD]: req.method } if (clientIp) { persistent[addresses.HTTP_CLIENT_IP] = clientIp } const results = waf.run({ persistent }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function incomingHttpEndTranslator ({ req, res }) { const persistent = {} // we need to keep this to support other body parsers // TODO: no need to analyze it if it was already done by the body-parser hook if (req.body !== undefined && req.body !== null) { persistent[addresses.HTTP_INCOMING_BODY] = req.body } // we need to keep this to support other cookie parsers if (req.cookies !== null && typeof req.cookies === 'object') { persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies } // we need to keep this to support nextjs const query = req.query if (query !== null && typeof query === 'object') { persistent[addresses.HTTP_INCOMING_QUERY] = query } if (apiSecuritySampler.sampleRequest(req, res, true)) { persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true } } if (Object.keys(persistent).length) { waf.run({ persistent }, req) } waf.disposeContext(req) const storedHeaders = storedResponseHeaders.get(req) || {} Reporter.finishRequest(req, res, storedHeaders) if (storedHeaders) { storedResponseHeaders.delete(req) } } function onPassportVerify ({ framework, login, user, success, abortController }) { const store = storage('legacy').getStore() const rootSpan = store?.req && web.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportVerify') return } const results = UserTracking.trackLogin(framework, login, user, success, rootSpan) handleResults(results?.actions, store.req, store.req.res, rootSpan, abortController) } function onPassportDeserializeUser ({ user, abortController }) { const store = storage('legacy').getStore() const rootSpan = store?.req && web.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportDeserializeUser') return } const results = UserTracking.trackUser(user, rootSpan) handleResults(results?.actions, store.req, store.req.res, rootSpan, abortController) } function onExpressSession ({ req, res, sessionId, abortController }) { const rootSpan = web.root(req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onExpressSession') return } const isSdkCalled = rootSpan.context()._tags['usr.session_id'] if (isSdkCalled) return const results = waf.run({ persistent: { [addresses.USER_SESSION_ID]: sessionId } }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function onRequestQueryParsed ({ req, res, query, abortController }) { if (!query || typeof query !== 'object') return if (!req) { const store = storage('legacy').getStore() req = store?.req } const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ persistent: { [addresses.HTTP_INCOMING_QUERY]: query } }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function onRequestProcessParams ({ req, res, abortController, params }) { const rootSpan = web.root(req) if (!rootSpan) return if (!params || typeof params !== 'object' || !Object.keys(params).length) return const results = waf.run({ persistent: { [addresses.HTTP_INCOMING_PARAMS]: params } }, req) handleResults(results?.actions, req, res, rootSpan, abortController) } function onResponseBody ({ req, res, body }) { if (!body || typeof body !== 'object') return if (!apiSecuritySampler.sampleRequest(req, res)) return // we don't support blocking at this point, so no results needed waf.run({ persistent: { [addresses.HTTP_INCOMING_RESPONSE_BODY]: body } }, req) } function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) { if (Object.keys(responseHeaders).length) { storedResponseHeaders.set(req, responseHeaders) } // TODO: do not call waf if inside block() // if (isBlocking()) { // return // } // avoid "write after end" error if (isBlocked(res) || callBlockDelegation(res)) { abortController?.abort() return } // avoid double waf call if (responseAnalyzedSet.has(res)) { return } const rootSpan = web.root(req) if (!rootSpan) return responseHeaders = { ...responseHeaders } delete responseHeaders['set-cookie'] const results = waf.run({ persistent: { [addresses.HTTP_INCOMING_RESPONSE_CODE]: String(statusCode), [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders } }, req) responseAnalyzedSet.add(res) handleResults(results?.actions, req, res, rootSpan, abortController) } function onResponseSetHeader ({ res, abortController }) { if (isBlocked(res)) { abortController?.abort() } } function handleResults (actions, req, res, rootSpan, abortController) { if (!actions || !req || !res || !rootSpan || !abortController) return const blockingAction = getBlockingAction(actions) if (blockingAction) { block(req, res, rootSpan, abortController, blockingAction) } } function disable () { isEnabled = false config = null RuleManager.clearAllRules() appsecTelemetry.disable() graphql.disable() rasp.disable() remoteConfig.disableWafUpdate() apiSecuritySampler.disable() // Channel#unsubscribe() is undefined for non active channels if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed) if (multerParser.hasSubscribers) multerParser.unsubscribe(onRequestBodyParsed) if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser) if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator) if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator) if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify) if (passportUser.hasSubscribers) passportUser.unsubscribe(onPassportDeserializeUser) if (expressSession.hasSubscribers) expressSession.unsubscribe(onExpressSession) if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed) if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed) if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed) if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams) if (fastifyBodyParser.hasSubscribers) fastifyBodyParser.unsubscribe(onRequestBodyParsed) if (fastifyQueryParams.hasSubscribers) fastifyQueryParams.unsubscribe(onRequestQueryParsed) if (fastifyCookieParser.hasSubscribers) fastifyCookieParser.unsubscribe(onRequestCookieParser) if (fastifyPathParams.hasSubscribers) fastifyPathParams.unsubscribe(onRequestProcessParams) if (routerParam.hasSubscribers) routerParam.unsubscribe(onRequestProcessParams) if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody) if (fastifyResponseChannel.hasSubscribers) fastifyResponseChannel.unsubscribe(onResponseBody) if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead) if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader) } module.exports = { enable, disable, incomingHttpStartTranslator, incomingHttpEndTranslator }