@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/agentic-security.js +37 -0
  3. package/dist/119.index.js +197 -0
  4. package/dist/128.index.js +129 -0
  5. package/dist/1293.index.js +5689 -0
  6. package/dist/1336.index.js +101 -101
  7. package/dist/1351.index.js +133 -0
  8. package/dist/1387.index.js +271 -0
  9. package/dist/1437.index.js +23 -0
  10. package/dist/1445.index.js +110 -0
  11. package/dist/1505.index.js +151 -0
  12. package/dist/1557.index.js +293 -0
  13. package/dist/1621.index.js +311 -0
  14. package/dist/1751.index.js +160 -0
  15. package/dist/1765.index.js +727 -0
  16. package/dist/1861.index.js +690 -0
  17. package/dist/1898.index.js +116 -0
  18. package/dist/1906.index.js +433 -0
  19. package/dist/1920.index.js +1 -291
  20. package/dist/208.index.js +1631 -0
  21. package/dist/2199.index.js +0 -239
  22. package/dist/2223.index.js +159 -0
  23. package/dist/2530.index.js +152 -0
  24. package/dist/2565.index.js +116 -0
  25. package/dist/2574.index.js +127 -0
  26. package/dist/2620.index.js +345 -0
  27. package/dist/2773.index.js +98 -0
  28. package/dist/2775.index.js +32 -0
  29. package/dist/2966.index.js +171 -0
  30. package/dist/2972.index.js +87 -0
  31. package/dist/2991.index.js +131 -0
  32. package/dist/3063.index.js +833 -0
  33. package/dist/3075.index.js +227 -0
  34. package/dist/3121.index.js +297 -0
  35. package/dist/3314.index.js +263 -0
  36. package/dist/3480.index.js +1822 -0
  37. package/dist/3481.index.js +24 -0
  38. package/dist/3506.index.js +268 -0
  39. package/dist/3533.index.js +591 -0
  40. package/dist/3653.index.js +450 -0
  41. package/dist/3822.index.js +131 -0
  42. package/dist/3868.index.js +292 -0
  43. package/dist/3917.index.js +152 -0
  44. package/dist/3918.index.js +192 -0
  45. package/dist/4090.index.js +288 -0
  46. package/dist/4121.index.js +1190 -0
  47. package/dist/4142.index.js +0 -239
  48. package/dist/4336.index.js +436 -0
  49. package/dist/4387.index.js +532 -0
  50. package/dist/4413.index.js +302 -0
  51. package/dist/4476.index.js +4510 -0
  52. package/dist/4491.index.js +133 -0
  53. package/dist/4538.index.js +185 -0
  54. package/dist/4684.index.js +319 -0
  55. package/dist/4740.index.js +165 -0
  56. package/dist/4780.index.js +209 -0
  57. package/dist/4802.index.js +225 -0
  58. package/dist/4812.index.js +379 -0
  59. package/dist/4835.index.js +126 -0
  60. package/dist/494.index.js +226 -0
  61. package/dist/4991.index.js +549 -0
  62. package/dist/5199.index.js +305 -0
  63. package/dist/5216.index.js +109 -0
  64. package/dist/5439.index.js +301 -0
  65. package/dist/5443.index.js +97 -0
  66. package/dist/5496.index.js +377 -0
  67. package/dist/550.index.js +2 -1
  68. package/dist/5801.index.js +588 -0
  69. package/dist/5858.index.js +223 -0
  70. package/dist/5933.index.js +143 -0
  71. package/dist/601.index.js +23 -0
  72. package/dist/6173.index.js +273 -0
  73. package/dist/6291.index.js +1871 -0
  74. package/dist/6312.index.js +666 -0
  75. package/dist/634.index.js +16182 -0
  76. package/dist/644.index.js +87 -0
  77. package/dist/6508.index.js +283 -0
  78. package/dist/6560.index.js +645 -0
  79. package/dist/6576.index.js +422 -0
  80. package/dist/6579.index.js +271 -0
  81. package/dist/6745.index.js +305 -0
  82. package/dist/6818.index.js +21642 -0
  83. package/dist/6972.index.js +167 -0
  84. package/dist/6995.index.js +163 -0
  85. package/dist/7249.index.js +191 -0
  86. package/dist/7372.index.js +1197 -0
  87. package/dist/7405.index.js +23 -6
  88. package/dist/7546.index.js +541 -0
  89. package/dist/76.index.js +448 -0
  90. package/dist/7879.index.js +163 -0
  91. package/dist/8025.index.js +130 -0
  92. package/dist/8054.index.js +248 -0
  93. package/dist/8097.index.js +201 -0
  94. package/dist/8126.index.js +191 -0
  95. package/dist/8178.index.js +78 -0
  96. package/dist/818.index.js +212 -0
  97. package/dist/8382.index.js +93 -0
  98. package/dist/8408.index.js +261 -0
  99. package/dist/8540.index.js +174 -0
  100. package/dist/8635.index.js +802 -0
  101. package/dist/8688.index.js +410 -0
  102. package/dist/8699.index.js +779 -0
  103. package/dist/8891.index.js +457 -0
  104. package/dist/8916.index.js +885 -0
  105. package/dist/9146.index.js +170 -0
  106. package/dist/9168.index.js +841 -0
  107. package/dist/9292.index.js +369 -0
  108. package/dist/9382.index.js +338 -48
  109. package/dist/9422.index.js +866 -0
  110. package/dist/9440.index.js +453 -0
  111. package/dist/9479.index.js +152 -0
  112. package/dist/9481.index.js +220 -0
  113. package/dist/9539.index.js +100 -0
  114. package/dist/9547.index.js +103 -0
  115. package/dist/9560.index.js +41 -61
  116. package/dist/9692.index.js +90 -0
  117. package/dist/9800.index.js +213 -1
  118. package/dist/9899.index.js +138 -0
  119. package/dist/9981.index.js +206 -0
  120. package/dist/999.index.js +117 -0
  121. package/dist/agentic-security.mjs +28 -28
  122. package/dist/agentic-security.mjs.sha256 +1 -1
  123. package/package.json +31 -6
  124. package/src/context-intelligence/CLAUDE.md +4 -0
  125. package/src/context-intelligence/adapters/haskell-hie.js +28 -1
  126. package/src/context-intelligence/candidates.js +2 -1
  127. package/src/context-intelligence/canonical.js +25 -0
  128. package/src/context-intelligence/capabilities.js +2 -1
  129. package/src/context-intelligence/cli.js +16 -10
  130. package/src/context-intelligence/config.js +4 -1
  131. package/src/context-intelligence/controls.js +108 -10
  132. package/src/context-intelligence/counterexample.js +200 -0
  133. package/src/context-intelligence/cross-route.js +111 -0
  134. package/src/context-intelligence/entrypoints.js +2 -2
  135. package/src/context-intelligence/evaluation/index.js +11 -4
  136. package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
  137. package/src/context-intelligence/findings.js +166 -0
  138. package/src/context-intelligence/invariants.js +197 -0
  139. package/src/context-intelligence/queue.js +6 -1
  140. package/src/context-intelligence/reachability.js +10 -4
  141. package/src/context-intelligence/review.js +129 -16
  142. package/src/dataflow/access-paths.js +11 -0
  143. package/src/dataflow/analysis-limits.js +66 -0
  144. package/src/dataflow/engine.js +272 -20
  145. package/src/dataflow/service-edges.js +174 -0
  146. package/src/dataflow/summaries.js +7 -1
  147. package/src/discovery/judge.js +2 -1
  148. package/src/engine.js +82 -14
  149. package/src/fix/apply-fix-service.js +55 -8
  150. package/src/fix/batch-journal.js +84 -0
  151. package/src/fix/exploit-replay.js +51 -0
  152. package/src/fix/footprint.js +65 -0
  153. package/src/fix/patch-policy.js +49 -0
  154. package/src/fix/secret-persistence.js +39 -0
  155. package/src/fix/test-commands.js +84 -0
  156. package/src/fix/verification-adapters.js +97 -0
  157. package/src/fix/verification-record.js +243 -0
  158. package/src/fix/verify-patch.js +89 -0
  159. package/src/ir/CLAUDE.md +5 -0
  160. package/src/ir/callgraph.js +23 -1
  161. package/src/ir/module-resolver.js +315 -0
  162. package/src/ir/parser-js.js +21 -2
  163. package/src/ir/semantic-graph.js +245 -0
  164. package/src/lineage/build-attested-correlation.js +63 -0
  165. package/src/lineage/deployment-identity.js +76 -0
  166. package/src/mcp/tools.js +13 -34
  167. package/src/pipeline/canonical-output.js +64 -0
  168. package/src/pipeline/detector-runner.js +7 -0
  169. package/src/pipeline/scan-health.js +14 -0
  170. package/src/pipeline/supervised-analyzers.js +125 -0
  171. package/src/pipeline/supervised-child.js +17 -0
  172. package/src/pipeline/telemetry.js +79 -0
  173. package/src/posture/accuracy-scorecard.js +98 -9
  174. package/src/posture/artifact-registry.js +5 -0
  175. package/src/posture/assurance/evidence-registry.js +215 -0
  176. package/src/posture/assurance/release-package-verify.js +135 -0
  177. package/src/posture/assurance/release-package.js +118 -0
  178. package/src/posture/assurance/release-verdict.js +94 -0
  179. package/src/posture/assurance/waiver.js +200 -0
  180. package/src/posture/compliance-truth.js +100 -0
  181. package/src/posture/evaluation/scan-child.js +3 -1
  182. package/src/posture/evaluation/why-missed.js +3 -1
  183. package/src/posture/exploitability-evidence.js +141 -0
  184. package/src/posture/fix-verify.js +4 -2
  185. package/src/posture/fleet/exposure-ranking.js +115 -0
  186. package/src/posture/fleet/service-chain-graph.js +150 -0
  187. package/src/posture/graph-reconciliation.js +154 -0
  188. package/src/posture/provenance/coverage.js +103 -0
  189. package/src/posture/provenance/git-evidence.js +84 -0
  190. package/src/posture/provenance/origin-resolver.js +7 -1
  191. package/src/posture/provenance/predicate-replay.js +14 -4
  192. package/src/posture/runtime-correlation.js +61 -15
  193. package/src/posture/runtime-explanation.js +116 -0
  194. package/src/posture/runtime-trace-ingest.js +194 -0
  195. package/src/posture/scan-checkpoint.js +38 -10
  196. package/src/posture/waf-ingest.js +7 -2
  197. package/src/report/index.js +4 -0
  198. package/src/report/oscal.js +38 -0
  199. package/src/report/perspectives.js +150 -0
  200. package/src/sast/_comment-strip.js +8 -1
  201. package/src/sast/ruby.js +26 -0
  202. package/src/util/string-memo.js +38 -0
