@clear-capabilities/agentic-security-scanner 0.172.1 → 0.174.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/CHANGELOG.md +26 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +135 -11
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +27 -27
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +2 -0
- package/src/context-intelligence/config.js +3 -1
- package/src/context-intelligence/controls.js +49 -1
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +2 -2
- package/src/context-intelligence/review.js +68 -4
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +282 -24
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/language/haskell-findings.js +2 -0
- package/src/language/haskell-guards.js +1 -1
- package/src/language/haskell-ir.js +3 -1
- package/src/language/haskell-models.js +94 -0
- package/src/language/haskell-security-rules.js +20 -0
- package/src/language/nixos-hardening.js +72 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/sast/zip-slip.js +79 -0
- package/src/util/string-memo.js +38 -0
|
@@ -138,12 +138,69 @@ export function checkEgressForFiles(files, { scanRoot, model }) {
|
|
|
138
138
|
// other endpoint needs --allow-remote (or review.allowRemote), on top of the egress policy below, never instead of it.
|
|
139
139
|
if (model.remote && !model.remoteApproved) return { allowed: false, reason: 'remote-endpoint-not-approved (pass --allow-remote or set review.allowRemote after reviewing the egress policy)', file: files[0] || null, evaluated: false };
|
|
140
140
|
for (const file of files) {
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// Fail closed. A policy check that throws (a corrupt policy file, an unreadable path, a bug) is not an allow: the answer is "deny", with the
|
|
142
|
+
// reason, and nothing is sent. This is the only place that decides whether a prompt may leave the machine.
|
|
143
|
+
let d;
|
|
144
|
+
try { d = evaluateEgress({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file }); }
|
|
145
|
+
catch (err) { return { allowed: false, reason: `egress-policy-error: ${String((err && err.message) || err).slice(0, 120)}`, file, evaluated: true, failedClosed: true }; }
|
|
146
|
+
if (!d || d.allowed !== true) return { allowed: false, reason: (d && d.reason) || 'egress-policy-returned-no-decision', file, evaluated: true, ...(d ? {} : { failedClosed: true }) };
|
|
143
147
|
}
|
|
144
148
|
return { allowed: true, evaluated: true };
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// ---- model output hygiene -------------------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A model's text is untrusted. It can carry a command for a human or a script to run ("run curl ... | sh to confirm"). Nothing here executes model text, but
|
|
155
|
+
* these strings reach reports, tickets and fix suggestions that people copy from, so shell-looking content is removed and counted, never passed on.
|
|
156
|
+
*/
|
|
157
|
+
const FENCE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
|
|
158
|
+
const CMD_WORD = '(?:curl|wget|bash|sh|zsh|sudo|su|rm|chmod|chown|nc|ncat|netcat|python3?|node|perl|ruby|php|powershell|pwsh|cmd(?:\\.exe)?|eval|exec|scp|ssh|dd|mkfs|kill|pkill|git\\s+push|npm\\s+(?:i|install|publish)|pip3?\\s+install|docker\\s+run)';
|
|
159
|
+
const INLINE_CMD = new RegExp('`[^`\\n]*\\b' + CMD_WORD + '\\b[^`\\n]*`|\\$\\([^)]*\\)|\\b(?:curl|wget)\\s+[^\\s]+[^\\n]*\\|\\s*(?:sh|bash)\\b|(?:^|\\n)\\s*(?:\\$|#)\\s+' + CMD_WORD + '\\b[^\\n]*', 'gi');
|
|
160
|
+
export function stripUntrustedCommands(text) {
|
|
161
|
+
if (typeof text !== 'string' || !text) return { text: text ?? '', stripped: 0 };
|
|
162
|
+
let n = 0;
|
|
163
|
+
let out = text.replace(FENCE, () => { n++; return '[command block removed]'; });
|
|
164
|
+
out = out.replace(INLINE_CMD, () => { n++; return '[command removed]'; });
|
|
165
|
+
return { text: out, stripped: n };
|
|
166
|
+
}
|
|
167
|
+
function sanitizeReviewStrings(review) {
|
|
168
|
+
let total = 0;
|
|
169
|
+
const walk = (v, key) => {
|
|
170
|
+
if (typeof v === 'string') {
|
|
171
|
+
if (['promptHash', 'responseHash', 'contextHash', 'entrypointId', 'file', 'role', 'kind', 'conclusion', 'likelihood', 'questionPack', 'status'].includes(key)) return v;
|
|
172
|
+
const r = stripUntrustedCommands(v); total += r.stripped; return r.text;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, key));
|
|
175
|
+
if (v && typeof v === 'object') { const o = {}; for (const [k, x] of Object.entries(v)) o[k] = walk(x, k); return o; }
|
|
176
|
+
return v;
|
|
177
|
+
};
|
|
178
|
+
const out = walk(review, '');
|
|
179
|
+
if (total) out.untrustedCommandsStripped = total;
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Where a suggestion came from and what checked it. Attached to every review so a reader never mistakes a model's words for a finding. */
|
|
184
|
+
function provenanceOf(model, promptHash) {
|
|
185
|
+
let host = null; try { host = model.endpoint ? new URL(model.endpoint).hostname : null; } catch { host = null; }
|
|
186
|
+
const loopback = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
187
|
+
return { producer: 'model', provider: model.provider ?? null, model: model.model ?? null, endpointClass: !model.endpoint ? 'none' : loopback ? 'loopback' : 'remote', promptHash, checkedBy: 'deterministic-citation-verifier', promotedToFinding: false };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- prompt anonymization ---------------------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/** Replace each real file path with a stable pseudonym (`f_<hash>.<ext>`). The mapping stays in memory; only the pseudonyms leave. */
|
|
193
|
+
export function pseudonymizer(files) {
|
|
194
|
+
const toPseudo = new Map(); const toReal = new Map();
|
|
195
|
+
for (const f of [...new Set(files)].sort()) {
|
|
196
|
+
const ext = (/\.[A-Za-z0-9]{1,6}$/.exec(f) || [''])[0];
|
|
197
|
+
const p = `f_${sha256(f).slice(0, 10)}${ext}`;
|
|
198
|
+
toPseudo.set(f, p); toReal.set(p, f);
|
|
199
|
+
}
|
|
200
|
+
const swap = (text, map) => { let t = text; for (const [a, b] of [...map].sort((x, y) => y[0].length - x[0].length)) t = t.split(a).join(b); return t; };
|
|
201
|
+
return { toPseudo: (t) => swap(t, toPseudo), toReal: (t) => swap(t, toReal), size: toPseudo.size };
|
|
202
|
+
}
|
|
203
|
+
|
|
147
204
|
// ---- response parsing and verification ------------------------------------------------------------------------
|
|
148
205
|
|
|
149
206
|
function extractJson(raw) {
|
|
@@ -352,7 +409,12 @@ export async function runReviewOnce({ pack, entrypoints, manifests, packageIndex
|
|
|
352
409
|
const badRedact = redacted.find(r => !r.ok);
|
|
353
410
|
if (badRedact) return { state: 'skipped_by_policy', reason: `redaction refused: ${badRedact.reason}` };
|
|
354
411
|
|
|
355
|
-
|
|
412
|
+
// Policy-driven anonymization: file paths are replaced by pseudonyms before the prompt is built, and mapped back in the answer before its citations
|
|
413
|
+
// are checked against the real snapshot. The model never sees a real path, and what is hashed is what was sent.
|
|
414
|
+
const anon = limits.anonymizePrompts ? pseudonymizer(files) : null;
|
|
415
|
+
const sendTexts = anon ? redacted.map(r => anon.toPseudo(r.text)) : redacted.map(r => r.text);
|
|
416
|
+
const sendEntrypoints = anon ? entrypoints.map(e => JSON.parse(anon.toPseudo(JSON.stringify(e)))) : entrypoints;
|
|
417
|
+
const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints: sendEntrypoints, packageTexts: sendTexts, contextHash: manifests[0].contextHash });
|
|
356
418
|
const est = estimateTokens(prompt);
|
|
357
419
|
if (est.tokens > limits.maxTokensPerJob) return { state: 'skipped_by_policy', reason: `prompt is about ${est.tokens} tokens, over the ${limits.maxTokensPerJob} per-job limit` };
|
|
358
420
|
|
|
@@ -364,7 +426,9 @@ export async function runReviewOnce({ pack, entrypoints, manifests, packageIndex
|
|
|
364
426
|
return { state: 'failed', reason: `model call failed: ${String((err && err.message) || err).slice(0, 160)}`, retryable: isRetryableError(err) };
|
|
365
427
|
}
|
|
366
428
|
const usage = { inputTokensEstimated: est.tokens, outputTokensEstimated: Math.ceil(String(raw || '').length / 4), source: 'estimate', redactions: redacted.reduce((n, r) => n + r.redactions, 0) };
|
|
367
|
-
const
|
|
429
|
+
const rawReal = anon && typeof raw === 'string' ? anon.toReal(raw) : raw;
|
|
430
|
+
const review0 = verifyReview({ raw: rawReal, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
|
|
431
|
+
const review = { ...sanitizeReviewStrings(review0), modelProvenance: provenanceOf(model, promptHash), ...(anon ? { promptAnonymized: true } : {}) };
|
|
368
432
|
if (review.status === 'declined') return { state: 'partial', reason: `answer declined: ${review.declineReasons.join(',')}`, review, usage };
|
|
369
433
|
return { state: 'succeeded', reason: null, review, usage };
|
|
370
434
|
}
|
|
@@ -67,6 +67,17 @@ export function pathIsCoveredByPrefix(path, prefix) {
|
|
|
67
67
|
* - `set = {"x.z"}` does NOT cover "x.y" ✗
|
|
68
68
|
* - `set = {"x.y.z"}` does NOT cover "x.y" ✗ (we don't propagate UP)
|
|
69
69
|
*/
|
|
70
|
+
/**
|
|
71
|
+
* True iff some path in `set` lies STRICTLY BELOW `path` (`o.cmd` is below `o`). Reading `o` as a whole value (passing it to a function, serialising
|
|
72
|
+
* it) reads every field, so a tainted field taints the whole read; reading `o.safe` does not, because `o.cmd` is not below `o.safe`.
|
|
73
|
+
*/
|
|
74
|
+
export function hasTaintedDescendant(set, path) {
|
|
75
|
+
if (!set || typeof path !== 'string') return false;
|
|
76
|
+
const prefix = `${path}.`;
|
|
77
|
+
for (const p of set) if (typeof p === 'string' && p.startsWith(prefix)) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
export function isCoveredBy(set, path) {
|
|
71
82
|
if (!set || typeof path !== 'string') return false;
|
|
72
83
|
if (set.has(path)) return true;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Dynamic features the static analysis cannot resolve, reported instead of silently approximated (E2-03).
|
|
2
|
+
//
|
|
3
|
+
// Where the engine meets one of these it takes the sound direction (a write through an unknown key taints the whole container, a read through
|
|
4
|
+
// an unknown key reads the whole container) and loses precision. That is the right trade, but it must be visible: a result that depended on it is
|
|
5
|
+
// less precise than one that did not, and a reviewer should be able to see which functions contain such constructs.
|
|
6
|
+
//
|
|
7
|
+
// Pure: reads the Layer-1 IR only.
|
|
8
|
+
const LIMIT_KINDS = Object.freeze({
|
|
9
|
+
'computed-write': 'a write through a key that is not a literal (`o[k] = v`): the engine taints the whole container `o`, so clean sibling fields read afterwards are over-approximated as tainted',
|
|
10
|
+
'computed-read': 'a read through a key that is not a literal (`o[k]`): the engine cannot tell which field is read, so any tainted field of `o` reaches it',
|
|
11
|
+
'eval-like': 'code built at run time (`eval`, `new Function`, `setTimeout` with a string): its content is not analysed',
|
|
12
|
+
'dynamic-require': 'a module name computed at run time (`require(name)`, `import(name)`): the callee is unknown, so the call stays unresolved',
|
|
13
|
+
'reflective-call': 'a call through reflection (`Reflect.apply`, `fn.call/apply` on a computed target, `obj[name]()`): the callee is unknown',
|
|
14
|
+
'getter-or-proxy': 'property access that may run user code (`Proxy`, `Object.defineProperty` accessors): reads and writes may have effects the engine does not model',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const expressions = (e, acc = []) => {
|
|
18
|
+
if (!e || typeof e !== 'object') return acc;
|
|
19
|
+
acc.push(e);
|
|
20
|
+
for (const k of ['object', 'callee', 'left', 'right', 'value']) if (e[k] && typeof e[k] === 'object') expressions(e[k], acc);
|
|
21
|
+
for (const k of ['args', 'parts', 'elements', 'props', 'branches']) if (Array.isArray(e[k])) for (const x of e[k]) expressions(x && x.value && !x.kind ? x.value : x, acc);
|
|
22
|
+
return acc;
|
|
23
|
+
};
|
|
24
|
+
const nameOf = (c) => (typeof c === 'string' ? c : c && c.kind === 'ident' ? c.name : c && c.kind === 'member' ? `${nameOf(c.object)}.${c.prop}` : '');
|
|
25
|
+
|
|
26
|
+
/** @returns {{kind:string, line:number, detail:string, effect:string}[]} */
|
|
27
|
+
function limitsOfFunction(fn) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const add = (kind, line, detail) => out.push({ kind, line: Number.isInteger(line) ? line : (fn.line || 0), detail, effect: LIMIT_KINDS[kind] });
|
|
30
|
+
for (const n of Object.values(fn.cfg?.nodes || {})) {
|
|
31
|
+
if (n.kind === 'assign') {
|
|
32
|
+
if (typeof n.target === 'string' && /\.\*$/.test(n.target)) add('computed-write', n.line, n.target);
|
|
33
|
+
for (const x of expressions(n.source)) inspect(x, n.line, add);
|
|
34
|
+
} else if (n.kind === 'call') {
|
|
35
|
+
inspect({ kind: 'call', callee: n.callee, args: n.args }, n.line, add);
|
|
36
|
+
for (const a of n.args || []) for (const x of expressions(a)) inspect(x, n.line, add);
|
|
37
|
+
} else if (n.kind === 'return' && n.value) for (const x of expressions(n.value)) inspect(x, n.line, add);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inspect(x, line, add) {
|
|
43
|
+
if (x.kind === 'member' && x.prop === '*') add('computed-read', line, `${nameOf(x.object)}[…]`);
|
|
44
|
+
if (x.kind === 'call') {
|
|
45
|
+
const c = nameOf(x.callee);
|
|
46
|
+
if (/^(?:eval|Function|new Function|setTimeout|setInterval)$/.test(c)) { if (c === 'eval' || c === 'Function' || c === 'new Function' || (x.args && x.args[0] && x.args[0].kind !== 'function-value' && x.args[0].kind !== 'ident' && x.args[0].kind !== 'literal')) add('eval-like', line, c); else if ((c === 'setTimeout' || c === 'setInterval') && x.args && x.args[0] && x.args[0].kind === 'literal' && typeof x.args[0].value === 'string') add('eval-like', line, `${c}(string)`); }
|
|
47
|
+
if (c === 'require' && x.args && x.args[0] && x.args[0].kind !== 'literal') add('dynamic-require', line, 'require(<expression>)');
|
|
48
|
+
if (/^Reflect\.(?:apply|construct|get|set)$/.test(c)) add('reflective-call', line, c);
|
|
49
|
+
if (x.callee && x.callee.kind === 'member' && x.callee.prop === '*') add('reflective-call', line, `${nameOf(x.callee.object)}[…](…)`);
|
|
50
|
+
if (/^(?:Object\.defineProperty|Object\.defineProperties|Object\.setPrototypeOf)$/.test(c) || c === 'Proxy' || c === 'new Proxy') add('getter-or-proxy', line, c);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{perFile:Record<string,{functions:object[]}>}} projectIR
|
|
56
|
+
* @returns {{file:string, qid:string, function:string, kind:string, line:number, detail:string, effect:string}[]} sorted, stable
|
|
57
|
+
*/
|
|
58
|
+
export function analysisLimits(projectIR) {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const file of Object.keys(projectIR?.perFile || {}).sort()) {
|
|
61
|
+
for (const fn of projectIR.perFile[file].functions || []) {
|
|
62
|
+
for (const l of limitsOfFunction(fn)) out.push({ file, qid: String(fn.qid).replace(/@\d+$/, ''), function: fn.name, ...l });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || (a.kind < b.kind ? -1 : 1)));
|
|
66
|
+
}
|