dd-trace 5.108.0 → 5.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ci/init.js +4 -3
- package/index.d.ts +88 -25
- package/loader-hook.mjs +88 -2
- package/package.json +19 -14
- package/packages/datadog-esbuild/index.js +26 -0
- package/packages/datadog-esbuild/src/utils.js +46 -3
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/connect.js +4 -3
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/fastify.js +4 -12
- package/packages/datadog-instrumentations/src/graphql.js +17 -6
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/koa.js +3 -2
- package/packages/datadog-instrumentations/src/mariadb.js +4 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
- package/packages/datadog-instrumentations/src/next.js +44 -10
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/restify.js +4 -3
- package/packages/datadog-instrumentations/src/router.js +18 -5
- package/packages/datadog-instrumentations/src/selenium.js +1 -2
- package/packages/datadog-instrumentations/src/tedious.js +28 -0
- package/packages/datadog-instrumentations/src/vitest.js +323 -7
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +14 -16
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +108 -99
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +8 -3
- package/packages/datadog-plugin-http2/src/client.js +5 -2
- package/packages/datadog-plugin-mysql/src/index.js +7 -2
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-undici/src/index.js +5 -2
- package/packages/datadog-plugin-vitest/src/index.js +11 -2
- package/packages/datadog-webpack/index.js +17 -1
- package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/aiguard/sdk.js +13 -1
- package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
- package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/index.js +10 -6
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/appsec/reporter.js +1 -0
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
- package/packages/dd-trace/src/config/defaults.js +22 -3
- package/packages/dd-trace/src/config/generated-config-types.d.ts +538 -1
- package/packages/dd-trace/src/config/helper.js +133 -33
- package/packages/dd-trace/src/config/index.js +4 -1
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/supported-configurations.json +45 -5
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +49 -35
- package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
- package/packages/dd-trace/src/debugger/index.js +5 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
- package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/exporters/common/request.js +1 -20
- package/packages/dd-trace/src/exporters/common/url.js +34 -0
- package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
- package/packages/dd-trace/src/guardrails/index.js +8 -5
- package/packages/dd-trace/src/index.js +6 -4
- package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
- package/packages/dd-trace/src/llmobs/index.js +16 -5
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
- package/packages/dd-trace/src/llmobs/sdk.js +5 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
- package/packages/dd-trace/src/llmobs/tagger.js +55 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
- package/packages/dd-trace/src/log/index.js +18 -20
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -1
- package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
- package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
- package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
- package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
- package/packages/dd-trace/src/opentelemetry/span.js +14 -3
- package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
- package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
- package/packages/dd-trace/src/opentracing/span_context.js +17 -0
- package/packages/dd-trace/src/plugin_manager.js +7 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +36 -3
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
- package/packages/dd-trace/src/plugins/util/test.js +80 -0
- package/packages/dd-trace/src/plugins/util/url.js +68 -1
- package/packages/dd-trace/src/plugins/util/web.js +19 -36
- package/packages/dd-trace/src/priority_sampler.js +1 -13
- package/packages/dd-trace/src/profiling/config.js +129 -198
- package/packages/dd-trace/src/profiling/constants.js +4 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
- package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
- package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
- package/packages/dd-trace/src/profiling/oom.js +71 -0
- package/packages/dd-trace/src/profiling/profiler.js +35 -23
- package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
- package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
- package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
- package/packages/dd-trace/src/ritm.js +4 -2
- package/packages/dd-trace/src/serverless.js +1 -2
- package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
- package/packages/dd-trace/src/span_format.js +2 -1
- package/packages/dd-trace/src/span_processor.js +6 -0
- package/packages/dd-trace/src/telemetry/send-data.js +12 -10
- package/packages/dd-trace/src/tracer.js +15 -1
- package/packages/dd-trace/src/util.js +14 -0
- package/register.js +58 -1
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
- package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { TTLCache } = require('../../../../../vendor/dist/@isaacs/ttlcache')
|
|
4
|
+
const web = require('../../plugins/util/web')
|
|
5
|
+
const log = require('../../log')
|
|
6
|
+
const { AUTO_REJECT, USER_REJECT } = require('../../../../../ext/priority')
|
|
7
|
+
const { keepTrace } = require('../../priority_sampler')
|
|
8
|
+
const { ASM } = require('../../standalone/product')
|
|
9
|
+
const { isBlocked } = require('../blocking')
|
|
10
|
+
|
|
11
|
+
const MAX_SIZE = 4096
|
|
12
|
+
|
|
13
|
+
const SamplingDecision = Object.freeze({
|
|
14
|
+
SAMPLE: 'sample',
|
|
15
|
+
MISSING_ROUTE: 'missing_route',
|
|
16
|
+
SKIP: 'skip',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let enabled
|
|
20
|
+
let asmStandaloneEnabled
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {TTLCache}
|
|
24
|
+
*/
|
|
25
|
+
let sampledRequests
|
|
26
|
+
|
|
27
|
+
class NoopTTLCache {
|
|
28
|
+
clear () {}
|
|
29
|
+
set (_key, _value) {}
|
|
30
|
+
has (_key) { return false }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configure ({ appsec, apmTracingEnabled }) {
|
|
34
|
+
enabled = appsec.apiSecurity.enabled
|
|
35
|
+
asmStandaloneEnabled = apmTracingEnabled === false
|
|
36
|
+
sampledRequests = appsec.apiSecurity.sampleDelay === 0
|
|
37
|
+
? new NoopTTLCache()
|
|
38
|
+
: new TTLCache({ max: MAX_SIZE, ttl: appsec.apiSecurity.sampleDelay * 1000 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function disable () {
|
|
42
|
+
enabled = false
|
|
43
|
+
sampledRequests?.clear()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {import('http').IncomingMessage} req
|
|
48
|
+
* @param {import('http').ServerResponse} res
|
|
49
|
+
* @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
|
|
50
|
+
* @returns {'sample' | 'missing_route' | 'skip'}
|
|
51
|
+
*/
|
|
52
|
+
function sampleRequest (req, res, record = false) {
|
|
53
|
+
if (!enabled) return SamplingDecision.SKIP
|
|
54
|
+
|
|
55
|
+
const rootSpan = web.root(req)
|
|
56
|
+
if (!rootSpan) return SamplingDecision.SKIP
|
|
57
|
+
|
|
58
|
+
if (!asmStandaloneEnabled) {
|
|
59
|
+
let priority = getSpanPriority(rootSpan)
|
|
60
|
+
if (!priority) {
|
|
61
|
+
rootSpan._prioritySampler?.sample(rootSpan)
|
|
62
|
+
priority = getSpanPriority(rootSpan)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (priority === AUTO_REJECT || priority === USER_REJECT) {
|
|
66
|
+
return SamplingDecision.SKIP
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const resolved = resolveSamplingKey(req, res)
|
|
71
|
+
if (!resolved) return SamplingDecision.SKIP
|
|
72
|
+
|
|
73
|
+
if (record && resolved.route === null) {
|
|
74
|
+
if (resolved.status === 404 || isBlocked(res)) return SamplingDecision.SKIP
|
|
75
|
+
return SamplingDecision.MISSING_ROUTE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sampledRequests.has(resolved.key)) return SamplingDecision.SKIP
|
|
79
|
+
|
|
80
|
+
if (asmStandaloneEnabled) {
|
|
81
|
+
keepTrace(rootSpan, ASM)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (record) {
|
|
85
|
+
sampledRequests.set(resolved.key, undefined)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return SamplingDecision.SAMPLE
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {import('http').IncomingMessage} req
|
|
93
|
+
* @param {import('http').ServerResponse} res
|
|
94
|
+
* @returns {boolean} Whether this request's endpoint is currently recorded in the TTL cache.
|
|
95
|
+
*/
|
|
96
|
+
function wasSampled (req, res) {
|
|
97
|
+
const resolved = resolveSamplingKey(req, res)
|
|
98
|
+
return resolved !== null && sampledRequests.has(resolved.key)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveSamplingKey (req, res) {
|
|
102
|
+
const method = req.method
|
|
103
|
+
const status = res.statusCode
|
|
104
|
+
|
|
105
|
+
if (!method || !status) {
|
|
106
|
+
log.warn('[ASM] Unsupported groupkey for API security')
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const context = web.getContext(req)
|
|
111
|
+
const route = getRouteOrEndpoint(context, status)
|
|
112
|
+
|
|
113
|
+
// route === null signals "no route information at all". An empty string is still a valid
|
|
114
|
+
// route (dd-trace-js represents the express root path '/' as an empty path segment).
|
|
115
|
+
return { method, status, route, key: method + (route ?? '') + status }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getRouteOrEndpoint (context, statusCode) {
|
|
119
|
+
// The router plugin populates `context.paths` whenever the framework matched something.
|
|
120
|
+
// For express's root '/' route the matched path is normalized to '' (see datadog-plugin-router),
|
|
121
|
+
// so `paths.length > 0` is the signal that the framework provided route information — even when
|
|
122
|
+
// the joined string is empty. An empty `paths` array means no router involvement at all.
|
|
123
|
+
const paths = context?.paths
|
|
124
|
+
if (paths !== undefined && paths.length > 0) {
|
|
125
|
+
return paths.join('')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (statusCode === 404) return null
|
|
129
|
+
|
|
130
|
+
const endpoint = context?.span?.context()?.getTag?.('http.endpoint')
|
|
131
|
+
if (endpoint) return endpoint
|
|
132
|
+
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getSpanPriority (span) {
|
|
137
|
+
const spanContext = span.context?.()
|
|
138
|
+
return spanContext._sampling?.priority
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
configure,
|
|
143
|
+
disable,
|
|
144
|
+
sampleRequest,
|
|
145
|
+
wasSampled,
|
|
146
|
+
SamplingDecision,
|
|
147
|
+
}
|
|
@@ -23,6 +23,7 @@ module.exports = {
|
|
|
23
23
|
fsOperationStart: dc.channel('apm:fs:operation:start'),
|
|
24
24
|
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
|
|
25
25
|
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
26
|
+
httpClientResponseStart: dc.channel('apm:http:client:response:start'),
|
|
26
27
|
httpClientResponseFinish: dc.channel('apm:http:client:response:finish'),
|
|
27
28
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
28
29
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
@@ -14,19 +14,32 @@ const {
|
|
|
14
14
|
const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
|
|
15
15
|
const UINT64_MAX = (1n << 64n) - 1n
|
|
16
16
|
|
|
17
|
+
const SUPPORTED_RESPONSE_BODY_MIME_TYPES = new Set([
|
|
18
|
+
'application/json',
|
|
19
|
+
'text/json',
|
|
20
|
+
'application/x-www-form-urlencoded',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE =
|
|
24
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_type_invalid'
|
|
25
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING =
|
|
26
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_length_missing'
|
|
27
|
+
const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG =
|
|
28
|
+
'_dd.appsec.downstream_request.response_body_ignored.content_length_too_big'
|
|
29
|
+
|
|
17
30
|
let config
|
|
18
31
|
let samplingRate
|
|
19
32
|
let globalRequestCounter
|
|
20
33
|
let bodyAnalysisCount
|
|
21
34
|
let downstreamAnalysisCount
|
|
22
|
-
let
|
|
35
|
+
let responseBodyIgnoredCount
|
|
23
36
|
|
|
24
37
|
function enable (_config) {
|
|
25
38
|
config = _config
|
|
26
39
|
globalRequestCounter = 0n
|
|
27
40
|
bodyAnalysisCount = new WeakMap()
|
|
28
41
|
downstreamAnalysisCount = new WeakMap()
|
|
29
|
-
|
|
42
|
+
responseBodyIgnoredCount = new WeakMap()
|
|
30
43
|
|
|
31
44
|
const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
|
|
32
45
|
samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
|
|
@@ -43,49 +56,84 @@ function disable () {
|
|
|
43
56
|
globalRequestCounter = null
|
|
44
57
|
bodyAnalysisCount = null
|
|
45
58
|
downstreamAnalysisCount = null
|
|
46
|
-
|
|
59
|
+
responseBodyIgnoredCount = null
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/**
|
|
50
|
-
*
|
|
51
|
-
* @
|
|
52
|
-
* @param {string} outgoingUrl the URL being requested.
|
|
53
|
-
* @returns {boolean} the stored decision
|
|
63
|
+
* @param {string|string[]|undefined} contentLength raw content-length header value.
|
|
64
|
+
* @returns {number|null} parsed content length or null when invalid.
|
|
54
65
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
function parseContentLengthHeader (contentLength) {
|
|
67
|
+
if (contentLength == null) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const value = Array.isArray(contentLength) ? contentLength[0] : contentLength
|
|
72
|
+
const parsed = Number.parseInt(String(value), 10)
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parsed
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param {import('http').IncomingMessage} req
|
|
65
|
-
* @param {string}
|
|
82
|
+
* Increments a response-body-ignored counter on the service-entry span.
|
|
83
|
+
* @param {import('http').IncomingMessage} req originating request.
|
|
84
|
+
* @param {string} tag full `_dd.appsec.downstream_request.response_body_ignored.*` span tag.
|
|
66
85
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
86
|
+
function recordResponseBodyIgnored (req, tag) {
|
|
87
|
+
const span = web.root(req)
|
|
88
|
+
if (!span) return
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
let counts = responseBodyIgnoredCount.get(req)
|
|
91
|
+
if (!counts) {
|
|
92
|
+
counts = {}
|
|
93
|
+
responseBodyIgnoredCount.set(req, counts)
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
const current = counts[tag] || 0
|
|
97
|
+
const next = current + 1
|
|
98
|
+
counts[tag] = next
|
|
99
|
+
span.setTag(tag, next)
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @param {import('http').IncomingMessage}
|
|
81
|
-
* @
|
|
82
|
-
* @returns {boolean} true when the downstream response body should be captured.
|
|
103
|
+
* @param {import('http').IncomingMessage} originatingReq inbound request (for metrics).
|
|
104
|
+
* @param {import('http').IncomingMessage} res downstream response.
|
|
105
|
+
* @returns {boolean} whether downstream response body should be collected for AppSec.
|
|
83
106
|
*/
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
function evaluateResponseBodyCollection (originatingReq, res) {
|
|
108
|
+
const maxBytes = config.appsec.apiSecurity.maxDownstreamBodyBytes
|
|
109
|
+
|
|
110
|
+
const mime = extractMimeType(res.headers?.['content-type'])
|
|
111
|
+
if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
|
|
112
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE)
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const declaredContentLength = parseContentLengthHeader(res.headers?.['content-length'])
|
|
117
|
+
if (declaredContentLength == null || declaredContentLength === 0) {
|
|
118
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING)
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (declaredContentLength > maxBytes) {
|
|
123
|
+
recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG)
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
88
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Probabilistic gate for downstream response body capture (rate + per-request cap).
|
|
132
|
+
* Only used from {@link planResponseBodyCollection}; does not increment {@link bodyAnalysisCount}.
|
|
133
|
+
* @param {import('http').IncomingMessage} req originating server request.
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function shouldSampleBody (req) {
|
|
89
137
|
globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
|
|
90
138
|
|
|
91
139
|
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
@@ -97,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
|
|
|
97
145
|
// Replace 1000n with the accuraccy that we want to maintain
|
|
98
146
|
const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
|
|
99
147
|
|
|
100
|
-
|
|
148
|
+
return hashed <= threshold
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {import('http').IncomingMessage} res downstream HTTP response.
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
function isRedirectResponse (res) {
|
|
156
|
+
const location = res.headers?.location || ''
|
|
157
|
+
return res.statusCode >= 300 && res.statusCode < 400 && !!location
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Plans downstream response body capture on the instrumentation ctx when response headers arrive.
|
|
162
|
+
* Redirect responses (3xx + Location) are ignored; each outbound hop is evaluated independently
|
|
163
|
+
* when its own non-redirect response arrives.
|
|
164
|
+
* @param {import('http').IncomingMessage} originatingReq incoming server request.
|
|
165
|
+
* @param {import('http').IncomingMessage} res downstream response.
|
|
166
|
+
* @param {object} ctx http client instrumentation context (mutated).
|
|
167
|
+
*/
|
|
168
|
+
function planResponseBodyCollection (originatingReq, res, ctx) {
|
|
169
|
+
if (!config?.appsec.apiSecurity) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
101
172
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
incrementBodyAnalysisCount(req)
|
|
173
|
+
if (isRedirectResponse(res)) {
|
|
174
|
+
return
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
|
|
177
|
+
if (!shouldSampleBody(originatingReq)) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (evaluateResponseBodyCollection(originatingReq, res)) {
|
|
182
|
+
ctx.shouldCollectBody = true
|
|
183
|
+
incrementBodyAnalysisCount(originatingReq)
|
|
184
|
+
}
|
|
108
185
|
}
|
|
109
186
|
|
|
110
187
|
/**
|
|
@@ -145,25 +222,6 @@ function extractRequestData (ctx) {
|
|
|
145
222
|
return addresses
|
|
146
223
|
}
|
|
147
224
|
|
|
148
|
-
/**
|
|
149
|
-
* Checks if a response is a redirect
|
|
150
|
-
* @param {import('http').IncomingMessage} req incoming server request.
|
|
151
|
-
* @param {import('http').IncomingMessage} res downstream response object.
|
|
152
|
-
* @returns {boolean} is redirect.
|
|
153
|
-
*/
|
|
154
|
-
function handleRedirectResponse (req, res) {
|
|
155
|
-
const isRedirect = res.statusCode >= 300 && res.statusCode < 400
|
|
156
|
-
const redirectLocation = res.headers?.location || ''
|
|
157
|
-
|
|
158
|
-
if (isRedirect && redirectLocation) {
|
|
159
|
-
// Store the body collection decision for the redirect target
|
|
160
|
-
storeRedirectBodyCollectionDecision(req, redirectLocation)
|
|
161
|
-
return true
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return false
|
|
165
|
-
}
|
|
166
|
-
|
|
167
225
|
/**
|
|
168
226
|
* Extracts response data for WAF analysis.
|
|
169
227
|
* @param {import('http').IncomingMessage} res downstream response object.
|
|
@@ -291,13 +349,8 @@ function extractMimeType (contentType) {
|
|
|
291
349
|
module.exports = {
|
|
292
350
|
enable,
|
|
293
351
|
disable,
|
|
294
|
-
|
|
295
|
-
handleRedirectResponse,
|
|
352
|
+
planResponseBodyCollection,
|
|
296
353
|
incrementDownstreamAnalysisCount,
|
|
297
354
|
extractRequestData,
|
|
298
355
|
extractResponseData,
|
|
299
|
-
// exports for tests
|
|
300
|
-
parseBody,
|
|
301
|
-
getMethod,
|
|
302
|
-
storeRedirectBodyCollectionDecision,
|
|
303
356
|
}
|
|
@@ -2,24 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const OPEN_PAREN = 0x28
|
|
6
|
+
const CLOSE_PAREN = 0x29
|
|
7
|
+
const EQUALS = 0x3D
|
|
8
|
+
const TILDE = 0x7E
|
|
9
|
+
const LESS = 0x3C
|
|
10
|
+
const GREATER = 0x3E
|
|
7
11
|
|
|
12
|
+
// Linear scanner for LDAP assertion-filter values. For each parenthesised group
|
|
13
|
+
// "(attr <op> value)" where <op> is "=", "~=", "<=", or ">=" and no nested
|
|
14
|
+
// parenthesis appears before the operator, report the value range [opEnd, ')').
|
|
15
|
+
// The cursor only ever moves forward, so total work is O(input length).
|
|
8
16
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
9
17
|
try {
|
|
10
|
-
|
|
18
|
+
const value = evidence?.value
|
|
11
19
|
const tokens = []
|
|
20
|
+
if (typeof value !== 'string') return tokens
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const length = value.length
|
|
23
|
+
let cursor = 0
|
|
24
|
+
|
|
25
|
+
while (cursor < length) {
|
|
26
|
+
const open = value.indexOf('(', cursor)
|
|
27
|
+
if (open === -1) break
|
|
28
|
+
|
|
29
|
+
let scan = open + 1
|
|
30
|
+
let opStart = -1
|
|
31
|
+
let opLen = 0
|
|
32
|
+
|
|
33
|
+
while (scan < length) {
|
|
34
|
+
const code = value.charCodeAt(scan)
|
|
35
|
+
if (code === OPEN_PAREN || code === CLOSE_PAREN) break
|
|
36
|
+
if (code === EQUALS) {
|
|
37
|
+
opStart = scan
|
|
38
|
+
opLen = 1
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
if ((code === TILDE || code === LESS || code === GREATER) &&
|
|
42
|
+
scan + 1 < length && value.charCodeAt(scan + 1) === EQUALS) {
|
|
43
|
+
opStart = scan
|
|
44
|
+
opLen = 2
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
scan++
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (opStart === -1) {
|
|
51
|
+
cursor = open + 1
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const close = value.indexOf(')', opStart + opLen)
|
|
56
|
+
if (close === -1) break
|
|
57
|
+
|
|
58
|
+
const start = opStart + opLen
|
|
59
|
+
if (start < close) {
|
|
60
|
+
tokens.push({ start, end: close })
|
|
61
|
+
}
|
|
62
|
+
cursor = close + 1
|
|
22
63
|
}
|
|
64
|
+
|
|
23
65
|
return tokens
|
|
24
66
|
} catch (e) {
|
|
25
67
|
log.debug('[ASM] Error extracting sensitive ranges', e)
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
5
|
const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@'
|
|
6
|
-
|
|
6
|
+
// The key class excludes `?` and `#` so the greedy quantifier is bounded per fragment.
|
|
7
|
+
// Query keys cannot legitimately contain those characters (they delimit query/fragment
|
|
8
|
+
// boundaries), so excluding them preserves match semantics for valid URLs while keeping
|
|
9
|
+
// the regex linear on arbitrary input.
|
|
10
|
+
const QUERY_FRAGMENT = '[?#&]([^=&;?#]+)=([^?#&]+)'
|
|
7
11
|
const pattern = new RegExp([AUTHORITY, QUERY_FRAGMENT].join('|'), 'gmi')
|
|
8
12
|
|
|
9
13
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
@@ -17,6 +17,11 @@ const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyz
|
|
|
17
17
|
|
|
18
18
|
const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
19
19
|
|
|
20
|
+
// Upper bound on the evidence-string length the redaction analyzers will scan. Oversized
|
|
21
|
+
// values bypass the analyzer entirely and are emitted as a fully-redacted placeholder.
|
|
22
|
+
// Counted in JS string characters (UTF-16 code units), not bytes.
|
|
23
|
+
const MAX_EVIDENCE_LENGTH = 32_768
|
|
24
|
+
|
|
20
25
|
class SensitiveHandler {
|
|
21
26
|
constructor () {
|
|
22
27
|
this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
|
|
@@ -53,11 +58,31 @@ class SensitiveHandler {
|
|
|
53
58
|
|
|
54
59
|
scrubEvidence (vulnerabilityType, evidence, sourcesIndexes, sources) {
|
|
55
60
|
const sensitiveAnalyzer = this._sensitiveAnalyzers.get(vulnerabilityType)
|
|
56
|
-
if (sensitiveAnalyzer) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
if (!sensitiveAnalyzer) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Oversized evidence: skip the analyzer and emit a fully-redacted placeholder. Mark
|
|
66
|
+
// every source backing this vulnerability as redacted so the formatter strips their
|
|
67
|
+
// raw `value` before they leave the process.
|
|
68
|
+
if (typeof evidence.value === 'string' && evidence.value.length > MAX_EVIDENCE_LENGTH) {
|
|
69
|
+
const redactedSources = []
|
|
70
|
+
for (const sourceIndex of sourcesIndexes) {
|
|
71
|
+
const source = sources[sourceIndex]
|
|
72
|
+
if (source && !source.redacted) {
|
|
73
|
+
source.pattern = ''.padEnd(source.value.length, REDACTED_SOURCE_BUFFER)
|
|
74
|
+
source.redacted = true
|
|
75
|
+
}
|
|
76
|
+
if (!redactedSources.includes(sourceIndex)) {
|
|
77
|
+
redactedSources.push(sourceIndex)
|
|
78
|
+
}
|
|
60
79
|
}
|
|
80
|
+
return { redactedValueParts: [{ redacted: true }], redactedSources }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sensitiveRanges = sensitiveAnalyzer(evidence)
|
|
84
|
+
if (evidence.ranges || sensitiveRanges?.length) {
|
|
85
|
+
return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
|
|
61
86
|
}
|
|
62
87
|
return null
|
|
63
88
|
}
|
|
@@ -38,7 +38,7 @@ const waf = require('./waf')
|
|
|
38
38
|
const addresses = require('./addresses')
|
|
39
39
|
const Reporter = require('./reporter')
|
|
40
40
|
const appsecTelemetry = require('./telemetry')
|
|
41
|
-
const
|
|
41
|
+
const apiSecurity = require('./api_security')
|
|
42
42
|
const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking')
|
|
43
43
|
const { getActiveRequest } = require('./store')
|
|
44
44
|
const UserTracking = require('./user_tracking')
|
|
@@ -71,7 +71,7 @@ function enable (_config) {
|
|
|
71
71
|
|
|
72
72
|
Reporter.init(_config.appsec, _config.inferredProxyServicesEnabled)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
apiSecurity.configure(_config)
|
|
75
75
|
|
|
76
76
|
UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false)
|
|
77
77
|
|
|
@@ -233,14 +233,18 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
233
233
|
// This hook runs before span finish, so ensure route/endpoint tags are available before API Security sampling runs.
|
|
234
234
|
web.setRouteOrEndpointTag(req)
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
const apiSecSamplingDecision = apiSecurity.sampleRequest(req, res, true)
|
|
237
|
+
if (apiSecSamplingDecision === apiSecurity.SamplingDecision.SAMPLE) {
|
|
237
238
|
persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true }
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
let wafResult
|
|
240
242
|
if (!isEmpty(persistent)) {
|
|
241
|
-
waf.run({ persistent }, req)
|
|
243
|
+
wafResult = waf.run({ persistent }, req)
|
|
242
244
|
}
|
|
243
245
|
|
|
246
|
+
apiSecurity.reportRequest(req, apiSecSamplingDecision, wafResult)
|
|
247
|
+
|
|
244
248
|
waf.disposeContext(req)
|
|
245
249
|
|
|
246
250
|
const storedHeaders = storedResponseHeaders.get(req) || {}
|
|
@@ -339,7 +343,7 @@ function onRequestProcessParams ({ req, res, abortController, params }) {
|
|
|
339
343
|
|
|
340
344
|
function onResponseBody ({ req, res, body }) {
|
|
341
345
|
if (!body || typeof body !== 'object') return
|
|
342
|
-
if (
|
|
346
|
+
if (apiSecurity.sampleRequest(req, res) !== apiSecurity.SamplingDecision.SAMPLE) return
|
|
343
347
|
|
|
344
348
|
// we don't support blocking at this point, so no results needed
|
|
345
349
|
waf.run({
|
|
@@ -513,7 +517,7 @@ function disable () {
|
|
|
513
517
|
|
|
514
518
|
appsecRemoteConfig.disableWafUpdate()
|
|
515
519
|
|
|
516
|
-
|
|
520
|
+
apiSecurity.disable()
|
|
517
521
|
|
|
518
522
|
// Channel#unsubscribe() is undefined for non active channels
|
|
519
523
|
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { format } = require('url')
|
|
4
4
|
const {
|
|
5
5
|
httpClientRequestStart,
|
|
6
|
+
httpClientResponseStart,
|
|
6
7
|
httpClientResponseFinish,
|
|
7
8
|
} = require('../channels')
|
|
8
9
|
const addresses = require('../addresses')
|
|
@@ -21,6 +22,7 @@ function enable (_config) {
|
|
|
21
22
|
downstream.enable(_config)
|
|
22
23
|
|
|
23
24
|
httpClientRequestStart.subscribe(analyzeSsrf)
|
|
25
|
+
httpClientResponseStart.subscribe(planResponseBodyCollection)
|
|
24
26
|
httpClientResponseFinish.subscribe(handleResponseFinish)
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -28,6 +30,7 @@ function disable () {
|
|
|
28
30
|
downstream.disable()
|
|
29
31
|
|
|
30
32
|
if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
|
|
33
|
+
if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
|
|
31
34
|
if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -37,9 +40,6 @@ function analyzeSsrf (ctx) {
|
|
|
37
40
|
|
|
38
41
|
if (!req || !outgoingUrl) return
|
|
39
42
|
|
|
40
|
-
// Determine if we should collect the response body based on sampling rate and redirect URL
|
|
41
|
-
ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
|
|
42
|
-
|
|
43
43
|
const requestAddresses = downstream.extractRequestData(ctx)
|
|
44
44
|
|
|
45
45
|
const ephemeral = {
|
|
@@ -56,13 +56,23 @@ function analyzeSsrf (ctx) {
|
|
|
56
56
|
downstream.incrementDownstreamAnalysisCount(req)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Channel handler: plans downstream response body capture once response headers are available.
|
|
61
|
+
* @param {{ ctx: object, res: import('http').IncomingMessage }} payload channel payload.
|
|
62
|
+
*/
|
|
63
|
+
function planResponseBodyCollection ({ ctx, res }) {
|
|
64
|
+
const originatingRequest = getActiveRequest()
|
|
65
|
+
if (!originatingRequest || !res) return
|
|
66
|
+
|
|
67
|
+
downstream.planResponseBodyCollection(originatingRequest, res, ctx)
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Finalizes body collection for the response and triggers RASP analysis.
|
|
61
|
-
* @param {
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* }} payload event payload from the channel.
|
|
72
|
+
* @param {object} params event payload from the channel.
|
|
73
|
+
* @param {object} params.ctx instrumentation context.
|
|
74
|
+
* @param {import('http').IncomingMessage} params.res downstream response.
|
|
75
|
+
* @param {string|Buffer|null} params.body collected body.
|
|
66
76
|
*/
|
|
67
77
|
function handleResponseFinish ({ ctx, res, body }) {
|
|
68
78
|
// downstream response object
|
|
@@ -71,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
|
|
|
71
81
|
const originatingRequest = getActiveRequest()
|
|
72
82
|
if (!originatingRequest) return
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
|
|
76
|
-
const responseBody = evaluateBody ? body : null
|
|
84
|
+
const responseBody = ctx.shouldCollectBody ? body : null
|
|
77
85
|
runResponseEvaluation(res, originatingRequest, responseBody)
|
|
78
86
|
}
|
|
79
87
|
|