@@ -1 +1 @@
1
- edcc9e08f076b2bc50946c512557c15038e337fa568bc490586fc882ca1c11bc agentic-security.mjs
1
+ eab94f59f539296c01e17e2d914f419e131e2b4d1234865c011f755f11a64441 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.172.0",
3
+ "version": "0.173.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -43,13 +43,13 @@
43
43
  "@babel/core": "^8.0.5",
44
44
  "@babel/preset-react": "^8.0.1",
45
45
  "@babel/preset-typescript": "^8.0.1",
46
- "js-yaml": "^5.4.2",
46
+ "js-yaml": "^5.5.0",
47
47
  "safe-regex": "^2.1.1"
48
48
  },
49
49
  "optionalDependencies": {
50
50
  "tree-sitter-wasms": "0.1.13",
51
51
  "web-tree-sitter": "0.20.8",
52
- "z3-solver": "^5.3.0"
52
+ "z3-solver": "^5.3.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.6.5",
@@ -67,6 +67,17 @@
67
67
  "test": "node ../scripts/run-unit-tests.mjs",
68
68
  "test:extras": "AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
69
69
  "loop": "node ../scripts/loop-engineering/run.mjs",
70
+ "test:engineering-excellence": "node --test --test-concurrency=1 test/engineering-excellence/e0-01-inventory.test.js test/engineering-excellence/e0-02-baseline.test.js test/engineering-excellence/e0-03-contract.test.js test/engineering-excellence/e0-04-task-manifest.test.js test/engineering-excellence/e0-05-status-runner.test.js test/engineering-excellence/e0-06-backlog.test.js test/engineering-excellence/e1-01-dataset-provenance.test.js test/engineering-excellence/e1-02-splits.test.js test/engineering-excellence/e1-03-scoring.test.js test/engineering-excellence/e1-04-baseline-run.test.js test/engineering-excellence/e1-05-forensics.test.js test/engineering-excellence/e1-05-baseline-forensics.test.js test/engineering-excellence/e1-06-adversarial-controls.test.js test/engineering-excellence/e1-07-realcode-policy.test.js test/engineering-excellence/e1-07-baseline-gate.test.js test/engineering-excellence/e2-06-cross-language.test.js test/engineering-excellence/e2-08-suppression-attribution.test.js test/engineering-excellence/e2-09-semantic-gates.test.js test/engineering-excellence/e4-06-dci-real-app.test.js test/engineering-excellence/e6-08-perf-budget.test.js test/engineering-excellence/e8-02-e2e-demo.test.js test/engineering-excellence/e8-03-regression-report.test.js test/engineering-excellence/e8-04-docs.test.js test/engineering-excellence/e8-05-handoff.test.js test/engineering-excellence/e8-06-release-decision.test.js test/engineering-excellence/u01-ruby-path-resolver.test.js test/engineering-excellence/e1-08-scorecard.test.js test/engineering-excellence/e3-01-patch-routes.test.js test/engineering-excellence/e3-02-fix-verification-record.test.js test/engineering-excellence/e3-03-central-verifier.test.js test/engineering-excellence/e3-04-exploit-replay.test.js test/engineering-excellence/e3-05-functional-preservation.test.js test/engineering-excellence/e3-06-patch-policy.test.js test/engineering-excellence/e3-07-rollback-crash-safety.test.js test/engineering-excellence/e3-08-fix-benchmark.test.js test/engineering-excellence/e2-01-semantic-graph.test.js test/engineering-excellence/e2-02-cross-file-resolution.test.js test/engineering-excellence/e2-03-heap-fields-aliases.test.js test/engineering-excellence/e2-04-async-callbacks-frameworks.test.js test/engineering-excellence/e2-05-interprocedural-contexts.test.js test/engineering-excellence/e2-07-service-edges.test.js test/engineering-excellence/e4-01-dci-audit.test.js test/engineering-excellence/e4-02-invariants.test.js test/engineering-excellence/e4-03-cross-route.test.js test/engineering-excellence/e4-04-counterexample.test.js test/engineering-excellence/e4-05-llm-safeguards.test.js test/engineering-excellence/e4-07-dci-findings.test.js test/engineering-excellence/e6-01-telemetry.test.js test/engineering-excellence/e5-01-runtime-audit.test.js test/engineering-excellence/e5-02-build-identity.test.js test/engineering-excellence/e5-03-privacy-ingest.test.js test/engineering-excellence/e5-04-graph-reconciliation.test.js test/engineering-excellence/e5-05-exploitability-evidence.test.js test/engineering-excellence/e5-06-service-chain-graph.test.js test/engineering-excellence/e5-07-runtime-explanation.test.js test/engineering-excellence/e6-05-supervised-analyzers.test.js test/engineering-excellence/e6-06-determinism.test.js test/engineering-excellence/e6-07-adversarial-boundaries.test.js test/engineering-excellence/e6-03-shared-preprocessing.test.js test/engineering-excellence/e6-04-analysis-cache.test.js test/engineering-excellence/e7-01-evidence-contracts.test.js test/engineering-excellence/e7-03-release-package.test.js test/engineering-excellence/e7-04-compliance-truth.test.js test/engineering-excellence/e7-05-waivers-tenancy.test.js test/engineering-excellence/e7-02-provenance-coverage.test.js test/engineering-excellence/e7-06-fleet-exposure.test.js test/engineering-excellence/e7-07-perspectives.test.js test/engineering-excellence/e7-08-assurance-e2e.test.js test/engineering-excellence/e6-02-completion.test.js",
71
+ "ee:status": "node ../scripts/engineering-excellence/status.mjs --once",
72
+ "ee:init": "node ../scripts/engineering-excellence/init.mjs",
73
+ "ee:inventory": "node ../scripts/engineering-excellence/inventory.mjs",
74
+ "ee:contract": "node ../scripts/engineering-excellence/contract.mjs",
75
+ "ee:backlog": "node ../scripts/engineering-excellence/backlog.mjs",
76
+ "ee:dataset-audit": "node ../scripts/engineering-excellence/dataset-audit.mjs",
77
+ "ee:independent-baseline": "node ../scripts/engineering-excellence/independent-baseline.mjs",
78
+ "ee:dci-audit": "node ../scripts/engineering-excellence/dci-audit.mjs",
79
+ "ee:runtime-audit": "node ../scripts/engineering-excellence/runtime-audit.mjs",
80
+ "ee:ir-matrix": "node ../scripts/engineering-excellence/ir-matrix.mjs",
70
81
  "test:loop-real-prd": "node --test ../scripts/loop-engineering/test/loop-real-prd.test.js",
71
82
  "test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js ../scripts/loop-engineering/test/loop-assurance-profile.test.js ../scripts/loop-engineering/test/loop-assurance-runtime.test.js ../scripts/loop-engineering/test/loop-bounds.test.js ../scripts/loop-engineering/test/loop-spend.test.js ../scripts/loop-engineering/test/loop-ci-attest.test.js",
72
83
  "test:python": "node ../scripts/run-python-tests.mjs",
@@ -121,7 +132,7 @@
121
132
  "verification:replay-example": "node ../scripts/verification-replay-example.mjs",
122
133
  "test:lifecycle": "node --test test/ci-hang-protection.test.js test/ci-test-shards.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/trust-boundary.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js",
123
134
  "test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
124
- "test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/git-diff-candidates.test.js",
135
+ "test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/premortem.test.js test/context-intelligence/git-diff-candidates.test.js",
125
136
  "eval:independent": "node ../bench/independent-eval/runner.mjs",
126
137
  "eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
127
138
  "smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
@@ -137,6 +148,9 @@
137
148
  "bench:cve-replay:ci": "node ../bench/cve-replay/runner.mjs --ci",
138
149
  "bench:cve-replay:check": "node ../bench/cve-replay/runner.mjs --check-baseline",
139
150
  "bench:cve-replay:update-baseline": "node ../bench/cve-replay/runner.mjs --update-baseline",
151
+ "bench:realcode:report": "node ../bench/independent/realcode.mjs report",
152
+ "bench:realcode:check": "node ../bench/independent/realcode.mjs check --tier release",
153
+ "bench:realcode:check:quick": "node ../bench/independent/realcode.mjs check --tier quick",
140
154
  "bench:independent:gate:check": "node ../scripts/independent-population-gate.mjs",
141
155
  "bench:independent:update-gate-baseline": "node ../scripts/independent-population-gate.mjs --update-baseline",
142
156
  "bench:router-replay": "node ../bench/router-replay/runner.mjs",
@@ -163,9 +177,12 @@
163
177
  "bench:self-scan": "node ../bench/self-scan/measure.mjs",
164
178
  "bench:self-scan:check": "node ../bench/self-scan/check.mjs",
165
179
  "bench:nist-800-171-labels:check": "node ../bench/nist-800-171-labels/check.mjs",
166
- "bench:mutation:check": "node ../bench/mutation/runner.mjs",
180
+ "bench:dci-real-app": "node ../bench/dci-real-app/runner.mjs", "bench:dci-real-app:check": "node ../bench/dci-real-app/runner.mjs --check", "ee:perf-budget": "node ../scripts/engineering-excellence/perf-budget.mjs", "ee:perf-budget:check": "node ../scripts/engineering-excellence/perf-budget.mjs --check", "ee:e2e-demo": "node ../scripts/engineering-excellence/e2e-demo.mjs", "ee:e2e-demo:check": "node ../scripts/engineering-excellence/e2e-demo.mjs --check", "ee:doc-captures": "node ../scripts/engineering-excellence/doc-captures.mjs", "ee:doc-captures:check": "node ../scripts/engineering-excellence/doc-captures.mjs --check", "ee:regression-report": "node ../scripts/engineering-excellence/regression-report.mjs", "ee:regression-report:check": "node ../scripts/engineering-excellence/regression-report.mjs --check", "ee:final-report": "node ../scripts/engineering-excellence/final-report.mjs", "ee:final-report:check": "node ../scripts/engineering-excellence/final-report.mjs --check", "bench:mutation:check": "node ../bench/mutation/runner.mjs",
167
181
  "baseline:capture": "node ../scripts/baseline-capture.mjs",
168
182
  "bench:protection-verdict:check": "node ../bench/protection-verdict/runner.mjs",
183
+ "bench:independent:forensics": "node ../bench/independent/forensics.mjs",
184
+ "bench:fix-verify": "node ../bench/fix-verification/runner.mjs",
185
+ "bench:fix-verify:check": "node ../bench/fix-verification/runner.mjs --check",
169
186
  "bench:fix-correctness": "node ../bench/fix-correctness/runner.mjs",
170
187
  "bench:iac-coverage": "node ../bench/iac-coverage/runner.mjs",
171
188
  "bench:prompt-injection": "node ../bench/prompt-injection/runner.mjs",
@@ -241,7 +258,15 @@
241
258
  "test:nixos-host": "node --test test/nix/nixos-host-runtime.test.js",
242
259
  "test:language-gates": "node --test --test-concurrency=1 test/language/language-benchmark-gates.test.js",
243
260
  "test:language-slow": "node --test --test-concurrency=1 test/language/language-doc-examples-metrics.test.js test/language/language-end-to-end.test.js test/language/language-package-ci.test.js test/language/language-scan-modes.test.js",
244
- "test:chrome": "AGENTIC_SECURITY_CHROME_TESTS=1 node --test test/export-image.test.js test/server/cmd-dataflow-export.test.js"
261
+ "test:chrome": "AGENTIC_SECURITY_CHROME_TESTS=1 node --test test/export-image.test.js test/server/cmd-dataflow-export.test.js",
262
+ "ee:parse-reuse": "node ../scripts/engineering-excellence/parse-reuse-measure.mjs",
263
+ "ee:analysis-cache": "node ../scripts/engineering-excellence/analysis-cache-measure.mjs",
264
+ "ee:evidence-contracts": "node ../scripts/engineering-excellence/evidence-inventory.mjs",
265
+ "release:verify-package": "node ../scripts/verify-release-package.mjs",
266
+ "ee:completion": "node ../scripts/engineering-excellence/completion-analysis.mjs",
267
+ "ee:reproduce-timeouts": "node ../scripts/engineering-excellence/reproduce-timeouts.mjs",
268
+ "ee:provenance-measure": "node ../scripts/engineering-excellence/provenance-measure.mjs",
269
+ "ee:provenance-baseline": "node ../scripts/engineering-excellence/provenance-baseline.mjs"
245
270
  },
