@contrast/route-coverage 1.45.2 → 1.46.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 +1 -9
- package/lib/install/express/express5.js +17 -19
- package/package.json +8 -8
- package/lib/normalized-url-mapper.js +0 -174
package/lib/index.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const { callChildComponentMethodsSync, Event } = require('@contrast/common');
|
|
19
|
-
const NormalizedUrlMapper = require('./normalized-url-mapper');
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* @param {import('.').Core & {
|
|
@@ -36,21 +35,14 @@ module.exports = function init(core) {
|
|
|
36
35
|
const routeQueue = new Map();
|
|
37
36
|
|
|
38
37
|
const routeIdentifier = (method, signature) => `${method}.${signature}`;
|
|
39
|
-
const routeCoverage = core.routeCoverage = {
|
|
40
|
-
_normalizedUrlMapper: new NormalizedUrlMapper(),
|
|
41
|
-
|
|
42
|
-
uriPathToNormalizedUrl(uriPath) {
|
|
43
|
-
return this._normalizedUrlMapper.map(uriPath);
|
|
44
|
-
},
|
|
45
38
|
|
|
39
|
+
const routeCoverage = core.routeCoverage = {
|
|
46
40
|
discover(info) {
|
|
47
41
|
const id = routeIdentifier(info.method, info.signature);
|
|
48
42
|
if (routeInfo.get(id)) return;
|
|
49
43
|
|
|
50
44
|
logger.trace({ info }, 'Discovered new route:');
|
|
51
45
|
routeInfo.set(id, info);
|
|
52
|
-
this._normalizedUrlMapper.handleDiscover(info);
|
|
53
|
-
|
|
54
46
|
},
|
|
55
47
|
|
|
56
48
|
discoveryFinished() {
|
|
@@ -386,10 +386,14 @@ class ExpressInstrumentation {
|
|
|
386
386
|
// `value` is a terminal Layer with observable signatures.
|
|
387
387
|
// emit discovery after appending metadata.
|
|
388
388
|
if (value[kMetaKey]) {
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
const observables = this.generateObservables(metas, value.handle);
|
|
390
|
+
if (observables) {
|
|
391
|
+
if (!value[kMetaKey].observables) {
|
|
392
|
+
value[kMetaKey].observables = observables;
|
|
393
|
+
} else {
|
|
394
|
+
Object.assign(value[kMetaKey].observables, observables);
|
|
395
|
+
}
|
|
391
396
|
}
|
|
392
|
-
Object.assign(value[kMetaKey].observables, this.generateObservables(metas, value.handle));
|
|
393
397
|
self.discover(value[kMetaKey]);
|
|
394
398
|
}
|
|
395
399
|
}
|
|
@@ -411,31 +415,28 @@ class ExpressInstrumentation {
|
|
|
411
415
|
maybeLayer?.constructor?.name == 'Layer' &&
|
|
412
416
|
!maybeLayer?.stack?.length
|
|
413
417
|
) {
|
|
414
|
-
//
|
|
415
418
|
let _data = data.get(maybeLayer);
|
|
419
|
+
|
|
416
420
|
if (!_data) {
|
|
417
|
-
_data = {
|
|
421
|
+
_data = { paths: [] };
|
|
418
422
|
data.set(maybeLayer, _data);
|
|
419
423
|
}
|
|
420
424
|
|
|
421
425
|
// you can mount a router on itself
|
|
422
426
|
// prevent infinitely recursing into self-mounted routers
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (isNested) {
|
|
432
|
-
// todo: we don't support recursive router discovery/observation case atm
|
|
433
|
-
// stop to avoid infinite traversal
|
|
427
|
+
for (const visitedPath of _data.paths) {
|
|
428
|
+
// these conditions indicate recursive nesting at particular path
|
|
429
|
+
if (
|
|
430
|
+
path.length > visitedPath.length &&
|
|
431
|
+
visitedPath.every((el, i) => path[i] == el)
|
|
432
|
+
) {
|
|
434
433
|
path.pop();
|
|
435
434
|
continue loopKeys;
|
|
436
435
|
}
|
|
437
436
|
}
|
|
438
437
|
|
|
438
|
+
_data.paths.push([...path]); // copy because path argument mutates
|
|
439
|
+
|
|
439
440
|
const halt = cb(path, key, maybeLayer, target) === false;
|
|
440
441
|
if (halt) return;
|
|
441
442
|
}
|
|
@@ -500,9 +501,6 @@ class ExpressInstrumentation {
|
|
|
500
501
|
// build signature lookup based on each template (normalizeUri)
|
|
501
502
|
const map = templates.reduce((acc, routeTemplate) => {
|
|
502
503
|
if (!routeTemplate) routeTemplate = '/';
|
|
503
|
-
if (routeTemplate?.includes?.('typecheck')) {
|
|
504
|
-
// console.dir({ info, template });
|
|
505
|
-
}
|
|
506
504
|
acc[routeTemplate] = `${type}.${method}('${routeTemplate}', ${formattedHandler})`;
|
|
507
505
|
return acc;
|
|
508
506
|
}, {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/route-coverage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.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)",
|
|
@@ -20,14 +20,14 @@
|
|
|
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.35.0",
|
|
24
|
+
"@contrast/config": "1.50.0",
|
|
25
|
+
"@contrast/core": "1.55.0",
|
|
26
|
+
"@contrast/dep-hooks": "1.24.0",
|
|
27
27
|
"@contrast/fn-inspect": "^4.3.0",
|
|
28
|
-
"@contrast/logger": "1.
|
|
29
|
-
"@contrast/patcher": "1.
|
|
30
|
-
"@contrast/scopes": "1.
|
|
28
|
+
"@contrast/logger": "1.28.0",
|
|
29
|
+
"@contrast/patcher": "1.27.0",
|
|
30
|
+
"@contrast/scopes": "1.25.0",
|
|
31
31
|
"semver": "^7.6.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2025 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
|
-
const {
|
|
18
|
-
get,
|
|
19
|
-
set,
|
|
20
|
-
primordials: { StringPrototypeSubstr, StringPrototypeSplit }
|
|
21
|
-
} = require('@contrast/common');
|
|
22
|
-
|
|
23
|
-
class NormalizedUrlMapper {
|
|
24
|
-
constructor() {
|
|
25
|
-
this._db = {
|
|
26
|
-
// index by static routes e.g.
|
|
27
|
-
// '/' => {}
|
|
28
|
-
// '/home' => {}
|
|
29
|
-
static: new Map(),
|
|
30
|
-
// or segment-count for parameterized routes
|
|
31
|
-
// '2' => { '/users/:id', '/profile/:id' }
|
|
32
|
-
// '3' => { '/users/:id/orders', '/profile/:id/address' }
|
|
33
|
-
parameterized: {},
|
|
34
|
-
// regex is used instead of string path
|
|
35
|
-
regex: new Map(),
|
|
36
|
-
// dynamic beyond parameterization e.g. regex syntax, '/abc+', '/abc*', '/[v1|v1.1]/users'.
|
|
37
|
-
// we could key off of segment length here like we do above.
|
|
38
|
-
dynamic: {},
|
|
39
|
-
};
|
|
40
|
-
this._defaultDynamicRe = /\(|\?|\||\[|\*|\+|\{/;
|
|
41
|
-
this._hapiDynamicRe = /\(|\?|\||\[|\*|\+/;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
_getPathSegments(uriPath) {
|
|
45
|
-
if (!uriPath?.length) return null;
|
|
46
|
-
return StringPrototypeSplit.call(StringPrototypeSubstr.call(uriPath, 1), '/');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
_looksDynamic(segment, framework) {
|
|
50
|
-
if (framework == 'hapi') {
|
|
51
|
-
return this._hapiDynamicRe.test(segment);
|
|
52
|
-
}
|
|
53
|
-
// app.get('/::fiddle') will handle requests to '/:fiddle'
|
|
54
|
-
if (segment.includes('::')) return true;
|
|
55
|
-
return this._defaultDynamicRe.test(segment);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
_looksParamaterized(segment, framework) {
|
|
59
|
-
if (framework == 'hapi') {
|
|
60
|
-
return segment.startsWith('{');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// no point iterating last character
|
|
64
|
-
for (let idx = 0; idx < segment.length - 1; idx++) {
|
|
65
|
-
// '/foo::bar' is not parameterized (fastify) - maps to '/foo:bar'
|
|
66
|
-
// make sure ':' appears by itself
|
|
67
|
-
if (
|
|
68
|
-
segment[idx] == ':' &&
|
|
69
|
-
segment[idx - 1] != ':' &&
|
|
70
|
-
segment[idx + 1] != ':'
|
|
71
|
-
) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Tries to map a raw URL path to the "normalized" value used to declare the route.
|
|
81
|
-
* Aims to be as performant as possible.
|
|
82
|
-
* @param {string} uriPath path without queries
|
|
83
|
-
* @returns {}
|
|
84
|
-
*/
|
|
85
|
-
_query(uriPath) {
|
|
86
|
-
// check if static first
|
|
87
|
-
if (this._db.static.has(uriPath)) return this._db.static.get(uriPath);
|
|
88
|
-
|
|
89
|
-
// else check dynamic routes by segment count
|
|
90
|
-
const _segments = this._getPathSegments(uriPath);
|
|
91
|
-
const entriesToCheck = this._db.parameterized[_segments.length];
|
|
92
|
-
|
|
93
|
-
if (entriesToCheck) {
|
|
94
|
-
for (const [, route] of entriesToCheck.entries()) {
|
|
95
|
-
const { segments } = route;
|
|
96
|
-
if (segments.every((seg, idx) => !seg || seg == _segments[idx])) {
|
|
97
|
-
return route;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Registers route discovery meta into _db.
|
|
107
|
-
* @param {import('@contrast/common').RouteInfo} routeInfo
|
|
108
|
-
*/
|
|
109
|
-
handleDiscover(routeInfo) {
|
|
110
|
-
if (!routeInfo.normalizedUrl) {
|
|
111
|
-
// todo should log but don't have core
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let segments;
|
|
116
|
-
let dbIndex;
|
|
117
|
-
let isParameterized = false;
|
|
118
|
-
let isDynamic = false;
|
|
119
|
-
const isRegExp = routeInfo.normalizedUrl?.constructor?.name == 'RegExp';
|
|
120
|
-
|
|
121
|
-
if (!isRegExp) {
|
|
122
|
-
segments = this._getPathSegments(routeInfo.normalizedUrl);
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < segments.length; i++) {
|
|
125
|
-
const segment = segments[i];
|
|
126
|
-
// these heuristic checks may not scale for all frameworks. we may want to dispatch to
|
|
127
|
-
// a framework-specific strategy that can specialize in mapping to the _db entry index.
|
|
128
|
-
if (this._looksDynamic(segment, routeInfo.framework)) {
|
|
129
|
-
isDynamic = true;
|
|
130
|
-
} else if (this._looksParamaterized(segment, routeInfo.framework)) {
|
|
131
|
-
isParameterized = true;
|
|
132
|
-
// replace segments to check with undefined if parameterized
|
|
133
|
-
segments[i] = undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const meta = {
|
|
139
|
-
normalizedUrl: routeInfo.normalizedUrl,
|
|
140
|
-
signature: routeInfo.signature,
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if (isDynamic) {
|
|
144
|
-
dbIndex = `dynamic.${segments.length}`;
|
|
145
|
-
} else if (isParameterized) {
|
|
146
|
-
// can be both dynamic and parameterized, e.g. '/:file{.:ext}', '/api/(v1|v2)/:user'
|
|
147
|
-
// but that's not what we want in this case
|
|
148
|
-
dbIndex = `parameterized.${segments.length}`;
|
|
149
|
-
meta.segments = segments;
|
|
150
|
-
} else if (isRegExp) {
|
|
151
|
-
dbIndex = 'regex';
|
|
152
|
-
} else {
|
|
153
|
-
dbIndex = 'static';
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ensure appropriate collection is there
|
|
157
|
-
if (!get(this._db, dbIndex)) set(this._db, dbIndex, new Map());
|
|
158
|
-
get(this._db, dbIndex).set(routeInfo.normalizedUrl, meta);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Returns the normalizedUrl associated with raw uriPath. If the
|
|
163
|
-
* value can't be determined from internal _db, this return null.
|
|
164
|
-
* @param {string} uriPath
|
|
165
|
-
* @returns {string}
|
|
166
|
-
*/
|
|
167
|
-
map(uriPath) {
|
|
168
|
-
/** @type import('@contrast/common').RouteInfo */
|
|
169
|
-
const record = this._query(uriPath);
|
|
170
|
-
return record ? record.normalizedUrl : null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
module.exports = NormalizedUrlMapper;
|