@clear-capabilities/agentic-security-scanner 0.152.0 → 0.153.1

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.
@@ -1 +1 @@
1
- bead9ace9ed7ff90f2651e838dd0ead1f160e03da759d994a44b8ea46df23740 agentic-security.mjs
1
+ 1e727ef20f7fd2c897af4598b51db75771cc0b75e07dd00a30bd840d34736b10 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.152.0",
3
+ "version": "0.153.1",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -48,7 +48,8 @@
48
48
  },
49
49
  "optionalDependencies": {
50
50
  "tree-sitter-wasms": "0.1.13",
51
- "web-tree-sitter": "0.20.8"
51
+ "web-tree-sitter": "0.20.8",
52
+ "z3-solver": "^5.2.0"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^26.5.1",
@@ -70,7 +71,7 @@
70
71
  "test:glob": "node --test test/glob-compat.test.js",
71
72
  "test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js",
72
73
  "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/release-workflow.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js",
73
- "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js",
74
+ "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js",
74
75
  "test:mcp": "node --test test/mcp.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js",
75
76
  "test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
76
77
  "test:lineage": "node --test test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js",
@@ -29,6 +29,9 @@
29
29
  // callers (BFS-bounded) to keep the slicer fast.
30
30
 
31
31
  import { accessPathOf, pathIsCoveredByPrefix } from './access-paths.js';
32
+ import { matchSinkOrSanitizer } from './catalog.js';
33
+ import { safeSanitizerOutputRegex as _safeSanitizerOutputRegex } from './string-domain.js';
34
+ import { _collectExprVars } from './engine.js';
32
35
 
33
36
  const SLICE_BUDGET_NODES = 200;
34
37
  const SLICE_BUDGET_CALLERS = 5;
@@ -55,10 +58,20 @@ function _reverseEdges(cfg) {
55
58
  *
56
59
  * Returns an ordered list of trace steps (source-first):
57
60
  * [
58
- * { line, kind: 'source', label, varName, path },
59
- * { line, kind: 'assign', from, to, path },
60
- * { line, kind: 'call', callee, argPath, path },
61
- * { line, kind: 'sink', callee, argIndex, path },
61
+ * { line, kind: 'source', label, varName, path },
62
+ * { line, kind: 'assign', from, to, path },
63
+ * { line, kind: 'sanitize', callee, sanitizerId, path }, // a RECOGNIZED
64
+ * // catalog sanitizer
65
+ * // applied on the path
66
+ * { line, kind: 'call', callee, sanitizerId: null, path }, // any OTHER
67
+ * // call — best-effort
68
+ * // continuity only;
69
+ * // a sound consumer
70
+ * // (e.g. the SMT
71
+ * // encoder) must NOT
72
+ * // treat this as a
73
+ * // known transform
74
+ * { line, kind: 'sink', callee, argIndex, path },
62
75
  * ]
63
76
  */
64
77
  export function sliceBackward(fn, sinkNode, sinkArgPath) {
@@ -127,6 +140,63 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
127
140
  }
128
141
  // Otherwise, follow the def of the new query path upstream.
129
142
  work.push({ nid, queryPath: srcAp });
143
+ } else if (node.source && node.source.kind === 'call') {
144
+ // Next-gen taint capability #5 (SMT path feasibility rebuild):
145
+ // `accessPathOf` returns null for a CALL expression by design (it
146
+ // only handles pure ident/member chains), so `const clean =
147
+ // sanitize(x); sink(clean);` — arguably the single most common
148
+ // real-world sanitizer idiom — previously dead-ended the slice
149
+ // right here with no 'source' step ever appended: `srcAp` is null,
150
+ // so neither branch above fires, and the bare "walk predecessors
151
+ // regardless" loop below can only ever find a DIFFERENT def of the
152
+ // SAME queryPath, never continue into the call's own argument.
153
+ // Fixed by treating a call-shaped RHS as a real, tracked step: a
154
+ // recognized CATALOG SANITIZER gets its own 'sanitize' kind (the
155
+ // SMT encoder below trusts ONLY this shape, via the sanitizer's
156
+ // proven output-regex, to constrain a value); anything else gets a
157
+ // generic 'call' kind, for explainability continuity only — the
158
+ // SMT encoder must refuse to build a sound proof across an opaque
159
+ // call it cannot reason about, never treat it as either a
160
+ // transparent passthrough or an implicit sanitizer. Deliberately
161
+ // single-argument, intraprocedural only, matching this whole
162
+ // module's own documented v1 scope: continues into the FIRST
163
+ // argument carrying a resolvable access path, not every argument
164
+ // (a sanitizer/helper's OTHER arguments are typically
165
+ // configuration, e.g. `escape(x, {mode: 'html'})`, not additional
166
+ // taint sources).
167
+ const callee = node.source.callee;
168
+ const args = Array.isArray(node.source.args) ? node.source.args : [];
169
+ let argPath = null;
170
+ for (const a of args) { const p = accessPathOf(a); if (p) { argPath = p; break; } }
171
+ const hits = matchSinkOrSanitizer(callee, fn.file);
172
+ const sanitizerHit = Array.isArray(hits) ? hits.find(h => h.kind === 'sanitizer') : null;
173
+ const calleeName = typeof callee === 'string' ? callee
174
+ : callee && callee.kind === 'ident' ? callee.name
175
+ : callee && callee.kind === 'member' && typeof callee.prop === 'string' ? callee.prop
176
+ : null;
177
+ // `outputRegexSource` is a SEPARATE, narrower signal than
178
+ // `sanitizerId` above: the broad 706-entry catalog match is fine
179
+ // for EXPLAINABILITY (labelling a step "this looks like a
180
+ // sanitizer call" for a human reading the trace), but it matches
181
+ // by bare callee NAME — a project-local function that merely
182
+ // happens to be named the same as a cataloged sanitizer (e.g. a
183
+ // local `sanitize()` helper that does something unrelated) would
184
+ // match it too. A soundness-critical consumer (the SMT
185
+ // infeasibility check) must never treat that broad match as
186
+ // license to assume a specific, provable output shape — only
187
+ // `safeSanitizerOutputRegex`'s own small, hand-vetted set of
188
+ // unambiguous global builtins earns that trust.
189
+ const outputRegexSource = calleeName ? _safeSanitizerOutputRegex(calleeName) : null;
190
+ out.push({
191
+ line: node.line || 0,
192
+ kind: sanitizerHit ? 'sanitize' : 'call',
193
+ callee: calleeName,
194
+ sanitizerId: sanitizerHit ? sanitizerHit.id : null,
195
+ outputRegexSource: outputRegexSource ? outputRegexSource.source : null,
196
+ outputRegexFlags: outputRegexSource ? outputRegexSource.flags : null,
197
+ path: queryPath,
198
+ });
199
+ if (argPath) work.push({ nid, queryPath: argPath });
130
200
  }
131
201
  }