246
271
  "author": "Ross Young <ross@clearcapabilities.com>",
247
272
  "license": "PolyForm-Internal-Use-1.0.0",
@@ -10,6 +10,10 @@ Deterministic Context Intelligence (the `context` command). Opt-in and additive:
10
10
  - Model output cannot set severity. A citation outside the bundle invalidates the review. Severity comes from the existing confirmation tier.
11
11
  - Store writes are atomic and refuse links and unsafe paths. Stored packages are re-checked against their hashes.
12
12
  - A job with no verified result is missing coverage, never a clean result.
13
+ - Names from the repository (route strings, file names) are data: sanitised before they are printed, never placed in prompt text outside the fence.
14
+ - Source reaches a non-loopback endpoint only with an explicit approval, and the egress policy still applies after it.
15
+ - A citation is checked for range AND for being about the cited lines. `no_issue_observed` needs a control the deterministic pass also found.
16
+ - Orderings that reach a hashed value use `cmpCodeUnit`, never the locale.
13
17
  - Evaluation is synthetic. Scorecards must say so and must not state an accuracy claim. Holdout use is ledgered.
14
18
 
15
19
  ## Where things are
@@ -85,15 +85,42 @@ export function loadHieMetadata(rootReal, hieDir, { fileContents }) {
85
85
  return { status: 'ok', tool: doc.tool, compiler: doc.compiler, declarations, references, exportHash: hashOf({ declarations, references }) };
86
86
  }
