@contrast/dep-hooks
Advanced tools
Comparing version 1.4.0 to 1.5.0
@@ -1,26 +0,94 @@ | ||
/* | ||
* Copyright: 2024 Contrast Security, Inc | ||
* Contact: support@contrastsecurity.com | ||
* License: Commercial | ||
* NOTICE: This Software and the patented inventions embodied within may only be | ||
* used as part of Contrast Security’s commercial offerings. Even though it is | ||
* made available through public repositories, use of this Software is subject to | ||
* the applicable End User Licensing Agreement found at | ||
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed | ||
* between Contrast Security and the End User. The Software may not be reverse | ||
* engineered, modified, repackaged, sold, redistributed or otherwise used in a | ||
* way not consistent with the End User License Agreement. | ||
declare function _exports(core: { | ||
readonly logger: import('@contrast/logger').Logger; | ||
depHooks: DepHooks; | ||
}): DepHooks; | ||
declare namespace _exports { | ||
export { DepHooks }; | ||
export { Descriptor }; | ||
} | ||
export = _exports; | ||
export type Descriptor = { | ||
/** | ||
* module name, as passed to `require`, to handle. | ||
*/ | ||
name: string; | ||
/** | ||
* alternative to `name`, taking precedent when provided. | ||
*/ | ||
module?: string | undefined; | ||
/** | ||
* if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked. | ||
*/ | ||
file?: string | undefined; | ||
/** | ||
* if provided, hooks will only execute against an installed module that matches the SemVer version range | ||
*/ | ||
version?: string | undefined; | ||
}; | ||
/** | ||
* Allows clients to register function handlers which run as a 'post-hook' at | ||
* require-time. | ||
*/ | ||
import RequireHook from '@contrast/require-hook'; // TODO: fix these types | ||
import { Logger } from '@contrast/logger'; | ||
interface Core { | ||
readonly logger: Logger; | ||
depHooks: RequireHook; | ||
declare class DepHooks { | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger?: import('pino').Logger | undefined); | ||
/** @type {import('pino').Logger=} */ | ||
logger: import('pino').Logger | undefined; | ||
originalLoad: any; | ||
/** @type {HandlerInvoker} */ | ||
invoker: HandlerInvoker; | ||
/** @type {ExportHandlerRegistry} */ | ||
registry: ExportHandlerRegistry; | ||
/** @type {WeakMap<Object, Object>} */ | ||
requiredModules: WeakMap<Object, Object>; | ||
/** @type {Set<string>} */ | ||
resets: Set<string>; | ||
/** | ||
* Registers handlers to run afer the described module is required. | ||
* @template {Object} T | ||
* @param {Descriptor | string} descriptor describes the module to hook | ||
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute after require | ||
*/ | ||
resolve<T extends Object>(descriptor: Descriptor | string, ...handlers: ExportHookDescriptor.Handler<T>[]): void; | ||
/** | ||
* Provided with an export, a collection of handlers, and metadata, will | ||
* invoke only the handlers which have not yet run on the export instance. | ||
* @template {Object} T | ||
* @param {T} xport the exported value of a required module | ||
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute on require | ||
* @param {import('./package-finder').Metadata} metadata the export's metadata | ||
* @returns {T} | ||
*/ | ||
runRequireHandlers<T_1 extends Object>(xport: T_1, handlers: ExportHookDescriptor.Handler<T_1>[], metadata: import('./package-finder').Metadata): T_1; | ||
/** | ||
* Checks if module name exists in resets set. If so, it will remove it from | ||
* the set as well as remove it from the invoker WeakMap. This will force | ||
* instrumentation handlers to re-run. This use case is only used for testing | ||
* of the node agent in certain cases. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {T} xport the exported value of a required module | ||
*/ | ||
maybeClearHandlers<T_2 extends Object>(request: string, xport: T_2): void; | ||
/** | ||
* Overrides the Module._load method to run registered handlers _after_ | ||
* the modules have loaded. This method is invoked by require. | ||
*/ | ||
install(): void; | ||
/** | ||
* Resets Module's _load method to the original value. | ||
*/ | ||
uninstall(): void; | ||
/** | ||
* Resets the seen handlers for a given module - they will be re-run on next | ||
* require. | ||
* @param {string} request the string passed to require() | ||
*/ | ||
reset(request: string): void; | ||
} | ||
declare function init(core: Core): RequireHook; | ||
export = init; | ||
import HandlerInvoker = require("./handler-invoker"); | ||
import ExportHandlerRegistry = require("./export-handler-registry"); | ||
import ExportHookDescriptor = require("./export-hook-descriptor"); | ||
//# sourceMappingURL=index.d.ts.map |
153
lib/index.js
@@ -15,13 +15,148 @@ /* | ||
*/ | ||
// @ts-check | ||
'use strict'; | ||
const RequireHook = require('@contrast/require-hook'); | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Module = require('node:module'); | ||
const ExportHandlerRegistry = require('./export-handler-registry'); | ||
const ExportHookDescriptor = require('./export-hook-descriptor'); | ||
const HandlerInvoker = require('./handler-invoker'); | ||
/** | ||
* @typedef {Object} Descriptor | ||
* @property {string} name module name, as passed to `require`, to handle. | ||
* @property {string=} module alternative to `name`, taking precedent when provided. | ||
* @property {string=} file if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked. | ||
* @property {string=} version if provided, hooks will only execute against an installed module that matches the SemVer version range | ||
*/ | ||
/** | ||
* Coerces a string into a minimal object that can be made into a descriptor. | ||
* @param {Descriptor | string} descriptor The export descriptor | ||
* @returns {Descriptor} | ||
*/ | ||
const normalizeDescriptor = (descriptor) => { | ||
if (typeof descriptor === 'string') { | ||
return { name: descriptor }; | ||
} | ||
if (descriptor.module) { | ||
descriptor.name = descriptor.module; | ||
} | ||
return descriptor; | ||
}; | ||
/** | ||
* Allows clients to register function handlers which run as a 'post-hook' at | ||
* require-time. | ||
*/ | ||
class DepHooks { | ||
/** | ||
* @param {import('pino').Logger=} logger | ||
*/ | ||
constructor(logger) { | ||
/** @type {import('pino').Logger=} */ | ||
this.logger = logger; | ||
// set this up before we start patching Module methods | ||
this.originalLoad = Reflect.get(Module, '_load'); | ||
/** @type {HandlerInvoker} */ | ||
this.invoker = new HandlerInvoker(logger); | ||
/** @type {ExportHandlerRegistry} */ | ||
this.registry = new ExportHandlerRegistry(logger); | ||
/** @type {WeakMap<Object, Object>} */ | ||
this.requiredModules = new WeakMap(); | ||
/** @type {Set<string>} */ | ||
this.resets = new Set(); | ||
} | ||
/** | ||
* Registers handlers to run afer the described module is required. | ||
* @template {Object} T | ||
* @param {Descriptor | string} descriptor describes the module to hook | ||
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute after require | ||
*/ | ||
resolve(descriptor, ...handlers) { | ||
const normalized = normalizeDescriptor(descriptor); | ||
const info = ExportHookDescriptor.create({ ...normalized, handlers }); | ||
this.registry.update(info); | ||
} | ||
/** | ||
* Provided with an export, a collection of handlers, and metadata, will | ||
* invoke only the handlers which have not yet run on the export instance. | ||
* @template {Object} T | ||
* @param {T} xport the exported value of a required module | ||
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute on require | ||
* @param {import('./package-finder').Metadata} metadata the export's metadata | ||
* @returns {T} | ||
*/ | ||
runRequireHandlers(xport, handlers, metadata) { | ||
return this.invoker.invoke(xport, handlers, metadata); | ||
} | ||
/** | ||
* Checks if module name exists in resets set. If so, it will remove it from | ||
* the set as well as remove it from the invoker WeakMap. This will force | ||
* instrumentation handlers to re-run. This use case is only used for testing | ||
* of the node agent in certain cases. | ||
* @template {Object} T | ||
* @param {string} request the string passed to require() | ||
* @param {T} xport the exported value of a required module | ||
*/ | ||
maybeClearHandlers(request, xport) { | ||
if (this.resets.has(request)) { | ||
this.resets.delete(request); | ||
this.invoker.reset(xport); | ||
} | ||
} | ||
/** | ||
* Overrides the Module._load method to run registered handlers _after_ | ||
* the modules have loaded. This method is invoked by require. | ||
*/ | ||
install() { | ||
this.logger?.trace('Applying Module._load override'); | ||
const self = this; | ||
/** | ||
* @this {Module} | ||
* @param {string} request the string passed to require() | ||
* @param {Module} parent the module executing require() | ||
* @param {boolean} isMain indicates whether the module executing require() is the entry point | ||
*/ | ||
const __loadOverride = function __loadOverride(request, parent, isMain) { | ||
let xportSubstitution; | ||
const exportHandlerInfo = self.registry.query(request, parent, isMain); | ||
const xport = Reflect.apply(self.originalLoad, this, [ | ||
request, | ||
parent, | ||
isMain, | ||
]); | ||
if (exportHandlerInfo) { | ||
self.maybeClearHandlers(request, xport); | ||
xportSubstitution = self.runRequireHandlers(self.requiredModules.get(xport) ?? xport, exportHandlerInfo.handlers, exportHandlerInfo.metadata); | ||
self.requiredModules.set(xport, xportSubstitution); | ||
} | ||
return self.requiredModules.get(xport) ?? xport; | ||
}; | ||
Reflect.set(Module, '_load', __loadOverride); | ||
} | ||
/** | ||
* Resets Module's _load method to the original value. | ||
*/ | ||
uninstall() { | ||
this.logger?.trace('Removing Module._load override'); | ||
Reflect.set(Module, '_load', this.originalLoad); | ||
} | ||
/** | ||
* Resets the seen handlers for a given module - they will be re-run on next | ||
* require. | ||
* @param {string} request the string passed to require() | ||
*/ | ||
reset(request) { | ||
this.resets.add(request); | ||
} | ||
} | ||
/** | ||
* @param {{ | ||
* readonly logger: import('@contrast/logger').Logger; | ||
* depHooks: DepHooks; | ||
* }} core | ||
* @returns {DepHooks} | ||
*/ | ||
module.exports = function init(core) { | ||
core.depHooks = new RequireHook( | ||
core.logger.child({ name: 'contrast:dep-hooks' }) | ||
); | ||
return core.depHooks; | ||
core.depHooks = new DepHooks(core.logger.child({ name: 'contrast:dep-hooks' })); | ||
return core.depHooks; | ||
}; | ||
module.exports.DepHooks = DepHooks; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@contrast/dep-hooks", | ||
"version": "1.4.0", | ||
"description": "Wrapper around the Contrast require-hook library", | ||
"version": "1.5.0", | ||
"description": "Post hooks for Module.prototype.require", | ||
"license": "SEE LICENSE IN LICENSE", | ||
@@ -13,12 +13,14 @@ "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)", | ||
"engines": { | ||
"npm": ">=6.13.7 <7 || >= 8.3.1", | ||
"node": ">= 16.9.1" | ||
"npm": ">=6.13.7 <7 || >=8.3.1", | ||
"node": ">=16.9.1" | ||
}, | ||
"scripts": { | ||
"build": "tsc --build src/", | ||
"test": "../scripts/test.sh" | ||
}, | ||
"dependencies": { | ||
"@contrast/logger": "1.9.0", | ||
"@contrast/require-hook": "^5.0.0" | ||
"@contrast/find-package-json": "^1.1.0", | ||
"@contrast/logger": "1.10.0", | ||
"semver": "^7.6.3" | ||
} | ||
} |
102
README.md
@@ -1,18 +0,94 @@ | ||
# `@contrast/dep-hooks` | ||
# @contrast/dep-hooks | ||
Register hooks for module loads. | ||
Intercept calls to `require` in order to modify or replace exports. | ||
This provides a factory wrapper around `@contrast/require-hook`. | ||
## Usage | ||
RequireHook class exposes the following methods: | ||
### Class: `DepHooks` | ||
`resolve(descriptor, ...handlers)` - Registers handlers to run after the described | ||
module is required. | ||
* descriptor:String - descriptor that describes the module to hook | ||
* handlers:Function - functions that are executed after the require | ||
#### Instantiation | ||
`reset(request)` - Resets the seen handlers for a given module - they will be re-run | ||
on next require. | ||
* request:String - request the string passed to require() | ||
[https://github.com/Contrast-Security-Inc/node-require-hook]() | ||
```javascript | ||
const DepHooks = require('./lib'); | ||
const depHooks = new DepHooks(); | ||
``` | ||
The `DepHooks` constructor accepts a [`pino`](https://github.com/pinojs/pino) | ||
logger as an argument. | ||
#### `.resolve(descriptor, ...handlers)` | ||
Options: | ||
- `descriptor`: This can be a string or an object describing the module you want | ||
to intercept. If a string is used, or if the version field of the descriptor | ||
isn't set, all versions of the described module will be matched. Descriptors | ||
can have a `name`, `version`, and `file` property. | ||
- `handlers`: The remaning arguments are the handlers which will be invoked when | ||
the described module is `require`'d. Each handler is passed the exported | ||
module and metadata including the module's root directory and its name and | ||
version as seen in its `package.json` file. If a handler returns a truthy | ||
value, then that value will replace the return value of `require`. | ||
_**Note:**_ Registered handlers run _once_ per unique instance of an export | ||
matching a descriptor. | ||
#### `.install()` | ||
This will monkey-patch `Module.prototype.require` so that exports can be | ||
intercepted. The monkey-patching will only happen once regardless of how many | ||
times this is invoked. | ||
#### `.uninstall()` | ||
This will reset `Module.prototype.require` to its value before being | ||
monkey-patched by the instance. | ||
## Examples | ||
**Use case:** For `express` versions greater than or equal to 4, intercept the | ||
export of the package's `lib/view.js` file (relative to the package's base | ||
directory) and apply a tag to the exported function. | ||
```javascript | ||
const DepHooks = require('./lib'); | ||
const depHooks = new DepHooks(); | ||
depHooks.resolve( | ||
{ | ||
name: 'express', | ||
version: '>=4', | ||
file: 'lib/view.js', | ||
}, | ||
(xport, metadata) => { | ||
// Read from the package.json: | ||
// - metadata.name | ||
// - metadata.version | ||
// Absolute path to file: | ||
// - metadata.packageDir | ||
// xport === function View() { /*...*/ } | ||
xport['I was intercepted'] = true; | ||
}, | ||
); | ||
``` | ||
**Use case:** Intercept all versions of `body-parser` and replace the exported | ||
functions. | ||
```javascript | ||
const DepHooks = require('./lib'); | ||
const depHooks = new DepHooks(); | ||
depHooks.resolve({ name: 'body-parser' }, (xport, metadata) => { | ||
// Read from the package.json: | ||
// - metadata.name | ||
// - metadata.version | ||
// Absolute path to file: | ||
// - metadata.packageDir | ||
// xport === function bodyParser() { /*...*/ } | ||
return function bodyParserReplacement() { | ||
/*...*/ | ||
}; | ||
}); | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
110084
28
991
95
3
2
1
+ Addedsemver@^7.6.3
+ Added@contrast/config@1.33.0(transitive)
+ Added@contrast/logger@1.10.0(transitive)
+ Addedyaml@2.6.0(transitive)
- Removed@contrast/require-hook@^5.0.0
- Removed@contrast/config@1.32.0(transitive)
- Removed@contrast/logger@1.9.0(transitive)
- Removed@contrast/require-hook@5.1.0(transitive)
- Removedyaml@2.6.1(transitive)
Updated@contrast/logger@1.10.0