forked from DataDog/dd-trace-js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloader-hook.mjs
More file actions
196 lines (161 loc) · 7.94 KB
/
Copy pathloader-hook.mjs
File metadata and controls
196 lines (161 loc) · 7.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/* eslint n/no-unsupported-features/node-builtins: ['error', { ignores: ['module.registerHooks'] }] */
import * as Module from 'node:module'
import { pathToFileURL } from 'node:url'
import { isMainThread } from 'node:worker_threads'
import { createHook, supportsSyncHooks } from 'import-in-the-middle/create-hook.mjs'
import { initialize as origInitialize, load as origLoad } from 'import-in-the-middle/hook.mjs'
import * as rewriterLoader from './packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs'
// This file must support Node.js 12.0.0 syntax
const { builtinModules } = Module
const require = Module.createRequire(import.meta.url)
// The query marks initialize.mjs's application-thread preload; loader workers use the full graph.
const isInitializeMainThread = isMainThread && import.meta.url.endsWith('?initialize')
let syncImportInTheMiddleHook
let regexpEscape
// Substrings of resolved URLs that import-in-the-middle must never wrap: re-export
// shims and internal helper graphs that break when proxied, plus iitm's own files
// (via `middle`). One alternation so a single test() covers every excluded load.
export const iitmExclusionRegExp = /middle|langsmith|openai\/_shims|openai\/resources\/chat\/completions\/messages|openai\/agents-core\/dist\/shims|@anthropic-ai\/sdk\/_shims/
// Instrumented bare specifiers (`import 'express'`, builtins, symlinked or
// workspace packages) match against the specifier (the Set); their files inside
// node_modules match against the URL (the alternation). regexpEscape guards
// against a regex metacharacter entering a package name.
const includeModuleNames = new Set()
let moduleNameAlternation = ''
if (!isInitializeMainThread) {
require('./packages/dd-trace/src/guardrails/apply-pm2-env.js')
const regexpEscapeModule = require('./vendor/dist/escape-string-regexp/index.js')
const hooks = require('./packages/datadog-instrumentations/src/helpers/hooks.js')
const { isRelativeRequire } = require('./packages/datadog-instrumentations/src/helpers/shared-utils.js')
regexpEscape = regexpEscapeModule.default
for (const moduleName of Object.keys(hooks)) {
// Relative hooks resolve outside node_modules and are not instrumented here.
if (isRelativeRequire(moduleName)) continue
includeModuleNames.add(moduleName)
// iitm matches a built-in by its node: specifier too, so mirror that and
// wrap `import 'node:crypto'` as well as `import 'crypto'`.
if (builtinModules.includes(moduleName)) includeModuleNames.add(`node:${moduleName}`)
if (moduleNameAlternation !== '') moduleNameAlternation += '|'
moduleNameAlternation += regexpEscape(moduleName)
}
}
const nodeModulesIncludeSource = `node_modules/(?:${moduleNameAlternation})/(?!node_modules).+`
function initialize (data = {}) {
prepareImportInTheMiddleOptions(data)
return origInitialize(data)
}
function prepareImportInTheMiddleOptions (data = {}) {
// A consumer-owned shouldInclude predicate takes over the wrapping decision, so
// iitm ignores the include/exclude arrays. Building the matcher here keeps the
// synchronous and asynchronous loaders on one matching implementation.
// Lazily required and read without its registered default so a loader worker
// never loads the configuration defaults table. The default is unset regardless.
const { getValueFromEnvSources } = require('./packages/dd-trace/src/config/helper.js')
data.shouldInclude = createShouldInclude(getValueFromEnvSources('DD_IAST_SECURITY_CONTROLS_CONFIGURATION', true))
return data
}
/**
* Builds the import-in-the-middle `shouldInclude(url, specifier)` predicate. iitm
* calls it for every resolved module and wraps the module when the result is
* truthy; supplying it replaces iitm's include/exclude list scan.
*
* @param {string} [securityControlsConfig] Raw `DD_IAST_SECURITY_CONTROLS_CONFIGURATION`;
* each entry's module path is instrumented in addition to the hook table.
*/
function createShouldInclude (securityControlsConfig) {
const includeRegExp = new RegExp(buildIncludeSource(securityControlsConfig))
/**
* @param {string} url Resolved module URL (`file:`, `node:`, ...).
* @param {string} specifier Original import specifier.
*/
return function shouldInclude (url, specifier) {
return (includeModuleNames.has(specifier) || includeRegExp.test(url)) && !iitmExclusionRegExp.test(url)
}
}
/**
* Appends each `DD_IAST_SECURITY_CONTROLS_CONFIGURATION` module path — the third
* `:`-separated segment of every `;`-separated `<type>:<marks>:<module>:<...>` entry —
* to the include alternation, escaped.
*
* @param {string} [securityControlsConfig] Raw `DD_IAST_SECURITY_CONTROLS_CONFIGURATION`.
*/
function buildIncludeSource (securityControlsConfig) {
if (!securityControlsConfig) return nodeModulesIncludeSource
let includeSource = nodeModulesIncludeSource
for (const entry of securityControlsConfig.split(';')) {
if (!entry) continue
const first = entry.indexOf(':')
if (first === -1) continue
const second = entry.indexOf(':', first + 1)
if (second === -1) continue
const third = entry.indexOf(':', second + 1)
const subpath = entry.slice(second + 1, third === -1 ? undefined : third).trim()
if (subpath) includeSource += `|${regexpEscape(subpath)}`
}
return includeSource
}
function load (url, context, nextLoad) {
return rewriterLoader.load(url, context, (url, context) => origLoad(url, context, nextLoad))
}
function loadSync (url, context, nextLoad) {
if (isCommonJSLoad(context)) {
return getSyncImportInTheMiddleHook().loadSync(url, context, nextLoad)
}
return rewriterLoader.loadSync(url, context, (url, context) => {
return getSyncImportInTheMiddleHook().loadSync(url, context, nextLoad)
})
}
function isCommonJSLoad (context) {
if (context.format) return context.format === 'commonjs'
// Sync hooks report CommonJS require() dependency loads with a `require`
// condition but no format. If a format is present, trust it instead: ESM
// loaded through require() reports `format: 'module'` and still needs rewrite.
const conditions = context.conditions
if (!conditions) return false
for (let i = 0; i < conditions.length; i++) {
if (conditions[i] === 'require') return true
}
return false
}
function getSyncImportInTheMiddleHook () {
if (syncImportInTheMiddleHook) {
return syncImportInTheMiddleHook
}
const importInTheMiddleRegisterHooksUrl = pathToFileURL(
require.resolve('import-in-the-middle/register-hooks.mjs')
).href
syncImportInTheMiddleHook = createHook({ url: importInTheMiddleRegisterHooksUrl })
return syncImportInTheMiddleHook
}
function registerSyncLoaderHooks (data = {}) {
// The synchronous loader strips the source of a require() pulled into the iitm
// ESM graph so Node loads it natively, but module.registerHooks rejected that
// nullish CommonJS source until nodejs/node#59929 (released in 22.22.3, 24.11.1,
// 25.1.0 and 26.0.0). On versions that ship registerHooks but predate the fix,
// fall back to the asynchronous loader instead of crashing mid-graph.
if (!supportsSyncHooks()) {
return false
}
const syncHook = getSyncImportInTheMiddleHook()
if (
typeof Module.registerHooks !== 'function' ||
typeof syncHook.applyOptions !== 'function' ||
typeof syncHook.loadSync !== 'function' ||
typeof syncHook.resolveSync !== 'function'
) {
return false
}
// Node built-ins are instrumented under the synchronous loader as well: iitm
// reads a built-in's exports through process.getBuiltinModule(), which
// bypasses the registered hooks and therefore cannot re-enter them. The
// synchronous and asynchronous loaders share the same option preparation so
// that `import http from 'node:http'` is wrapped on both paths.
syncHook.applyOptions(prepareImportInTheMiddleOptions(data))
Module.registerHooks({
resolve: syncHook.resolveSync,
load: loadSync,
})
return true
}
export { resolve } from 'import-in-the-middle/hook.mjs'
export { initialize, load, registerSyncLoaderHooks }