87
87
 
88
+ function uncorroboratedReferences(graph, references, fileContents) {
89
+ const byName = new Map();
90
+ for (const sym of graph.symbols) {
91
+ if (sym.kind !== 'function' || sym.id.includes('::<anon>@')) continue;
92
+ if (!byName.has(sym.name)) byName.set(sym.name, []);
93
+ byName.get(sym.name).push(sym);
94
+ }
95
+ const bad = [];
96
+ for (const r of references) {
97
+ const owners = byName.get(r.from) || [];
98
+ if (owners.length !== 1) continue; // an ambiguous or unknown name is already counted as unmatched and adds nothing
99
+ const sym = owners[0];
100
+ const text = fileContents[sym.file];
101
+ const callee = String(r.to).split(/[.:]/).pop();
102
+ const lineText = typeof text === 'string' ? text.split('\n')[r.line - 1] : undefined;
103
+ const inside = r.line >= sym.startLine && r.line <= sym.endLine;
104
+ if (!inside || lineText === undefined || !callee || !lineText.includes(callee)) bad.push(`${r.from}->${r.to}@${r.line}`);
105
+ }
106
+ return bad;
107
+ }
108
+
88
109
  /**
89
110
  * Merge an imported export into the static graph. Returns { graph, semantic } where `semantic` carries the provenance
90
111
  * and the static-versus-compiler difference report.
91
112
  */
92
113
  export function mergeHieIntoGraph(graph, meta, fileContents) {
93
- void fileContents;
94
114
  if (meta.status !== 'ok') {
95
115
  return { graph, semantic: { source: ['static-ir'], status: meta.status, reason: meta.reason || null, detail: meta.detail || null, diff: null } };
96
116
  }
117
+ // The export is untrusted even when its file hashes match: it is checked into the repository it describes. A reference
118
+ // is accepted only if the line it names, inside the declaration it names, actually mentions the callee. One that does
119
+ // not degrades the whole import, so a crafted export cannot add edges that the source does not contain.
120
+ const uncorroborated = uncorroboratedReferences(graph, meta.references, fileContents || {});
121
+ if (uncorroborated.length) {
122
+ return { graph, semantic: { source: ['static-ir'], status: 'degraded', reason: 'reference-not-in-source', detail: uncorroborated.slice(0, 5).join(', '), diff: null } };
123
+ }
97
124
  const idsByName = new Map();
98
125
  for (const s of graph.symbols) {
99
126
  if (s.kind !== 'function' || s.id.includes('::<anon>@')) continue;
@@ -101,7 +101,8 @@ export async function adjudicate(candidates, opts = {}) {
101
101
  origin: 'context-intelligence', questionPack: c.questionPack, witnesses: c.witnesses, witnessCount: c.witnesses.length,
102
102
  sourceSpanEvidence: c.sourceSpanEvidence, scopeIntegrity: c.scopeIntegrity, reviewStatus: c.reviewStatus,
103
103
  uncertainties: c.uncertainties, recommendedValidation: c.recommendedValidation,
104
- note: 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
104
+ ...(c.deterministic ? { deterministic: c.deterministic } : {}),
105
+ note: c.deterministic ? 'Derived from the code analysis and a deterministic counterexample attempt; no model produced this. Severity comes from the confirmation tier only.' : 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
105
106
  },
106
107
  };
107
108
  };
@@ -14,6 +14,31 @@ export function sha256(text) {
14
14
  return crypto.createHash('sha256').update(text).digest('hex');
15
15
  }
16
16
 
