@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5
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 +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -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 +379 -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 +841 -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/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- 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 +144 -0
- package/src/shared/frontend-root.js +52 -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
package/src/engine.js
CHANGED
|
@@ -206,6 +206,7 @@ import { applyPathConstraints } from './posture/path-predicates.js';
|
|
|
206
206
|
import { buildProjectIR, buildProjectIRAsync } from './ir/index.js';
|
|
207
207
|
import { collectIrStats, irStatsTarget, writeIrStats } from './ir/ir-stats.js';
|
|
208
208
|
import { runDeepAnalysis } from './dataflow/index.js';
|
|
209
|
+
import { buildLineageGraph } from './lineage/index.js';
|
|
209
210
|
// v3 next-gen — Pillars 1, 4, 5, 6, 8, 9.
|
|
210
211
|
import { annotateCloneClusters, findCloneOutliers } from './posture/semantic-clone.js';
|
|
211
212
|
import { annotateAiProvenance } from './posture/ai-code-fingerprint.js';
|
|
@@ -9229,6 +9230,95 @@ function _deterministicFileTimings(timings) {
|
|
|
9229
9230
|
: (_inCi ? 'not requested (deep analysis defaults to off in CI)' : 'not requested')),
|
|
9230
9231
|
failure: _deepFailure,
|
|
9231
9232
|
};
|
|
9233
|
+
// Sub-project E, increment 5 — Data Flow Explorer lineage graph.
|
|
9234
|
+
// DELIBERATELY INDEPENDENT of AGENTIC_SECURITY_DEEP/_deepEnabled above
|
|
9235
|
+
// (see src/lineage/DESIGN_GRAPH_BUILDER.md §9.5): lineage analysis has no
|
|
9236
|
+
// degraded/non-IR-backed mode, so gating it on deep mode ALSO being on
|
|
9237
|
+
// would make AGENTIC_SECURITY_LINEAGE_DEEP=1 alone silently produce
|
|
9238
|
+
// nothing whenever deep mode itself was off.
|
|
9239
|
+
//
|
|
9240
|
+
// Undisclosed-until-final-review side effect (N-4): this block populates
|
|
9241
|
+
// the SAME `_sharedIR` memo the privacy-taint block further below reads
|
|
9242
|
+
// (`_privacyIrBacked = !!(_sharedIR && _sharedIR.perFile)`), so setting
|
|
9243
|
+
// AGENTIC_SECURITY_LINEAGE_DEEP=1 also upgrades privacy-taint annotation
|
|
9244
|
+
// to IR-backed (`privacyIrBacked: true` instead of `false`) even with
|
|
9245
|
+
// AGENTIC_SECURITY_DEEP itself off. A real, deliberate-once-noticed
|
|
9246
|
+
// consequence of the memo sharing, not a bug — but worth knowing about
|
|
9247
|
+
// before being surprised by it.
|
|
9248
|
+
const _lineageRequested = process.env.AGENTIC_SECURITY_LINEAGE_DEEP === '1';
|
|
9249
|
+
const _lineageStatus = {
|
|
9250
|
+
requested: _lineageRequested,
|
|
9251
|
+
enabled: false,
|
|
9252
|
+
reason: _lineageRequested ? null : 'not requested',
|
|
9253
|
+
failure: null,
|
|
9254
|
+
};
|
|
9255
|
+
let _lineageGraph = null;
|
|
9256
|
+
if (_lineageRequested) {
|
|
9257
|
+
_lineageStatus.enabled = true;
|
|
9258
|
+
const _lineageBudgetMs = parseInt(process.env.AGENTIC_SECURITY_LINEAGE_TIMEOUT_MS || '300000', 10);
|
|
9259
|
+
try {
|
|
9260
|
+
// Reuses the SAME _sharedIR memo _deepEnabled's own block above uses —
|
|
9261
|
+
// if deep mode already built it, this is free; if not, this is what
|
|
9262
|
+
// first triggers the build (the whole reason this gate is independent).
|
|
9263
|
+
const { perFile, callGraph } = _sharedIR || (_sharedIR = await _buildIR());
|
|
9264
|
+
const _lr = buildLineageGraph(callGraph, {
|
|
9265
|
+
// scanRoot can be null (e.g. an MCP scan_diff caller, or
|
|
9266
|
+
// runFullScan invoked directly with no scanRoot) — fall back to
|
|
9267
|
+
// undefined rather than inventing a repository name from the
|
|
9268
|
+
// process's own CWD basename, which would make lineage node ids
|
|
9269
|
+
// depend on wherever the process happened to be launched from and
|
|
9270
|
+
// collide across two unrelated repos sharing a directory basename.
|
|
9271
|
+
// `undefined` lets graph-builder.js's own `opts.repository ?? 'repo'`
|
|
9272
|
+
// default apply — the single source of truth for that literal.
|
|
9273
|
+
repository: scanRoot ? path.basename(path.resolve(scanRoot)) : undefined,
|
|
9274
|
+
// Milestone 2, Sub-project G, increment 1 (FR-408/AC-09): the real
|
|
9275
|
+
// scan root, threaded straight through — distinct from `repository`
|
|
9276
|
+
// above, which is only a basename. `index.js`'s own `buildLineageGraph`
|
|
9277
|
+
// uses this ONLY to load `.agentic-security/privacy-policy.json`
|
|
9278
|
+
// (never to derive `repository`/node ids), mirroring how `fileContents`
|
|
9279
|
+
// was threaded through for Sub-project B's own transit-evidence scan.
|
|
9280
|
+
scanRoot,
|
|
9281
|
+
deterministic: isDeterministic(),
|
|
9282
|
+
perFile,
|
|
9283
|
+
fileContents,
|
|
9284
|
+
});
|
|
9285
|
+
if (_lr.status === 'complete') {
|
|
9286
|
+
_lineageGraph = _lr.graph;
|
|
9287
|
+
if (_lr.elapsedMs > _lineageBudgetMs) {
|
|
9288
|
+
// Same "measured, not truly interrupted" disclosure _deepEnabled's
|
|
9289
|
+
// own elapsed > budgetMs branch already makes for IR-taint — see
|
|
9290
|
+
// src/lineage/DESIGN_GRAPH_BUILDER.md §9.5.
|
|
9291
|
+
aF.push({
|
|
9292
|
+
id: `lineage-timeout:${scanRoot || ''}`,
|
|
9293
|
+
file: '(lineage-engine)', line: 0,
|
|
9294
|
+
vuln: `Lineage graph build exceeded ${_lineageBudgetMs}ms budget (${_lr.elapsedMs}ms used) — results may be incomplete`,
|
|
9295
|
+
severity: 'info',
|
|
9296
|
+
parser: 'LINEAGE',
|
|
9297
|
+
confidence: 0.5,
|
|
9298
|
+
});
|
|
9299
|
+
}
|
|
9300
|
+
} else if (_lr.status === 'failed') {
|
|
9301
|
+
_lineageStatus.failure = _lr.failure;
|
|
9302
|
+
}
|
|
9303
|
+
// status === 'not_available' needs no extra handling: _lineageGraph
|
|
9304
|
+
// stays null, and enabled:true/failure:null correctly reads as "ran,
|
|
9305
|
+
// produced nothing" (a genuinely empty/malformed callGraph), distinct
|
|
9306
|
+
// from "never ran" (requested:false).
|
|
9307
|
+
} catch (e) {
|
|
9308
|
+
// Lineage is best-effort, mirroring _deepEnabled's own catch above
|
|
9309
|
+
// (engine.js, `_deepFailure = String((e && e.message) || e);`):
|
|
9310
|
+
// anything OUTSIDE buildLineageGraph's own try/catch — the _buildIR()
|
|
9311
|
+
// call itself, path.resolve, etc. — must never kill the scan. Without
|
|
9312
|
+
// this, a throw here (e.g. from buildCallGraph, which
|
|
9313
|
+
// buildProjectIR/buildProjectIRAsync do NOT guard per-file the way
|
|
9314
|
+
// per-file parsing is guarded) previously took down the entire scan
|
|
9315
|
+
// the moment AGENTIC_SECURITY_LINEAGE_DEEP=1 was set — see
|
|
9316
|
+
// src/lineage/DESIGN_GRAPH_BUILDER.md §9.5 item 1 and the Global
|
|
9317
|
+
// Constraint this violated ("a lineage-build failure must never fail
|
|
9318
|
+
// the scan"). Regression-tested in test/lineage-fault-injection.test.js.
|
|
9319
|
+
_lineageStatus.failure = String((e && e.message) || e);
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9232
9322
|
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
9233
9323
|
if (_deepCallGraph) {
|
|
9234
9324
|
try {
|
|
@@ -10585,6 +10675,7 @@ function _deterministicFileTimings(timings) {
|
|
|
10585
10675
|
engineErrors: { cppDataflowParseErrors: _cppDataflowParseErrors.value },
|
|
10586
10676
|
deepStatus: _deepStatus,
|
|
10587
10677
|
analyzerCoverage: summarizeCoverageForScanHealth(_coverageLedger),
|
|
10678
|
+
lineageStatus: _lineageStatus,
|
|
10588
10679
|
});
|
|
10589
10680
|
// FR-207: stale vulnerability feeds, calibration data, and compliance
|
|
10590
10681
|
// evidence are real assurance gaps, not just findings the feed omits --
|
|
@@ -10600,7 +10691,7 @@ function _deterministicFileTimings(timings) {
|
|
|
10600
10691
|
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10601
10692
|
});
|
|
10602
10693
|
} // end if (!skipAnnotators) — FR-PROV-029
|
|
10603
|
-
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger};}
|
|
10694
|
+
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus};}
|
|
10604
10695
|
|
|
10605
10696
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
10606
10697
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
package/src/ir/CLAUDE.md
CHANGED
|
@@ -10,6 +10,7 @@ consumed by `scanner/src/dataflow/` for taint analysis.
|
|
|
10
10
|
| JS / TS | `parser-js.js` | `@babel/parser`. **Taint-recall PRD (80%) Tier 3: JSX had ZERO IR modeling** — every `JSXElement` fell through `exprOf`'s switch to `{kind:'unknown'}`, so `return <div dangerouslySetInnerHTML={{__html: html}} />` (React's canonical XSS sink, and a real corpus miss) silently dropped `html`'s taint entirely, even though the plain-JS member-write form (`x.dangerouslySetInnerHTML = {...}`) was already cataloged. Deliberately narrow, not general JSX modeling: `exprOf`'s new `JSXElement` case (via `_findDangerouslySetInnerHTML`) extracts ONLY the `dangerouslySetInnerHTML` attribute's `__html` property value — found on the element itself or, recursively, any descendant (the attribute can sit on a nested element, not just the one directly returned) — and lowers it to a synthetic call (`__jsx_dangerously_set_inner_html__`), targeted by a sink of the same name (`react-jsx-dangerouslySetInnerHTML`) sibling to the existing member-write entry. Ordinary JSX children (`<div>{unsafeText}</div>`) are deliberately NOT modeled — React auto-escapes children by default, so that shape isn't itself a vulnerability the way `dangerouslySetInnerHTML` is. Guarded by `test/catalog-xss-p4.test.js`. |
|
|
11
11
|
| Python | `parser-py-cst.js` | Python 3.8+ stdlib `ast` via subprocess (default when available) |
|
|
12
12
|
| Python | `parser-py.js` | Hand-rolled regex parser (fallback when python3 missing) |
|
|
13
|
+
| (tool capability probe) | `chrome-probe.mjs` | Chrome/Chromium binary discovery — optional local tool, detects and probes availability via `probeChromeAvailable()`, following `parser-py-cst.js`'s established graceful-degradation pattern. Consumed by Milestone 4's image export functions (`exportPng`/`exportPdf`/`exportSvg` in `scripts/export-image.mjs`) for headless rendering. |
|
|
13
14
|
| Java | `parser-java.js` | `java-parser` npm package (**async only** — the deep path in `engine.js` therefore awaits `buildProjectIRAsync` when any `.java` file is present, and uses the sync builder otherwise).
|
|
14
15
|
⚠ Three defects made Java taint impossible until v0.136.3+: the sync-only call site; a CST walk looking for `blockStatement` on a `block` (java-parser nests `block → blockStatements → blockStatement`), which emptied every method CFG; and `exprFromCst` missing the `primary → primaryPrefix + primarySuffix` form that models **every** method call. Guarded by `test/java-taint-flow.test.js`. Real parameter names (previously always `params: []`, marked "deferred") plus Spring `@RequestParam`/`@PathVariable`/`@RequestBody`/`@RequestHeader` param annotations (`fn.paramAnnotations`) are extracted in one CST walk over `formalParameterList` (PRD R14(a) Task 5). Varargs parameters (`String... args`) live under a distinct `variableArityParameter` node this walk doesn't extract from — they're gracefully dropped, not corrupted or crashed on. **PRD R9 (partial): `fn.calls` is now populated** via the shared `call-sites.js#callSitesFromCfg` (the same language-agnostic helper `parser-py-cst.js` uses) — Java's CFG nodes (`call`, `assign`, `return`, `if`) already matched the documented contract. This creates real cross-file call-graph edges for Java (`callgraph.js`'s `edges`/`callersOf`/`resolveKnownCallee`, previously always empty for Java) — it does NOT change the generic tainted-call-argument fallback in `engine.js`'s `exprTaint`, which reads CFG expression args directly off `expr.args` and already worked for Java independent of `fn.calls`. However, **same-class (intra-class) method calls do not resolve, in both the bare and `this.`-qualified forms**: `parser-java.js` names functions `"App.buildCmd"` (class-qualified), but a bare call extracts `"buildCmd"` (unqualified) and `callgraph.js`'s name-based resolution cannot match them — this is the most idiomatic Java call shape (private helpers, intra-class delegation) and remains a real gap, documented here as a candidate follow-up PRD item (a per-file bare-tail fallback in `callgraph.js`, mirroring the existing `~bare~`-key collision-refusal pattern, would plausibly fix it without touching `parser-java.js`). `this.buildCmd(id)` is not merely unresolved, it's worse: `parser-java.js:76` lowers any `this.`-qualified call whose prefix isn't a plain FQN to the literal callee string `"unknown"`, so it doesn't fail closed, it fails to a fabricated name. Guarded by `test/parser-java-calls.test.js` (the test's honest caveat discloses this; the fixture shows that edges exist but unresolved) (mirrors `test/parser-rb-calls.test.js`, the identical `fn.calls` wiring, though Ruby's unqualified names make bare-call resolution work there). **PRD R8: `walkStmts` now recurses into `for`/`try`/`switch`/`do`/bare-block bodies** (previously only `if`/`while` were walked — every other braced statement kind silently dropped its body from the CFG, including try-with-resources, the single most idiomatic JDBC shape). A fix round closed two further gaps the initial review found: enhanced-for (`for (x : xs)`) now synthesizes an assign binding the loop variable to the iterated expression, so the variable itself carries taint provenance (mirroring `parser-js.js`'s `ForOfStatement` pattern); and Java 14+ arrow-form `switch` (`case 1 -> …`) is now recognized via a second CST-shape branch. Deferred, not fixed: `forInit`/`forUpdate` clauses of a basic 3-clause `for` loop are still not walked (only the loop body is); `synchronized` blocks and labeled statements are still fully dropped. Guarded by `test/parser-java-control-flow.test.js`. **Measured `bench/layer-recall` impact: unchanged, 1/25 before and after** — the fix is real and directly proven by the dedicated unit tests above, but this corpus's existing 25 Java fixtures happen not to place a sink genuinely inside a braced control-flow body (the ones with `if`/`try` syntax use it as a single-line guard clause ahead of a flat-level sink, not a nested one) — see the PRD R8 status entry for the full explanation and the same finding for C#. **Taint-recall PRD (80%) Tier 4: chained-call CST fix.** `exprFromCst`'s `primaryPrefix` handling previously used `.find(Boolean)` to grab the FIRST `methodInvocationSuffix` in a `primary` node's `primarySuffix[]` array and discarded the rest — so any 2+-level fluent chain (`DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xml)`, `new SpelExpressionParser().parseExpression(expr).getValue()`) resolved to the wrong (inner, usually untainted) call, and the real sink call at the end of the chain was invisible to the taint walker. Rewritten to walk the full `primarySuffix[]` array in source order, dot-joining bare member-access segments (`{Dot, Identifier}` suffixes) into the callee name and accumulating args from every `methodInvocationSuffix` encountered — same outermost-first accumulation convention (`args.concat(soFar)`) as the `_followChain` helper shared by the other five hand-rolled parsers (this PRD's earlier, language-general fix for the identical class of bug: a trailing call/member continuation being silently dropped rather than followed). A companion catalog entry (`java-spel-getvalue`, receiver-scoped to `^parseExpression`, `argIndex: 'all'`) was added because the chain fix shifts the terminal callee segment from `parseExpression` to `getValue` — same "terminal segment shift" pattern documented for Kotlin/Go/Java elsewhere in this PRD. Fixes `CVE-2018-1000632-java-xxe` (DocumentBuilderFactory chain) and `CVE-2018-1273-java-code-injection` (SpelExpressionParser chain). Guarded by two new cases in `test/java-taint-flow.test.js`. **UPDATE (Tier 3 command-injection audit): the `_SHELL_META` gap this note originally flagged is fixed.** `CVE-2016-10033-java-cmdi`'s real-world shape `Runtime.getRuntime().exec("ping " + host)` was blocked by a SEPARATE, language-agnostic core-engine bug (`engine.js`'s `literalSkeletonMatchesFamily`/`_SHELL_META`, requiring a shell metacharacter in the STATIC portion of a CWE-78 concat — backwards for command injection, see `dataflow/CLAUDE.md`), now removed entirely. The corpus fixture also needed a Tier 2 fixture enrichment (`@RequestParam` on the `host` parameter — the committed fixture had no cataloged source at all, a second, independent blocker this note's original git-stash bisection had isolated away from before concluding `_SHELL_META` was the (sole, at the time) remaining issue). Both fixed together; `CVE-2016-10033-java-cmdi` now scores `pre:TP post:TN`. **Taint-recall PRD (80%) Tier 5: a THIRD, independent chain-CST bug — a constructor STARTING a chain.** Found via `CVE-2019-3799-spring-ssrf-shape`'s real-world shape `new URL(url).openStream()`: the chained-call fix above (the `primarySuffix[]` walk) only handles a chain that starts from an FQN/identifier prefix (`prefix?.children?.fqnOrRefType?.[0]`) — a chain whose FIRST segment is itself `new X(args)` takes a completely different CST shape and was never checked, so both the class name and the constructor's own args were silently dropped, leaving only the trailing `.openStream()` call with no receiver. Root-caused via temporary `console.error(Object.keys(prefix?.children))` tracing (the first guess, matching the STANDALONE `new X(args)` handling elsewhere in this file at `unqualifiedClassInstanceCreationExpression` directly off `node`, was wrong — a chain-starting constructor sits one level deeper, under `prefix?.children?.newExpression?.[0]?.children?.unqualifiedClassInstanceCreationExpression?.[0]`). Fixed by extending the same `primaryPrefix` chain-seed logic to also recognize this shape (`ctorPrefix`), extracting the class name and constructor args exactly like the FQN case, so the chain fix now covers both "chain starts on an identifier" and "chain starts on a constructor". Companion catalog fix: `java-url-openstream` was previously keyed to the (now-unreachable-via-chain) bare `callee: 'URL'`; corrected to `callee: 'openStream', receiver: '^URL'` — the same "terminal segment shift" pattern this PRD documents repeatedly elsewhere (Kotlin/Go). Guarded by `test/catalog-path-ssrf-p3.test.js`. |
|
|
15
16
|
| Go | `parser-go.js` | Hand-rolled. **Taint-recall PRD (80%) Tier 4: inline anonymous closures.** Go's dominant HTTP-handler-registration idiom across every framework this file targets (net/http, gin, echo, fiber, chi) is an inline closure passed as the LAST argument to a registration call — `app.Get(path, func(c *fiber.Ctx) error { ... })`, `http.HandleFunc(path, func(w, r) { ... })`. This parser previously had ZERO support for anonymous functions at all: `FUNC_RE` requires a NAME between `func` and `(`, so an inline closure never matched it, and `_lowerExpr`'s generic call-matching regex mis-parsed `func(params) rtype { body }` as a call to something literally named "func" — `matchBalancedCall` correctly captured only `(params)` as that "call"'s own args, and the return-type token plus the ENTIRE closure body were silently discarded (no `.method(...)` continuation follows a return type, so `_followChain` found nothing to recover). This was very likely this PRD's single most consequential Go gap — it made every framework's route-handler BODY invisible to taint analysis regardless of what it did — found via this PRD's Tier 3 command-injection audit, not the parent PRD's original per-language sweep. Fixed by `_extractTrailingClosureArg` detecting a statement's LAST top-level call argument as an inline closure literal and INLINING its body directly into the enclosing function's CFG right after the registration call — a permissive, recall-preserving approximation (the closure genuinely runs later, but what matters for taint is that its statements become real CFG nodes at all), mirroring the Kotlin trailing-lambda / Ruby trailing-block precedent elsewhere in this PRD. The closure's OWN parameter (`c`, `w`, `r`) needs no synthetic taint binding, unlike Kotlin's `.forEach { x -> ... }`: a framework context object isn't itself a source — `c.Query(...)` is recognized by the EXISTING member/call-source catalog matching regardless of which function scope `c` was declared in. Chained calls before the closure arg (`app.Group("/api").Get(path, func(){...})`) are not specially handled — out of scope, matches this file's "handle the dominant shape" precedent elsewhere. Guarded by `test/parser-go.test.js`. Two catalog/engine precision bugs surfaced in the SAME audit, both fixed alongside: `go-os-exec-command`'s `argIndex: 0` checked the WRONG argument (the real dangerous shape is `exec.Command("/bin/sh", "-c", tainted)`, where arg 0 is always the literal interpreter and the tainted content sits later) — widened to `argIndex: 'all'`, gated by a new `match.requireLiteralArg` precision primitive (`catalog.js`/`engine.js`) requiring arg 0 to literally be a shell interpreter, so the safe array-execve form (`exec.Command("ping", "-c", "1", host)`) does not spuriously fire; and a "terminal segment shift" (same pattern as `kt-xpath-evaluate`/`java-spel-getvalue`/`go-r-uquery-get` elsewhere in this PRD) for `exec.Command(...).Output()`/`.Run()`/`.CombinedOutput()`/`.Start()` — chaining a Cmd-execution method directly onto `exec.Command(...)`, the dominant idiom for actually RUNNING the command, collapses the chain into one dotted string whose last segment is no longer "Command". Guarded by `test/catalog-command-injection-p4.test.js`. **Measured `bench/layer-recall` impact: real movement — command-injection (this corpus's dominant Go family) moved from 5/23 → 18/23 across this fix plus the sibling `literalSkeletonMatchesFamily`/CWE-78 engine fix (`dataflow/CLAUDE.md`).** |
|
|
@@ -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') {
|