@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.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 +139 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +12 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// chrome-probe.mjs — Milestone 4, sub-project PNG/SVG/PDF export.
|
|
2
|
+
//
|
|
3
|
+
// Chrome/Chromium binary discovery, mirroring parser-py-cst.js's own
|
|
4
|
+
// already-proven probePythonAvailable() pattern exactly — this
|
|
5
|
+
// codebase's established convention for "optional local tool, detect
|
|
6
|
+
// and degrade gracefully" (see that file's own header comment for the
|
|
7
|
+
// full rationale this file inherits without repeating).
|
|
8
|
+
|
|
9
|
+
import * as cp from 'node:child_process';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
|
|
12
|
+
let _capability = null;
|
|
13
|
+
|
|
14
|
+
// A malformed env var (e.g. AGENTIC_SECURITY_CHROME_PROBE_TIMEOUT_MS=oops)
|
|
15
|
+
// must fall back to the default, never reach spawnSync as NaN — found by
|
|
16
|
+
// the final whole-branch review: `Number('oops')` is NaN, and NaN as a
|
|
17
|
+
// spawnSync `timeout` option throws ERR_OUT_OF_RANGE synchronously.
|
|
18
|
+
// _tryBinary's own try/catch swallows that throw and returns null for
|
|
19
|
+
// EVERY candidate, so probeChromeAvailable would misreport
|
|
20
|
+
// `no-chrome-found` on a machine with a perfectly working Chrome install.
|
|
21
|
+
//
|
|
22
|
+
// Blank/whitespace-only is treated as unset (falls back), not as `0` —
|
|
23
|
+
// found by this fix's own scoped re-review: `Number('')` is `0`, and a
|
|
24
|
+
// naive `n >= 0` guard let an empty-but-exported env var (a blanked
|
|
25
|
+
// .env line, an unset-but-exported CI var) through as a real `timeout:
|
|
26
|
+
// 0`, which spawnSync treats as NO timeout at all — an unbounded hang
|
|
27
|
+
// risk inside this exact capability probe, the failure class
|
|
28
|
+
// parser-py-cst.js's own probePythonAvailable() is already careful
|
|
29
|
+
// about. An explicit `"0"` is still honored (matches this file's
|
|
30
|
+
// pre-fix behavior for that literal value).
|
|
31
|
+
//
|
|
32
|
+
// Must be a safe INTEGER, not merely finite — found by a second scoped
|
|
33
|
+
// re-review, reproduced live: `Number.isFinite(1.5)` is true, so a
|
|
34
|
+
// fractional env var (e.g. "1.5") passed this guard unchanged and then
|
|
35
|
+
// hit spawnSync's own `timeout` option, which throws ERR_OUT_OF_RANGE
|
|
36
|
+
// for any non-integer — the exact class of uncaught throw this whole
|
|
37
|
+
// function exists to prevent, just for a different malformed input
|
|
38
|
+
// than the original NaN case. `-0` is rejected too (Object.is check):
|
|
39
|
+
// it is a safe integer and `-0 >= 0`, so without the explicit check it
|
|
40
|
+
// would silently mean "no timeout", one character away from the "0"
|
|
41
|
+
// this function deliberately allows.
|
|
42
|
+
// Exported test-only — the outcome-based integration tests (a real
|
|
43
|
+
// probe/export succeeding or not) can't distinguish `timeout: 0` from
|
|
44
|
+
// `timeout: <default>` when the underlying command finishes quickly
|
|
45
|
+
// either way, so a direct table of this function's own input/output
|
|
46
|
+
// pairs is the only thing that actually pins the "0"-vs-default
|
|
47
|
+
// boundary — found by a third scoped re-review: the blank-env-var
|
|
48
|
+
// integration test below passed even with the round-2 blank-handling
|
|
49
|
+
// bug fully reintroduced, for exactly this reason.
|
|
50
|
+
export function _validTimeoutMs(raw, fallback) {
|
|
51
|
+
const s = String(raw ?? '').trim();
|
|
52
|
+
if (!s) return fallback;
|
|
53
|
+
const n = Number(s);
|
|
54
|
+
if (!Number.isSafeInteger(n) || n < 0 || Object.is(n, -0)) return fallback;
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
const PROBE_TIMEOUT_MS = _validTimeoutMs(process.env.AGENTIC_SECURITY_CHROME_PROBE_TIMEOUT_MS, 5000);
|
|
58
|
+
|
|
59
|
+
// Real, common install locations, per platform. Deliberately excludes
|
|
60
|
+
// AGENTIC_SECURITY_CHROME_PATH — that override is handled separately in
|
|
61
|
+
// probeChromeAvailable() as an authoritative, non-fallback choice (see
|
|
62
|
+
// the comment there for why).
|
|
63
|
+
//
|
|
64
|
+
// KNOWN-GOOD ABSOLUTE PATHS ARE TRIED FIRST, PATH-resolvable bare names
|
|
65
|
+
// LAST — found by the final whole-branch review, reproduced live: with
|
|
66
|
+
// bare names tried first, a `chrome` shell script anywhere earlier on
|
|
67
|
+
// PATH than the real browser is silently preferred and then executed
|
|
68
|
+
// with this process's own render arguments. The `--version` output
|
|
69
|
+
// check (`_tryBinary` below) is a functional smoke test, not a security
|
|
70
|
+
// gate — it cannot distinguish a real Chrome/Chromium binary from any
|
|
71
|
+
// script that echoes a matching string. Preferring the well-known
|
|
72
|
+
// install locations narrows, though does not eliminate, this exposure
|
|
73
|
+
// (a compromised PATH entry named exactly `chrome`/`google-chrome`/etc.
|
|
74
|
+
// is still tried if no absolute-path candidate exists on this machine —
|
|
75
|
+
// this feature already assumes local machine trust, same as every other
|
|
76
|
+
// "optional local tool" this codebase shells out to). The `linux` list
|
|
77
|
+
// below was empty in this fix's first pass — found by the scoped
|
|
78
|
+
// re-review to make the whole reordering a no-op on the platform CI
|
|
79
|
+
// images actually run on, since every Linux candidate was a bare PATH
|
|
80
|
+
// name. Package-manager and snap install locations are now included.
|
|
81
|
+
function _candidatePaths() {
|
|
82
|
+
const onPath = ['google-chrome-stable', 'google-chrome', 'chromium-browser', 'chromium', 'chrome'];
|
|
83
|
+
const platformPaths = {
|
|
84
|
+
darwin: [
|
|
85
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
86
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
87
|
+
`${process.env.HOME || ''}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
88
|
+
],
|
|
89
|
+
win32: [
|
|
90
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
91
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
92
|
+
`${process.env.LOCALAPPDATA || ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
93
|
+
],
|
|
94
|
+
linux: [
|
|
95
|
+
'/usr/bin/google-chrome-stable',
|
|
96
|
+
'/usr/bin/google-chrome',
|
|
97
|
+
'/usr/bin/chromium-browser',
|
|
98
|
+
'/usr/bin/chromium',
|
|
99
|
+
'/opt/google/chrome/chrome',
|
|
100
|
+
'/snap/bin/chromium',
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const platformSpecific = platformPaths[process.platform] || [];
|
|
104
|
+
return [...platformSpecific, ...onPath];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tries one candidate binary. Returns {ok:true, chrome:bin} on a real,
|
|
108
|
+
// working Chrome/Chromium, or null on any failure (missing binary, spawn
|
|
109
|
+
// error, non-zero exit, unrecognized --version output) — never throws.
|
|
110
|
+
function _tryBinary(bin) {
|
|
111
|
+
if (!bin) return null;
|
|
112
|
+
// An absolute-path candidate that doesn't exist can't spawn — skip the
|
|
113
|
+
// spawnSync call entirely rather than let it throw ENOENT.
|
|
114
|
+
if (bin.includes('/') || bin.includes('\\')) {
|
|
115
|
+
if (!fs.existsSync(bin)) return null;
|
|
116
|
+
}
|
|
117
|
+
let r;
|
|
118
|
+
try {
|
|
119
|
+
r = cp.spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: PROBE_TIMEOUT_MS });
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
if (r.error || r.status !== 0) return null;
|
|
122
|
+
const out = (r.stdout || r.stderr || '');
|
|
123
|
+
if (!/Chrom(e|ium)/i.test(out)) return null;
|
|
124
|
+
return { ok: true, chrome: bin };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resetChromeProbe() { _capability = null; }
|
|
128
|
+
|
|
129
|
+
export function probeChromeAvailable() {
|
|
130
|
+
if (_capability) return _capability;
|
|
131
|
+
|
|
132
|
+
// An explicit override is authoritative, not just "try this one first
|
|
133
|
+
// then fall back": an operator (or CI environment) who deliberately
|
|
134
|
+
// pointed AGENTIC_SECURITY_CHROME_PATH at a binary wants a failure
|
|
135
|
+
// reported when that binary doesn't work — not a silent switch to
|
|
136
|
+
// whatever else this process happens to auto-detect on the machine.
|
|
137
|
+
const fromEnv = process.env.AGENTIC_SECURITY_CHROME_PATH;
|
|
138
|
+
if (fromEnv) {
|
|
139
|
+
const hit = _tryBinary(fromEnv);
|
|
140
|
+
_capability = hit || { ok: false, reason: 'chrome-path-invalid' };
|
|
141
|
+
return _capability;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const bin of _candidatePaths()) {
|
|
145
|
+
const hit = _tryBinary(bin);
|
|
146
|
+
if (hit) { _capability = hit; return _capability; }
|
|
147
|
+
}
|
|
148
|
+
_capability = { ok: false, reason: 'no-chrome-found' };
|
|
149
|
+
return _capability;
|
|
150
|
+
}
|
package/src/ir/parser-js.js
CHANGED
|
@@ -47,6 +47,30 @@ import presetTypescript from '@babel/preset-typescript';
|
|
|
47
47
|
let _nodeIdSeq = 0;
|
|
48
48
|
function nextNodeId() { return 'n' + (++_nodeIdSeq); }
|
|
49
49
|
|
|
50
|
+
// Round-5 lineage-engine finding, generalized in round 6: resolve a Babel
|
|
51
|
+
// object-property-shaped node's key (`ObjectProperty` from either an
|
|
52
|
+
// `ObjectExpression` or an `ObjectPattern` — both share the same
|
|
53
|
+
// `{computed, key}` shape) to either its real, distinct property name, or
|
|
54
|
+
// the explicit unknown marker '*' when the key is computed and not itself
|
|
55
|
+
// a resolvable literal. A computed key that IS a literal (`{[42]: v}`,
|
|
56
|
+
// `{['literal']: v}` — Babel still marks these `computed: true`) still
|
|
57
|
+
// resolves to that literal's own string form; a computed key that is an
|
|
58
|
+
// arbitrary expression (most commonly an Identifier, e.g. `{[field]: v}`)
|
|
59
|
+
// resolves to '*', NEVER to the key expression's own variable name — using
|
|
60
|
+
// `p.key.name` unconditionally there would attribute the property to the
|
|
61
|
+
// key expression's OWN name as if it were a real, distinct property,
|
|
62
|
+
// colliding e.g. `{ field: a, [field]: b }` onto one shared 'field' key.
|
|
63
|
+
// Originally fixed only for `ObjectExpression` (round 5); round 6 found the
|
|
64
|
+
// identical bug, unfixed, in `lhsPath`'s `ObjectPattern` (destructuring)
|
|
65
|
+
// branch and extracted this shared helper so a third instance of the same
|
|
66
|
+
// bug can't appear in some future object-key-reading code path. See
|
|
67
|
+
// DESIGN_INTRAPROCEDURAL.md §4 and CLAUDE.md's "three hop types" note.
|
|
68
|
+
function resolveObjectKey(p) {
|
|
69
|
+
return p.computed
|
|
70
|
+
? (p.key?.value != null ? String(p.key.value) : '*')
|
|
71
|
+
: (p.key?.name || (p.key?.value != null ? String(p.key.value) : '*'));
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
// Compact a Babel AST node into our exprDesc.
|
|
51
75
|
function exprOf(n) {
|
|
52
76
|
if (!n) return { kind: 'unknown' };
|
|
@@ -91,10 +115,50 @@ function exprOf(n) {
|
|
|
91
115
|
return { kind: 'union', branches: [exprOf(n.consequent), exprOf(n.alternate)] };
|
|
92
116
|
case 'ObjectExpression': return {
|
|
93
117
|
kind: 'object',
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
// Round-5 lineage-engine finding: for a COMPUTED property
|
|
119
|
+
// (`{[k]: v}`, `p.computed === true`), `p.key` is an arbitrary
|
|
120
|
+
// expression node, not a name — and an Identifier key expression
|
|
121
|
+
// (`k`) HAS a `.name` ('k'), same as a real, non-computed property
|
|
122
|
+
// literally named `k` would. Using `p.key.name` unconditionally
|
|
123
|
+
// therefore attributed a computed property to the key EXPRESSION's
|
|
124
|
+
// own variable name as if it were a real, distinct property name,
|
|
125
|
+
// colliding `{ k: user.ssn, [k]: user.email }` onto one shared 'k'
|
|
126
|
+
// key. A computed key IS still resolvable when it's itself a literal
|
|
127
|
+
// (`{[42]: v}`, `{['literal']: v}` — Babel marks these
|
|
128
|
+
// `computed: true` too, but `p.key.value` gives the real value), so
|
|
129
|
+
// only an Identifier/expression computed key (no `.value`) falls
|
|
130
|
+
// back to the literal string '*' — mirroring the EXISTING computed-
|
|
131
|
+
// MEMBER-access convention (`n.computed` handling just above, and
|
|
132
|
+
// `lhsPath`'s computed-write handling below) rather than inventing a
|
|
133
|
+
// new one. See DESIGN_INTRAPROCEDURAL.md §4 and CLAUDE.md's "three
|
|
134
|
+
// hop types" note.
|
|
135
|
+
//
|
|
136
|
+
// A `SpreadElement` (`{...user}`) previously fell out of the
|
|
137
|
+
// `.filter` entirely — `{...user}` and `{}` were byte-identical in
|
|
138
|
+
// the emitted IR, a real, pre-existing false negative (the identity
|
|
139
|
+
// vanishes, not merged, not flattened — found and documented but
|
|
140
|
+
// not fixed by a final review of an earlier plan). Represented here
|
|
141
|
+
// as a distinct `{spread: true, value: <expr>}` shape (no `key`
|
|
142
|
+
// field) rather than a regular `{key, value}` property, so
|
|
143
|
+
// `scanner/src/lineage/engine.js`'s `resolveExprIdentities` can
|
|
144
|
+
// merge the spread source's OWN field structure directly into this
|
|
145
|
+
// object's structure (see that file for the full mechanism) — a
|
|
146
|
+
// DIFFERENT, more precise handling than a computed-unknown-key
|
|
147
|
+
// property gets (that case folds into one opaque residual bucket; a
|
|
148
|
+
// spread's contents are fully known, just not yet assigned a key in
|
|
149
|
+
// THIS literal, so they can and should stay field-distinguished).
|
|
150
|
+
// Every consumer of `expr.props` outside this package
|
|
151
|
+
// (scanner/src/dataflow/engine.js, higher-order.js,
|
|
152
|
+
// privacy-deep-walker.js — audited before this change) reads only
|
|
153
|
+
// `p.value`, never `p.key`, so this shape is compatible with all of
|
|
154
|
+
// them without any change there — and several gain correct taint/
|
|
155
|
+
// privacy detection through object spread as a direct result, since
|
|
156
|
+
// the spread's value is no longer silently absent from `props`.
|
|
157
|
+
props: (n.properties || []).map(p => {
|
|
158
|
+
if (p.type === 'SpreadElement') return { spread: true, value: exprOf(p.argument) };
|
|
159
|
+
if (p.type !== 'ObjectProperty' || !p.key) return null;
|
|
160
|
+
return { key: resolveObjectKey(p), value: exprOf(p.value) };
|
|
161
|
+
}).filter(Boolean),
|
|
98
162
|
};
|
|
99
163
|
case 'ArrayExpression': return { kind: 'array', elements: (n.elements || []).map(exprOf) };
|
|
100
164
|
case 'SpreadElement': return exprOf(n.argument);
|
|
@@ -170,10 +234,33 @@ function lhsPath(n) {
|
|
|
170
234
|
return base + '.' + prop;
|
|
171
235
|
}
|
|
172
236
|
if (n.type === 'ObjectPattern') {
|
|
173
|
-
// Destructured: return an array of (key, alias) pairs the caller can
|
|
237
|
+
// Destructured: return an array of (key, alias) pairs the caller can
|
|
238
|
+
// iterate. Round 6: uses the same `resolveObjectKey` helper
|
|
239
|
+
// `ObjectExpression` uses above, so a non-literal computed key
|
|
240
|
+
// (`const { [field]: value } = user`) resolves to '*' instead of
|
|
241
|
+
// fabricating a colliding key from the key variable's own name — see
|
|
242
|
+
// `resolveObjectKey`'s header comment for the full rationale.
|
|
243
|
+
//
|
|
244
|
+
// A `RestElement` (`const {a, ...rest} = obj`) has its bound identifier
|
|
245
|
+
// at `.argument`, not `.value` (only a real `ObjectProperty` has
|
|
246
|
+
// `.value`) — using `.value` unconditionally here made `lhsPath`
|
|
247
|
+
// resolve to `null` for every rest binding (`lhsPath(undefined)` hits
|
|
248
|
+
// the `if (!n) return null;` guard before ever reaching the
|
|
249
|
+
// `RestElement` case below), so the consumer's `if (!alias) continue;`
|
|
250
|
+
// guard silently skipped it entirely: a real, pre-existing FR-306
|
|
251
|
+
// violation where the rest binding vanished with no trace, not merged,
|
|
252
|
+
// not flattened. `resolveObjectKey(p)` already correctly resolves to
|
|
253
|
+
// '*' for a RestElement (it has no `.key`/`.computed`), which is
|
|
254
|
+
// exactly the right marker: the consumer already treats a `'*'`-keyed
|
|
255
|
+
// destructured binding as a computed-unknown-key selection
|
|
256
|
+
// (`{kind:'member', object: initExpr, prop: '*'}`), and the lineage
|
|
257
|
+
// engine's existing wildcard-selection machinery (built for `obj[k]`)
|
|
258
|
+
// already resolves that correctly — conservatively aggregating the
|
|
259
|
+
// source's full field set, flagged widened. No engine-level change
|
|
260
|
+
// needed for this fix.
|
|
174
261
|
return { kind: 'object-pattern', props: (n.properties || []).map(p => ({
|
|
175
|
-
key:
|
|
176
|
-
alias: lhsPath(p.value),
|
|
262
|
+
key: resolveObjectKey(p),
|
|
263
|
+
alias: lhsPath(p.type === 'RestElement' ? p.argument : p.value),
|
|
177
264
|
}))};
|
|
178
265
|
}
|
|
179
266
|
if (n.type === 'ArrayPattern') {
|