17
+ /**
18
+ * Code-unit order, independent of the host locale. `localeCompare` orders by the process locale, so the same checkout
19
+ * could be sorted differently on two machines; every ordering that reaches a hashed value goes through this instead.
20
+ */
21
+ export function cmpCodeUnit(a, b) {
22
+ const x = String(a); const y = String(b);
23
+ return x < y ? -1 : x > y ? 1 : 0;
24
+ }
25
+
26
+ // Control characters (C0, DEL, C1), the bidirectional override/isolate marks and the line/paragraph separators. A
27
+ // repository controls the text of its own route strings and file names, so anything printed to a terminal or placed
28
+ // in a prompt outside the untrusted fence must not be able to carry an escape sequence or reorder what a reader sees.
29
+ const UNSAFE_DISPLAY = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
30
+
31
+ /** Make text safe to print: unsafe characters become '?'. Newline and tab are kept; use `safeLabel` for one-line values. */
32
+ export function cleanForTerminal(text) {
33
+ return String(text).replace(UNSAFE_DISPLAY, '?');
34
+ }
35
+
36
+ /** A one-line, bounded, printable form of a name taken from source (a route path, a file name). */
37
+ export function safeLabel(text, max = 120) {
38
+ const t = String(text).replace(/[\n\r\t]+/g, ' ').replace(UNSAFE_DISPLAY, '?').replace(/ {2,}/g, ' ').trim();
39
+ return t.length > max ? `${t.slice(0, max - 3)}...` : t;
40
+ }
41
+
17
42
  /** CRLF/CR -> LF so a Windows checkout and a Unix one produce the same spans and hashes. */
