@contrast/protect 1.3.0 → 1.5.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/error-handlers/index.js +7 -4
- package/lib/error-handlers/install/express4.js +2 -3
- package/lib/error-handlers/install/{fastify3.js → fastify.js} +13 -15
- package/lib/error-handlers/install/hapi.js +75 -0
- package/lib/error-handlers/install/koa2.js +1 -2
- package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
- package/lib/hardening/constants.js +20 -0
- package/lib/hardening/handlers.js +65 -0
- package/lib/hardening/index.js +29 -0
- package/lib/hardening/install/node-serialize0.js +59 -0
- package/lib/index.d.ts +3 -21
- package/lib/index.js +6 -46
- package/lib/input-analysis/handlers.js +198 -39
- package/lib/input-analysis/index.js +9 -6
- package/lib/input-analysis/install/body-parser1.js +20 -18
- package/lib/input-analysis/install/cookie-parser1.js +13 -15
- package/lib/input-analysis/install/express4.js +8 -13
- package/lib/input-analysis/install/fastify.js +86 -0
- package/lib/input-analysis/install/formidable1.js +4 -5
- package/lib/input-analysis/install/hapi.js +106 -0
- package/lib/input-analysis/install/http.js +80 -30
- package/lib/input-analysis/install/koa-body5.js +5 -10
- package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
- package/lib/input-analysis/install/koa2.js +13 -24
- package/lib/input-analysis/install/multer1.js +5 -6
- package/lib/input-analysis/install/qs6.js +7 -11
- package/lib/input-analysis/install/universal-cookie4.js +3 -7
- package/lib/input-analysis/ip-analysis.js +76 -0
- package/lib/input-analysis/virtual-patches.js +109 -0
- package/lib/input-tracing/handlers/index.js +92 -23
- package/lib/input-tracing/index.js +14 -18
- package/lib/input-tracing/install/child-process.js +13 -7
- package/lib/input-tracing/install/eval.js +60 -0
- package/lib/input-tracing/install/fs.js +4 -2
- package/lib/input-tracing/install/function.js +60 -0
- package/lib/input-tracing/install/http.js +63 -0
- package/lib/input-tracing/install/mongodb.js +20 -20
- package/lib/input-tracing/install/mysql.js +3 -2
- package/lib/input-tracing/install/postgres.js +5 -4
- package/lib/input-tracing/install/sequelize.js +7 -5
- package/lib/input-tracing/install/sqlite3.js +6 -4
- package/lib/input-tracing/install/vm.js +132 -0
- package/lib/make-source-context.js +8 -49
- package/lib/policy.js +134 -0
- package/lib/semantic-analysis/handlers.js +161 -0
- package/lib/semantic-analysis/index.js +38 -0
- package/package.json +7 -9
- package/lib/input-analysis/install/fastify3.js +0 -107
- package/lib/utils.js +0 -84
|
@@ -0,0 +1,161 @@
|
|
|
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 {
|
|
19
|
+
BLOCKING_MODES,
|
|
20
|
+
ProtectRuleMode: { OFF },
|
|
21
|
+
InputType,
|
|
22
|
+
isString,
|
|
23
|
+
simpleTraverse
|
|
24
|
+
} = require('@contrast/common');
|
|
25
|
+
|
|
26
|
+
const SINK_EXPLOIT_PATTERN_START = /(?:^|\\|\/)(?:sh|bash|zsh|ksh|tcsh|csh|fish|cmd)/;
|
|
27
|
+
const stripWhiteSpace = (str) => str.replace(/\s/g, '');
|
|
28
|
+
|
|
29
|
+
// The sink instrumentation for this rule is in `protect/lib/input-tracing/install/child-process.js
|
|
30
|
+
module.exports = function(core) {
|
|
31
|
+
const { protect: { agentLib, semanticAnalysis, throwSecurityException } } = core;
|
|
32
|
+
|
|
33
|
+
function handleResult(sourceContext, sinkContext, ruleId, mode, finding) {
|
|
34
|
+
const sinkResults = sourceContext.findings.semanticResultsMap[ruleId];
|
|
35
|
+
const result = {
|
|
36
|
+
blocked: false,
|
|
37
|
+
findings: { command: sinkContext.value },
|
|
38
|
+
sinkContext,
|
|
39
|
+
...finding
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
sourceContext.findings.semanticResultsMap[ruleId] = sinkResults ? [...sinkResults, result] : [result];
|
|
43
|
+
|
|
44
|
+
if (BLOCKING_MODES.includes(mode)) {
|
|
45
|
+
result.blocked = true;
|
|
46
|
+
const blockInfo = [mode, ruleId];
|
|
47
|
+
sourceContext.findings.securityException = blockInfo;
|
|
48
|
+
throwSecurityException(sourceContext);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
semanticAnalysis.handleCmdInjectionSemanticDangerous = function(sourceContext, sinkContext) {
|
|
53
|
+
const ruleId = 'cmd-injection-semantic-dangerous-paths';
|
|
54
|
+
const mode = sourceContext.policy[ruleId];
|
|
55
|
+
|
|
56
|
+
if (mode == OFF) return;
|
|
57
|
+
|
|
58
|
+
const result = agentLib.containsDangerousPath(sinkContext.value);
|
|
59
|
+
|
|
60
|
+
if (result) {
|
|
61
|
+
handleResult(sourceContext, sinkContext, ruleId, mode);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
semanticAnalysis.handleCmdInjectionSemanticChainedCommands = function(sourceContext, sinkContext) {
|
|
66
|
+
const ruleId = 'cmd-injection-semantic-chained-commands';
|
|
67
|
+
const mode = sourceContext.policy[ruleId];
|
|
68
|
+
|
|
69
|
+
if (mode == OFF) return;
|
|
70
|
+
|
|
71
|
+
const indexOfChaining = agentLib.indexOfChaining(sinkContext.value);
|
|
72
|
+
|
|
73
|
+
if (indexOfChaining != -1) {
|
|
74
|
+
handleResult(sourceContext, sinkContext, ruleId, mode);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
semanticAnalysis.handleCommandInjectionCommandBackdoors = function(sourceContext, sinkContext) {
|
|
79
|
+
const ruleId = 'cmd-injection-command-backdoors';
|
|
80
|
+
const mode = sourceContext.policy[ruleId];
|
|
81
|
+
|
|
82
|
+
if (mode == OFF) return;
|
|
83
|
+
|
|
84
|
+
const finding = findBackdoorInjection(sourceContext, sinkContext.value);
|
|
85
|
+
|
|
86
|
+
if (finding) {
|
|
87
|
+
handleResult(sourceContext, sinkContext, ruleId, mode, finding);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return semanticAnalysis;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Backdoor detection logic:
|
|
96
|
+
* - command is >= 2 chars
|
|
97
|
+
* - iterates over every piece of request and checks
|
|
98
|
+
* - the full value is the param to sink
|
|
99
|
+
* - the value matches a regex and ends the param to the sink
|
|
100
|
+
*/
|
|
101
|
+
function findBackdoorInjection(sourceContext, command) {
|
|
102
|
+
if (command?.length < 2) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const valuesOfInterest = {
|
|
107
|
+
[InputType.QUERYSTRING]: sourceContext.parsedQuery,
|
|
108
|
+
[InputType.PARAMETER_VALUE]: sourceContext.parsedParams,
|
|
109
|
+
[InputType.BODY]: sourceContext.parsedBody,
|
|
110
|
+
[InputType.COOKIE_VALUE]: sourceContext.parsedCookies,
|
|
111
|
+
[InputType.HEADER]: sourceContext.reqData.headers,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
let found = false;
|
|
115
|
+
for (let inputType in valuesOfInterest) {
|
|
116
|
+
const values = valuesOfInterest[inputType];
|
|
117
|
+
|
|
118
|
+
if (values && Object.keys(values).length) {
|
|
119
|
+
simpleTraverse(values, (path, type, value, obj) => {
|
|
120
|
+
if (
|
|
121
|
+
!found &&
|
|
122
|
+
value &&
|
|
123
|
+
type === 'Value' &&
|
|
124
|
+
isString(value) &&
|
|
125
|
+
isBackdoorDetected(value, command)
|
|
126
|
+
) {
|
|
127
|
+
let key;
|
|
128
|
+
key = inputType === InputType.HEADER ? obj.indexOf(command) - 1 : path[path.length - 1];
|
|
129
|
+
if (Number.isInteger(key) && obj[key]) {
|
|
130
|
+
key = obj[key];
|
|
131
|
+
}
|
|
132
|
+
path = path.length === 1 ? [] : Array.from(path).slice(0, path.length - 1);
|
|
133
|
+
inputType = path.length > 1 ? InputType.JSON_VALUE : inputType;
|
|
134
|
+
|
|
135
|
+
found = { key, inputType, path, value: command };
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* strips the whitespace of the request value and the command,
|
|
146
|
+
* checks if the command equals the request value
|
|
147
|
+
* or if the command looks like the start of a shell execution
|
|
148
|
+
* and ends with the request value passed to the sink
|
|
149
|
+
*
|
|
150
|
+
* @param {string} value from request key
|
|
151
|
+
*/
|
|
152
|
+
function isBackdoorDetected(requestValue, command) {
|
|
153
|
+
const normalizedValue = stripWhiteSpace(requestValue);
|
|
154
|
+
const normalizedCommand = stripWhiteSpace(command);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
normalizedValue === normalizedCommand ||
|
|
158
|
+
(normalizedCommand.endsWith(normalizedValue) &&
|
|
159
|
+
SINK_EXPLOIT_PATTERN_START.test(normalizedCommand))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* SEMANTIC ANALYSIS is a STAGE of Protect.
|
|
20
|
+
* The information about it can be found under some of the separate rules we support for this stage:
|
|
21
|
+
* https://protect-spec.prod.dotnet.contsec.com/rules/cmd-injection-semantic-dangerous-paths.html#semantic-analysis
|
|
22
|
+
* https://protect-spec.prod.dotnet.contsec.com/rules/cmd-injection-semantic-chained-commands.html#semantic-analysis
|
|
23
|
+
*
|
|
24
|
+
* To view other STAGES see https://protect-spec.prod.dotnet.contsec.com/guide/protect-types.html#protection-types
|
|
25
|
+
* @param {object} core composed dependencies
|
|
26
|
+
* @returns {object}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = function(core) {
|
|
29
|
+
const semanticAnalysis = core.protect.semanticAnalysis = {};
|
|
30
|
+
|
|
31
|
+
// load the interfaces that will be used by input tracing instrumentation
|
|
32
|
+
require('./handlers')(core);
|
|
33
|
+
|
|
34
|
+
// There is no `.install()` method as this STAGE does not introduce side effects on its own,
|
|
35
|
+
// it uses the instrumentation that's already in place for INPUT TRACING.
|
|
36
|
+
|
|
37
|
+
return semanticAnalysis;
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Protect support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
],
|
|
10
10
|
"main": "lib/index.js",
|
|
11
11
|
"types": "lib/index.d.ts",
|
|
12
|
-
"bin": {
|
|
13
|
-
"contrast-transpile": "lib/cli-rewriter.js"
|
|
14
|
-
},
|
|
15
12
|
"engines": {
|
|
16
13
|
"npm": ">= 8.4.0",
|
|
17
14
|
"node": ">= 14.15.0"
|
|
@@ -22,12 +19,13 @@
|
|
|
22
19
|
"dependencies": {
|
|
23
20
|
"@babel/template": "^7.16.7",
|
|
24
21
|
"@babel/types": "^7.16.8",
|
|
25
|
-
"@contrast/agent-lib": "^
|
|
26
|
-
"@contrast/common": "1.
|
|
27
|
-
"@contrast/core": "1.
|
|
28
|
-
"@contrast/
|
|
29
|
-
"@contrast/
|
|
22
|
+
"@contrast/agent-lib": "^5.1.0",
|
|
23
|
+
"@contrast/common": "1.1.1",
|
|
24
|
+
"@contrast/core": "1.4.0",
|
|
25
|
+
"@contrast/esm-hooks": "1.1.5",
|
|
26
|
+
"@contrast/scopes": "1.1.1",
|
|
30
27
|
"builtin-modules": "^3.2.0",
|
|
28
|
+
"ipaddr.js": "^2.0.1",
|
|
31
29
|
"semver": "^7.3.7"
|
|
32
30
|
}
|
|
33
31
|
}
|
|
@@ -1,107 +0,0 @@
|
|
|
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 { patchType } = require('../constants');
|
|
19
|
-
const { isSecurityException } = require('../../security-exception');
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Function that exports an install method to patch Fastify framework with our instrumentation
|
|
23
|
-
* @param {Object} core - the core Contrast object in v5
|
|
24
|
-
* @return {Object} object with install method and the other relative functions exported for testing purposes
|
|
25
|
-
*/
|
|
26
|
-
module.exports = (core) => {
|
|
27
|
-
const {
|
|
28
|
-
depHooks,
|
|
29
|
-
patcher,
|
|
30
|
-
logger,
|
|
31
|
-
scopes: { sources },
|
|
32
|
-
protect: { inputAnalysis },
|
|
33
|
-
} = core;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* registers a depHook for fastify module instrumentation
|
|
37
|
-
*/
|
|
38
|
-
function install() {
|
|
39
|
-
depHooks.resolve({ name: 'fastify', version: '>=3.0.0' }, (fastify) => patchFastify(fastify));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The patch function for the depHooks callback
|
|
44
|
-
* @param {Object} fastify the fastify object returned from requiring the module
|
|
45
|
-
* @returns a patched fastify object
|
|
46
|
-
*/
|
|
47
|
-
function patchFastify(fastify) {
|
|
48
|
-
return patcher.patch(fastify, {
|
|
49
|
-
name: 'fastify.build',
|
|
50
|
-
patchType,
|
|
51
|
-
post({ result: server }) {
|
|
52
|
-
server.addHook('preValidation', preValidationHook);
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Fastify lifecycle hook as defined in official docs.
|
|
59
|
-
* @external https://www.fastify.io/docs/latest/Reference/Hooks/#prevalidation
|
|
60
|
-
* @param {Fastify.Request} request incoming request
|
|
61
|
-
* @param {Fastify.Reply} reply unbuilt outgoing response
|
|
62
|
-
* @param {Function} done callback to signal the hook is finished.
|
|
63
|
-
*/
|
|
64
|
-
function preValidationHook(request, reply, done) {
|
|
65
|
-
const sourceContext = sources.getStore()?.protect;
|
|
66
|
-
let securityException;
|
|
67
|
-
|
|
68
|
-
if (!sourceContext) {
|
|
69
|
-
logger.debug('source context not available in fastify prevalidation hook');
|
|
70
|
-
} else {
|
|
71
|
-
try {
|
|
72
|
-
if (request.params) {
|
|
73
|
-
sourceContext.parsedParams = request.params;
|
|
74
|
-
inputAnalysis.handleUrlParams(sourceContext, request.params);
|
|
75
|
-
}
|
|
76
|
-
if (request.cookies) {
|
|
77
|
-
sourceContext.parsedCookies = request.cookies;
|
|
78
|
-
inputAnalysis.handleCookies(sourceContext, request.cookies);
|
|
79
|
-
}
|
|
80
|
-
if (request.body) {
|
|
81
|
-
sourceContext.parsedBody = request.body;
|
|
82
|
-
inputAnalysis.handleParsedBody(sourceContext, request.body);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (request.query) {
|
|
86
|
-
sourceContext.parsedQuery = request.query;
|
|
87
|
-
inputAnalysis.handleQueryParams(sourceContext, request.query);
|
|
88
|
-
}
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (isSecurityException(err)) {
|
|
91
|
-
securityException = err;
|
|
92
|
-
} else {
|
|
93
|
-
logger.error({ err }, 'Unexpected error during input analysis');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
done(securityException);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const fastify3Instrumentation = inputAnalysis.fastify3Instrumentation = {
|
|
102
|
-
preValidationHook,
|
|
103
|
-
install,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return fastify3Instrumentation;
|
|
107
|
-
};
|
package/lib/utils.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
/**
|
|
19
|
-
* simpleTraverse() walks an object and calls a user function for each key
|
|
20
|
-
* and string value. It is a "simple traverse" in that it
|
|
21
|
-
* 1) doesn't make value callbacks unless the value is a non-empty string
|
|
22
|
-
* 2) it only recognizes items that can be expressed in JSON, i.e., POJO
|
|
23
|
-
* and arrays.
|
|
24
|
-
* 3) it doesn't make callbacks for array indexes (though they appear in
|
|
25
|
-
* the path). array indexes are always numeric and are not a threat.
|
|
26
|
-
*
|
|
27
|
-
* N.B. the path array that is passed to the callback is a dynamic path; new
|
|
28
|
-
* keys are pushed and popped onto the path as simpleTraverse() walks the
|
|
29
|
-
* object. in order to capture the path at the time of the callback, the
|
|
30
|
-
* callback must copy the array, e.g., `path.slice()`, in order to "freeze"
|
|
31
|
-
* it at the time of the callback. the reason for this is that most keys/values
|
|
32
|
-
* are not going to be of interest, and there is no reason to create a new array
|
|
33
|
-
* unless the key/value is of interest.
|
|
34
|
-
*
|
|
35
|
-
* @param {Object} obj the object to traverse
|
|
36
|
-
* @param {Function} cb(path, type, value) is called for each non-array-index key
|
|
37
|
-
* and string value. It is not called for non-string or empty-string Values.
|
|
38
|
-
* path {[String]} the path prior to the 'Key' or 'Value'; includes array indexes.
|
|
39
|
-
* type {String} 'Key' or 'Value'
|
|
40
|
-
* value {String} the Key or Leaf string
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
43
|
-
function simpleTraverse(obj, cb) {
|
|
44
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const path = [];
|
|
48
|
-
/* eslint-disable complexity */
|
|
49
|
-
function traverse(obj) {
|
|
50
|
-
const isArray = Array.isArray(obj);
|
|
51
|
-
for (const k in obj) {
|
|
52
|
-
if (isArray) {
|
|
53
|
-
// if it is an array, store each index in path but don't call the
|
|
54
|
-
// callback on the index itself as they are just numeric strings.
|
|
55
|
-
path.push(k);
|
|
56
|
-
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
57
|
-
traverse(obj[k]);
|
|
58
|
-
} else if (typeof obj[k] === 'string' && obj[k]) {
|
|
59
|
-
cb(path, 'Value', obj[k]);
|
|
60
|
-
}
|
|
61
|
-
path.pop();
|
|
62
|
-
} else if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
63
|
-
cb(path, 'Key', k);
|
|
64
|
-
path.push(k);
|
|
65
|
-
traverse(obj[k]);
|
|
66
|
-
path.pop();
|
|
67
|
-
} else {
|
|
68
|
-
cb(path, 'Key', k);
|
|
69
|
-
// only callback if the value is a non-empty string
|
|
70
|
-
if (typeof obj[k] === 'string' && obj[k]) {
|
|
71
|
-
path.push(k);
|
|
72
|
-
cb(path, 'Value', obj[k]);
|
|
73
|
-
path.pop();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
traverse(obj);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
module.exports = {
|
|
83
|
-
simpleTraverse,
|
|
84
|
-
};
|