@contrast/route-coverage 1.20.6 → 1.21.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/lib/index.js +0 -1
- package/lib/install/express.js +1 -0
- package/lib/install/fastify.js +94 -77
- package/lib/install/hapi.js +9 -2
- package/lib/install/koa.js +53 -33
- package/lib/install/restify.js +2 -1
- package/lib/utils/methods.js +45 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
package/lib/install/express.js
CHANGED
|
@@ -30,6 +30,7 @@ const fnInspect = require('@contrast/fn-inspect');
|
|
|
30
30
|
const { createSignature, patchType } = require('../utils/route-info');
|
|
31
31
|
const { ArrayPrototypeJoin, StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
32
32
|
|
|
33
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Express
|
|
33
34
|
module.exports = function init(core) {
|
|
34
35
|
const { patcher, depHooks, routeCoverage } = core;
|
|
35
36
|
const discover = (route) => routeCoverage.discover(route);
|
package/lib/install/fastify.js
CHANGED
|
@@ -12,99 +12,116 @@
|
|
|
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
|
|
16
15
|
'use strict';
|
|
17
16
|
|
|
18
|
-
const {
|
|
19
|
-
const {
|
|
17
|
+
const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
18
|
+
const { getFastifyMethods } = require('./../utils/methods');
|
|
19
|
+
const { patchType } = require('./../utils/route-info');
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {import('..').Core & {
|
|
25
|
-
* routeCoverage: import('..').RouteCoverage & {
|
|
26
|
-
* fastify?: import('@contrast/common').Installable
|
|
27
|
-
* }
|
|
28
|
-
* }} core
|
|
29
|
-
*/
|
|
21
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Fastify
|
|
30
22
|
module.exports = function init(core) {
|
|
23
|
+
/*
|
|
24
|
+
Fastify has two ways of defining routes:
|
|
25
|
+
1. fastify.route({ method: '<method>', url: '<url>', handler: <fn> })
|
|
26
|
+
2. fastify.<method>('<url>', <fn>)
|
|
27
|
+
(See NODE-3483 for more detail)
|
|
28
|
+
We need a way to keep track of which routes were fully declared using .route
|
|
29
|
+
So, we instrument the route method below and add identifying info to this array
|
|
30
|
+
*/
|
|
31
|
+
const fullyDeclaredRoutes = [];
|
|
31
32
|
const { patcher, depHooks, routeCoverage } = core;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (Array.isArray(method)) {
|
|
39
|
-
method.forEach((method) => {
|
|
40
|
-
emitRouteCoverage(url, method);
|
|
41
|
-
});
|
|
42
|
-
} else {
|
|
43
|
-
emitRouteCoverage(url, method);
|
|
34
|
+
const isFullyDeclared = (method, url) => isString(method) && fullyDeclaredRoutes.includes(method + url);
|
|
35
|
+
function fullyDeclare(method, url) {
|
|
36
|
+
if (isString(method)) {
|
|
37
|
+
fullyDeclaredRoutes.push(method + url);
|
|
38
|
+
if (method === 'GET') fullyDeclaredRoutes.push(`HEAD${url}`);
|
|
44
39
|
}
|
|
45
|
-
|
|
46
|
-
// TODO
|
|
47
|
-
observationListener(routeOptions, url);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {RouteOptions} routeOptions
|
|
52
|
-
* @param {string} url
|
|
53
|
-
*/
|
|
54
|
-
function observationListener(routeOptions, url) {
|
|
55
|
-
patcher.patch(routeOptions, 'handler', {
|
|
56
|
-
name: 'fastify.routeOptions.handler',
|
|
57
|
-
patchType: 'route-coverage',
|
|
58
|
-
pre(data) {
|
|
59
|
-
const [request] = data.args;
|
|
60
|
-
const { method } = request.raw;
|
|
61
|
-
const [parsedUrl] = request.url.split(/\?/);
|
|
62
|
-
emitObservation(parsedUrl, url, method);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
40
|
}
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* @param {string} method
|
|
70
|
-
*/
|
|
71
|
-
function emitRouteCoverage(url, method) {
|
|
42
|
+
function createRouteInfo(method, url, routePath) {
|
|
43
|
+
const fullyDeclared = isFullyDeclared(method, routePath);
|
|
72
44
|
method = StringPrototypeToLowerCase.call(method);
|
|
73
|
-
const event = { signature: createSignature(url, method), url, method, normalizedUrl: url, framework: 'fastify' };
|
|
74
|
-
routeCoverage.discover(event);
|
|
75
|
-
}
|
|
76
45
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
46
|
+
const signature = fullyDeclared
|
|
47
|
+
? `fastify.route({ method: '${method}', url: '${url}', handler: [Function] })`
|
|
48
|
+
: `fastify.${method}('${url}', [Function])`;
|
|
49
|
+
|
|
50
|
+
const routeInfo = {
|
|
51
|
+
signature,
|
|
52
|
+
url,
|
|
53
|
+
method,
|
|
54
|
+
normalizedUrl: url,
|
|
55
|
+
framework: 'fastify'
|
|
56
|
+
};
|
|
57
|
+
return routeInfo;
|
|
85
58
|
}
|
|
86
59
|
|
|
87
60
|
return core.routeCoverage.fastify = {
|
|
88
61
|
install() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
62
|
+
// The routePath property used below was introduced in 3.2.0
|
|
63
|
+
return depHooks.resolve({ name: 'fastify', version: '>=3.2.0' }, (fastify) => patcher.patch(fastify, {
|
|
64
|
+
name: 'fastify',
|
|
65
|
+
patchType,
|
|
66
|
+
post({ result: server }) {
|
|
67
|
+
patcher.patch(server, 'route', {
|
|
68
|
+
name: 'server.route',
|
|
69
|
+
patchType,
|
|
70
|
+
pre({ args }) {
|
|
71
|
+
const [{ method, url }] = args;
|
|
72
|
+
if (!isString(url)) return;
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(method)) {
|
|
75
|
+
method.forEach((verb) => {
|
|
76
|
+
fullyDeclare(verb, url);
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
fullyDeclare(method, url);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.addHook('onRoute', (routeOptions) => {
|
|
84
|
+
if (!routeOptions) return;
|
|
85
|
+
|
|
86
|
+
const { method, url, routePath } = routeOptions;
|
|
87
|
+
if (!method || !url || !routePath || !isString(routePath)) return;
|
|
88
|
+
|
|
89
|
+
let routeInfo;
|
|
90
|
+
const FASTIFY_METHODS = getFastifyMethods(server.version);
|
|
91
|
+
if (Array.isArray(method)) {
|
|
92
|
+
// If a route was defined using .all this method property will be an
|
|
93
|
+
// array of all methods supported by Fastify
|
|
94
|
+
if (FASTIFY_METHODS.every(m => method.includes(m))) {
|
|
95
|
+
routeInfo = createRouteInfo('all', url, routePath);
|
|
96
|
+
routeCoverage.discover(routeInfo);
|
|
97
|
+
} else {
|
|
98
|
+
method.forEach((verb) => {
|
|
99
|
+
routeInfo = createRouteInfo(verb, url, routePath);
|
|
100
|
+
routeCoverage.discover(routeInfo);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
routeInfo = createRouteInfo(method, url, routePath);
|
|
105
|
+
routeCoverage.discover(routeInfo);
|
|
106
|
+
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
patcher.patch(routeOptions, 'handler', {
|
|
109
|
+
name: 'fastify.routeOptions.handler',
|
|
110
|
+
patchType,
|
|
111
|
+
pre({ args }) {
|
|
112
|
+
const [req] = args;
|
|
113
|
+
const method = StringPrototypeToLowerCase.call(req.raw?.method);
|
|
114
|
+
const [url] = req.url.split(/\?/);
|
|
115
|
+
routeCoverage.observe({ ...routeInfo, url, method });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
server.addHook('onReady', (done) => {
|
|
120
|
+
routeCoverage.discoveryFinished();
|
|
121
|
+
return done();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
108
125
|
}
|
|
109
126
|
};
|
|
110
127
|
};
|
package/lib/install/hapi.js
CHANGED
|
@@ -15,14 +15,21 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { StringPrototypeToLowerCase } = require('@contrast/common');
|
|
18
|
-
const {
|
|
18
|
+
const { patchType } = require('./../utils/route-info');
|
|
19
19
|
|
|
20
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Hapi
|
|
20
21
|
module.exports = function init(core) {
|
|
21
22
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
23
|
|
|
23
24
|
function emitRouteCoverage(url, method) {
|
|
24
25
|
method = StringPrototypeToLowerCase.call(method);
|
|
25
|
-
const event = {
|
|
26
|
+
const event = {
|
|
27
|
+
signature: `server.route({ method: '${method}', path: '${url}', handler: [Function] })`,
|
|
28
|
+
url,
|
|
29
|
+
method,
|
|
30
|
+
normalizedUrl: url,
|
|
31
|
+
framework: 'hapi'
|
|
32
|
+
};
|
|
26
33
|
routeCoverage.discover(event);
|
|
27
34
|
}
|
|
28
35
|
|
package/lib/install/koa.js
CHANGED
|
@@ -14,56 +14,76 @@
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { METHODS } = require('./../utils/methods');
|
|
18
|
+
const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
18
19
|
const { createSignature, patchType } = require('./../utils/route-info');
|
|
19
20
|
|
|
21
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Koa
|
|
20
22
|
module.exports = function init(core) {
|
|
21
23
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
24
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (req) {
|
|
33
|
-
const { url: reqUrl, method } = req;
|
|
34
|
-
const [url] = reqUrl.split(/\?/);
|
|
35
|
-
routeCoverage.observe({ url, method: StringPrototypeToLowerCase.call(method || ''), normalizedUrl: path });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await next();
|
|
25
|
+
function createRouteInfo(method, url) {
|
|
26
|
+
method = StringPrototypeToLowerCase.call(method);
|
|
27
|
+
const routeInfo = {
|
|
28
|
+
signature: createSignature(url, method),
|
|
29
|
+
url,
|
|
30
|
+
method,
|
|
31
|
+
normalizedUrl: url,
|
|
32
|
+
framework: 'koa'
|
|
39
33
|
};
|
|
34
|
+
return routeInfo;
|
|
35
|
+
}
|
|
40
36
|
|
|
41
37
|
return core.routeCoverage.koa = {
|
|
42
38
|
install() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
|
|
40
|
+
// Koa uses its own routing library @koa/router to define routes before
|
|
41
|
+
// mounting them on the app with .use so instrumenting use and traversing
|
|
42
|
+
// the constructed routes is the more technically correct approach than
|
|
43
|
+
// instrumenting the routing library
|
|
44
|
+
patcher.patch(Koa.prototype, 'use', {
|
|
45
|
+
name: 'Koa.prototype.use',
|
|
46
|
+
patchType,
|
|
47
|
+
pre: ({ args }) => {
|
|
48
|
+
if (args?.length === 0) return;
|
|
49
|
+
const [router] = args;
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
name: 'koaRouter.prototype',
|
|
49
|
-
patchType,
|
|
50
|
-
post: ({ args, result: layer }) => {
|
|
51
|
-
const [path, methods] = args;
|
|
52
|
-
if (!path || !methods || Array.isArray(path)) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
51
|
+
if (!router?.router) return;
|
|
55
52
|
|
|
53
|
+
router.router.stack.forEach((Layer) => {
|
|
54
|
+
const { methods, path } = Layer;
|
|
55
|
+
if (!path || !isString(path)) return;
|
|
56
|
+
|
|
57
|
+
let routeInfo;
|
|
56
58
|
if (methods.length === 0) {
|
|
57
|
-
|
|
59
|
+
routeInfo = createRouteInfo('use', path);
|
|
60
|
+
routeCoverage.discover(routeInfo);
|
|
61
|
+
} else if (METHODS.every(m => methods.includes(m))) {
|
|
62
|
+
// If a route was defined using .all this methods property will be an
|
|
63
|
+
// array of all methods supported by Koa
|
|
64
|
+
routeInfo = createRouteInfo('all', path);
|
|
65
|
+
routeCoverage.discover(routeInfo);
|
|
58
66
|
} else {
|
|
59
67
|
methods.forEach((method) => {
|
|
60
|
-
|
|
68
|
+
routeInfo = createRouteInfo(method, path);
|
|
69
|
+
routeCoverage.discover(routeInfo);
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
if (!Layer.stack || Layer.stack.length === 0) return;
|
|
74
|
+
async function observationMiddleware(ctx, next) {
|
|
75
|
+
if (!ctx.request) return;
|
|
76
|
+
const { url: reqUrl, method } = ctx.request;
|
|
77
|
+
const [url] = reqUrl.split(/\?/);
|
|
78
|
+
routeCoverage.observe({ ...routeInfo, url, method: StringPrototypeToLowerCase.call(method) });
|
|
79
|
+
await next();
|
|
80
|
+
}
|
|
81
|
+
// If two routes share middleware, the same stack is used
|
|
82
|
+
// To add our observation middleware without adding them to all routes
|
|
83
|
+
// we need to create a shallow copy
|
|
84
|
+
Layer.stack = [observationMiddleware, ...Layer.stack];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
67
87
|
});
|
|
68
88
|
});
|
|
69
89
|
}
|
package/lib/install/restify.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
|
|
18
18
|
const { createSignature, patchType } = require('../utils/route-info');
|
|
19
19
|
|
|
20
|
+
// Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Restify
|
|
20
21
|
module.exports = function init(core) {
|
|
21
22
|
const { patcher, depHooks, routeCoverage } = core;
|
|
22
23
|
const discover = (route) => routeCoverage.discover(route);
|
|
@@ -25,7 +26,7 @@ module.exports = function init(core) {
|
|
|
25
26
|
function createRoute(url, method) {
|
|
26
27
|
method = StringPrototypeToLowerCase.call(method);
|
|
27
28
|
return {
|
|
28
|
-
signature: createSignature(url, method, '
|
|
29
|
+
signature: createSignature(url, method, 'server'),
|
|
29
30
|
method,
|
|
30
31
|
url,
|
|
31
32
|
normalizedUrl: url,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line node/no-extraneous-require
|
|
18
|
+
const semver = require('semver');
|
|
19
|
+
const { METHODS } = require('http');
|
|
20
|
+
|
|
21
|
+
function getFastifyMethods(version) {
|
|
22
|
+
return [
|
|
23
|
+
'DELETE',
|
|
24
|
+
'GET',
|
|
25
|
+
'HEAD',
|
|
26
|
+
'PATCH',
|
|
27
|
+
'POST',
|
|
28
|
+
'PUT',
|
|
29
|
+
'OPTIONS',
|
|
30
|
+
...semver.gte(version, '4.4.0') ?
|
|
31
|
+
[
|
|
32
|
+
'PROPFIND',
|
|
33
|
+
'PROPPATCH',
|
|
34
|
+
'MKCOL',
|
|
35
|
+
'COPY',
|
|
36
|
+
'MOVE',
|
|
37
|
+
'LOCK',
|
|
38
|
+
'UNLOCK',
|
|
39
|
+
'TRACE',
|
|
40
|
+
'SEARCH'
|
|
41
|
+
] : []
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { getFastifyMethods, METHODS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/route-coverage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
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)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "../scripts/test.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@contrast/common": "1.
|
|
20
|
+
"@contrast/common": "1.22.0",
|
|
21
21
|
"@contrast/fn-inspect": "^4.0.0"
|
|
22
22
|
}
|
|
23
23
|
}
|