@contrast/assess 1.6.0 → 1.8.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/dataflow/propagation/index.js +3 -0
- package/lib/dataflow/propagation/install/JSON/index.js +33 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +290 -0
- package/lib/dataflow/propagation/install/buffer.js +79 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
- package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
- package/lib/dataflow/propagation/install/sequelize.js +310 -0
- package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
- package/lib/dataflow/propagation/install/string/match.js +2 -2
- package/lib/dataflow/propagation/install/string/replace.js +9 -4
- package/lib/dataflow/sinks/common.js +10 -1
- package/lib/dataflow/sinks/index.js +30 -1
- package/lib/dataflow/sinks/install/express/index.js +29 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
- package/lib/dataflow/sinks/install/http.js +20 -5
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
- package/lib/dataflow/sinks/install/mongodb.js +297 -82
- package/lib/dataflow/sinks/install/mssql.js +9 -4
- package/lib/dataflow/sinks/install/mysql.js +20 -4
- package/lib/dataflow/sinks/install/postgres.js +25 -12
- package/lib/dataflow/sinks/install/sequelize.js +142 -0
- package/lib/dataflow/sinks/install/sqlite3.js +9 -4
- package/lib/dataflow/sources/handler.js +144 -26
- package/lib/dataflow/sources/index.js +6 -8
- package/lib/dataflow/sources/install/body-parser1.js +133 -0
- package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
- package/lib/dataflow/sources/install/express/index.js +31 -0
- package/lib/dataflow/sources/install/express/params.js +81 -0
- package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
- package/lib/dataflow/sources/install/http.js +32 -18
- package/lib/dataflow/sources/install/querystring.js +75 -0
- package/lib/dataflow/tag-utils.js +68 -1
- package/package.json +3 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function (core) {
|
|
21
|
+
core.assess.dataflow.sources.expressInstrumentation = {
|
|
22
|
+
install() {
|
|
23
|
+
callChildComponentMethodsSync(this, 'install');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
require('./params')(core);
|
|
28
|
+
require('./parsedUrl')(core);
|
|
29
|
+
|
|
30
|
+
return core.assess.dataflow.sources.expressInstrumentation;
|
|
31
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../../../propagation/common');
|
|
20
|
+
|
|
21
|
+
module.exports = function init(core) {
|
|
22
|
+
const { depHooks, patcher, logger } = core;
|
|
23
|
+
|
|
24
|
+
core.assess.dataflow.sources.expressInstrumentation.params = {
|
|
25
|
+
install() {
|
|
26
|
+
const name = 'Layer.prototype.match';
|
|
27
|
+
depHooks.resolve(
|
|
28
|
+
{ name: 'express', file: 'lib/router/layer.js' },
|
|
29
|
+
(Layer) => {
|
|
30
|
+
patcher.patch(Layer.prototype, 'match', {
|
|
31
|
+
name,
|
|
32
|
+
patchType,
|
|
33
|
+
post(data) {
|
|
34
|
+
const layer = data.obj;
|
|
35
|
+
|
|
36
|
+
// we can exit early if
|
|
37
|
+
// the layer doesn't match the request or
|
|
38
|
+
// the layer doesn't recognize any parameters
|
|
39
|
+
if (!data.result || !layer.keys || layer.keys.length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
44
|
+
|
|
45
|
+
if (!sourceContext) {
|
|
46
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sourceContext.parsedParams) {
|
|
51
|
+
logger.trace({ name }, 'values already tracked');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
core.assess.dataflow.sources.handle({
|
|
57
|
+
context: 'req.params',
|
|
58
|
+
name,
|
|
59
|
+
inputType: InputType.PARAMETER_VALUE,
|
|
60
|
+
stacktraceOpts: {
|
|
61
|
+
constructorOpt: data.hooked,
|
|
62
|
+
prependFrames: [data.orig]
|
|
63
|
+
},
|
|
64
|
+
data: layer.params,
|
|
65
|
+
sourceContext
|
|
66
|
+
});
|
|
67
|
+
sourceContext.parsedParams = true;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.error({ err, name }, 'unable to handle source');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return Layer;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return core.assess.dataflow.sources.expressInstrumentation.params;
|
|
81
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../../common');
|
|
20
|
+
|
|
21
|
+
module.exports = function init(core) {
|
|
22
|
+
const {
|
|
23
|
+
assess: {
|
|
24
|
+
dataflow: { sources }
|
|
25
|
+
},
|
|
26
|
+
depHooks,
|
|
27
|
+
patcher,
|
|
28
|
+
scopes
|
|
29
|
+
} = core;
|
|
30
|
+
|
|
31
|
+
core.assess.dataflow.sources.expressInstrumentation.parsedUrl = {
|
|
32
|
+
install() {
|
|
33
|
+
depHooks.resolve(
|
|
34
|
+
{ name: 'express', file: 'lib/middleware/init.js' },
|
|
35
|
+
/** @param {import('express/lib/middleware/init')} mw */
|
|
36
|
+
(mw) => {
|
|
37
|
+
const name = 'express.middleware.init';
|
|
38
|
+
patcher.patch(mw, 'init', {
|
|
39
|
+
name,
|
|
40
|
+
patchType,
|
|
41
|
+
post(data) {
|
|
42
|
+
data.result = patcher.patch(data.result, {
|
|
43
|
+
name: 'express.middleware.init.expressInit',
|
|
44
|
+
patchType,
|
|
45
|
+
pre(data) {
|
|
46
|
+
const { args: [req] } = data;
|
|
47
|
+
patcher.patch(data.args, '2', {
|
|
48
|
+
name: 'express.middleware.init.expressInit.next',
|
|
49
|
+
patchType,
|
|
50
|
+
pre(data) {
|
|
51
|
+
const sourceContext = scopes.sources.getStore()?.assess;
|
|
52
|
+
if (!sourceContext) return;
|
|
53
|
+
|
|
54
|
+
const sourceInfo = {
|
|
55
|
+
context: 'req._parsedUrl',
|
|
56
|
+
data: req._parsedUrl,
|
|
57
|
+
name,
|
|
58
|
+
sourceContext,
|
|
59
|
+
stacktraceOpts: {
|
|
60
|
+
constructorOpt: data.hooked
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
sources.handle({
|
|
65
|
+
...sourceInfo,
|
|
66
|
+
inputType: InputType.URI,
|
|
67
|
+
keys: ['href', 'path', 'pathname'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sources.handle({
|
|
71
|
+
...sourceInfo,
|
|
72
|
+
inputType: InputType.QUERYSTRING,
|
|
73
|
+
keys: ['query', 'search'],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return core.assess.dataflow.sources.expressInstrumentation.parsedUrl;
|
|
87
|
+
};
|
|
@@ -19,9 +19,9 @@ const { toLowerCase, InputType } = require('@contrast/common');
|
|
|
19
19
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const {
|
|
22
|
-
scopes
|
|
22
|
+
scopes,
|
|
23
23
|
instrumentation: { instrument },
|
|
24
|
-
assess: { dataflow
|
|
24
|
+
assess: { dataflow },
|
|
25
25
|
patcher,
|
|
26
26
|
} = core;
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ module.exports = function(core) {
|
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
const [, req, res] = data.args;
|
|
43
|
-
const store = sources.getStore();
|
|
43
|
+
const store = scopes.sources.getStore();
|
|
44
44
|
|
|
45
45
|
if (!store) {
|
|
46
46
|
logger.debug('cannot acquire store for assess request handling');
|
|
@@ -97,7 +97,6 @@ module.exports = function(core) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const headers = {};
|
|
100
|
-
const sourceInputType = InputType.HEADER;
|
|
101
100
|
const sourceName = 'ClientRequest';
|
|
102
101
|
|
|
103
102
|
store.assess = {
|
|
@@ -107,22 +106,37 @@ module.exports = function(core) {
|
|
|
107
106
|
findings: {},
|
|
108
107
|
};
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const sourceInfo = {
|
|
110
|
+
name: sourceName,
|
|
111
|
+
stacktraceOpts: {
|
|
112
|
+
constructorOpt: data.hooked,
|
|
113
|
+
prependFrames: [data.orig]
|
|
114
|
+
},
|
|
115
|
+
sourceContext: store.assess
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
{
|
|
112
120
|
context: 'req.headers',
|
|
121
|
+
inputType: InputType.HEADER,
|
|
113
122
|
data: req.headers,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
...sourceInfo,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
context: 'req',
|
|
127
|
+
keys: ['url'],
|
|
128
|
+
inputType: InputType.URI,
|
|
129
|
+
data: req,
|
|
130
|
+
...sourceInfo,
|
|
131
|
+
}
|
|
132
|
+
].forEach((sourceData) => {
|
|
133
|
+
const { inputType } = sourceData;
|
|
134
|
+
try {
|
|
135
|
+
dataflow.sources.handle(sourceData);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logger.error({ err, inputType, name: sourceName }, 'unable to handle http source');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
126
140
|
|
|
127
141
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
128
142
|
const header = toLowerCase(req.rawHeaders[i]);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../common');
|
|
20
|
+
|
|
21
|
+
module.exports = (core) => {
|
|
22
|
+
const { depHooks, patcher, logger } = core;
|
|
23
|
+
|
|
24
|
+
core.assess.dataflow.sources.querystringInstrumentation = {
|
|
25
|
+
install() {
|
|
26
|
+
const name = 'querystring.parse';
|
|
27
|
+
depHooks.resolve({ name: 'querystring' },
|
|
28
|
+
(querystring) => patcher.patch(querystring, 'parse', {
|
|
29
|
+
name,
|
|
30
|
+
patchType,
|
|
31
|
+
post({ args, hooked, orig, result }) {
|
|
32
|
+
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
33
|
+
const inputType = InputType.QUERYSTRING;
|
|
34
|
+
|
|
35
|
+
if (!sourceContext) {
|
|
36
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sourceContext.parsedQuery) {
|
|
41
|
+
logger.trace({ name }, 'values already tracked');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// We only run analysis for the `querystring` result when it's used
|
|
46
|
+
// as the framework's query parser
|
|
47
|
+
if (sourceContext.reqData?.queries === args[0]) {
|
|
48
|
+
try {
|
|
49
|
+
core.assess.dataflow.sources.handle({
|
|
50
|
+
context: 'req.query',
|
|
51
|
+
name,
|
|
52
|
+
inputType,
|
|
53
|
+
stacktraceOpts: {
|
|
54
|
+
constructorOpt: hooked,
|
|
55
|
+
prependFrames: [orig]
|
|
56
|
+
},
|
|
57
|
+
data: result,
|
|
58
|
+
sourceContext
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// we do not set the `parsedQuery` value here so that frameworks
|
|
62
|
+
// may handle queries in their own more specific manner.
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error({ err, name }, 'unable to handle source');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return core.assess.dataflow.sources.querystringInstrumentation;
|
|
74
|
+
};
|
|
75
|
+
|
|
@@ -68,6 +68,57 @@ function atomicSubset(tags, subsetStart, len) {
|
|
|
68
68
|
return ret;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function atomicMerge(firstTagRanges, secondTagRanges) {
|
|
72
|
+
const mergedRanges = [];
|
|
73
|
+
let i = 0;
|
|
74
|
+
let j = 0;
|
|
75
|
+
|
|
76
|
+
while (i < firstTagRanges.length && j < secondTagRanges.length) {
|
|
77
|
+
const start1 = firstTagRanges[i];
|
|
78
|
+
const end1 = firstTagRanges[i + 1];
|
|
79
|
+
const start2 = secondTagRanges[j];
|
|
80
|
+
const end2 = secondTagRanges[j + 1];
|
|
81
|
+
|
|
82
|
+
if (end1 < start2) {
|
|
83
|
+
mergedRanges.push(start1, end1);
|
|
84
|
+
i += 2;
|
|
85
|
+
} else if (end2 < start1) {
|
|
86
|
+
mergedRanges.push(start2, end2);
|
|
87
|
+
j += 2;
|
|
88
|
+
} else {
|
|
89
|
+
const mergedStart = Math.min(start1, start2);
|
|
90
|
+
const mergedEnd = Math.max(end1, end2);
|
|
91
|
+
mergedRanges.push(mergedStart, mergedEnd);
|
|
92
|
+
i += 2;
|
|
93
|
+
j += 2;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
while (i < firstTagRanges.length) {
|
|
98
|
+
mergedRanges.push(firstTagRanges[i], firstTagRanges[i + 1]);
|
|
99
|
+
i += 2;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
while (j < secondTagRanges.length) {
|
|
103
|
+
mergedRanges.push(secondTagRanges[j], secondTagRanges[j + 1]);
|
|
104
|
+
j += 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Merge adjacent ranges
|
|
108
|
+
const finalMergedRanges = [];
|
|
109
|
+
for (let k = 0; k < mergedRanges.length; k += 2) {
|
|
110
|
+
const start = mergedRanges[k];
|
|
111
|
+
let end = mergedRanges[k + 1];
|
|
112
|
+
while (k + 2 < mergedRanges.length && mergedRanges[k + 2] - end === 1) {
|
|
113
|
+
end = mergedRanges[k + 3];
|
|
114
|
+
k += 2;
|
|
115
|
+
}
|
|
116
|
+
finalMergedRanges.push(start, end);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return finalMergedRanges;
|
|
120
|
+
}
|
|
121
|
+
|
|
71
122
|
function createAppendTags(firstTags, secondTags, offset) {
|
|
72
123
|
const ret = Object.create(null);
|
|
73
124
|
const firstTagsObject = ensureObject(firstTags);
|
|
@@ -115,8 +166,24 @@ function createFullLengthCopyTags(tags, resultLength) {
|
|
|
115
166
|
return Object.keys(ret).length ? ret : null;
|
|
116
167
|
}
|
|
117
168
|
|
|
169
|
+
function createMergedTags(firstTags, secondTags) {
|
|
170
|
+
const ret = Object.create(null);
|
|
171
|
+
const firstTagsObject = ensureObject(firstTags);
|
|
172
|
+
const secondTagsObject = ensureObject(secondTags);
|
|
173
|
+
const tagNames = new Set([...Object.keys(firstTagsObject), ...Object.keys(secondTagsObject)]);
|
|
174
|
+
|
|
175
|
+
for (const tagName of tagNames) {
|
|
176
|
+
const newTagRanges = atomicMerge(ensureTagsImmutable(firstTagsObject, tagName), ensureTagsImmutable(secondTagsObject, tagName));
|
|
177
|
+
|
|
178
|
+
newTagRanges.length && (ret[tagName] = newTagRanges);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return Object.keys(ret).length ? ret : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
118
184
|
module.exports = {
|
|
119
185
|
createSubsetTags,
|
|
120
186
|
createAppendTags,
|
|
121
|
-
createFullLengthCopyTags
|
|
187
|
+
createFullLengthCopyTags,
|
|
188
|
+
createMergedTags
|
|
122
189
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@contrast/distringuish": "^4.1.0",
|
|
17
|
-
"@contrast/scopes": "1.
|
|
18
|
-
"@contrast/common": "1.
|
|
17
|
+
"@contrast/scopes": "1.4.0",
|
|
18
|
+
"@contrast/common": "1.11.0",
|
|
19
19
|
"parseurl": "^1.3.3"
|
|
20
20
|
}
|
|
21
21
|
}
|