132
202
 
@@ -178,9 +248,36 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
178
248
  }
179
249
  if (!sinkNode) { skipped++; continue; }
180
250
  // The tainted arg path — derive from the tainted argument's expression.
181
- const taintedArg = (sinkNode.args || [])[f.argIndex];
182
- const argPath = accessPathOf(taintedArg) || `arg[${f.argIndex}]`;
183
- const slice = sliceBackward(fn, sinkNode, argPath);
251
+ // `f.argIndex` can be a real number, the string 'all' (any arg tainted —
252
+ // no single index to point at) or 'rhs' (a member-write sink) — only a
253
+ // plain numeric index names one real argument to slice from.
254
+ const argExprs = sinkNode.args || [];
255
+ const taintedArg = typeof f.argIndex === 'number' ? argExprs[f.argIndex] : undefined;
256
+ let argPath = accessPathOf(taintedArg);
257
+ let slice = argPath ? sliceBackward(fn, sinkNode, argPath) : null;
258
+ // Next-gen taint capability #5 (SMT path-feasibility rebuild):
259
+ // `accessPathOf` only ever succeeds for a BARE ident/member chain — the
260
+ // single most common real shape, a sink argument built by string
261
+ // concatenation (`'SELECT ...' + id`, a template literal, string.format
262
+ // args, …), is a `binary`/`tpl`/`call` expression, so `accessPathOf`
263
+ // returns null and the slice used to fall back to the unmatchable
264
+ // placeholder string `arg[${f.argIndex}]` — no real variable is ever
265
+ // named that, so `sliceBackward` could only ever return the bare sink
266
+ // node, structurally regardless of AGENTIC_SECURITY_BACKWARD_SLICE.
267
+ // Fixed by extracting every free variable/access-path referenced
268
+ // ANYWHERE in the tainted argument expression (the same tree-walker
269
+ // `engine.js`'s own `_sourcesReachingExpr` uses to attribute a finding
270
+ // to its real source) and trying each one in turn, keeping the first
271
+ // that yields a genuine (more-than-just-the-sink) slice.
272
+ if ((!slice || slice.length <= 1) && taintedArg) {
273
+ const vars = new Set();
274
+ _collectExprVars(taintedArg, vars);
275
+ for (const candidate of vars) {
276
+ const candidateSlice = sliceBackward(fn, sinkNode, candidate);
277
+ if (candidateSlice && candidateSlice.length > 1) { slice = candidateSlice; break; }
278
+ if (!slice) slice = candidateSlice;
279
+ }
280
+ }
184
281
  if (slice && slice.length) {
185
282
  f.backwardSlice = slice;
186
283
  f.pathSteps = (f.pathSteps || []).concat(slice.map(s => ({
@@ -39,6 +39,24 @@
39
39
  // either a monorepo or a federated-scan API. We DO emit cross-service
40
40
  // findings when the local service is on the receiving end of an edge,
41
41
  // based on the declared upstream taint contract.
42
+ //
43
+ // SCOPE NOTE (next-gen taint capability #7 rebuild): step 1 of the
44
+ // algorithm above ("mark every consume entry-point as tainted-by-default")
45
+ // is NOT implemented anywhere in this file, or anywhere else in the
46
+ // engine — there is no mechanism that seeds a NEW taint source from a
47
+ // `consumes` declaration (e.g. marking a Kafka consumer callback's
48
+ // message parameter as tainted). This module only ANNOTATES findings the
49
+ // taint engine already discovers via its own normal source catalog
50
+ // (`catalog.js`) with cross-service context; it never makes a
51
+ // previously-invisible flow visible. In practice this means an HTTP
52
+ // edge benefits fully (HTTP request sources — body/query/header/cookie —
53
+ // are extensively cataloged already), while a Kafka/queue edge benefits
54
+ // only when the consumed payload happens to ALSO reach an already-
55
+ // cataloged source through some other path — which is rare, since raw
56
+ // message-queue payloads have no catalog entry of their own. Seeding new
57
+ // sources from `consumes` declarations would be a genuinely separate,
58
+ // larger capability; left as a deliberate, documented boundary here
59
+ // rather than attempted in this rebuild.
42
60
 
43
61
  import * as fs from 'node:fs';
44
62
  import * as path from 'node:path';
@@ -116,6 +134,52 @@ export function incomingEdges(graph, currentService) {
116
134
  return (graph.edges || []).filter(e => e.to === currentService.name);
117
135
  }
118
136
 
137
+ /**
138
+ * Split a URL path into its non-empty segments, e.g. "/balances/:id" ->
139
+ * ["balances", ":id"].
140
+ */
141
+ function _pathSegments(p) {
142
+ return String(p || '').split('/').filter(Boolean);
143
+ }
144
+
145
+ /**
146
+ * True when a path segment is a parameter placeholder, in EITHER of the
147
+ * two conventions this file's own schema comment documents:
148
+ * Express/Koa-style `:userId` or template-style `{userId}`.
149
+ */
150
+ function _isWildcardSegment(seg) {
151
+ return /^:.+$/.test(seg) || /^\{[^}]+\}$/.test(seg);
152
+ }
153
+
154
+ /**
155
+ * Structural path-template match between an `exposes[].route` entry
156
+ * (e.g. "GET /balances/:userId") and an `edges[].path` entry (e.g.
157
+ * "/balances/{userId}").
158
+ *
159
+ * The previous implementation did `expose.route.includes(edge.path)` — a
160
+ * crude substring check. This file's OWN header comment documents the
161
+ * two sides using DIFFERENT placeholder syntaxes (`:userId` vs
162
+ * `{userId}`), so that substring check could never succeed on the
163
+ * example it was written to describe — confirmed by direct reproduction
164
+ * against that exact example, which returned zero contracts. Fixed by
165
+ * comparing SEGMENT-BY-SEGMENT: a literal segment must match exactly, a
166
+ * placeholder segment on EITHER side matches any segment on the other
167
+ * (since we don't know the upstream's real parameter name convention
168
+ * without parsing it, and the security question — "does an edge exist
169
+ * pointing at this exact path shape" — doesn't depend on knowing it).
170
+ * The route's leading HTTP method token (if present) is stripped before
171
+ * comparing; method itself is not matched (this schema has no per-edge
172
+ * method field), matching the pre-existing, unaudited behavior.
173
+ */
174
+ function _pathTemplateMatches(exposeRoute, edgePath) {
175
+ if (!exposeRoute || !edgePath) return false;
176
+ const routePath = String(exposeRoute).replace(/^[A-Z]+\s+/, '');
177
+ const a = _pathSegments(routePath);
178
+ const b = _pathSegments(String(edgePath).split('?')[0]);
179
+ if (a.length !== b.length) return false;
180
+ return a.every((seg, i) => seg === b[i] || _isWildcardSegment(seg) || _isWildcardSegment(b[i]));
181
+ }
182
+
119
183
  /**
120
184
  * For each consume entry on the current service, locate the upstream
121
185
  * `exposes` entry that produces it (matched by path/topic) and return
@@ -131,7 +195,7 @@ export function upstreamTaintContract(graph, currentService) {
131
195
  const upstream = graph.services[edge.from];
132
196
  if (!upstream) continue;
133
197
  for (const expose of upstream.exposes || []) {
134
- const matches = (edge.via === 'http' && edge.path && expose.route && expose.route.includes(edge.path.split('?')[0]))
198
+ const matches = (edge.via === 'http' && edge.path && expose.route && _pathTemplateMatches(expose.route, edge.path))
135
199
  || (edge.via === 'kafka' && edge.topic && consume.source === edge.topic);
136
200
  if (matches) {
137
201
  contracts.push({
@@ -148,10 +212,82 @@ export function upstreamTaintContract(graph, currentService) {
148
212
  return contracts;
149
213
  }
150
214
 
215
+ // Escapes every regex metacharacter in a literal field name before it is
216
+ // embedded in a `\b...\b` pattern. The previous version only replaced the
217
+ // FIRST `.` (`field.replace('.', '\\.')`, no `/g` flag) — any OTHER dot in
218
+ // a multi-segment field name (e.g. "a.b.c") was left as a live regex
219
+ // metacharacter matching any character, silently over-broadening the
220
+ // match. Fixed generally, for every metacharacter, not just repeated dots.
221
+ function _escapeRegex(s) {
222
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
223
+ }
224
+
225
+ // Provenance values (catalog.js's per-source `provenance` field, e.g.
226
+ // 'http-body' for req.body) that mean "this value arrived over an
227
+ // incoming HTTP request" — the signal used to correlate a finding with a
228
+ // declared `via: 'http'` cross-service edge.
229
+ const HTTP_PROVENANCE = new Set(['http-body', 'url-param', 'path-param', 'header', 'cookie']);
230
+
231
+ // Every provenance value attached anywhere on a finding: the primary
232
+ // source (`f.sourceProvenance`) plus every step in its source chain
233
+ // (`f.chain[].provenance`) — a finding can have more than one source
234
+ // reaching the sink, and any one of them may be the HTTP-originated one.
235
+ function _findingProvenances(f) {
236
+ const provs = new Set();
237
+ if (f && f.sourceProvenance) provs.add(f.sourceProvenance);
238
+ if (f && Array.isArray(f.chain)) {
239
+ for (const c of f.chain) { if (c && c.provenance) provs.add(c.provenance); }
240
+ }
241
+ return provs;
242
+ }
243
+
244
+ // Every textual source LABEL attached anywhere on a finding (the catalog
245
+ // entry's own fixed label, e.g. "req.body" — never the specific accessed
246
+ // property, which the engine does not retain on the finding today). Used
247
+ // only as a best-effort fallback for non-HTTP transports (Kafka/queue),
248
+ // where there is no provenance category to correlate against at all.
249
+ function _findingSourceText(f) {
250
+ const parts = [];
251
+ if (f && f.source && f.source.label) parts.push(f.source.label);
252
+ if (f && Array.isArray(f.chain)) {
253
+ for (const c of f.chain) { if (c && c.label) parts.push(c.label); }
254
+ }
255
+ return parts.join(' ');
256
+ }
257
+
151
258
  /**
152
259
  * Annotate findings whose source matches a cross-service taint contract.
153
- * Adds `crossService: { from, via, path, taintedFields }` and bumps
154
- * severity by one tier (medium → high, high → critical).
260
+ * Adds `crossService: { from, to, via, taintedFields, matchedField,
261
+ * precision }` and bumps severity by one tier (medium → high, high →
262
+ * critical).
263
+ *
264
+ * The previous implementation matched a contract's declared field NAMES
265
+ * as literal text against `f.source.snippet`/`f.source.expr` — fields
266
+ * that confirmed direct code reading never exist on a real finding (the
267
+ * real shape, set in `engine.js`, is `f.source = {file, line, label}`).
268
+ * That made this function a structural no-op for every real taint-engine
269
+ * finding, verified via a direct `runScan()` reproduction of the exact
270
+ * scenario this file's own header describes.
271
+ *
272
+ * Fixed with two correlation strategies, chosen by transport, since a
273
+ * real finding never carries the SPECIFIC field name that reached the
274
+ * sink (only the catalog source's fixed label, e.g. "req.body" — see
275
+ * `_findingSourceText`'s own comment):
276
+ * - `via: 'http'`: correlate by PROVENANCE — an HTTP request source is
277
+ * already reliably categorized by the catalog (`http-body`,
278
+ * `url-param`, `header`, `cookie`, …), so this is a real, sound
279
+ * signal rather than a text-match guess. `matchedField` is left null
280
+ * and `precision: 'transport'` records honestly that the match is at
281
+ * the endpoint level, not a specific declared field — the security
282
+ * conclusion (this value arrived across a declared trust boundary)
283
+ * holds regardless of which exact field it was.
284
+ * - every other transport (Kafka/queue): no provenance category exists
285
+ * for a raw message payload (per this file's own SCOPE NOTE), so the
286
+ * best available signal is literal field-name text matching against
287
+ * whatever source labels the finding does carry — kept as a
288
+ * best-effort fallback, `precision: 'field'` when it hits, but this
289
+ * will rarely fire until source-seeding from `consumes` declarations
290
+ * exists (documented as a separate, not-yet-attempted capability).
155
291
  */
156
292
  export function annotateCrossServiceFindings(findings, graph, currentService) {
157
293
  if (!Array.isArray(findings) || !graph || !currentService) return { annotated: 0, bumped: 0 };
@@ -159,18 +295,25 @@ export function annotateCrossServiceFindings(findings, graph, currentService) {
159
295
  if (!contracts.length) return { annotated: 0, bumped: 0 };
160
296
  let annotated = 0, bumped = 0;
161
297
  for (const f of findings) {
162
- const sourceExpr = (f.source && (f.source.snippet || f.source.expr)) || f.snippet || '';
163
298
  for (const contract of contracts) {
164
- const matches = contract.taintedFields.some(field => {
165
- const pat = new RegExp(`\\b${field.replace('.', '\\.')}\\b`);
166
- return pat.test(sourceExpr);
167
- });
168
- if (!matches) continue;
299
+ let matchedField = null;
300
+ let matched = false;
301
+ if (contract.via === 'http') {
302
+ const provs = _findingProvenances(f);
303
+ matched = [...provs].some(p => HTTP_PROVENANCE.has(p));
304
+ } else {
305
+ const text = _findingSourceText(f);
306
+ matchedField = contract.taintedFields.find(field => new RegExp(`\\b${_escapeRegex(field)}\\b`).test(text)) || null;
307
+ matched = !!matchedField;
308
+ }
309
+ if (!matched) continue;
169
310
  f.crossService = {
170
311
  from: contract.upstreamService,
171
312
  to: currentService.name,
172
313
  via: contract.via,
173
- taintedField: contract.taintedFields.find(field => new RegExp(`\\b${field.replace('.', '\\.')}\\b`).test(sourceExpr)),
314
+ taintedFields: contract.taintedFields,
315
+ matchedField,
316
+ precision: matchedField ? 'field' : 'transport',
174
317
  };
175
318
  annotated++;
176
319
  // Severity bump.
@@ -199,4 +342,4 @@ export function runCrossServiceTaint(scanRoot, findings) {
199
342
  return annotateCrossServiceFindings(findings, graph, current);
200
343
  }
201
344
 
202
- export const _internals = { _normalizeGraph, SERVICES_FILE_NAMES };
345
+ export const _internals = { _normalizeGraph, SERVICES_FILE_NAMES, _pathTemplateMatches, _escapeRegex };
@@ -547,7 +547,15 @@ function exprTaint(expr, state, callContext) {
547
547
  // Collects the variable / access-path roots referenced by `expr` and returns
548
548
  // the _taintSources entries whose varName matches one of those roots. This
549
549
  // replaces "first source we ever saw" with "sources tied to this argument."
550
- function _collectExprVars(expr, out) {
550
+ // Exported (next-gen taint capability #5, SMT path-feasibility rebuild) so
551
+ // backward.js's `annotateBackwardSlices` can reuse the SAME free-variable
552
+ // extraction for a COMPOUND tainted-sink-argument expression (`'SELECT...'
553
+ // + id`, not a bare identifier) — `accessPathOf` alone returns null for
554
+ // anything but a pure ident/member chain, which previously made
555
+ // `annotateBackwardSlices` fall back to an unmatchable placeholder string
556
+ // for the overwhelmingly common "string-concatenation into a sink" shape,
557
+ // silently degrading the backward slice to just the bare sink node.
558
+ export function _collectExprVars(expr, out) {
551
559
  if (!expr) return;
552
560
  if (typeof expr === 'string') { out.add(expr); return; }
553
561
  if (expr.kind === 'ident' && expr.name) { out.add(expr.name); return; }
@@ -2602,6 +2610,20 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
2602
2610
  // backward-slice annotation was permanently a no-op regardless of
2603
2611
  // AGENTIC_SECURITY_BACKWARD_SLICE.
2604
2612
  ...(f._funcQid ? { _funcQid: f._funcQid } : {}),
2613
+ // argIndex: which sink argument is the tainted one (a number, or the
2614
+ // literal 'all'/'rhs') — set upstream at the finding's creation site
2615
+ // but, exactly like `_funcQid`/`callee` above, silently dropped by
2616
+ // this same allowlist before backward.js's `annotateBackwardSlices`
2617
+ // ever saw it. Without it, `annotateBackwardSlices` falls back to
2618
+ // the placeholder string `arg[undefined]` for EVERY finding (it can
2619
+ // never be a real access path, since no source ever names a
2620
+ // variable that literally), so `sliceBackward` always started and
2621
+ // ended at the bare sink node with no source/sanitize steps at all
2622
+ // — the backward-slice pass was a structural no-op for real findings
2623
+ // regardless of AGENTIC_SECURITY_BACKWARD_SLICE, discovered while
2624
+ // rebuilding the SMT path-feasibility mechanism (next-gen taint
2625
+ // capability #5) to depend on a REAL slice for a sound proof.
2626
+ ...(f.argIndex !== undefined ? { argIndex: f.argIndex } : {}),
2605
2627
  // callee: kept as plain `callee` (not underscore-prefixed) to match
2606
2628
  // backward.js's own contract, which reads `f.callee` on both real and
2607
2629
  // fake-fixture findings throughout its module and test suite. It is