@clear-capabilities/agentic-security-scanner 0.77.0 → 0.79.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/bin/.agentic-security/findings.json +1907 -0
- package/bin/.agentic-security/last-scan.json +1907 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +166 -0
- package/bin/.agentic-security/streak.json +20 -0
- package/bin/agentic-security.js +55 -9
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/476.index.js +5 -5
- package/dist/637.index.js +1 -1
- package/dist/700.index.js +138 -0
- package/dist/718.index.js +159 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +5 -0
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +181 -8
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +270 -19
- package/src/integrations/index.js +2 -1
- package/src/ir/callgraph.js +27 -7
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- package/src/llm-validator/index.js +7 -5
- package/src/mcp/audit.js +5 -0
- package/src/posture/calibration-drift.js +2 -1
- package/src/posture/calibration.js +16 -1
- package/src/posture/fix-history.js +8 -2
- package/src/posture/profile.js +4 -5
- package/src/posture/rule-overrides.js +2 -3
- package/src/posture/rule-pack-signing.js +2 -3
- package/src/posture/rule-synthesis.js +5 -6
- package/src/posture/security-trend.js +4 -7
- package/src/posture/state-dir.js +124 -0
- package/src/posture/streak.js +3 -0
- package/src/posture/suppressions.js +5 -8
- package/src/posture/triage.js +16 -5
- package/src/posture/validator-metrics.js +3 -6
- package/src/report/index.js +23 -2
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +78 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/rust.js +26 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/binary-metadata.js +124 -0
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/vendor-detect.js +144 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3ec7e435269654d09e2168e1cd8a6586f88b9c2edce86f48c18a2dc5f321e358 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin \u2014 SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"prepublishOnly": "npm run build && node -e \"const fs=require('fs');const cur=fs.existsSync('CHANGELOG.md')?fs.readFileSync('CHANGELOG.md','utf8'):'';const src=fs.readFileSync('../CHANGELOG.md','utf8');if(cur && cur!==src){console.error('scanner/CHANGELOG.md has local edits that differ from ../CHANGELOG.md.');console.error('Refusing to overwrite. Either commit the edit upstream first or rm scanner/CHANGELOG.md to accept the upstream copy.');process.exit(1);}fs.writeFileSync('CHANGELOG.md',src);\"",
|
|
55
55
|
"test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
56
56
|
"test:smoke": "node --test test/smoke.test.js",
|
|
57
|
-
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js",
|
|
58
|
-
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js",
|
|
59
|
-
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js",
|
|
57
|
+
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js",
|
|
58
|
+
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js",
|
|
59
|
+
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js",
|
|
60
60
|
"test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js",
|
|
61
61
|
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js",
|
|
62
62
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
|
|
@@ -55,22 +55,24 @@ const ASYNC_ITER_BODY_SOURCES = new Set([
|
|
|
55
55
|
* AST shape expected (parser-js.js neutral):
|
|
56
56
|
* { kind: 'call', callee: { kind: 'member', object: <expr>, prop: <string> }, args: [...] }
|
|
57
57
|
*/
|
|
58
|
-
export function describeChain(callExpr) {
|
|
58
|
+
export function describeChain(callExpr, opts = {}) {
|
|
59
59
|
if (!callExpr || callExpr.kind !== 'call') return null;
|
|
60
60
|
const ops = [];
|
|
61
61
|
let cur = callExpr;
|
|
62
|
-
// Walk leftward through .then/.catch/.finally chain.
|
|
63
62
|
while (cur && cur.kind === 'call' && cur.callee && cur.callee.kind === 'member' && PROMISE_CHAIN_METHODS.has(cur.callee.prop)) {
|
|
64
63
|
const arg = (cur.args || [])[0];
|
|
65
64
|
ops.unshift({
|
|
66
|
-
kind: cur.callee.prop,
|
|
65
|
+
kind: cur.callee.prop,
|
|
67
66
|
callback: arg && (arg.kind === 'ident' || arg.kind === 'arrow' || arg.kind === 'function') ? arg : null,
|
|
68
67
|
argIndex: 0,
|
|
69
68
|
});
|
|
70
69
|
cur = cur.callee.object;
|
|
71
70
|
}
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
let isPromise = isPromiseRoot(cur);
|
|
72
|
+
if (!isPromise && opts.summaryCache && opts.callGraph && cur && cur.kind === 'call') {
|
|
73
|
+
const name = typeof cur.callee === 'string' ? cur.callee : (cur.callee?.name || null);
|
|
74
|
+
if (name) isPromise = isAsyncSourceFromSummary(name, opts.summaryCache, opts.callGraph);
|
|
75
|
+
}
|
|
74
76
|
return { ops, rootCallee: cur, isPromise };
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -84,13 +86,20 @@ function isPromiseRoot(expr) {
|
|
|
84
86
|
}
|
|
85
87
|
if (c.kind === 'member') {
|
|
86
88
|
if (c.object && c.object.kind === 'ident' && c.object.name === 'Promise' && PROMISE_STATIC_METHODS.has(c.prop)) return true;
|
|
87
|
-
// any .xxxAsync() pattern, or .then-chainable: too noisy to assume; require
|
|
88
|
-
// an explicit await elsewhere or a known callee.
|
|
89
89
|
return /Async$/.test(c.prop) || /^(fetch|json|text|blob|formData)$/.test(c.prop);
|
|
90
90
|
}
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export function isAsyncSourceFromSummary(calleeName, summaryCache, callGraph) {
|
|
95
|
+
if (!calleeName || !summaryCache || !callGraph) return false;
|
|
96
|
+
const resolved = callGraph.resolve ? callGraph.resolve(calleeName) : null;
|
|
97
|
+
const qid = resolved && (resolved.qid || resolved);
|
|
98
|
+
if (typeof qid !== 'string') return false;
|
|
99
|
+
const sum = summaryCache.get(qid, new Set());
|
|
100
|
+
return !!(sum && sum.returnTainted);
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
/**
|
|
95
104
|
* Given a chain descriptor + a `sourceTainted` boolean indicating whether
|
|
96
105
|
* the root callee's resolved value is tainted, return whether each callback
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Pre-computed taint summaries for popular npm/pip packages.
|
|
2
|
+
//
|
|
3
|
+
// When the taint engine encounters a call to an external function that
|
|
4
|
+
// the call graph can't resolve (e.g., lodash.merge from node_modules),
|
|
5
|
+
// it checks this registry as a fallback. Each entry describes whether
|
|
6
|
+
// the function's return value carries taint and whether any parameters
|
|
7
|
+
// are mutated (tainted by reference).
|
|
8
|
+
//
|
|
9
|
+
// Format: same as SummaryCache entries.
|
|
10
|
+
// { returnTainted: bool, mutatedParams: Set<paramIndex-as-string> }
|
|
11
|
+
//
|
|
12
|
+
// Convention: param indices are STRING keys ('0', '1', ...) because
|
|
13
|
+
// SummaryCache uses param names, and for external functions we don't
|
|
14
|
+
// know names — we use positional indices instead.
|
|
15
|
+
|
|
16
|
+
const S = (returnTainted, mutatedIndices = []) => ({
|
|
17
|
+
returnTainted,
|
|
18
|
+
mutatedParams: new Set(mutatedIndices.map(String)),
|
|
19
|
+
taintedGlobals: new Set(),
|
|
20
|
+
findings: [],
|
|
21
|
+
_builtin: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const BUILTIN_SUMMARIES = new Map([
|
|
25
|
+
// ── Lodash ──────────────────────────────────────────────────────────────
|
|
26
|
+
['_.merge', S(true, [0])],
|
|
27
|
+
['_.defaultsDeep', S(true, [0])],
|
|
28
|
+
['_.defaults', S(true, [0])],
|
|
29
|
+
['_.extend', S(true, [0])],
|
|
30
|
+
['_.assign', S(true, [0])],
|
|
31
|
+
['_.assignIn', S(true, [0])],
|
|
32
|
+
['_.set', S(false, [0])],
|
|
33
|
+
['_.get', S(true)],
|
|
34
|
+
['_.pick', S(true)],
|
|
35
|
+
['_.omit', S(true)],
|
|
36
|
+
['_.cloneDeep', S(true)],
|
|
37
|
+
['_.clone', S(true)],
|
|
38
|
+
['_.map', S(true)],
|
|
39
|
+
['_.filter', S(true)],
|
|
40
|
+
['_.find', S(true)],
|
|
41
|
+
['_.reduce', S(true)],
|
|
42
|
+
['_.flatten', S(true)],
|
|
43
|
+
['_.compact', S(true)],
|
|
44
|
+
['_.uniq', S(true)],
|
|
45
|
+
['_.groupBy', S(true)],
|
|
46
|
+
['_.keyBy', S(true)],
|
|
47
|
+
['_.values', S(true)],
|
|
48
|
+
['_.keys', S(false)],
|
|
49
|
+
['_.identity', S(true)],
|
|
50
|
+
|
|
51
|
+
// ── Node.js core ────────────────────────────────────────────────────────
|
|
52
|
+
['JSON.parse', S(true)],
|
|
53
|
+
['JSON.stringify', S(true)],
|
|
54
|
+
['Buffer.from', S(true)],
|
|
55
|
+
['Buffer.concat', S(true)],
|
|
56
|
+
['querystring.parse', S(true)],
|
|
57
|
+
['url.parse', S(true)],
|
|
58
|
+
['path.join', S(true)],
|
|
59
|
+
['path.resolve', S(true)],
|
|
60
|
+
['util.format', S(true)],
|
|
61
|
+
|
|
62
|
+
// ── Express / HTTP ──────────────────────────────────────────────────────
|
|
63
|
+
['express.json', S(false)],
|
|
64
|
+
['express.urlencoded',S(false)],
|
|
65
|
+
['bodyParser.json', S(false)],
|
|
66
|
+
['cors', S(false)],
|
|
67
|
+
|
|
68
|
+
// ── Database clients ────────────────────────────────────────────────────
|
|
69
|
+
['pool.query', S(true)],
|
|
70
|
+
['client.query', S(true)],
|
|
71
|
+
['db.query', S(true)],
|
|
72
|
+
['db.all', S(true)],
|
|
73
|
+
['db.get', S(true)],
|
|
74
|
+
['db.run', S(false)],
|
|
75
|
+
['knex.raw', S(true)],
|
|
76
|
+
['knex.select', S(true)],
|
|
77
|
+
|
|
78
|
+
// ── HTTP clients ────────────────────────────────────────────────────────
|
|
79
|
+
['axios.get', S(true)],
|
|
80
|
+
['axios.post', S(true)],
|
|
81
|
+
['axios.put', S(true)],
|
|
82
|
+
['axios.patch', S(true)],
|
|
83
|
+
['axios.delete', S(true)],
|
|
84
|
+
['axios.request', S(true)],
|
|
85
|
+
['fetch', S(true)],
|
|
86
|
+
['got', S(true)],
|
|
87
|
+
['got.get', S(true)],
|
|
88
|
+
['got.post', S(true)],
|
|
89
|
+
['superagent.get', S(true)],
|
|
90
|
+
['superagent.post', S(true)],
|
|
91
|
+
|
|
92
|
+
// ── Crypto / hashing (return is derived, not tainted) ───────────────────
|
|
93
|
+
['crypto.createHash', S(false)],
|
|
94
|
+
['crypto.randomBytes',S(false)],
|
|
95
|
+
['bcrypt.hash', S(false)],
|
|
96
|
+
['bcrypt.compare', S(false)],
|
|
97
|
+
|
|
98
|
+
// ── Sanitizers (return is clean) ────────────────────────────────────────
|
|
99
|
+
['parseInt', S(false)],
|
|
100
|
+
['parseFloat', S(false)],
|
|
101
|
+
['Number', S(false)],
|
|
102
|
+
['Boolean', S(false)],
|
|
103
|
+
['encodeURIComponent',S(false)],
|
|
104
|
+
['encodeURI', S(false)],
|
|
105
|
+
['DOMPurify.sanitize',S(false)],
|
|
106
|
+
['validator.escape', S(false)],
|
|
107
|
+
['he.encode', S(false)],
|
|
108
|
+
|
|
109
|
+
// ── Python stdlib (matched by callee name) ──────────────────────────────
|
|
110
|
+
['json.loads', S(true)],
|
|
111
|
+
['json.dumps', S(true)],
|
|
112
|
+
['int', S(false)],
|
|
113
|
+
['float', S(false)],
|
|
114
|
+
['str', S(true)],
|
|
115
|
+
['shlex.quote', S(false)],
|
|
116
|
+
['html.escape', S(false)],
|
|
117
|
+
['bleach.clean', S(false)],
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
export function lookupBuiltinSummary(calleeName) {
|
|
121
|
+
if (!calleeName || typeof calleeName !== 'string') return null;
|
|
122
|
+
const direct = BUILTIN_SUMMARIES.get(calleeName);
|
|
123
|
+
if (direct) return direct;
|
|
124
|
+
const lastDot = calleeName.lastIndexOf('.');
|
|
125
|
+
if (lastDot > 0) {
|
|
126
|
+
const short = calleeName.slice(lastDot + 1);
|
|
127
|
+
const fallback = BUILTIN_SUMMARIES.get(short);
|
|
128
|
+
if (fallback) return null;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -139,12 +139,32 @@ export const CATALOG = [
|
|
|
139
139
|
{ kind: 'source', id: 'go-gin-query', language: 'go', framework: 'gin', match: { type: 'call', callee: 'Query' }, label: 'c.Query (gin)' },
|
|
140
140
|
{ kind: 'source', id: 'go-gin-bindjson',language:'go', framework: 'gin', match: { type: 'call', callee: 'BindJSON' }, label: 'c.BindJSON (gin)' },
|
|
141
141
|
{ kind: 'source', id: 'go-echo-param', language: 'go', framework: 'echo', match: { type: 'call', callee: 'Param' }, label: 'c.Param (echo)' },
|
|
142
|
+
{ kind: 'source', id: 'go-gin-postform', language: 'go', framework: 'gin', match: { type: 'call', callee: 'PostForm' }, label: 'c.PostForm (gin)' },
|
|
143
|
+
{ kind: 'source', id: 'go-gin-shouldbind',language: 'go', framework: 'gin', match: { type: 'call', callee: 'ShouldBind' }, label: 'c.ShouldBind (gin)' },
|
|
144
|
+
{ kind: 'source', id: 'go-gin-shouldbindjson',language:'go',framework:'gin', match: { type: 'call', callee: 'ShouldBindJSON' },label: 'c.ShouldBindJSON (gin)' },
|
|
145
|
+
{ kind: 'source', id: 'go-echo-formvalue',language: 'go', framework: 'echo', match: { type: 'call', callee: 'FormValue' }, label: 'c.FormValue (echo)' },
|
|
146
|
+
{ kind: 'source', id: 'go-echo-queryparam',language:'go', framework: 'echo', match: { type: 'call', callee: 'QueryParam' }, label: 'c.QueryParam (echo)' },
|
|
147
|
+
{ kind: 'source', id: 'go-echo-bind', language: 'go', framework: 'echo', match: { type: 'call', callee: 'Bind' }, label: 'c.Bind (echo)' },
|
|
148
|
+
{ kind: 'source', id: 'go-chi-urlparam', language: 'go', framework: 'chi', match: { type: 'call', callee: 'URLParam' }, label: 'chi.URLParam' },
|
|
149
|
+
{ kind: 'source', id: 'go-r-postformvalue',language:'go', framework:'net/http',match:{type:'call',callee:'PostFormValue'}, label: 'r.PostFormValue' },
|
|
150
|
+
{ kind: 'source', id: 'go-fiber-body', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Body' }, label: 'c.Body (fiber)' },
|
|
151
|
+
{ kind: 'source', id: 'go-fiber-query', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Query' }, label: 'c.Query (fiber)' },
|
|
152
|
+
{ kind: 'source', id: 'go-fiber-params', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Params' }, label: 'c.Params (fiber)' },
|
|
153
|
+
{ kind: 'source', id: 'go-fiber-formvalue',language:'go', framework: 'fiber', match: { type: 'call', callee: 'FormValue' }, label: 'c.FormValue (fiber)' },
|
|
154
|
+
{ kind: 'source', id: 'go-fiber-cookies', language: 'go', framework: 'fiber', match: { type: 'call', callee: 'Cookies' }, label: 'c.Cookies (fiber)' },
|
|
155
|
+
{ kind: 'source', id: 'go-fiber-bodyparser',language:'go',framework:'fiber', match: { type: 'call', callee: 'BodyParser' }, label: 'c.BodyParser (fiber)' },
|
|
156
|
+
{ kind: 'source', id: 'go-buffalo-param', language: 'go', framework: 'buffalo',match: { type: 'call', callee: 'Param' }, label: 'c.Param (buffalo)' },
|
|
157
|
+
{ kind: 'source', id: 'go-buffalo-request',language:'go', framework:'buffalo',match: { type: 'member', object: 'c', prop: 'Request' }, label: 'c.Request (buffalo)' },
|
|
158
|
+
{ kind: 'source', id: 'go-gorilla-vars', language: 'go', framework: 'gorilla',match: { type: 'call', callee: 'Vars' }, label: 'mux.Vars (gorilla)' },
|
|
142
159
|
|
|
143
160
|
// ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
|
|
144
161
|
{ kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
|
|
145
162
|
{ kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
|
|
146
163
|
{ kind: 'source', id: 'rb-rails-session', language: 'rb', framework: 'rails', match: { type: 'global', name: 'session' }, label: 'session (Rails)' },
|
|
147
164
|
{ kind: 'source', id: 'rb-env', language: 'rb', framework: 'stdlib',match: { type: 'global', name: 'ENV' }, label: 'ENV (Ruby)' },
|
|
165
|
+
{ kind: 'source', id: 'rb-sinatra-request-body',language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'body'}, label: 'request.body (Sinatra)' },
|
|
166
|
+
{ kind: 'source', id: 'rb-sinatra-request-env', language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'env'}, label: 'request.env (Sinatra)' },
|
|
167
|
+
{ kind: 'source', id: 'rb-sinatra-request-params',language:'rb',framework:'sinatra',match:{type:'member',object:'request',prop:'params'},label: 'request.params (Sinatra)' },
|
|
148
168
|
|
|
149
169
|
// ─── SOURCES (PHP) ────────────────────────────────────────────────────────
|
|
150
170
|
{ kind: 'source', id: 'php-request', language: 'php', framework: 'core', match: { type: 'global', name: '_REQUEST' }, label: '$_REQUEST' },
|
|
@@ -152,6 +172,13 @@ export const CATALOG = [
|
|
|
152
172
|
{ kind: 'source', id: 'php-post', language: 'php', framework: 'core', match: { type: 'global', name: '_POST' }, label: '$_POST' },
|
|
153
173
|
{ kind: 'source', id: 'php-cookie', language: 'php', framework: 'core', match: { type: 'global', name: '_COOKIE' }, label: '$_COOKIE' },
|
|
154
174
|
{ kind: 'source', id: 'php-server', language: 'php', framework: 'core', match: { type: 'global', name: '_SERVER' }, label: '$_SERVER' },
|
|
175
|
+
{ kind: 'source', id: 'php-symfony-query', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'query' }, label: '$request->query (Symfony)' },
|
|
176
|
+
{ kind: 'source', id: 'php-symfony-request', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'request' }, label: '$request->request (Symfony)' },
|
|
177
|
+
{ kind: 'source', id: 'php-symfony-cookies', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'cookies' }, label: '$request->cookies (Symfony)' },
|
|
178
|
+
{ kind: 'source', id: 'php-symfony-headers', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'headers' }, label: '$request->headers (Symfony)' },
|
|
179
|
+
{ kind: 'source', id: 'php-symfony-files', language: 'php', framework: 'symfony', match: { type: 'member', object: '$request', prop: 'files' }, label: '$request->files (Symfony)' },
|
|
180
|
+
{ kind: 'source', id: 'php-symfony-content', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'getContent' }, label: '$request->getContent() (Symfony)' },
|
|
181
|
+
{ kind: 'source', id: 'php-symfony-get', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'get' }, label: '$request->get() (Symfony)' },
|
|
155
182
|
|
|
156
183
|
// ─── SINKS (SQL — Python) ─────────────────────────────────────────────────
|
|
157
184
|
{ kind: 'sink', id: 'py-cursor-execute', language: 'py', framework: 'dbapi', match: { type: 'call', callee: 'execute' }, argIndex: 0,
|
|
@@ -189,6 +216,74 @@ export const CATALOG = [
|
|
|
189
216
|
vuln: { name: 'Native SQL Injection (EntityManager.createNativeQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
190
217
|
remediation: 'Use setParameter on the resulting Query.' } },
|
|
191
218
|
|
|
219
|
+
// ─── SINKS (SQL — Go) ──────────────────────────────────────────────────────
|
|
220
|
+
{ kind: 'sink', id: 'go-db-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
|
|
221
|
+
vuln: { name: 'SQL Injection (db.Query)', severity: 'critical', cwe: 'CWE-89',
|
|
222
|
+
remediation: 'Use parameterized queries: db.Query("SELECT * FROM t WHERE id = $1", id).' } },
|
|
223
|
+
{ kind: 'sink', id: 'go-db-queryrow', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'QueryRow' }, argIndex: 0,
|
|
224
|
+
vuln: { name: 'SQL Injection (db.QueryRow)', severity: 'critical', cwe: 'CWE-89',
|
|
225
|
+
remediation: 'Use parameterized queries: db.QueryRow("... WHERE id = $1", id).' } },
|
|
226
|
+
{ kind: 'sink', id: 'go-db-exec', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
227
|
+
vuln: { name: 'SQL Injection (db.Exec)', severity: 'critical', cwe: 'CWE-89',
|
|
228
|
+
remediation: 'Use parameterized queries with placeholder args.' } },
|
|
229
|
+
{ kind: 'sink', id: 'go-gorm-raw', language: 'go', framework: 'gorm', match: { type: 'call', callee: 'Raw' }, argIndex: 0,
|
|
230
|
+
vuln: { name: 'SQL Injection (gorm.Raw)', severity: 'critical', cwe: 'CWE-89',
|
|
231
|
+
remediation: 'Use gorm.Where with parameterized placeholders: db.Where("name = ?", name).' } },
|
|
232
|
+
{ kind: 'sink', id: 'go-gorm-exec', language: 'go', framework: 'gorm', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
233
|
+
vuln: { name: 'SQL Injection (gorm.Exec)', severity: 'critical', cwe: 'CWE-89',
|
|
234
|
+
remediation: 'Use parameterized queries: db.Exec("UPDATE t SET x = ?", val).' } },
|
|
235
|
+
{ kind: 'sink', id: 'go-fmt-fprintf', language: 'go', framework: 'fmt', match: { type: 'call', callee: 'Fprintf' }, argIndex: 1,
|
|
236
|
+
vuln: { name: 'XSS (fmt.Fprintf to ResponseWriter)', severity: 'high', cwe: 'CWE-79',
|
|
237
|
+
remediation: 'Use html/template for HTML output, not fmt.Fprintf with user input.' } },
|
|
238
|
+
|
|
239
|
+
// ─── SINKS (SQL — PHP) ─────────────────────────────────────────────────────
|
|
240
|
+
{ kind: 'sink', id: 'php-mysqli-query', language: 'php', framework: 'mysqli', match: { type: 'call', callee: 'mysqli_query' }, argIndex: 1,
|
|
241
|
+
vuln: { name: 'SQL Injection (mysqli_query)', severity: 'critical', cwe: 'CWE-89',
|
|
242
|
+
remediation: 'Use prepared statements: $stmt = $conn->prepare("SELECT * WHERE id = ?"); $stmt->bind_param("i", $id);' } },
|
|
243
|
+
{ kind: 'sink', id: 'php-pdo-query', language: 'php', framework: 'pdo', match: { type: 'call', callee: 'query' }, argIndex: 0,
|
|
244
|
+
vuln: { name: 'SQL Injection (PDO::query)', severity: 'critical', cwe: 'CWE-89',
|
|
245
|
+
remediation: 'Use PDO::prepare with bound parameters.' } },
|
|
246
|
+
{ kind: 'sink', id: 'php-pdo-exec', language: 'php', framework: 'pdo', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
247
|
+
vuln: { name: 'SQL Injection (PDO::exec)', severity: 'critical', cwe: 'CWE-89',
|
|
248
|
+
remediation: 'Use PDO::prepare with bound parameters.' } },
|
|
249
|
+
{ kind: 'sink', id: 'php-laravel-db-raw', language: 'php', framework: 'laravel', match: { type: 'call', callee: 'raw' }, argIndex: 0,
|
|
250
|
+
vuln: { name: 'SQL Injection (DB::raw)', severity: 'critical', cwe: 'CWE-89',
|
|
251
|
+
remediation: 'Use parameterized bindings: DB::select("SELECT * WHERE id = ?", [$id]).' } },
|
|
252
|
+
{ kind: 'sink', id: 'php-exec', language: 'php', framework: 'core', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
253
|
+
vuln: { name: 'Command Injection (exec)', severity: 'critical', cwe: 'CWE-78',
|
|
254
|
+
remediation: 'Use escapeshellarg() on each argument and avoid shell metacharacters.' } },
|
|
255
|
+
{ kind: 'sink', id: 'php-system', language: 'php', framework: 'core', match: { type: 'call', callee: 'system' }, argIndex: 0,
|
|
256
|
+
vuln: { name: 'Command Injection (system)', severity: 'critical', cwe: 'CWE-78',
|
|
257
|
+
remediation: 'Avoid system(); use proc_open with an argv array instead.' } },
|
|
258
|
+
{ kind: 'sink', id: 'php-shell-exec', language: 'php', framework: 'core', match: { type: 'call', callee: 'shell_exec' }, argIndex: 0,
|
|
259
|
+
vuln: { name: 'Command Injection (shell_exec)', severity: 'critical', cwe: 'CWE-78',
|
|
260
|
+
remediation: 'Avoid shell_exec(); sanitize with escapeshellarg() if unavoidable.' } },
|
|
261
|
+
|
|
262
|
+
// ─── SINKS (SQL/CMD — Ruby) ───────────────────────────────────────────────
|
|
263
|
+
{ kind: 'sink', id: 'rb-ar-where-string', language: 'rb', framework: 'rails', match: { type: 'call', callee: 'where' }, argIndex: 0,
|
|
264
|
+
vuln: { name: 'SQL Injection (ActiveRecord where string)', severity: 'critical', cwe: 'CWE-89',
|
|
265
|
+
remediation: 'Use hash conditions: User.where(name: params[:name]).' } },
|
|
266
|
+
{ kind: 'sink', id: 'rb-ar-find-by-sql', language: 'rb', framework: 'rails', match: { type: 'call', callee: 'find_by_sql' }, argIndex: 0,
|
|
267
|
+
vuln: { name: 'SQL Injection (find_by_sql)', severity: 'critical', cwe: 'CWE-89',
|
|
268
|
+
remediation: 'Use parameterized SQL: find_by_sql(["SELECT * WHERE id = ?", id]).' } },
|
|
269
|
+
{ kind: 'sink', id: 'rb-system', language: 'rb', framework: 'stdlib', match: { type: 'call', callee: 'system' }, argIndex: 0,
|
|
270
|
+
vuln: { name: 'Command Injection (Kernel.system)', severity: 'critical', cwe: 'CWE-78',
|
|
271
|
+
remediation: 'Use the array form: system("cmd", arg1, arg2).' } },
|
|
272
|
+
{ kind: 'sink', id: 'rb-exec', language: 'rb', framework: 'stdlib', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
273
|
+
vuln: { name: 'Command Injection (Kernel.exec)', severity: 'critical', cwe: 'CWE-78',
|
|
274
|
+
remediation: 'Use the array form: exec("cmd", arg1, arg2).' } },
|
|
275
|
+
{ kind: 'sink', id: 'rb-sinatra-erb', language: 'rb', framework: 'sinatra', match: { type: 'call', callee: 'erb' }, argIndex: 0,
|
|
276
|
+
vuln: { name: 'Server-Side Template Injection (Sinatra ERB)', severity: 'high', cwe: 'CWE-1336',
|
|
277
|
+
remediation: 'Use ERB auto-escaping. Never pass user input as the template name.' } },
|
|
278
|
+
|
|
279
|
+
// ─── SINKS (SQL — PHP / Symfony / Doctrine) ───────────────────────────────
|
|
280
|
+
{ kind: 'sink', id: 'php-symfony-createquery',language:'php',framework:'symfony',match:{type:'call',callee:'createQuery'}, argIndex: 0,
|
|
281
|
+
vuln: { name: 'DQL Injection (Doctrine createQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
282
|
+
remediation: 'Use DQL parameters: $em->createQuery("... WHERE e.id = :id")->setParameter("id", $id).' } },
|
|
283
|
+
{ kind: 'sink', id: 'php-doctrine-nativequery',language:'php',framework:'doctrine',match:{type:'call',callee:'createNativeQuery'},argIndex:0,
|
|
284
|
+
vuln: { name: 'SQL Injection (Doctrine createNativeQuery)', severity: 'critical', cwe: 'CWE-89',
|
|
285
|
+
remediation: 'Use bound parameters with createNativeQuery.' } },
|
|
286
|
+
|
|
192
287
|
// ─── SINKS (XSS / template — JS/TS / browser) ─────────────────────────────
|
|
193
288
|
{ kind: 'sink', id: 'js-innerHTML-assign', language: 'js', framework: 'dom', match: { type: 'member', object: '_any_', prop: 'innerHTML' }, argIndex: 'rhs',
|
|
194
289
|
vuln: { name: 'DOM XSS (innerHTML)', severity: 'high', cwe: 'CWE-79',
|
|
@@ -366,6 +461,18 @@ export const CATALOG = [
|
|
|
366
461
|
{ kind: 'source', id: 'py-tornado-get-arg', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_argument' }, argIndex: 0, label: 'tornado.get_argument', provenance: 'http-body' },
|
|
367
462
|
{ kind: 'source', id: 'py-tornado-get-args', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_arguments' }, argIndex: 0, label: 'tornado.get_arguments', provenance: 'http-body' },
|
|
368
463
|
{ kind: 'source', id: 'py-tornado-get-body', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_body_argument' }, argIndex: 0, label: 'tornado.get_body_argument', provenance: 'http-body' },
|
|
464
|
+
// Starlette / Litestar — async ASGI sources.
|
|
465
|
+
{ kind: 'source', id: 'py-starlette-json', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'json' }, label: 'request.json() (Starlette)', provenance: 'http-body' },
|
|
466
|
+
{ kind: 'source', id: 'py-starlette-form', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'form' }, label: 'request.form() (Starlette)', provenance: 'http-body' },
|
|
467
|
+
{ kind: 'source', id: 'py-starlette-body', language: 'py', framework: 'starlette', match: { type: 'call', callee: 'body' }, label: 'request.body() (Starlette)', provenance: 'http-body' },
|
|
468
|
+
{ kind: 'source', id: 'py-starlette-qparams',language: 'py', framework: 'starlette', match: { type: 'member', object: 'request', prop: 'query_params' }, label: 'request.query_params (Starlette)', provenance: 'url-param' },
|
|
469
|
+
{ kind: 'source', id: 'py-starlette-path', language: 'py', framework: 'starlette', match: { type: 'member', object: 'request', prop: 'path_params' }, label: 'request.path_params (Starlette)', provenance: 'path-param' },
|
|
470
|
+
{ kind: 'source', id: 'py-litestar-data', language: 'py', framework: 'litestar', match: { type: 'call', callee: 'data' }, label: 'request.data() (Litestar)', provenance: 'http-body' },
|
|
471
|
+
// Sanic — async Python web.
|
|
472
|
+
{ kind: 'source', id: 'py-sanic-args', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'args' }, label: 'request.args (Sanic)', provenance: 'url-param' },
|
|
473
|
+
{ kind: 'source', id: 'py-sanic-form', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'form' }, label: 'request.form (Sanic)', provenance: 'http-body' },
|
|
474
|
+
{ kind: 'source', id: 'py-sanic-json', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'json' }, label: 'request.json (Sanic)', provenance: 'http-body' },
|
|
475
|
+
{ kind: 'source', id: 'py-sanic-body', language: 'py', framework: 'sanic', match: { type: 'member', object: 'request', prop: 'body' }, label: 'request.body (Sanic)', provenance: 'http-body' },
|
|
369
476
|
// sys.argv — CLI input source. (os.environ already declared above.)
|
|
370
477
|
{ kind: 'source', id: 'py-sys-argv', language: 'py', framework: 'std', match: { type: 'member', object: 'sys', prop: 'argv' }, label: 'sys.argv', provenance: 'cli' },
|
|
371
478
|
// File reads.
|
|
@@ -216,4 +216,78 @@ export function federatedFindings(graph) {
|
|
|
216
216
|
return findings;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
// ── Intra-project cross-service taint ───────────────────────────────────────
|
|
220
|
+
//
|
|
221
|
+
// Detects HTTP client calls in one file whose target matches a route handler
|
|
222
|
+
// in another file within the same project. When tainted data flows into the
|
|
223
|
+
// client call's body, and the matching handler reads from req.body, emit a
|
|
224
|
+
// cross-service finding.
|
|
225
|
+
|
|
226
|
+
const _CLIENT_PATTERNS = [
|
|
227
|
+
{ re: /\bfetch\s*\(\s*['"`]([^'"`]+)['"`]/g, method: null, bodyArg: true },
|
|
228
|
+
{ re: /\baxios\.(\w+)\s*\(\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2, bodyArg: true },
|
|
229
|
+
{ re: /\brequests\.(\w+)\s*\(\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2, bodyArg: true },
|
|
230
|
+
{ re: /\bhttp\.NewRequest\s*\(\s*['"`](\w+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/g, method: 1, pathGroup: 2 },
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const _HANDLER_PATTERNS = [
|
|
234
|
+
{ re: /\bapp\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
235
|
+
{ re: /\brouter\.(get|post|put|patch|delete|HandleFunc)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
236
|
+
{ re: /\b@app\.(route|get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
function _normalizePath(p) {
|
|
240
|
+
return p.replace(/:[^/]+/g, '*').replace(/\{[^}]+\}/g, '*').replace(/<[^>]+>/g, '*').replace(/\/+$/, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function detectIntraProjectServiceEdges(fileContents) {
|
|
244
|
+
if (!fileContents || typeof fileContents !== 'object') return [];
|
|
245
|
+
const consumers = [];
|
|
246
|
+
const producers = [];
|
|
247
|
+
for (const [fp, raw] of Object.entries(fileContents)) {
|
|
248
|
+
if (!raw || typeof raw !== 'string') continue;
|
|
249
|
+
const lineOf = (idx) => raw.slice(0, idx).split('\n').length;
|
|
250
|
+
for (const pat of _CLIENT_PATTERNS) {
|
|
251
|
+
pat.re.lastIndex = 0;
|
|
252
|
+
for (const m of raw.matchAll(pat.re)) {
|
|
253
|
+
const method = pat.method ? (m[pat.method] || 'get').toLowerCase() : 'get';
|
|
254
|
+
const path = m[pat.pathGroup || 1];
|
|
255
|
+
consumers.push({ file: fp, line: lineOf(m.index), method, path: _normalizePath(path) });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const pat of _HANDLER_PATTERNS) {
|
|
259
|
+
pat.re.lastIndex = 0;
|
|
260
|
+
for (const m of raw.matchAll(pat.re)) {
|
|
261
|
+
const method = m[1].toLowerCase();
|
|
262
|
+
const path = m[2];
|
|
263
|
+
producers.push({ file: fp, line: lineOf(m.index), method: method === 'route' ? 'any' : method, path: _normalizePath(path) });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const findings = [];
|
|
268
|
+
for (const c of consumers) {
|
|
269
|
+
for (const p of producers) {
|
|
270
|
+
if (c.file === p.file) continue;
|
|
271
|
+
if (p.method !== 'any' && c.method !== p.method) continue;
|
|
272
|
+
if (c.path !== p.path && !c.path.endsWith(p.path)) continue;
|
|
273
|
+
findings.push({
|
|
274
|
+
id: `cross-service:${c.file}:${c.line}->${p.file}:${p.line}`,
|
|
275
|
+
file: p.file,
|
|
276
|
+
line: p.line,
|
|
277
|
+
vuln: 'Cross-Service Taint — HTTP client in one file targets handler in another',
|
|
278
|
+
severity: 'medium',
|
|
279
|
+
family: 'cross-service-taint',
|
|
280
|
+
cwe: 'CWE-346',
|
|
281
|
+
parser: 'CROSS-SERVICE',
|
|
282
|
+
confidence: 0.60,
|
|
283
|
+
description: `HTTP client call in ${c.file}:${c.line} targets the route handler at ${p.file}:${p.line}. If tainted data flows through the client body into the handler's sink, this is a cross-service injection path.`,
|
|
284
|
+
remediation: 'Validate and sanitize all data crossing service boundaries, even internal ones. Treat internal API inputs the same as external user input.',
|
|
285
|
+
source: { file: c.file, line: c.line, label: `${c.method.toUpperCase()} ${c.path}` },
|
|
286
|
+
sink: { file: p.file, line: p.line, label: `handler ${p.path}` },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return findings;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const _internal = { _parseSpec, _endpointsFor, _leafPathsOf, _responseFields, _requestFields, _normalizePath };
|