18
43
  export function normalizeText(text) {
19
44
  return String(text).replace(/\r\n?/g, '\n');
@@ -16,7 +16,8 @@ const BUILD = [
16
16
  { language: 'javascript/typescript', kind: 'http', frameworks: 'Express-style call registration (get/post/put/patch/delete/all, router.use, app.use)', status: 'verified', evidence: [T('build-js.test.js')], limits: 'Router mounts in another file and middleware factories are not followed (reported as router-mount-not-resolved); destructured and named imports are followed, re-exports and tsconfig path aliases are not.' },
17
17
  { language: 'python', kind: 'http', frameworks: 'decorator registration (Flask/FastAPI-style @app.route / @router.get), FastAPI Depends()/Security() guards', status: 'experimental', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Decorator guards are read from the decorator lines and Depends() guards from the signature (only dependencies named like an auth or user provider bind the principal); framework-level before_request hooks and Django URL conf are not modelled.' },
18
18
  { language: 'haskell', kind: 'http', frameworks: 'Scotty, WAI, Servant, Yesod (through the existing web model)', status: 'verified', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Static source analysis; the existing web model decides what a route is. Dynamic routes are reported unresolved.' },
19
- { language: 'java/kotlin/c#/go/php/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'No fixture proves this, so it is listed as unsupported even though some routes may resolve: Handlers are resolved only for decorator/annotation-style registration; no dedicated fixtures here.' },
19
+ { language: 'php', kind: 'http', frameworks: 'Laravel Route:: registration (the entry point only)', status: 'experimental', evidence: ['test/engineering-excellence/e4-01-dci-audit.test.js'], limits: 'Measured by the E4-01 audit on one fixture: the routes resolve, but no control or sensitive operation was extracted from the handlers.' },
20
+ { language: 'java/kotlin/c#/go/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'Measured by the E4-01 audit (docs/engineering-excellence/DCI_MATURITY.md) on one fixture per language (Spring, net/http, ASP.NET, Rails): no entry point was found, so no bundle is built.' },
20
21
  { language: 'any with a recognised surface', kind: 'webhook/queue/cron/upload/cli', frameworks: 'existing entry-point inventory', status: 'experimental', evidence: [T('build-js.test.js')], limits: 'Handler is the enclosing declaration of the surface; no framework wiring beyond that.' },
21
22
  { language: 'haskell', kind: 'compiler metadata import', frameworks: 'hie-export/1 JSON export of GHC declarations and references', status: 'experimental', evidence: [T('hie-adapter.test.js')], limits: 'Reads an export; never runs GHC or hiedb. The export is validated against the source snapshot. No export produced by a real GHC run is checked in.' },
22
23
  { language: 'any', kind: 'compiler metadata build', frameworks: 'automatic metadata generation', status: 'blocked', evidence: [], limits: 'Running a compiler on untrusted source needs a sandbox proof this build does not have (Template Haskell, custom Setup and plugins run code).' },
@@ -16,6 +16,7 @@ import { readStoreJson, writeStoreJson, readManifest, readPackageText, verifySto
16
16
  import { diffManifestSets, cacheReport } from './diff.js';
17
17
  import { readGitSnapshot, readSnapshot } from './files.js';
18
18
  import { QUESTION_PACKS, packById } from './prompts/packs.js';
19
+ import { cleanForTerminal } from './canonical.js';
19
20
  import { describeModel, resolveInvoker, buildReviewPrompt, redactPackageText, checkEgressForFiles } from './review.js';
20
21
  import { planJobs, runQueue, exportBatch, importBatch, HARD_LIMITS } from './queue.js';
21
22
  import { reviewsToCandidates, adjudicate } from './candidates.js';
@@ -26,21 +27,24 @@ const flag = (a, k) => (a.flags[k] === undefined ? undefined : a.flags[k]);
26
27
  const truthy = (v) => v === true || (typeof v === 'string' && v !== 'false' && v !== '0');
27
28
  const num = (v) => (v === undefined || v === true ? undefined : Number(v));
28
29
 
30
+ /** Everything this command prints goes through here: repository-supplied names must not carry escape sequences to a terminal. */
31
+ function say(text) { console.log(cleanForTerminal(text)); }
32
+
29
33
  function out(format, text, obj) {
30
- if (format === 'json') console.log(JSON.stringify(obj, null, 2));
31
- else if (format === 'jsonl') console.log(JSON.stringify(obj));
32
- else console.log(text);
34
+ if (format === 'json') say(JSON.stringify(obj, null, 2));
35
+ else if (format === 'jsonl') say(JSON.stringify(obj));
36
+ else say(text);
33
37
  }
34
38
 
35
39
  function usage() {
36
- console.log([
40
+ say([
37
41
  'Usage: agentic-security context <subcommand> [--root <dir>] [--format text|json|jsonl]',
38
42
  '',
39
43
  ' build Resolve entry points and write one inspectable context bundle each (no model, no network)',
40
44
  ' [--entrypoints http,webhook,queue,cron,upload,cli] [--profile compact|standard|extended] [--max-tokens N] [--route <label|id>] [--no-write]',
41
45
  ' inspect Show one bundle: --route <label|id> | --hash <contextHash> [--show-context] [--package N]',
42
46
  ' diff Routes added/removed/changed between two refs: --from <ref> [--to <ref>] (omit --to for the working tree)',
43
- ' review Focused one-call reviews over frozen bundles: --question <pack[,pack]> [--route x] [--local-only] [--concurrency N]',
47
+ ' review Focused one-call reviews over frozen bundles: --question <pack[,pack]> [--route x] [--local-only] [--allow-remote] [--concurrency N]',
44
48
  ' [--max-calls N] [--max-wall-seconds N] [--max-cost-usd N --cost-per-call-usd N] [--retry-failed] [--refute] [--strict]',
45
49
  ' [--dry-run] [--export-batch] [--import-batch <results.jsonl> --batch-id <id>]',
46
50
  ' status Coverage so far: entry points, bundles, review jobs',
@@ -62,7 +66,7 @@ async function doBuild(root, args, { write = true } = {}) {
62
66
  if (flag(args, 'max-tokens')) config.context.maxTokens = num(flag(args, 'max-tokens'));
63
67
  const events = [];
64
68
  const jsonl = flag(args, 'format') === 'jsonl';
65
- const built = await buildContext(root, { config, route: entrypointFilter(flag(args, 'route')), onEvent: (e) => { events.push(e); if (jsonl) console.log(JSON.stringify(e)); } });
69
+ const built = await buildContext(root, { config, route: entrypointFilter(flag(args, 'route')), onEvent: (e) => { events.push(e); if (jsonl) say(JSON.stringify(e)); } });
66
70
  if (!built.ok) return { ok: false, built, config, problems };
67
71
  let persisted = null;
68
72
  if (write) persisted = persistBuild(root, built, config);
@@ -155,7 +159,7 @@ async function cmdInspect(root, args, format) {
155
159
  ` boundaries (calls into libraries, not first-party code): ${m.boundaries.length}`,
156
160
  ];
157
161
  if (text) lines.push('', text.ok ? text.text : `(context text unavailable: ${text.code})`);
158
- console.log(lines.join('\n'));
162
+ say(lines.join('\n'));
159
163
  return 0;
160
164
  }
161
165
 
@@ -255,7 +259,7 @@ async function cmdExport(root, args, format) {
255
259
  if (!h || h === true) { out(format, 'context export: --hash <contextHash> is required', { ok: false }); return 1; }
256
260
  const r = exportBundle(root, String(h));
257
261
  if (!r.ok) { out(format, `context export: refused (${r.problems.join(', ')})`, { ok: false, problems: r.problems }); return 1; }
258
- console.log(JSON.stringify(r.export, null, 2));
262
+ say(JSON.stringify(r.export, null, 2));
259
263
  return 0;
260
264
  }
261
265
 
@@ -303,7 +307,7 @@ async function cmdReview(root, args, format) {
303
307
  const r = await doBuild(root, args, { write: true });
304
308
  if (!r.ok) { out(format, `context review: build failed (${r.built.reason})`, { ok: false, reason: r.built.reason }); return r.built.reason === 'too-many-files' ? 2 : 1; }
305
309
  const { built } = r;
306
- const model = describeModel();
310
+ const model = describeModel({ allowRemote: truthy(flag(args, 'allow-remote')) || r.config.review.allowRemote === true });
307
311
  const { jobs, droppedByJobLimit } = planJobs({ bundles: built.bundles, packs, model, maxJobs: Math.min(HARD_LIMITS.maxJobs, num(flag(args, 'max-jobs')) || HARD_LIMITS.maxJobs) });
308
312
  const loadJob = loadJobFrom(built, packs);
309
313
  const ctxBase = { root: built.root, fileContents: built.fileContents, model };
@@ -341,12 +345,14 @@ async function cmdReview(root, args, format) {
341
345
  try {
342
346
  result = await runQueue(jobs, {
343
347
  ...ctxBase, invoker, loadJob, signal: ac.signal, retryFailed: truthy(flag(args, 'retry-failed')),
344
- onEvent: jsonl ? (e) => console.log(JSON.stringify(e)) : undefined,
348
+ onEvent: jsonl ? (e) => say(JSON.stringify(e)) : undefined,
345
349
  limits: {
346
350
  concurrency: num(flag(args, 'concurrency')) || config.review.concurrency,
347
351
  maxCalls: num(flag(args, 'max-calls')) || config.review.maxCalls,
348
352
  maxWallSeconds: num(flag(args, 'max-wall-seconds')) || config.review.maxWallSeconds,
349
353
  maxTokensPerJob: config.review.maxTokensPerJob,
354
+ maxTokensPerRepo: config.review.maxTokensPerRepo,
355
+ anonymizePrompts: config.review.anonymizePrompts,
350
356
  maxRetries: config.review.maxRetries,
351
357
  jobTimeoutMs: (num(flag(args, 'job-timeout-seconds')) || 120) * 1000,
352
358
  maxCostUsd: num(flag(args, 'max-cost-usd')) ?? config.review.maxCostUsd,
@@ -22,7 +22,7 @@ export const DEFAULT_CONFIG = Object.freeze({
22
22
  maxFiles: 3000,
23
23
  semanticMetadata: { mode: 'off', haskellHieDir: null },
24
24
  context: { profile: 'standard', maxTokens: null, pruneLowValue: true, maxSymbols: 60, maxDepth: 12 },
25
- review: { enabled: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3 },
25
+ review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3, maxTokensPerRepo: null, anonymizePrompts: false },
26
26
  replay: { contentAddressed: true, retention: 'inherit' },
27
27
  });
28
28
 
@@ -63,6 +63,7 @@ export function normalizeConfig(raw) {
63
63
  },
64
64
  review: {
65
65
  enabled: rv.enabled === true,
66
+ allowRemote: rv.allowRemote === true,
66
67
  questionPacks: Array.isArray(rv.questionPacks) && rv.questionPacks.length ? rv.questionPacks.filter(x => typeof x === 'string') : d.review.questionPacks,
67
68
  concurrency: clamp(rv.concurrency, 1, 16, d.review.concurrency),
68
69
  maxCalls: clamp(rv.maxCalls, 1, 5000, d.review.maxCalls),
@@ -70,6 +71,8 @@ export function normalizeConfig(raw) {
70
71
  maxCostUsd: Number.isFinite(rv.maxCostUsd) && rv.maxCostUsd >= 0 ? rv.maxCostUsd : null,
71
72
  maxTokensPerJob: clamp(rv.maxTokensPerJob, 1000, 400000, d.review.maxTokensPerJob),
72
73
  maxRetries: clamp(rv.maxRetries, 0, 3, d.review.maxRetries),
74
+ maxTokensPerRepo: Number.isFinite(rv.maxTokensPerRepo) && rv.maxTokensPerRepo >= 1000 ? Math.trunc(rv.maxTokensPerRepo) : null,
75
+ anonymizePrompts: rv.anonymizePrompts === true,
73
76
  },
74
77
  replay: { contentAddressed: rp.contentAddressed !== false, retention: typeof rp.retention === 'string' ? rp.retention : 'inherit' },
75
78
  };
@@ -47,7 +47,46 @@ export function signatureText(lines, declLine, maxLines = 12) {
47
47
  for (let i = declLine - 1; i < Math.min(lines.length, declLine - 1 + maxLines); i++) { t += ' ' + lines[i]; if (/\)\s*(?:->[^:]*)?:\s*(?:#.*)?$/.test(lines[i])) break; }
48
48
  return t;
49
49
  }
50
- const OWNER_FIELD = /(?:owner|tenant|org(?:anization)?|account|created_?by|author|user_?id|userId|team|workspace)/i;
50
+ // An owner/tenant FIELD NAME, not any word that merely contains one: `ownerId`, `owner_id`, `tenant`, `getOwnerId`
51
+ // qualify; `owner_name` and `cols` do not (the trailing boundary rejects them).
52
+ const OWNER_KEY = '\\w*?(?:[Oo]wner|[Tt]enant|[Oo]rg(?:ani[sz]ation)?|[Aa]ccount|[Cc]reated_?[Bb]y|[Aa]uthor|[Tt]eam|[Ww]orkspace)(?:_?(?:[Ii]d|ID))?\\b|[Uu]ser_?(?:[Ii]d|ID)\\b';
53
+ const OWNER_KEY_RE = new RegExp(`(?<![\\w])(?:${OWNER_KEY})`);
54
+ const TENANT_WORD = /tenant|org|workspace|team|account/i;
55
+ // a SQL string comparing an owner column with a bound parameter: `owner_id = ?`, `owner_id = $1`, `tenant_id = :t`
56
+ const SQL_OWNER_PLACEHOLDER = new RegExp(`(?<![\\w])(?:${OWNER_KEY})\\s*(?:=|\\bIN\\b|\\bin\\b)\\s*\\(?\\s*(?:\\?|\\$\\d+|:\\w+|%s|@\\w+)`);
57
+ // an owner column compared with the principal written straight into the string: `owner_id = ${req.user.id}`
58
+ const SQL_OWNER_INTERPOLATED = new RegExp(`(?<![\\w])(?:${OWNER_KEY})\\s*=\\s*(?:'?\\s*\\+\\s*|\\$\\{\\s*)(?:${QUALIFIED_PRINCIPAL.source}|${BARE_PRINCIPAL.source})`);
59
+ const PRINCIPAL_GLOBAL = new RegExp(QUALIFIED_PRINCIPAL.source, 'g');
60
+ const STRING_LITERAL = /(["'`])(?:\\.|(?!\1).)*\1/g;
61
+
62
+ /**
63
+ * Is an owner/tenant FIELD actually tied to the principal in this text? The field and the principal must sit in the
64
+ * same clause (split on , ; && || braces), so `{ audit: req.user.id, cols: 'owner_name' }` is a decoy and
65
+ * `{ ownerId: req.user.id }` is a filter. A SQL string with an owner column against a bound placeholder counts when the
66
+ * principal is passed in the same call. Returns null or { level: 'qualified'|'bare', tenant }.
67
+ */
68
+ export function ownerBinding(text, extra = null) {
69
+ let best = null;
70
+ const take = (level, tenant) => { if (!best || (level === 'qualified' && best.level !== 'qualified')) best = { level, tenant }; };
71
+ const code = text.replace(STRING_LITERAL, (m) => (SQL_OWNER_PLACEHOLDER.test(m) || SQL_OWNER_INTERPOLATED.test(m) ? m : '""'));
72
+ for (const clause of code.split(/[,;{}]|&&|\|\|/)) {
73
+ // the principal's own name (`req.user`, `current_user`) is not an owner field, so it is removed before looking for one
74
+ const forKey = clause.replace(STRING_LITERAL, ' ').replace(PRINCIPAL_GLOBAL, ' ');
75
+ if (!OWNER_KEY_RE.test(forKey)) continue;
76
+ const pr = principalIn(clause, extra);
77
+ if (pr) take(pr, TENANT_WORD.test(clause));
78
+ }
79
+ for (const m of text.match(STRING_LITERAL) || []) {
80
+ const placeholder = SQL_OWNER_PLACEHOLDER.test(m);
81
+ if (placeholder) {
82
+ const pr = principalIn(text, extra);
83
+ if (pr) take(pr, TENANT_WORD.test(m));
84
+ }
85
+ if (SQL_OWNER_INTERPOLATED.test(m)) take('qualified', TENANT_WORD.test(m));
86
+ }
87
+ return best;
88
+ }
89
+
51
90
  const COMPARE = /(?:===|!==|==|!=|\.equals\(|\bis not\b|\bis\b|\bnot in\b|\bin\b|\.includes\(|\.has\()/;
52
91
  const REJECT = /(?:\breturn\b|\bthrow\b|\braise\b|\babort\b|\bexit\b|\b40[0-9]\b|\bforbidden\b|\bunauthori[sz]ed\b|\bHttpError\b|\breject\b|\bnot_?allowed\b|\bdeny\b|\bdenied\b|throwError|\bfinish\b|\bhalt\b|\bPermissionDenied\b|\bAccessDenied\b|status\()/i;
53
92
 
@@ -58,6 +97,13 @@ const DB_RECEIVER = /(?:^|\b)(?:db|database|conn|connection|pool|client|cursor|s
58
97
  const SQL_TEXT = /\b(?:select\s[\s\S]{1,120}\sfrom|insert\s+into|update\s+\w+\s+set|delete\s+from)\b/i;
59
98
  const HS_SENSITIVE = /(?:^|\.)(?:runDB|selectList|selectFirst|getBy|getJust|insert_?|insertEntity|update|updateWhere|delete|deleteWhere|replace|upsert|query|query_|execute|execute_|readFile|writeFile|callProcess|readProcess)$|^(?:Database\.|System\.Process\.|System\.Directory\.)/;
60
99
 
100
+ // Names that answer a question rather than enforce a rule.
101
+ const PREDICATE_NAME = /^(?:is|has|can|cannot|allow|owned|belongs)/i;
102
+ function bareCallStatement(trimmedLine, name) {
103
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ return new RegExp(`^(?:await\\s+)?${esc}\\s*\\(`).test(trimmedLine) && /\)\s*;?\s*(?:\/\/.*|\/\*.*)?$/.test(trimmedLine);
105
+ }
106
+
61
107
  const WRITE_VERB = /(?:insert|update|upsert|delete|remove|destroy|save|create|write|append|unlink|rm|exec|spawn|system|popen|post|put|patch|replace)/i;
62
108
 
63
109
  function kindOfAuthName(name) {
@@ -93,6 +139,51 @@ function clip(s) { return s.trim().slice(0, 160); }
93
139
  * lines the FILE's lines (0-based array)
94
140
  * Returns [{ symbolId, line, category:'control'|'sensitive', kind, subkind, evidence, basis, rejectionSeen, text }]
95
141
  */
142
+ /**
143
+ * Blank the comments (and Python docstrings) of a source file, keeping every character position and newline, so line numbers do not move.
144
+ * The analysis looks for calls and comparisons in code; a comment that SAYS `ownerId: req.user.id` or `requireUser(req)` is prose, and prose is the
145
+ * easiest thing for a hostile or merely careless author to write. Strings are left alone (the principal and SQL rules already read around them).
146
+ */
147
+ export function blankComments(text, file = '') {
148
+ const src = String(text || '');
149
+ const hash = /\.(?:py|rb|sh|php|pl|r)$/i.test(file);
150
+ const dashes = /\.l?hs$/i.test(file);
151
+ const cLike = !dashes && !/\.(?:py|rb|sh|pl|r)$/i.test(file);
152
+ const out = src.split('');
153
+ const n = src.length;
154
+ let i = 0;
155
+ const blank = (a, b) => { for (let k = a; k < b && k < n; k++) if (out[k] !== '\n') out[k] = ' '; };
156
+ while (i < n) {
157
+ const c = src[i]; const d = src[i + 1];
158
+ if (c === '"' || c === "'" || c === '`') {
159
+ if (/\.py$/i.test(file) && src.startsWith(c.repeat(3), i)) {
160
+ const end = src.indexOf(c.repeat(3), i + 3); const stop = end < 0 ? n : end + 3;
161
+ blank(i, stop); i = stop; continue; // a docstring or triple-quoted block is prose
162
+ }
163
+ let j = i + 1;
164
+ while (j < n && src[j] !== c && !(src[j] === '\n' && c !== '`')) { if (src[j] === '\\') j++; j++; }
165
+ i = j + 1; continue;
166
+ }
167
+ if (cLike && c === '/' && d === '*') { const end = src.indexOf('*/', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
168
+ if (cLike && c === '/' && d === '/' && (i === 0 || /[\s;,)}{\]]/.test(src[i - 1]))) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
169
+ if (hash && c === '#' && !(file.endsWith('.php') && d === '[')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
170
+ if (dashes && c === '-' && d === '-' && !/[!#$%&*+./<=>?@\\^|~:]/.test(src[i + 2] || ' ')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
171
+ if (dashes && c === '{' && d === '-') { const end = src.indexOf('-}', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
172
+ i++;
173
+ }
174
+ return out.join('');
175
+ }
176
+
177
+ function bodyAfterSignature(raw) {
178
+ const arrow = raw.indexOf('=>');
179
+ let rest = null;
180
+ if (arrow >= 0) rest = raw.slice(arrow + 2);
181
+ else { const m = /\)\s*\{(.*)$/.exec(raw); if (m) rest = m[1]; }
182
+ if (rest === null) return '';
183
+ const brace = /^\s*\{(.*)$/.exec(rest);
184
+ return brace ? brace[1] : rest;
185
+ }
186
+
96
187
  export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
97
188
  const out = [];
98
189
  const seen = new Set();
@@ -125,15 +216,22 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
125
216
  const calls = [];
126
217
  const re = /([A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)\s*\(/g;
127
218
  let m;
128
- if (!isDeclLine) { while ((m = re.exec(raw))) calls.push(m[1]); }
219
+ // A one-line handler (`app.get('/x', guard, async (req, res) => { res.json(await db.docs.findOne({ id })); })`) is its own declaration
220
+ // line, so skipping the whole line also skipped its body. Only the SIGNATURE is skipped: the calls after the arrow or the opening brace count.
221
+ const scanText = isDeclLine ? bodyAfterSignature(raw) : raw;
222
+ while ((m = re.exec(scanText))) calls.push(m[1]);
129
223
  if (haskell && !isDeclLine) {
130
224
  const hre = /\b([A-Z][\w.]*\.[a-z][\w']*|[a-z][\w']*)\b/g;
131
225
  while ((m = hre.exec(raw))) if (AUTH_NAME.test(m[1]) || HS_SENSITIVE.test(m[1])) calls.push(m[1]);
132
226
  }
133
227
  for (const name of calls) {
134
228
  if (AUTH_NAME.test(name) && !/^(?:if|for|while|switch|catch|function)$/.test(name)) {
135
- const rej = REJECT.test(lines.slice(ln - 1, ln + 3).join(' ')) || /^(?:require|ensure|enforce|assert|authori[sz]e|authenticate|protect)/i.test(name.split(/[.:]/).pop());
136
- push({ line: ln, category: 'control', kind: kindOfAuthName(name), subkind: null, evidence: 'inferred', basis: 'call-name', rejectionSeen: rej, text: clip(raw) });
229
+ const lastSeg = name.split(/[.:]/).pop();
230
+ // A predicate (isOwner, hasRole, can) only returns an answer. Called as a bare statement its answer is thrown away,
231
+ // so it guards nothing, whatever the lines around it say.
232
+ const discarded = !haskell && PREDICATE_NAME.test(lastSeg) && bareCallStatement(t, name);
233
+ const rej = !discarded && (REJECT.test(lines.slice(ln - 1, ln + 3).join(' ')) || /^(?:require|ensure|enforce|assert|authori[sz]e|authenticate|protect)/i.test(lastSeg));
234
+ push({ line: ln, category: 'control', kind: kindOfAuthName(name), subkind: null, evidence: 'inferred', basis: discarded ? 'call-name-result-discarded' : 'call-name', rejectionSeen: rej, text: clip(raw) });
137
235
  }
138
236
  const last = name.split(/[.:]/).pop();
139
237
  const recv = name.includes('.') ? name.slice(0, name.lastIndexOf('.')) : '';
@@ -149,9 +247,9 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
149
247
  push({ line: ln, category: 'sensitive', kind: sub, subkind: verb, evidence: 'observed', basis: 'call-name', rejectionSeen: null, text: clip(raw) });
150
248
  // a data operation whose own statement mentions the principal and an owner/tenant field is scoped by it
151
249
  const stmt = statementAt(lines, ln);
152
- const pr = sub.startsWith('data-') && OWNER_FIELD.test(stmt) ? principalIn(stmt, extraPrincipal) : null;
250
+ const pr = sub.startsWith('data-') ? ownerBinding(stmt, extraPrincipal) : null;
153
251
  if (pr) {
154
- push({ line: ln, category: 'control', kind: /tenant|org|workspace|team|account/i.test(stmt) ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr === 'qualified' ? 'observed' : 'inferred', basis: pr === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
252
+ push({ line: ln, category: 'control', kind: pr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr.level === 'qualified' ? 'observed' : 'inferred', basis: pr.level === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
155
253
  }
156
254
  }
157
255
  }
@@ -159,17 +257,17 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
159
257
  const verb = /\b(?:insert|update|delete)\b/i.test(raw) ? 'write' : 'read';
160
258
  push({ line: ln, category: 'sensitive', kind: `data-${verb}`, subkind: verb, evidence: 'observed', basis: 'sql-text', rejectionSeen: null, text: clip(raw) });
161
259
  const stmt = statementAt(lines, ln);
162
- const pr = OWNER_FIELD.test(stmt) ? principalIn(stmt, extraPrincipal) : null;
260
+ const pr = ownerBinding(stmt, extraPrincipal);
163
261
  if (pr) {
164
- push({ line: ln, category: 'control', kind: /tenant|org|workspace|team|account/i.test(stmt) ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr === 'qualified' ? 'observed' : 'inferred', basis: pr === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
262
+ push({ line: ln, category: 'control', kind: pr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr.level === 'qualified' ? 'observed' : 'inferred', basis: pr.level === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
165
263
  }
166
264
  }
167
265
 
168
266
  // a resource field compared with the principal: the ownership check itself
169
- const cmpPr = COMPARE.test(raw) && OWNER_FIELD.test(raw) && /\b(?:if|unless|assert|when|guard|require|expect|where|elif|&&|\|\|)\b|\?/.test(raw) ? principalIn(raw, extraPrincipal) : null;
267
+ const cmpPr = COMPARE.test(raw) && /\b(?:if|unless|assert|when|guard|require|expect|where|elif|&&|\|\|)\b|\?/.test(raw) ? ownerBinding(raw, extraPrincipal) : null;
170
268
  if (cmpPr) {
171
269
  const rej = REJECT.test(lines.slice(ln - 1, ln + 3).join(' '));
172
- push({ line: ln, category: 'control', kind: /tenant|org|workspace|team|account/i.test(raw) ? 'tenant-scope' : 'ownership', subkind: null, evidence: cmpPr === 'qualified' ? 'observed' : 'inferred', basis: cmpPr === 'qualified' ? 'comparison' : 'comparison-bare-identifier', rejectionSeen: rej, text: clip(raw) });
270
+ push({ line: ln, category: 'control', kind: cmpPr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: cmpPr.level === 'qualified' ? 'observed' : 'inferred', basis: cmpPr.level === 'qualified' ? 'comparison' : 'comparison-bare-identifier', rejectionSeen: rej, text: clip(raw) });
173
271
  }
174
272
  }
175
273
  // A function whose NAME says it is a guard and whose BODY shows a rejection path is a guard function. This is