@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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright: 2025 Contrast Security, Inc
1
+ Copyright: 2026 Contrast Security, Inc
2
2
  Contact: support@contrastsecurity.com
3
3
  License: Commercial
4
4
 
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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: 2025 Contrast Security, Inc
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')(core);
148
+ core.initComponentSync(require('./install/express'));
126
149
  require('./install/fastify')(core);
127
150
  require('./install/graphql')(core);
128
- require('./install/hapi')(core);
151
+ core.initComponentSync(require('./install/hapi'));
129
152
  require('./install/koa')(core);
130
- require('./install/restify')(core);
153
+ core.initComponentSync(require('./install/restify'));
131
154
  core.initComponentSync(require('./install/socket.io'));
132
155
 
133
156
  messages.on(Event.SERVER_LISTENING, () => {
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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}, ${formatHandler(handler)})`;
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: 2025 Contrast Security, Inc
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 { patchType, formatHandler } = require('./../../utils/route-info');
19
- const isArray = (arr) => Array.isArray(arr);
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
- return core.routeCoverage.fastifyExpress = {
24
- install() {
25
- const name = 'fastifyExpress';
26
- depHooks.resolve({ name: '@fastify/express', version: '*' }, (_xport) => patcher.patch(_xport, {
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
- const middleware = isArray(fn) ? fn : [fn];
39
- const formattedPath = isArray(url) ? `[${url.join(', ')}]` : url;
40
- const patchedMiddleware = middleware.map((f) => {
41
- const formattedHandler = formatHandler(f);
42
- const signature = `fastify.use(${formattedPath}, ${formattedHandler})`;
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 routeInfo = {
45
- signature,
46
- url: formattedPath,
47
- method: 'use',
48
- normalizedUrl: formattedPath,
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
- return patcher.patch(f, {
55
- name: 'middleware',
56
- patchType,
57
- post() {
58
- routeCoverage.observe(routeInfo);
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
- return !scopes.instrumentation.isLocked() ? scopes.instrumentation.run(store, next) : next();
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: 2025 Contrast Security, Inc
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, formatHandler } = require('./../../utils/route-info');
19
- const isArray = (arr) => Array.isArray(arr);
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 = formatHandler(f);
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: 2025 Contrast Security, Inc
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, formatHandler } = require('./../../utils/route-info');
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: ${formatHandler(handler)} })`
46
- : `fastify.${method}(${url}, ${formatHandler(handler)})`;
46
+ ? `fastify.route({ method: ${method}, url: ${url}, handler: ${formattedHandler} })`
47
+ : `fastify.${method}(${url}, ${formattedHandler})`;
47
48
 
48
49
  const routeInfo = {
49
50
  signature,
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
2
+ * Copyright: 2026 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
2
+ * Copyright: 2026 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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 { primordials: { StringPrototypeToLowerCase } } = require('@contrast/common');
18
- const { patchType } = require('./../utils/route-info');
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
- // Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Hapi
21
- module.exports = function init(core) {
22
- const { patcher, depHooks, routeCoverage } = core;
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
- const createSignature = (method, url) => `server.route({ method: '${method}', path: '${url}', handler: [Function] })`;
25
- function emitRouteCoverage(url, method) {
26
- method = StringPrototypeToLowerCase.call(method);
27
- const event = {
28
- signature: createSignature(method, url),
29
- url,
30
- method,
31
- normalizedUrl: url,
32
- framework: 'hapi'
33
- };
34
- routeCoverage.discover(event);
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
- return core.routeCoverage.hapi = {
38
- install() {
39
- return depHooks.resolve({ name: '@hapi/hapi', version: '>=18 <22' }, (hapi) => {
40
- ['server', 'Server'].forEach((server) => {
41
- patcher.patch(hapi, server, {
42
- name: `hapi.${server}`,
43
- patchType,
44
- post(data) {
45
- patcher.patch(data.result._core.router, 'add', {
46
- name: '_core.router.add',
47
- patchType,
48
- post(data) {
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
- const [{ method, path }] = data.args;
52
- if (!method || !path) return;
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
- if (Array.isArray(method)) {
55
- method.forEach((verb) => {
56
- emitRouteCoverage(path, verb);
57
- });
58
- } else {
59
- emitRouteCoverage(path, method);
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
- patcher.patch(data.result.route.settings, 'handler', {
63
- name: 'route.settings.handler',
64
- patchType,
65
- // this needs to be in a pre-hook so that the route
66
- // data is in the store before our dataflow hooks run
67
- pre({ args }) {
68
- const [{ method, path: url, route }] = args;
69
- //TODO: Will this signature always be associated with an existing route?
70
- const signature = createSignature(method, path);
71
- routeCoverage.observe({
72
- signature,
73
- url,
74
- method: StringPrototypeToLowerCase.call(method),
75
- normalizedUrl: route.path,
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
+ });
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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, formatHandler } = require('./../utils/route-info');
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}, ${formatHandler(handler)})`,
52
+ signature: `Router.${method}(${path}, ${core.routeCoverage.formatHandlerSync(handler)})`,
53
53
  method,
54
54
  url: path,
55
55
  normalizedUrl: path,
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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 { isString, primordials: { StringPrototypeToLowerCase, StringPrototypeSplit } } = require('@contrast/common');
18
- const { createSignature, patchType } = require('../utils/route-info');
19
-
20
- // Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Restify
21
- module.exports = function init(core) {
22
- const { patcher, depHooks, routeCoverage } = core;
23
- const discover = (route) => routeCoverage.discover(route);
24
- const observe = (route) => routeCoverage.observe(route);
25
-
26
- function createRoute(url, method) {
27
- method = StringPrototypeToLowerCase.call(method);
28
- return {
29
- signature: createSignature(url, method, 'server'),
30
- method,
31
- url,
32
- normalizedUrl: url,
33
- framework: 'restify'
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
- return core.routeCoverage.restify = {
38
- install() {
39
- depHooks.resolve({ name: 'restify', version: '>=10 <12' }, (restify) => {
40
- patcher.patch(restify, 'createServer', {
41
- name: 'restify.createServer',
42
- patchType,
43
- post({ result: server }) {
44
- patcher.patch(server.router, 'mount', {
45
- name: 'restify.router.mount',
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
- post({ result: route }) {
48
- const { path, method } = route;
49
- if (!path || !method || !isString(path)) return;
50
- const routeInfo = createRoute(path, method);
51
- discover(routeInfo);
52
-
53
- const [handler] = route.chain._stack;
54
- route.chain._stack[0] = patcher.patch(handler, {
55
- name: 'route.chain._stack[0].handler',
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
- post({ args }) {
58
- const [req] = args;
59
- const { url: reqUrl, method } = req;
60
- const [url] = StringPrototypeSplit.call(reqUrl, '?');
61
- observe({ ...routeInfo, method: StringPrototypeToLowerCase.call(method), url });
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
+
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
2
+ * Copyright: 2026 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
2
+ * Copyright: 2026 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2025 Contrast Security, Inc
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.52.0",
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.38.0",
24
- "@contrast/config": "1.54.1",
25
- "@contrast/core": "1.59.1",
26
- "@contrast/dep-hooks": "1.28.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.32.1",
29
- "@contrast/patcher": "1.31.1",
30
- "@contrast/scopes": "1.29.1",
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
  }