@contrast/route-coverage 1.52.0 → 1.53.1
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/LICENSE +1 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +27 -4
- package/lib/install/express.js +2 -4
- package/lib/install/fastify/fastify-express.js +53 -49
- package/lib/install/fastify/fastify-middie.js +6 -6
- package/lib/install/fastify/fastify.js +5 -4
- package/lib/install/fastify/index.js +1 -1
- package/lib/install/graphql.js +1 -1
- package/lib/install/hapi.js +128 -64
- package/lib/install/koa.js +3 -3
- package/lib/install/restify.js +227 -45
- package/lib/install/socket.io.js +1 -1
- package/lib/utils/methods.js +1 -1
- package/lib/utils/route-info.js +2 -27
- package/package.json +9 -8
package/LICENSE
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -18,17 +18,20 @@ import { Config } from '@contrast/config';
|
|
|
18
18
|
import { DepHooks } from '@contrast/dep-hooks';
|
|
19
19
|
import { Logger } from '@contrast/logger';
|
|
20
20
|
import { Patcher } from '@contrast/patcher';
|
|
21
|
+
import { Rewriter } from '@contrast/rewriter';
|
|
21
22
|
import { Scopes } from '@contrast/scopes';
|
|
22
23
|
|
|
23
24
|
export { RouteInfo };
|
|
24
25
|
|
|
25
26
|
export interface RouteCoverage extends Installable {
|
|
27
|
+
MAX_FILE_LENGTH: number,
|
|
26
28
|
DISCOVERY_QUEUE_EMPTY_MS: number;
|
|
27
29
|
discover(info: RouteInfo): void;
|
|
28
30
|
discoveryFinished(): void;
|
|
29
31
|
queue(info: RouteInfo): void;
|
|
30
32
|
queuingFinished(): void;
|
|
31
33
|
observe(info: RouteInfo): void;
|
|
34
|
+
formatHandlerSync(fn: Function, appDir?: string) : string;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface Core {
|
|
@@ -37,6 +40,7 @@ export interface Core {
|
|
|
37
40
|
readonly logger: Logger;
|
|
38
41
|
readonly messages: Messages;
|
|
39
42
|
readonly patcher: Patcher;
|
|
43
|
+
readonly rewriter: Rewriter,
|
|
40
44
|
readonly scopes: Scopes;
|
|
41
45
|
initComponentSync(c: any): void;
|
|
42
46
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
callChildComponentMethodsSync,
|
|
20
20
|
Event,
|
|
21
21
|
RouteType,
|
|
22
|
+
primordials: {
|
|
23
|
+
StringPrototypeSubstring,
|
|
24
|
+
StringPrototypeReplace,
|
|
25
|
+
}
|
|
22
26
|
} = require('@contrast/common');
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -41,6 +45,7 @@ module.exports = function init(core) {
|
|
|
41
45
|
const routeIdentifier = (method, signature) => `${method}.${signature}`;
|
|
42
46
|
|
|
43
47
|
const routeCoverage = core.routeCoverage = {
|
|
48
|
+
MAX_FILE_LENGTH: 40,
|
|
44
49
|
DISCOVERY_QUEUE_EMPTY_MS: 10_000,
|
|
45
50
|
discover(info) {
|
|
46
51
|
const id = routeIdentifier(info.method, info.signature);
|
|
@@ -120,14 +125,32 @@ module.exports = function init(core) {
|
|
|
120
125
|
callChildComponentMethodsSync(this, 'install');
|
|
121
126
|
setInterval(() => recentlyObserved.clear(), 10000).unref();
|
|
122
127
|
},
|
|
128
|
+
|
|
129
|
+
formatHandlerSync(handler, appDir) {
|
|
130
|
+
const info = core.rewriter.funcInfoSync(handler);
|
|
131
|
+
if (!info) return '[Function]';
|
|
132
|
+
|
|
133
|
+
let file = info.file ?
|
|
134
|
+
StringPrototypeReplace.call(info.file, appDir, '') :
|
|
135
|
+
'';
|
|
136
|
+
|
|
137
|
+
if (file.length > this.MAX_FILE_LENGTH) {
|
|
138
|
+
file = `...${StringPrototypeSubstring.call(file, file.length - this.MAX_FILE_LENGTH)}`;
|
|
139
|
+
}
|
|
140
|
+
const handlerName = info.method || handler.name || 'anonymous';
|
|
141
|
+
const formattedHandler = (file && Number.isFinite(info.lineNumber) && Number.isFinite(info.column)) ?
|
|
142
|
+
`[${handlerName} ${file} ${info.lineNumber}:${info.column}]` :
|
|
143
|
+
`[Function: ${handlerName}]`; // what util.inspect(handler) would return
|
|
144
|
+
return formattedHandler;
|
|
145
|
+
}
|
|
123
146
|
};
|
|
124
147
|
|
|
125
|
-
require('./install/express')
|
|
148
|
+
core.initComponentSync(require('./install/express'));
|
|
126
149
|
require('./install/fastify')(core);
|
|
127
150
|
require('./install/graphql')(core);
|
|
128
|
-
require('./install/hapi')
|
|
151
|
+
core.initComponentSync(require('./install/hapi'));
|
|
129
152
|
require('./install/koa')(core);
|
|
130
|
-
require('./install/restify')
|
|
153
|
+
core.initComponentSync(require('./install/restify'));
|
|
131
154
|
core.initComponentSync(require('./install/socket.io'));
|
|
132
155
|
|
|
133
156
|
messages.on(Event.SERVER_LISTENING, () => {
|
package/lib/install/express.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -30,7 +30,6 @@ const {
|
|
|
30
30
|
}
|
|
31
31
|
} = require('@contrast/common');
|
|
32
32
|
const Core = require('@contrast/core/lib/ioc/core');
|
|
33
|
-
const { formatHandler } = require('../utils/route-info');
|
|
34
33
|
|
|
35
34
|
const METHODS = [
|
|
36
35
|
'all',
|
|
@@ -88,7 +87,6 @@ class ExpressInstrumentation {
|
|
|
88
87
|
|
|
89
88
|
core.messages.on(Event.SERVER_LISTENING, () => {
|
|
90
89
|
let router;
|
|
91
|
-
|
|
92
90
|
self.listenFlag = true;
|
|
93
91
|
|
|
94
92
|
try {
|
|
@@ -482,7 +480,7 @@ class ExpressInstrumentation {
|
|
|
482
480
|
}
|
|
483
481
|
let template = ArrayPrototypeJoin.call(templates, '');
|
|
484
482
|
if (template == '') template = '/';
|
|
485
|
-
const signature = `${type}.${method}(${template}, ${
|
|
483
|
+
const signature = `${type}.${method}(${template}, ${core.routeCoverage.formatHandlerSync(handler)})`;
|
|
486
484
|
|
|
487
485
|
// this gets merged into meta.observables if same route handler is mounted at multiple paths
|
|
488
486
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -15,57 +15,61 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { RouteType } = require('@contrast/common');
|
|
18
|
-
const {
|
|
19
|
-
const
|
|
20
|
-
module.exports = function init(core) {
|
|
21
|
-
const { patcher, depHooks, routeCoverage, scopes } = core;
|
|
18
|
+
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
19
|
+
const { patchType } = require('./../../utils/route-info');
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
name,
|
|
28
|
-
patchType,
|
|
29
|
-
post(data) {
|
|
30
|
-
const store = { lock: true, name };
|
|
31
|
-
patcher.patch(data.args[0], 'use', {
|
|
32
|
-
name: 'use',
|
|
33
|
-
patchType,
|
|
34
|
-
around(next, data) {
|
|
35
|
-
const [url, fn] = data.args;
|
|
36
|
-
if (!url || !fn || !core.config.getEffectiveValue('assess.report_middleware_routes')) return next();
|
|
21
|
+
module.exports = Core.makeComponent({
|
|
22
|
+
name: 'core.routeCoverage.fastifyExpress',
|
|
23
|
+
factory: function init(core) {
|
|
24
|
+
const { patcher, depHooks, routeCoverage, scopes } = core;
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
return core.routeCoverage.fastifyExpress = {
|
|
27
|
+
install() {
|
|
28
|
+
const name = 'fastifyExpress';
|
|
29
|
+
depHooks.resolve({ name: '@fastify/express', version: '*' }, (_xport) => patcher.patch(_xport, {
|
|
30
|
+
name,
|
|
31
|
+
patchType,
|
|
32
|
+
post(data) {
|
|
33
|
+
const store = { lock: true, name };
|
|
34
|
+
patcher.patch(data.args[0], 'use', {
|
|
35
|
+
name: 'use',
|
|
36
|
+
patchType,
|
|
37
|
+
around(next, data) {
|
|
38
|
+
const [url, fn] = data.args;
|
|
39
|
+
if (!url || !fn || !core.config.getEffectiveValue('assess.report_middleware_routes')) return next();
|
|
43
40
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
type: RouteType.MIDDLEWARE,
|
|
50
|
-
framework: 'fastify'
|
|
51
|
-
};
|
|
52
|
-
routeCoverage.discover(routeInfo);
|
|
41
|
+
const middleware = Array.isArray(fn) ? fn : [fn];
|
|
42
|
+
const formattedPath = Array.isArray(url) ? `[${url.join(', ')}]` : url;
|
|
43
|
+
const patchedMiddleware = middleware.map((f) => {
|
|
44
|
+
const formattedHandler = core.routeCoverage.formatHandlerSync(f);
|
|
45
|
+
const signature = `fastify.use(${formattedPath}, ${formattedHandler})`;
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
const routeInfo = {
|
|
48
|
+
signature,
|
|
49
|
+
url: formattedPath,
|
|
50
|
+
method: 'use',
|
|
51
|
+
normalizedUrl: formattedPath,
|
|
52
|
+
type: RouteType.MIDDLEWARE,
|
|
53
|
+
framework: 'fastify'
|
|
54
|
+
};
|
|
55
|
+
routeCoverage.discover(routeInfo);
|
|
56
|
+
|
|
57
|
+
return patcher.patch(f, {
|
|
58
|
+
name: 'middleware',
|
|
59
|
+
patchType,
|
|
60
|
+
post() {
|
|
61
|
+
routeCoverage.observe(routeInfo);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
60
64
|
});
|
|
61
|
-
|
|
62
|
-
data.args[1] = patchedMiddleware;
|
|
65
|
+
data.args[1] = patchedMiddleware;
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
67
|
+
return !scopes.instrumentation.isLocked() ? scopes.instrumentation.run(store, next) : next();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { RouteType } = require('@contrast/common');
|
|
18
|
-
const { patchType
|
|
19
|
-
|
|
18
|
+
const { patchType } = require('./../../utils/route-info');
|
|
19
|
+
|
|
20
20
|
module.exports = function init(core) {
|
|
21
21
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
22
|
|
|
@@ -33,10 +33,10 @@ module.exports = function init(core) {
|
|
|
33
33
|
const [url, fn] = data.args;
|
|
34
34
|
if (!url || !fn || !core.config.getEffectiveValue('assess.report_middleware_routes')) return;
|
|
35
35
|
|
|
36
|
-
const middleware = isArray(fn) ? fn : [fn];
|
|
37
|
-
const formattedPath = isArray(url) ? `[${url.join(', ')}]` : url;
|
|
36
|
+
const middleware = Array.isArray(fn) ? fn : [fn];
|
|
37
|
+
const formattedPath = Array.isArray(url) ? `[${url.join(', ')}]` : url;
|
|
38
38
|
const patchedMiddleware = middleware.map((f) => {
|
|
39
|
-
const formattedHandler =
|
|
39
|
+
const formattedHandler = core.routeCoverage.formatHandlerSync(f);
|
|
40
40
|
const signature = `fastify.use(${formattedPath}, ${formattedHandler})`;
|
|
41
41
|
|
|
42
42
|
const routeInfo = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
primordials: { StringPrototypeToLowerCase, StringPrototypeSplit },
|
|
20
20
|
RouteType,
|
|
21
21
|
} = require('@contrast/common');
|
|
22
|
-
const { patchType
|
|
22
|
+
const { patchType } = require('./../../utils/route-info');
|
|
23
23
|
|
|
24
24
|
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Fastify
|
|
25
25
|
module.exports = function init(core) {
|
|
@@ -41,9 +41,10 @@ module.exports = function init(core) {
|
|
|
41
41
|
function createRouteInfo(method, url, fullyDeclared, type, handler) {
|
|
42
42
|
method = StringPrototypeToLowerCase.call(method);
|
|
43
43
|
|
|
44
|
+
const formattedHandler = core.routeCoverage.formatHandlerSync(handler);
|
|
44
45
|
const signature = fullyDeclared
|
|
45
|
-
? `fastify.route({ method: ${method}, url: ${url}, handler: ${
|
|
46
|
-
: `fastify.${method}(${url}, ${
|
|
46
|
+
? `fastify.route({ method: ${method}, url: ${url}, handler: ${formattedHandler} })`
|
|
47
|
+
: `fastify.${method}(${url}, ${formattedHandler})`;
|
|
47
48
|
|
|
48
49
|
const routeInfo = {
|
|
49
50
|
signature,
|
package/lib/install/graphql.js
CHANGED
package/lib/install/hapi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -12,76 +12,140 @@
|
|
|
12
12
|
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
+
// @ts-check
|
|
15
16
|
'use strict';
|
|
16
17
|
|
|
17
|
-
const {
|
|
18
|
-
const {
|
|
18
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
19
|
+
const { RouteType, set } = require('@contrast/common');
|
|
20
|
+
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
21
|
+
const { patchType } = require('../utils/route-info');
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The hapi `Route` class from lib/route.js is not defined or exported.
|
|
25
|
+
* @typedef {Object} Route
|
|
26
|
+
* @property {boolean} _special internal hapi property for special routes
|
|
27
|
+
* @property {string} method
|
|
28
|
+
* @property {string} path
|
|
29
|
+
* @property {Object} settings
|
|
30
|
+
* @property {(request: { method: string, path: string}) => any} settings.handler set by the Route constructor
|
|
31
|
+
*/
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
class HapiRouteCoverage {
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('..').Core & {
|
|
36
|
+
* routeCoverage: import('..').RouteCoverage;
|
|
37
|
+
* }} core
|
|
38
|
+
*/
|
|
39
|
+
constructor(core) {
|
|
40
|
+
set(core, 'routeCoverage.hapi', this);
|
|
41
|
+
this.core = core;
|
|
42
|
+
this.depHooks = core.depHooks;
|
|
43
|
+
this.patcher = core.patcher;
|
|
44
|
+
this.routeCoverage = core.routeCoverage;
|
|
45
|
+
this.registerScope = new AsyncLocalStorage();
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!data.args[0] || !data.result) return;
|
|
48
|
+
install() {
|
|
49
|
+
this.depHooks.resolve(
|
|
50
|
+
{ name: '@hapi/hapi', version: '>=18 <22', file: 'lib/server.js' },
|
|
51
|
+
/** @param {typeof import('@hapi/hapi').Server} Server */
|
|
52
|
+
(Server) => this.patchServer(Server),
|
|
53
|
+
);
|
|
54
|
+
this.depHooks.resolve(
|
|
55
|
+
{ name: '@hapi/hapi', version: '>=18 <22', file: 'lib/route.js' },
|
|
56
|
+
/** @param {abstract new () => Route} Route */
|
|
57
|
+
(Route) => this.patchRoute(Route),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Hapi
|
|
63
|
+
* @param {Route} route
|
|
64
|
+
*/
|
|
65
|
+
createSignature(route) {
|
|
66
|
+
const handler = this.core.routeCoverage.formatHandlerSync(this.patcher.unwrap(route.settings.handler));
|
|
67
|
+
return `server.route({ method: '${route.method}', path: '${route.path}', handler: ${handler} })`;
|
|
68
|
+
}
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
/** @param {typeof import('@hapi/hapi').Server} Server */
|
|
71
|
+
patchServer(Server) {
|
|
72
|
+
const self = this;
|
|
73
|
+
return this.patcher.patch(Server, {
|
|
74
|
+
name: 'hapi.Server',
|
|
75
|
+
patchType,
|
|
76
|
+
post({ result: server }) {
|
|
77
|
+
self.patcher.patch(server, 'register', {
|
|
78
|
+
name: 'server.register',
|
|
79
|
+
patchType,
|
|
80
|
+
around(next) {
|
|
81
|
+
if (self.registerScope.getStore()) return next();
|
|
82
|
+
return self.registerScope.run({ isMiddleware: true }, next);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
61
88
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
89
|
+
/**
|
|
90
|
+
* @param {Route} route
|
|
91
|
+
* @param {string} signature
|
|
92
|
+
* @param {RouteType} type
|
|
93
|
+
*/
|
|
94
|
+
patchRouteHandler(route, signature, type) {
|
|
95
|
+
const self = this;
|
|
96
|
+
this.patcher.patch(route.settings, 'handler', {
|
|
97
|
+
name: 'route.settings.handler',
|
|
98
|
+
patchType,
|
|
99
|
+
// this needs to be in a pre-hook so that the route
|
|
100
|
+
// data is in the store before our dataflow hooks run
|
|
101
|
+
pre({ args: [request] }) {
|
|
102
|
+
self.routeCoverage.observe({
|
|
103
|
+
signature,
|
|
104
|
+
method: request.method,
|
|
105
|
+
url: request.path,
|
|
106
|
+
normalizedUrl: route.path, // should also be defined at `request.route.path`
|
|
107
|
+
framework: 'hapi',
|
|
108
|
+
type,
|
|
83
109
|
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {abstract new () => Route} Route
|
|
116
|
+
*/
|
|
117
|
+
patchRoute(Route) {
|
|
118
|
+
const self = this;
|
|
119
|
+
return this.patcher.patch(Route, {
|
|
120
|
+
name: 'hapi.Route',
|
|
121
|
+
patchType,
|
|
122
|
+
post({ result: route }) {
|
|
123
|
+
if (route._special) return; // skip special internal routes
|
|
124
|
+
|
|
125
|
+
const signature = self.createSignature(route);
|
|
126
|
+
const type = self.registerScope.getStore()?.isMiddleware ? RouteType.MIDDLEWARE : RouteType.HTTP;
|
|
127
|
+
|
|
128
|
+
self.routeCoverage.discover({
|
|
129
|
+
signature,
|
|
130
|
+
method: route.method,
|
|
131
|
+
url: route.path,
|
|
132
|
+
normalizedUrl: route.path,
|
|
133
|
+
framework: 'hapi',
|
|
134
|
+
type,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
self.patchRouteHandler(route, signature, type);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = Core.makeComponent({
|
|
144
|
+
name: 'routeCoverage.hapi',
|
|
145
|
+
/**
|
|
146
|
+
* @param {import('..').Core & {
|
|
147
|
+
* routeCoverage: import('..').RouteCoverage;
|
|
148
|
+
* }} core
|
|
149
|
+
*/
|
|
150
|
+
factory: (core) => new HapiRouteCoverage(core),
|
|
151
|
+
});
|
package/lib/install/koa.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const { METHODS } = require('./../utils/methods');
|
|
18
18
|
const { isString, RouteType, primordials: { StringPrototypeToLowerCase, StringPrototypeSplit } } = require('@contrast/common');
|
|
19
|
-
const { patchType
|
|
19
|
+
const { patchType } = require('./../utils/route-info');
|
|
20
20
|
|
|
21
21
|
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Koa
|
|
22
22
|
module.exports = function init(core) {
|
|
@@ -49,7 +49,7 @@ module.exports = function init(core) {
|
|
|
49
49
|
const method = methods.length === 0 ? 'use' : METHODS.every(m => methods.includes(m)) ? 'all' : StringPrototypeToLowerCase.call(methods[methods.length - 1]);
|
|
50
50
|
if (method === 'use' && !core.config.getEffectiveValue('assess.report_middleware_routes')) return;
|
|
51
51
|
const routeInfo = {
|
|
52
|
-
signature: `Router.${method}(${path}, ${
|
|
52
|
+
signature: `Router.${method}(${path}, ${core.routeCoverage.formatHandlerSync(handler)})`,
|
|
53
53
|
method,
|
|
54
54
|
url: path,
|
|
55
55
|
normalizedUrl: path,
|
package/lib/install/restify.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -14,58 +14,240 @@
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const {
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
18
|
+
const {
|
|
19
|
+
isString,
|
|
20
|
+
primordials: {
|
|
21
|
+
StringPrototypeToLowerCase,
|
|
22
|
+
StringPrototypeSplit,
|
|
23
|
+
StringPrototypeSubstring,
|
|
24
|
+
},
|
|
25
|
+
set,
|
|
26
|
+
RouteType,
|
|
27
|
+
} = require('@contrast/common');
|
|
28
|
+
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
29
|
+
const { patchType } = require('../utils/route-info');
|
|
30
|
+
|
|
31
|
+
const COMPONENT_NAME = 'routeCoverage.restify';
|
|
32
|
+
const FRAMEWORK = 'restify';
|
|
33
|
+
|
|
34
|
+
module.exports = Core.makeComponent({
|
|
35
|
+
name: COMPONENT_NAME,
|
|
36
|
+
factory: (core) => new RestifyInstrumentation(core),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
class RestifyInstrumentation {
|
|
40
|
+
constructor(core) {
|
|
41
|
+
set(core, COMPONENT_NAME, this);
|
|
42
|
+
Object.defineProperty(this, 'core', { value: core });
|
|
43
|
+
this.routeScope = new AsyncLocalStorage();
|
|
44
|
+
this.conditionalHandlers = new WeakMap();
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
formatHandler(fn) {
|
|
48
|
+
return this.core.routeCoverage.formatHandlerSync(this.core.patcher.unwrap(fn));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
install() {
|
|
52
|
+
const self = this;
|
|
53
|
+
const { depHooks } = this.core;
|
|
54
|
+
|
|
55
|
+
depHooks.resolve({ name: 'restify', version: '>=10 <12' }, (restify, pkgMeta) => {
|
|
56
|
+
self.patchPlugins(restify, pkgMeta);
|
|
57
|
+
self.patchServer(restify, pkgMeta);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
patchPlugins(restify) {
|
|
62
|
+
const self = this;
|
|
63
|
+
const { patcher, routeCoverage } = this.core;
|
|
64
|
+
if (!restify.plugins?.conditionalHandler) return;
|
|
65
|
+
|
|
66
|
+
const name = 'restify.plugins.conditionalHandler';
|
|
67
|
+
patcher.patch(restify.plugins, 'conditionalHandler', {
|
|
68
|
+
name,
|
|
69
|
+
patchType,
|
|
70
|
+
around(next, data) {
|
|
71
|
+
const { args } = data;
|
|
72
|
+
const conditionals = Array.isArray(args[0]) ? args[0] : [args[0]];
|
|
73
|
+
const formattedHandlers = [];
|
|
74
|
+
|
|
75
|
+
// we have to do this before calling next() to get return value
|
|
76
|
+
// since restify potentially alters conditional.handlers
|
|
77
|
+
for (const conditional of conditionals) {
|
|
78
|
+
const isHandlerArr = Array.isArray(conditional.handler);
|
|
79
|
+
const target = isHandlerArr ? conditional.handler : conditional;
|
|
80
|
+
const propsIter = isHandlerArr ? Object.keys(conditional.handler) : ['handler'];
|
|
81
|
+
|
|
82
|
+
for (const propName of propsIter) {
|
|
83
|
+
if (typeof target[propName] !== 'function') continue;
|
|
84
|
+
|
|
85
|
+
const formattedHandler = self.formatHandler(target[propName]);
|
|
86
|
+
formattedHandlers.push(formattedHandler);
|
|
87
|
+
|
|
88
|
+
patcher.patch(target, propName, {
|
|
89
|
+
name: 'restify.plugins.conditionalHandler',
|
|
46
90
|
patchType,
|
|
47
|
-
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
pre(data) {
|
|
92
|
+
const { args: [req] } = data;
|
|
93
|
+
const store = self.routeScope.getStore();
|
|
94
|
+
if (!store) return;
|
|
95
|
+
|
|
96
|
+
for (const routeInfo of store.observables) {
|
|
97
|
+
if (routeInfo.signature.indexOf(formattedHandler) >= 0) {
|
|
98
|
+
routeCoverage.observe({
|
|
99
|
+
...routeInfo,
|
|
100
|
+
method: StringPrototypeToLowerCase.call(req.method),
|
|
101
|
+
url: StringPrototypeSplit.call(req.url, '?')[0],
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = next();
|
|
112
|
+
|
|
113
|
+
// save list of handlers that have been registered under the returned
|
|
114
|
+
// consolidated one. when this return value gets mounted at an actual
|
|
115
|
+
// path(s), we can lookup the handlers and discover each individually
|
|
116
|
+
// with that additional route info.
|
|
117
|
+
self.conditionalHandlers.set(result, formattedHandlers);
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
patchServer(restify, pkgMeta) {
|
|
125
|
+
const self = this;
|
|
126
|
+
const { logger, patcher, routeCoverage } = this.core;
|
|
127
|
+
|
|
128
|
+
patcher.patch(restify, 'createServer', {
|
|
129
|
+
name: 'restify.createServer',
|
|
130
|
+
patchType,
|
|
131
|
+
post({ result: server }) {
|
|
132
|
+
patcher.patch(server.router, 'mount', {
|
|
133
|
+
name: 'restify.router.mount',
|
|
134
|
+
patchType,
|
|
135
|
+
post({ result: route }) {
|
|
136
|
+
if (!route.path || !route.method || !isString(route.path)) {
|
|
137
|
+
logger.error({ route }, 'unable to process restify route');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { path } = route;
|
|
142
|
+
const method = StringPrototypeToLowerCase.call(route.method);
|
|
143
|
+
const baseInfo = {
|
|
144
|
+
method,
|
|
145
|
+
url: path,
|
|
146
|
+
normalizedUrl: path,
|
|
147
|
+
framework: FRAMEWORK,
|
|
148
|
+
type: RouteType.HTTP,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (let idx = 0; idx < route.chain._stack.length; idx++) {
|
|
152
|
+
const handler = route.chain._stack[idx];
|
|
153
|
+
const routeInfo = {
|
|
154
|
+
...baseInfo,
|
|
155
|
+
signature: `server.${method}(${path}, ${self.formatHandler(handler)})`,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (!self.conditionalHandlers.has(handler)) {
|
|
159
|
+
// "regular" handlers
|
|
160
|
+
routeCoverage.discover(routeInfo);
|
|
161
|
+
route.chain._stack[idx] = patcher.patch(route.chain._stack[idx], {
|
|
162
|
+
name: 'restify.route.chain._stack',
|
|
56
163
|
patchType,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
164
|
+
pre({ args: [req] }) {
|
|
165
|
+
routeCoverage.observe({
|
|
166
|
+
...routeInfo,
|
|
167
|
+
method: StringPrototypeToLowerCase.call(req.method),
|
|
168
|
+
url: StringPrototypeSplit.call(req.url, '?')[0],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
// "conditional" handlers dispatch to their registered handlers
|
|
174
|
+
const formattedHandlers = self.conditionalHandlers.get(handler);
|
|
175
|
+
const store = { observables: [] };
|
|
176
|
+
|
|
177
|
+
for (const formattedHandler of formattedHandlers) {
|
|
178
|
+
const routeInfo = {
|
|
179
|
+
...baseInfo,
|
|
180
|
+
signature: `server.${method}(${path}, ${formattedHandler})`,
|
|
181
|
+
};
|
|
182
|
+
routeCoverage.discover(routeInfo);
|
|
183
|
+
store.observables.push(routeInfo);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
route.chain._stack[idx] = patcher.patch(route.chain._stack[idx], {
|
|
187
|
+
name: 'restify.route.chain._stack',
|
|
188
|
+
patchType,
|
|
189
|
+
around(next) {
|
|
190
|
+
return self.routeScope.run(store, next);
|
|
62
191
|
}
|
|
63
192
|
});
|
|
64
193
|
}
|
|
65
|
-
}
|
|
194
|
+
}
|
|
66
195
|
}
|
|
67
196
|
});
|
|
197
|
+
|
|
198
|
+
self.patchMiddlewareChains(server, pkgMeta);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
patchMiddlewareChains(server) {
|
|
204
|
+
const self = this;
|
|
205
|
+
const { config, routeCoverage, patcher } = this.core;
|
|
206
|
+
|
|
207
|
+
if (!config.getEffectiveValue('assess.report_middleware_routes')) return;
|
|
208
|
+
|
|
209
|
+
for (const propName of ['preChain', 'useChain']) {
|
|
210
|
+
patcher.patch(server[propName], 'add', {
|
|
211
|
+
name: `restify.server.${propName}.add`,
|
|
212
|
+
patchType,
|
|
213
|
+
around(next, data) {
|
|
214
|
+
const len = data.obj._stack.length;
|
|
215
|
+
const ret = next();
|
|
216
|
+
|
|
217
|
+
if (data.obj._stack.length > len) {
|
|
218
|
+
const method = StringPrototypeSubstring.call(propName, 0, 3);
|
|
219
|
+
const baseData = {
|
|
220
|
+
method,
|
|
221
|
+
url: '/',
|
|
222
|
+
normalizedUrl: '/',
|
|
223
|
+
type: RouteType.MIDDLEWARE,
|
|
224
|
+
framework: FRAMEWORK,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (let idx = 0; idx < data.obj._stack.length; idx++) {
|
|
228
|
+
const routeData = {
|
|
229
|
+
...baseData,
|
|
230
|
+
signature: `server.${method}(${self.formatHandler(data.obj._stack[idx])})`,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
routeCoverage.discover(routeData);
|
|
234
|
+
patcher.patch(data.obj._stack, idx, {
|
|
235
|
+
name: `restify.server.${propName}`,
|
|
236
|
+
patchType,
|
|
237
|
+
pre({ args: [req] }) {
|
|
238
|
+
routeCoverage.observe({
|
|
239
|
+
...routeData,
|
|
240
|
+
method: StringPrototypeToLowerCase.call(req.method),
|
|
241
|
+
url: StringPrototypeSplit.call(req.url, '?')[0],
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return ret;
|
|
248
|
+
}
|
|
68
249
|
});
|
|
69
250
|
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
package/lib/install/socket.io.js
CHANGED
package/lib/utils/methods.js
CHANGED
package/lib/utils/route-info.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2026 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const patchType = 'route-coverage';
|
|
18
|
-
const { funcInfo } = require('@contrast/fn-inspect');
|
|
19
|
-
const { primordials: { StringPrototypeReplace, StringPrototypeSubstring } } = require('@contrast/common');
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
20
|
* Creates a formatted "signature" for a route
|
|
@@ -30,27 +28,4 @@ function createSignature(path, method = '', obj = 'Router', handler = '[Function
|
|
|
30
28
|
return `${obj}.${method}('${path}', ${handler})`;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
* Creates a formatted handler signature for a route
|
|
35
|
-
* @param {function} handler
|
|
36
|
-
* @param {string} appDir
|
|
37
|
-
* @return {string} formatted handler
|
|
38
|
-
*/
|
|
39
|
-
function formatHandler(handler, appDir) {
|
|
40
|
-
const info = funcInfo(handler);
|
|
41
|
-
if (!info) return '[Function]';
|
|
42
|
-
|
|
43
|
-
let file = info.file ?
|
|
44
|
-
StringPrototypeReplace.call(info.file, appDir, '') :
|
|
45
|
-
'';
|
|
46
|
-
if (file.length > 30) {
|
|
47
|
-
file = `...${StringPrototypeSubstring.call(file, file.length - 40)}`;
|
|
48
|
-
}
|
|
49
|
-
const handlerName = info.method || handler.name || 'anonymous';
|
|
50
|
-
const formattedHandler = (file && Number.isFinite(info.lineNumber) && Number.isFinite(info.column)) ?
|
|
51
|
-
`[${handlerName} ${file} ${info.lineNumber}:${info.column}]` :
|
|
52
|
-
`[Function: ${handlerName}]`; // what util.inspect(handler) would return
|
|
53
|
-
return formattedHandler;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
module.exports = { createSignature, patchType, formatHandler };
|
|
31
|
+
module.exports = { createSignature, patchType };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/route-coverage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.1",
|
|
4
4
|
"description": "Handles route discovery and observation",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
"test": "bash ../scripts/test.sh"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@contrast/common": "1.
|
|
24
|
-
"@contrast/config": "1.
|
|
25
|
-
"@contrast/core": "1.
|
|
26
|
-
"@contrast/dep-hooks": "1.
|
|
23
|
+
"@contrast/common": "1.39.0",
|
|
24
|
+
"@contrast/config": "1.55.0",
|
|
25
|
+
"@contrast/core": "1.60.0",
|
|
26
|
+
"@contrast/dep-hooks": "1.29.0",
|
|
27
27
|
"@contrast/fn-inspect": "^5.0.2",
|
|
28
|
-
"@contrast/logger": "1.
|
|
29
|
-
"@contrast/patcher": "1.
|
|
30
|
-
"@contrast/
|
|
28
|
+
"@contrast/logger": "1.33.0",
|
|
29
|
+
"@contrast/patcher": "1.32.0",
|
|
30
|
+
"@contrast/rewriter": "1.37.1",
|
|
31
|
+
"@contrast/scopes": "1.30.0",
|
|
31
32
|
"semver": "^7.6.0"
|
|
32
33
|
}
|
|
33
34
|
}
|