@clear-capabilities/agentic-security-scanner 0.74.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 (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,229 @@
1
+ // Differential / incremental taint (P4.3).
2
+ //
3
+ // Today every scan re-analyzes every file. On a 100k-LoC monorepo the
4
+ // deep-engine pass takes 4-8 minutes. PR-scoped re-analysis should be
5
+ // 10-50× faster.
6
+ //
7
+ // Strategy:
8
+ // 1. Persist a SHA-256 of each file's POST-COMMENT-STRIP source under
9
+ // `.agentic-security/incremental/files.json`.
10
+ // 2. Persist each function's SUMMARY (returnTainted / mutatedParams /
11
+ // taintedGlobals) keyed by `qid` under
12
+ // `.agentic-security/incremental/summaries.json`.
13
+ // 3. On the next scan, diff the file-hash map. For unchanged files,
14
+ // seed the SummaryCache with the persisted summaries.
15
+ // 4. For CHANGED files, invalidate their qids' summaries AND the
16
+ // summaries of any function that previously called into them
17
+ // (back-pointer set persisted alongside summaries).
18
+ //
19
+ // Safety:
20
+ // - The cache invalidates on rule-pack version change (any change to
21
+ // `catalog.js` bumps the rules.lock.json digest).
22
+ // - The cache invalidates on scanner version change.
23
+ // - On any inconsistency (truncated file, JSON parse error), the cache
24
+ // is dropped and we fall back to a full scan.
25
+ //
26
+ // This module is purely the persistence + invalidation layer. The engine
27
+ // is responsible for calling `seedSummaryCache` / `recordSummary` /
28
+ // `commitIncrementalState`.
29
+
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import * as crypto from 'node:crypto';
33
+
34
+ const STATE_DIR = '.agentic-security/incremental';
35
+ const FILES_PATH = 'files.json';
36
+ const SUMMARIES_PATH = 'summaries.json';
37
+ const VERSION_PATH = 'version.json';
38
+ const MAX_PERSISTED_SUMMARIES = 50000;
39
+
40
+ /** Compute the content hash used for file-equality detection. */
41
+ export function hashFileContent(stripped) {
42
+ return crypto.createHash('sha256').update(stripped || '').digest('hex');
43
+ }
44
+
45
+ /** Read the persisted state. Returns a fresh empty state on any error. */
46
+ export function readIncrementalState(projectRoot) {
47
+ const dir = path.join(projectRoot, STATE_DIR);
48
+ try {
49
+ const versionFp = path.join(dir, VERSION_PATH);
50
+ if (!fs.existsSync(versionFp)) return _emptyState();
51
+ const v = JSON.parse(fs.readFileSync(versionFp, 'utf8'));
52
+ return {
53
+ version: v,
54
+ files: _readJsonOrEmpty(path.join(dir, FILES_PATH), {}),
55
+ summaries: _readJsonOrEmpty(path.join(dir, SUMMARIES_PATH), {}),
56
+ };
57
+ } catch (_e) {
58
+ return _emptyState();
59
+ }
60
+ }
61
+
62
+ function _emptyState() {
63
+ return { version: null, files: {}, summaries: {} };
64
+ }
65
+
66
+ function _readJsonOrEmpty(fp, fallback) {
67
+ try {
68
+ if (!fs.existsSync(fp)) return fallback;
69
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
70
+ } catch (_e) {
71
+ return fallback;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Validate persisted state against the current rule-pack + scanner version.
77
+ * Returns `{ valid: boolean, reason?: string }`.
78
+ */
79
+ export function validateIncrementalState(state, currentVersion) {
80
+ if (!state || !state.version) return { valid: false, reason: 'no prior state' };
81
+ if (!currentVersion) return { valid: false, reason: 'no current version' };
82
+ if (state.version.scanner !== currentVersion.scanner) return { valid: false, reason: 'scanner version changed' };
83
+ if (state.version.rules !== currentVersion.rules) return { valid: false, reason: 'rule-pack changed' };
84
+ return { valid: true };
85
+ }
86
+
87
+ /**
88
+ * Diff the previous file-hash map against the current scan's hashes.
89
+ * Returns:
90
+ * { unchanged: [filePath, ...], changed: [filePath, ...], added: [...], removed: [...] }
91
+ */
92
+ export function diffFileHashes(prevFiles, currentHashes) {
93
+ const unchanged = [];
94
+ const changed = [];
95
+ const added = [];
96
+ const removed = [];
97
+ for (const [fp, h] of Object.entries(currentHashes)) {
98
+ if (!(fp in prevFiles)) added.push(fp);
99
+ else if (prevFiles[fp] === h) unchanged.push(fp);
100
+ else changed.push(fp);
101
+ }
102
+ for (const fp of Object.keys(prevFiles)) {
103
+ if (!(fp in currentHashes)) removed.push(fp);
104
+ }
105
+ return { unchanged, changed, added, removed };
106
+ }
107
+
108
+ /**
109
+ * Decide which previously-persisted summaries are still safe to reuse.
110
+ *
111
+ * summaries: persisted summary map { qid: summary }
112
+ * callerOfQid: persisted reverse-call-graph { qid: [callerQid, ...] }
113
+ * changedQids: Set of qids whose source files changed
114
+ *
115
+ * Returns: { reusable: Set<qid>, invalidated: Set<qid> }
116
+ */
117
+ export function pickReusableSummaries(summaries, callerOfQid, changedQids) {
118
+ const invalidated = new Set();
119
+ // Seed with directly-changed qids.
120
+ for (const q of changedQids) invalidated.add(q);
121
+ // BFS via reverse call graph — invalidate every transitive caller.
122
+ const stack = [...changedQids];
123
+ while (stack.length) {
124
+ const q = stack.pop();
125
+ const callers = callerOfQid?.[q] || [];
126
+ for (const c of callers) {
127
+ if (!invalidated.has(c)) {
128
+ invalidated.add(c);
129
+ stack.push(c);
130
+ }
131
+ }
132
+ }
133
+ const reusable = new Set();
134
+ for (const q of Object.keys(summaries || {})) {
135
+ if (!invalidated.has(q)) reusable.add(q);
136
+ }
137
+ return { reusable, invalidated };
138
+ }
139
+
140
+ /** Seed a SummaryCache instance from persisted summaries. */
141
+ export function seedSummaryCache(summaryCache, persisted, reusableQids) {
142
+ if (!summaryCache || !persisted) return 0;
143
+ let n = 0;
144
+ for (const qid of reusableQids) {
145
+ const s = persisted[qid];
146
+ if (!s) continue;
147
+ // Reconstitute Set fields (JSON dropped them).
148
+ const summary = {
149
+ returnTainted: !!s.returnTainted,
150
+ mutatedParams: new Set(s.mutatedParams || []),
151
+ taintedGlobals: new Set(s.taintedGlobals || []),
152
+ findings: Array.isArray(s.findings) ? s.findings : [],
153
+ };
154
+ // Use the bottom taint-state key — these are summaries that DON'T depend
155
+ // on entry taint state (e.g., pure functions). Higher-fidelity reuse
156
+ // would require persisting the entry-state hash too; deferred.
157
+ summaryCache.set(qid, new Set(), summary, null);
158
+ n++;
159
+ }
160
+ return n;
161
+ }
162
+
163
+ /**
164
+ * Serialize a SummaryCache for persistence. Only persists summaries with
165
+ * `_persistable: true` (set by the engine when the summary is independent
166
+ * of an entry taint-state — typically pure functions or terminal sinks).
167
+ *
168
+ * Returns a plain object `{ qid: summary }` safe for JSON.stringify.
169
+ */
170
+ export function serializeSummaries(summaryCache) {
171
+ const out = {};
172
+ if (!summaryCache || !summaryCache._cache) return out;
173
+ let count = 0;
174
+ for (const [key, summary] of summaryCache._cache) {
175
+ if (count >= MAX_PERSISTED_SUMMARIES) break;
176
+ if (!summary || summary._budgetExceeded || summary._recursive) continue;
177
+ const qid = key.split('::')[0];
178
+ if (!qid) continue;
179
+ out[qid] = {
180
+ returnTainted: !!summary.returnTainted,
181
+ mutatedParams: [...(summary.mutatedParams || [])],
182
+ taintedGlobals: [...(summary.taintedGlobals || [])],
183
+ findings: Array.isArray(summary.findings) ? summary.findings.slice(0, 50) : [],
184
+ };
185
+ count++;
186
+ }
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * Commit incremental state to disk. Idempotent — safe to call from anywhere.
192
+ *
193
+ * state.files { filepath: sha256 }
194
+ * state.summaries { qid: summary } (output of serializeSummaries)
195
+ * state.callers { qid: [callerQid] } (reverse call-graph)
196
+ * currentVersion { scanner, rules }
197
+ */
198
+ export function commitIncrementalState(projectRoot, state, currentVersion) {
199
+ if (!projectRoot) return false;
200
+ const dir = path.join(projectRoot, STATE_DIR);
201
+ try {
202
+ fs.mkdirSync(dir, { recursive: true });
203
+ fs.writeFileSync(path.join(dir, VERSION_PATH), JSON.stringify(currentVersion, null, 2));
204
+ fs.writeFileSync(path.join(dir, FILES_PATH), JSON.stringify(state.files || {}, null, 2));
205
+ const payload = {
206
+ summaries: state.summaries || {},
207
+ callers: state.callers || {},
208
+ };
209
+ fs.writeFileSync(path.join(dir, SUMMARIES_PATH), JSON.stringify(payload));
210
+ return true;
211
+ } catch (_e) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ /** Drop persisted state — used when a version mismatch is detected. */
217
+ export function dropIncrementalState(projectRoot) {
218
+ const dir = path.join(projectRoot, STATE_DIR);
219
+ try {
220
+ if (!fs.existsSync(dir)) return true;
221
+ for (const fn of [VERSION_PATH, FILES_PATH, SUMMARIES_PATH]) {
222
+ const fp = path.join(dir, fn);
223
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
224
+ }
225
+ return true;
226
+ } catch (_e) {
227
+ return false;
228
+ }
229
+ }
@@ -0,0 +1,181 @@
1
+ // Layer 2 entry point.
2
+ import { runTaintEngine } from './engine.js';
3
+ import { CATALOG, matchSource, matchSinkOrSanitizer, _catalogSize } from './catalog.js';
4
+ import { applyPathFeasibility } from './path-feasibility.js';
5
+ import { SummaryCache, entryStateFromCall } from './summaries.js';
6
+ import { rhsReachableFunctions, shouldAnalyzeUnderRhs } from './tabulation.js';
7
+ import { annotateBackwardSlices } from './backward.js';
8
+ import {
9
+ readIncrementalState, validateIncrementalState, diffFileHashes,
10
+ hashFileContent, pickReusableSummaries, seedSummaryCache,
11
+ serializeSummaries, commitIncrementalState,
12
+ } from './incremental.js';
13
+ import { buildPointsTo } from './points-to.js';
14
+ import { annotateSoftTaint } from './soft-taint.js';
15
+ import { runIfdsTaintEngine } from './ifds.js';
16
+ import { proveExploits } from './exploit-prover.js';
17
+ import { applyStubAwareFilter } from './stub-aware-filter.js';
18
+ import { loadProjectStubs } from '../ir/type-stubs.js';
19
+
20
+ export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
21
+ // Path-feasibility pass over every function before the taint walk.
22
+ let totalPruned = 0;
23
+ for (const fn of callGraph.functions.values()) {
24
+ const r = applyPathFeasibility(fn);
25
+ totalPruned += r.pruned;
26
+ }
27
+ // P2.1 — RHS-lite reachability slice. When AGENTIC_SECURITY_RHS=1 the
28
+ // engine narrows analysis to sink-reachable functions. Default OFF
29
+ // because it changes the finding-set composition.
30
+ if (process.env.AGENTIC_SECURITY_RHS === '1') {
31
+ const ctx = rhsReachableFunctions(perFileIR, callGraph);
32
+ if (ctx.reachable) {
33
+ opts = { ...opts, _rhsReachable: ctx.reachable, _rhsCheck: shouldAnalyzeUnderRhs };
34
+ }
35
+ }
36
+ // v0.69 — cross-scan incremental cache (AGENTIC_SECURITY_INCREMENTAL=1).
37
+ // Read persisted state, seed the SummaryCache with summaries from files
38
+ // whose content hasn't changed, then hand it to runTaintEngine. After,
39
+ // serialize the cache and commit to disk.
40
+ const incrementalEnabled = process.env.AGENTIC_SECURITY_INCREMENTAL === '1';
41
+ let preSeededCache = null;
42
+ let priorState = null;
43
+ let currentFileHashes = null;
44
+ if (incrementalEnabled && opts.scanRoot && opts.fileContents) {
45
+ priorState = readIncrementalState(opts.scanRoot);
46
+ const currentVersion = {
47
+ scanner: opts.scannerVersion || 'unknown',
48
+ rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
49
+ };
50
+ const valid = validateIncrementalState(priorState, currentVersion);
51
+ if (valid.valid) {
52
+ currentFileHashes = {};
53
+ for (const [fp, content] of Object.entries(opts.fileContents)) {
54
+ currentFileHashes[fp] = hashFileContent(content);
55
+ }
56
+ const diff = diffFileHashes(priorState.files || {}, currentFileHashes);
57
+ const changedQids = new Set();
58
+ // Map a changed file to the qids it owns. perFileIR exposes file→fns.
59
+ for (const fp of [...diff.changed, ...diff.added, ...diff.removed]) {
60
+ const ir = perFileIR[fp];
61
+ if (!ir) continue;
62
+ for (const fn of (ir.functions || [])) changedQids.add(fn.qid);
63
+ }
64
+ const persistedPayload = (priorState.summaries && priorState.summaries.summaries) || priorState.summaries || {};
65
+ const callerOfQid = (priorState.summaries && priorState.summaries.callers) || {};
66
+ const { reusable } = pickReusableSummaries(persistedPayload, callerOfQid, changedQids);
67
+ preSeededCache = new SummaryCache();
68
+ const seededN = seedSummaryCache(preSeededCache, persistedPayload, reusable);
69
+ preSeededCache._incrementalSeeded = seededN;
70
+ preSeededCache._incrementalReusable = reusable.size;
71
+ } else {
72
+ // Stale → caller should drop; we just don't seed.
73
+ priorState = null;
74
+ }
75
+ }
76
+ // v0.70 #2 — Steensgaard points-to / alias analysis. Built once before
77
+ // the worklist, passed via opts so the engine can resolve aliased
78
+ // mutations (`let a = obj; a.x = tainted; sink(obj.x)`).
79
+ let pointsToGraph = null;
80
+ if (process.env.AGENTIC_SECURITY_POINTS_TO === '1') {
81
+ try { pointsToGraph = buildPointsTo(perFileIR, callGraph); }
82
+ catch { pointsToGraph = null; }
83
+ }
84
+ // v0.71 #3 — IFDS alternative analyzer (AGENTIC_SECURITY_IFDS=1).
85
+ // Runs the formal Reps-Horwitz-Sagiv tabulation in parallel with the
86
+ // worklist engine. We MERGE findings — the IFDS solver may catch
87
+ // context-sensitive flows the k=2 cache joined out. Deduped by sink+line.
88
+ let findings = runTaintEngine(perFileIR, callGraph, {
89
+ ...opts,
90
+ summaryCache: preSeededCache || undefined,
91
+ _pointsTo: pointsToGraph || undefined,
92
+ });
93
+ if (process.env.AGENTIC_SECURITY_IFDS === '1') {
94
+ try {
95
+ const ifdsFindings = runIfdsTaintEngine(perFileIR, callGraph, opts);
96
+ const existing = new Set(findings.map(f => `${f.file}:${f.line}:${f.sink?.label || ''}`));
97
+ for (const f of ifdsFindings) {
98
+ const key = `${f.file}:${f.line}:${f.sink?.label || ''}`;
99
+ if (!existing.has(key)) findings.push(f);
100
+ }
101
+ } catch { /* IFDS failure should not fail the scan */ }
102
+ }
103
+ for (const f of findings) f._pathFeasibilityPruned = totalPruned;
104
+ if (preSeededCache) {
105
+ Object.defineProperty(findings, '_incrementalStats', {
106
+ value: {
107
+ seeded: preSeededCache._incrementalSeeded || 0,
108
+ reusable: preSeededCache._incrementalReusable || 0,
109
+ },
110
+ enumerable: false,
111
+ });
112
+ }
113
+ // P1.4 — backward slice (opt-in via AGENTIC_SECURITY_BACKWARD_SLICE=1).
114
+ if (process.env.AGENTIC_SECURITY_BACKWARD_SLICE === '1') {
115
+ findings = annotateBackwardSlices(findings, perFileIR, callGraph);
116
+ }
117
+ // v0.70 #6 — probabilistic / soft taint. Walks each finding's trace +
118
+ // chain, multiplies (1 - effectiveness) across sanitizers, demotes
119
+ // below-threshold findings to lower severity (never drops).
120
+ if (process.env.AGENTIC_SECURITY_SOFT_TAINT === '1') {
121
+ findings = annotateSoftTaint(findings);
122
+ }
123
+ // v0.73 — type-stub-aware filter. Consults the project's TS/.pyi/JAR
124
+ // stub signatures (loaded by ir/type-stubs.js when AGENTIC_SECURITY_TYPE_STUBS=1).
125
+ // If a finding's source type is provably non-stringy (number, boolean,
126
+ // Date, RegExp) AND the sink class can't be triggered by that type,
127
+ // demote the finding's severity.
128
+ if (process.env.AGENTIC_SECURITY_TYPE_STUBS === '1' && opts.scanRoot) {
129
+ try {
130
+ const stubs = loadProjectStubs(opts.scanRoot);
131
+ findings = applyStubAwareFilter(findings, stubs);
132
+ } catch { /* stub load failure must not fail the scan */ }
133
+ }
134
+ // v0.71 #9 — symbolic exploit proof. For each finding, run the SMT-lite
135
+ // infeasibility check (and optionally Z3 when AGENTIC_SECURITY_SYMEXEC_Z3=1
136
+ // AND z3-solver is installed). Attach _exploitInput / _provenUnreachable.
137
+ if (process.env.AGENTIC_SECURITY_SYMEXEC === '1') {
138
+ try {
139
+ const useZ3 = process.env.AGENTIC_SECURITY_SYMEXEC_Z3 === '1';
140
+ // proveExploits returns a Promise; we keep the deep pass synchronous
141
+ // by not awaiting — the prover runs eagerly with z3=null (sync path).
142
+ // For Z3 path, callers should use the async runDeepAnalysisAsync (TBD).
143
+ if (!useZ3) {
144
+ // Synchronous SMT-lite branch.
145
+ // proveExploits awaits z3 only when opts.useZ3=true; otherwise it
146
+ // returns synchronously through the same async function (Promise of
147
+ // sync result). We tolerate the Promise here since findings are
148
+ // mutated in place.
149
+ const p = proveExploits(findings, { useZ3: false });
150
+ if (p && typeof p.then === 'function') p.catch(() => {});
151
+ }
152
+ } catch { /* prover failure should not fail the scan */ }
153
+ }
154
+ // v0.69 — commit incremental state after a successful scan.
155
+ if (incrementalEnabled && opts.scanRoot && currentFileHashes) {
156
+ const cache = findings._summaryCache;
157
+ const summaries = cache ? serializeSummaries(cache) : {};
158
+ // Reverse call-graph (qid → callers) — derive from callGraph.
159
+ const callers = {};
160
+ if (callGraph && callGraph.functions) {
161
+ for (const fn of callGraph.functions.values()) {
162
+ if (!Array.isArray(fn.calls)) continue;
163
+ for (const callee of fn.calls) {
164
+ if (!callers[callee]) callers[callee] = [];
165
+ callers[callee].push(fn.qid);
166
+ }
167
+ }
168
+ }
169
+ commitIncrementalState(opts.scanRoot, {
170
+ files: currentFileHashes,
171
+ summaries,
172
+ callers,
173
+ }, {
174
+ scanner: opts.scannerVersion || 'unknown',
175
+ rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
176
+ });
177
+ }
178
+ return findings;
179
+ }
180
+
181
+ export { runTaintEngine, CATALOG, matchSource, matchSinkOrSanitizer, _catalogSize, applyPathFeasibility, SummaryCache, entryStateFromCall, rhsReachableFunctions, shouldAnalyzeUnderRhs, annotateBackwardSlices };
@@ -0,0 +1,192 @@
1
+ // Numeric range / abstract integer domain (P3.2).
2
+ //
3
+ // Today's path-feasibility module (`path-feasibility.js`) prunes branches
4
+ // only when the condition folds to a literal constant. This module adds an
5
+ // abstract domain over INTEGER VALUES so we can reason about ranges:
6
+ //
7
+ // const idx = 5;
8
+ // if (idx < 0) return; // ← provably dead
9
+ // if (idx >= arr.length) return; // ← provably dead if arr.length ≥ 6
10
+ // arr[idx] // ← bounds-check-safe
11
+ //
12
+ // const idx = parseInt(req.query.i);
13
+ // if (idx < 0 || idx >= 100) return;
14
+ // table[idx] // ← idx narrowed to [0,99]
15
+ //
16
+ // The abstract domain is the classical interval lattice with TOP / BOTTOM:
17
+ //
18
+ // TOP ≡ (-∞, +∞) — no information
19
+ // range(lo, hi) — closed interval; lo ≤ hi; lo,hi ∈ ℤ ∪ {-∞, +∞}
20
+ // BOTTOM — unreachable (use after a contradiction)
21
+ //
22
+ // Operations: join (∪), meet (∩), narrow-after-conditional, arithmetic
23
+ // (+, -, *, /), and a `decide` predicate over a relational test
24
+ // (lhs op rhs) returning 'true' | 'false' | 'maybe'.
25
+ //
26
+ // This is intentionally light-weight: no widening, no congruences, no
27
+ // strided intervals — just the things you need to prune ~30% of false-
28
+ // positive bounds-related paths in real code.
29
+
30
+ const NEG_INF = -Infinity;
31
+ const POS_INF = +Infinity;
32
+
33
+ export const TOP = Object.freeze({ kind: 'range', lo: NEG_INF, hi: POS_INF });
34
+ export const BOTTOM = Object.freeze({ kind: 'bottom' });
35
+
36
+ /** Build a closed interval [lo, hi]. Order is normalized. */
37
+ export function range(lo, hi) {
38
+ if (lo === undefined || lo === null) lo = NEG_INF;
39
+ if (hi === undefined || hi === null) hi = POS_INF;
40
+ if (lo > hi) return BOTTOM;
41
+ return { kind: 'range', lo, hi };
42
+ }
43
+
44
+ /** Convenience constructor for a single literal value. */
45
+ export function constant(n) {
46
+ if (typeof n !== 'number' || Number.isNaN(n)) return TOP;
47
+ if (!Number.isFinite(n)) return TOP;
48
+ return range(n, n);
49
+ }
50
+
51
+ function isBottom(a) { return a && a.kind === 'bottom'; }
52
+ function isTop(a) { return a && a.kind === 'range' && a.lo === NEG_INF && a.hi === POS_INF; }
53
+
54
+ /** Lattice join (least upper bound) — used at if/loop joins. */
55
+ export function join(a, b) {
56
+ if (!a || isBottom(a)) return b;
57
+ if (!b || isBottom(b)) return a;
58
+ return range(Math.min(a.lo, b.lo), Math.max(a.hi, b.hi));
59
+ }
60
+
61
+ /** Lattice meet (greatest lower bound) — used to narrow after a guard. */
62
+ export function meet(a, b) {
63
+ if (!a || !b) return BOTTOM;
64
+ if (isBottom(a) || isBottom(b)) return BOTTOM;
65
+ const lo = Math.max(a.lo, b.lo);
66
+ const hi = Math.min(a.hi, b.hi);
67
+ if (lo > hi) return BOTTOM;
68
+ return range(lo, hi);
69
+ }
70
+
71
+ /** Arithmetic. */
72
+ export function add(a, b) {
73
+ if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
74
+ return range(a.lo + b.lo, a.hi + b.hi);
75
+ }
76
+ export function sub(a, b) {
77
+ if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
78
+ return range(a.lo - b.hi, a.hi - b.lo);
79
+ }
80
+ export function mul(a, b) {
81
+ if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
82
+ const candidates = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
83
+ return range(Math.min(...candidates), Math.max(...candidates));
84
+ }
85
+
86
+ /**
87
+ * Decide a relational test `a op b` where a, b are ranges.
88
+ * Returns 'true' iff every concrete pair in a×b satisfies the test.
89
+ * Returns 'false' iff every concrete pair fails it.
90
+ * Returns 'maybe' otherwise (overlap → undecidable).
91
+ *
92
+ * Supported ops: '<', '<=', '>', '>=', '==', '!=', '===', '!=='.
93
+ */
94
+ export function decide(a, op, b) {
95
+ if (!a || !b || isBottom(a) || isBottom(b)) return 'maybe';
96
+ switch (op) {
97
+ case '<': return a.hi < b.lo ? 'true' : a.lo >= b.hi ? 'false' : 'maybe';
98
+ case '<=': return a.hi <= b.lo ? 'true' : a.lo > b.hi ? 'false' : 'maybe';
99
+ case '>': return a.lo > b.hi ? 'true' : a.hi <= b.lo ? 'false' : 'maybe';
100
+ case '>=': return a.lo >= b.hi ? 'true' : a.hi < b.lo ? 'false' : 'maybe';
101
+ case '==':
102
+ case '===': {
103
+ // True iff intervals reduce to the same singleton.
104
+ if (a.lo === a.hi && b.lo === b.hi && a.lo === b.lo) return 'true';
105
+ // False iff disjoint.
106
+ if (a.hi < b.lo || b.hi < a.lo) return 'false';
107
+ return 'maybe';
108
+ }
109
+ case '!=':
110
+ case '!==': {
111
+ if (a.hi < b.lo || b.hi < a.lo) return 'true';
112
+ if (a.lo === a.hi && b.lo === b.hi && a.lo === b.lo) return 'false';
113
+ return 'maybe';
114
+ }
115
+ default: return 'maybe';
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Narrow `a` by the assertion `a op b` having been observed as true.
121
+ * e.g. narrow(TOP, '>=', constant(0)) → range(0, +∞)
122
+ * narrow(TOP, '<', constant(10)) → range(-∞, 9)
123
+ *
124
+ * Returns a refined range; BOTTOM if the assertion is incompatible.
125
+ */
126
+ export function narrow(a, op, b) {
127
+ if (!a || !b) return a || TOP;
128
+ if (isBottom(a) || isBottom(b)) return BOTTOM;
129
+ switch (op) {
130
+ case '<': return meet(a, range(NEG_INF, b.hi - 1));
131
+ case '<=': return meet(a, range(NEG_INF, b.hi));
132
+ case '>': return meet(a, range(b.lo + 1, POS_INF));
133
+ case '>=': return meet(a, range(b.lo, POS_INF));
134
+ case '==':
135
+ case '===': return meet(a, b);
136
+ case '!=':
137
+ case '!==': {
138
+ // Only refine when b is a singleton matching a boundary of a.
139
+ if (b.lo === b.hi) {
140
+ if (a.lo === b.lo) return range(a.lo + 1, a.hi);
141
+ if (a.hi === b.lo) return range(a.lo, a.hi - 1);
142
+ }
143
+ return a;
144
+ }
145
+ default: return a;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Abstract an AST-ish expression into a range. Returns TOP for anything
151
+ * we can't fold. The parser shape mirrors what `path-feasibility.js`
152
+ * consumes: { kind: 'literal'|'ident'|'bin', ... }
153
+ *
154
+ * env: Map<varName, range> — the abstract store
155
+ */
156
+ export function abstractEval(expr, env) {
157
+ if (!expr) return TOP;
158
+ if (expr.kind === 'literal' && typeof expr.value === 'number' && Number.isFinite(expr.value)) {
159
+ return constant(expr.value);
160
+ }
161
+ if (expr.kind === 'ident' && env instanceof Map) {
162
+ return env.get(expr.name) || TOP;
163
+ }
164
+ if (expr.kind === 'bin') {
165
+ const l = abstractEval(expr.left, env);
166
+ const r = abstractEval(expr.right, env);
167
+ switch (expr.op) {
168
+ case '+': return add(l, r);
169
+ case '-': return sub(l, r);
170
+ case '*': return mul(l, r);
171
+ default: return TOP;
172
+ }
173
+ }
174
+ return TOP;
175
+ }
176
+
177
+ /** Render an interval for debugging / finding evidence. */
178
+ export function render(a) {
179
+ if (!a) return '⊤';
180
+ if (isBottom(a)) return '⊥';
181
+ if (isTop(a)) return '(-∞, +∞)';
182
+ const lo = a.lo === NEG_INF ? '-∞' : a.lo;
183
+ const hi = a.hi === POS_INF ? '+∞' : a.hi;
184
+ return `[${lo}, ${hi}]`;
185
+ }
186
+
187
+ /** True iff `a ⊑ b` (a is at least as precise as b). */
188
+ export function leq(a, b) {
189
+ if (isBottom(a)) return true;
190
+ if (isBottom(b)) return false;
191
+ return a.lo >= b.lo && a.hi <= b.hi;
192
+ }