entrygraph 0.1.7__tar.gz → 0.1.9__tar.gz

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 (174) hide show
  1. {entrygraph-0.1.7 → entrygraph-0.1.9}/PKG-INFO +1 -1
  2. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/_version.py +2 -2
  3. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/javascript.py +65 -0
  4. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/scanner.py +3 -1
  5. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/resolver.py +29 -8
  6. entrygraph-0.1.9/tests/fixtures/javascript/commonjs_app/server.js +15 -0
  7. entrygraph-0.1.9/tests/fixtures/python/fuzzy_sink/app.py +14 -0
  8. entrygraph-0.1.9/tests/test_commonjs.py +41 -0
  9. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_javascript.py +26 -0
  10. entrygraph-0.1.9/tests/test_fuzzy_sink.py +36 -0
  11. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_resolver.py +32 -2
  12. {entrygraph-0.1.7 → entrygraph-0.1.9}/.github/workflows/ci.yml +0 -0
  13. {entrygraph-0.1.7 → entrygraph-0.1.9}/.github/workflows/release.yml +0 -0
  14. {entrygraph-0.1.7 → entrygraph-0.1.9}/.gitignore +0 -0
  15. {entrygraph-0.1.7 → entrygraph-0.1.9}/LICENSE +0 -0
  16. {entrygraph-0.1.7 → entrygraph-0.1.9}/README.md +0 -0
  17. {entrygraph-0.1.7 → entrygraph-0.1.9}/RELEASING.md +0 -0
  18. {entrygraph-0.1.7 → entrygraph-0.1.9}/pyproject.toml +0 -0
  19. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/__init__.py +0 -0
  20. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/__main__.py +0 -0
  21. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/api.py +0 -0
  22. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/__init__.py +0 -0
  23. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/main.py +0 -0
  24. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/render.py +0 -0
  25. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/csharp.toml +0 -0
  26. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/go.toml +0 -0
  27. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/java.toml +0 -0
  28. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/javascript.toml +0 -0
  29. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  30. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  31. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/php.toml +0 -0
  32. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/python.toml +0 -0
  33. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/ruby.toml +0 -0
  34. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/rust.toml +0 -0
  35. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/__init__.py +0 -0
  36. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/engine.py +0 -0
  37. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/meta.py +0 -0
  38. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/models.py +0 -0
  39. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/queries.py +0 -0
  40. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/__init__.py +0 -0
  41. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  42. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/base.py +0 -0
  43. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  44. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
  45. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/golang.py +0 -0
  46. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/java.py +0 -0
  47. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
  48. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/php.py +0 -0
  49. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/python.py +0 -0
  50. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  51. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  52. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/frameworks.py +0 -0
  53. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/manifests.py +0 -0
  54. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/taint.py +0 -0
  55. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/errors.py +0 -0
  56. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/__init__.py +0 -0
  57. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/base.py +0 -0
  58. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/csharp.py +0 -0
  59. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/golang.py +0 -0
  60. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/ir.py +0 -0
  61. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/java.py +0 -0
  62. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/php.py +0 -0
  63. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/python.py +0 -0
  64. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/registry.py +0 -0
  65. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/ruby.py +0 -0
  66. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/rust.py +0 -0
  67. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/__init__.py +0 -0
  68. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/hashing.py +0 -0
  69. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/lang.py +0 -0
  70. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/walker.py +0 -0
  71. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/__init__.py +0 -0
  72. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/adjacency.py +0 -0
  73. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/cte.py +0 -0
  74. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/scoring.py +0 -0
  75. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/kinds.py +0 -0
  76. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/__init__.py +0 -0
  77. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/parsers.py +0 -0
  78. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/queries.py +0 -0
  79. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/__init__.py +0 -0
  80. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/worker.py +0 -0
  81. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/writer.py +0 -0
  82. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/py.typed +0 -0
  83. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/calls.scm +0 -0
  84. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  85. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/imports.scm +0 -0
  86. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/calls.scm +0 -0
  87. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/definitions.scm +0 -0
  88. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/imports.scm +0 -0
  89. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/calls.scm +0 -0
  90. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/definitions.scm +0 -0
  91. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/imports.scm +0 -0
  92. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/calls.scm +0 -0
  93. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  94. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/imports.scm +0 -0
  95. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/calls.scm +0 -0
  96. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/definitions.scm +0 -0
  97. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/imports.scm +0 -0
  98. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/calls.scm +0 -0
  99. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/definitions.scm +0 -0
  100. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/imports.scm +0 -0
  101. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/calls.scm +0 -0
  102. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  103. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/imports.scm +0 -0
  104. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/calls.scm +0 -0
  105. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/definitions.scm +0 -0
  106. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/imports.scm +0 -0
  107. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/__init__.py +0 -0
  108. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/externals.py +0 -0
  109. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/hierarchy.py +0 -0
  110. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/symbol_table.py +0 -0
  111. {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/results.py +0 -0
  112. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/conftest.py +0 -0
  113. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  114. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  115. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  116. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  117. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/go.mod +0 -0
  118. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/main.go +0 -0
  119. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/service.go +0 -0
  120. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/pom.xml +0 -0
  121. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  122. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  123. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  124. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  125. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/package.json +0 -0
  126. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  127. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  128. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  129. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/artisan +0 -0
  130. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/composer.json +0 -0
  131. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  132. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  133. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/db.py +0 -0
  134. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  135. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/services.py +0 -0
  136. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/cli.py +0 -0
  137. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  138. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/sanitizer/app.py +0 -0
  139. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/taint_source/handler.py +0 -0
  140. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  141. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  142. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  143. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  144. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  145. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  146. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_api.py +0 -0
  147. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_cli.py +0 -0
  148. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_engine_pragmas.py +0 -0
  149. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_entrypoint_expansion.py +0 -0
  150. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_entrypoints.py +0 -0
  151. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_csharp.py +0 -0
  152. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_go.py +0 -0
  153. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_java.py +0 -0
  154. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_php.py +0 -0
  155. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_python.py +0 -0
  156. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_ruby.py +0 -0
  157. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_rust.py +0 -0
  158. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_frameworks.py +0 -0
  159. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_hardening.py +0 -0
  160. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_incremental.py +0 -0
  161. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_indexer.py +0 -0
  162. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_lang.py +0 -0
  163. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_manifests.py +0 -0
  164. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_models.py +0 -0
  165. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_pool.py +0 -0
  166. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_reachability.py +0 -0
  167. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_render.py +0 -0
  168. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_scoring.py +0 -0
  169. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_sink_catalog.py +0 -0
  170. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint.py +0 -0
  171. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint_sanitizers.py +0 -0
  172. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint_sources.py +0 -0
  173. {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_walker.py +0 -0
  174. {entrygraph-0.1.7 → entrygraph-0.1.9}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index
5
5
  Project-URL: Repository, https://github.com/brettbergin/entrygraph
6
6
  Author-email: Brett Bergin <brettberginbc@yahoo.com>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.7'
22
- __version_tuple__ = version_tuple = (0, 1, 7)
21
+ __version__ = version = '0.1.9'
22
+ __version_tuple__ = version_tuple = (0, 1, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -238,12 +238,77 @@ class JavaScriptExtractor:
238
238
  )
239
239
  for node in caps.get("export.from", []):
240
240
  self._reexport(node, ctx, out)
241
+ for node in caps.get("require", []):
242
+ self._commonjs_require(node, ctx, out)
241
243
 
242
244
  for imp in out.imports:
243
245
  out.framework_signals.append(
244
246
  ("import", imp.module.split(".")[0] if "." in imp.module else imp.module)
245
247
  )
246
248
 
249
+ def _commonjs_require(self, call, ctx, out) -> None:
250
+ """CommonJS `require('m')` bindings -> RawImport, mirroring ESM handling.
251
+
252
+ ``const cp = require('m')`` -> whole module bound to ``cp``
253
+ ``const { x, y: z } = require('m')`` -> named imports ``x`` and ``y`` (as ``z``)
254
+ bare / chained / reassigned -> module-only (side-effect) import,
255
+ which still yields the framework signal even when no alias can be bound.
256
+ """
257
+ args = call.child_by_field_name("arguments")
258
+ spec = None
259
+ if args is not None:
260
+ for child in args.named_children:
261
+ if child.type == "string":
262
+ spec = node_text(child).strip("'\"`")
263
+ break
264
+ if not spec:
265
+ return # dynamic require (variable/template arg) — target unknowable
266
+ module = self._resolve_module(spec, ctx)
267
+ span = span_of(call)
268
+
269
+ parent = call.parent
270
+ name_node = None
271
+ if parent is not None and parent.type == "variable_declarator":
272
+ name_node = parent.child_by_field_name("name")
273
+ elif parent is not None and parent.type == "assignment_expression":
274
+ name_node = parent.child_by_field_name("left")
275
+
276
+ if name_node is not None and name_node.type == "identifier":
277
+ # Bind the alias to the module itself (same as an ESM default import),
278
+ # so `const cp = require('child_process'); cp.exec()` canonicalizes to
279
+ # child_process.exec.
280
+ out.imports.append(
281
+ RawImport(module=module, imported_name=None, alias=node_text(name_node), span=span)
282
+ )
283
+ return
284
+ if name_node is not None and name_node.type == "object_pattern":
285
+ for child in name_node.named_children:
286
+ if child.type == "shorthand_property_identifier_pattern": # { exec }
287
+ nm = node_text(child)
288
+ out.imports.append(
289
+ RawImport(module=module, imported_name=nm, alias=nm, span=span)
290
+ )
291
+ elif child.type == "pair_pattern": # { exec: run }
292
+ key = child.child_by_field_name("key")
293
+ value = child.child_by_field_name("value")
294
+ if key is None:
295
+ continue
296
+ alias = (
297
+ node_text(value)
298
+ if value is not None and value.type == "identifier"
299
+ else node_text(key)
300
+ )
301
+ out.imports.append(
302
+ RawImport(
303
+ module=module, imported_name=node_text(key), alias=alias, span=span
304
+ )
305
+ )
306
+ return
307
+ # bare `require('m')`, `require('m').foo`, `module.exports = require('m')`, etc.
308
+ out.imports.append(
309
+ RawImport(module=module, imported_name=None, alias=module.split(".")[-1], span=span)
310
+ )
311
+
247
312
  def _reexport(self, node, ctx, out) -> None:
248
313
  """`export { X as Y } from "./m"` / `export * from "./m"` -> RawReexport."""
249
314
  source = node.child_by_field_name("source")
@@ -432,7 +432,9 @@ def _write_edges_and_entrypoints(
432
432
  entrypoint_rows: list[dict] = []
433
433
 
434
434
  for path, x, is_package in extractions:
435
- resolver = FileResolver(x, module_ids[path], table, externals, is_package)
435
+ resolver = FileResolver(
436
+ x, module_ids[path], table, externals, is_package, sink_registry=sink_registry
437
+ )
436
438
  file_id = file_id_by_path[path]
437
439
  for edge in resolver.resolve():
438
440
  is_call = edge.kind is EdgeKind.CALLS
@@ -17,6 +17,7 @@ incremental re-resolution can heal them when the target appears.
17
17
  from __future__ import annotations
18
18
 
19
19
  from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING
20
21
 
21
22
  from entrygraph.extract.ir import FileExtraction, RawReference
22
23
  from entrygraph.kinds import Confidence, EdgeKind, SymbolKind
@@ -29,6 +30,9 @@ from entrygraph.resolve.hierarchy import (
29
30
  )
30
31
  from entrygraph.resolve.symbol_table import SymbolTable
31
32
 
33
+ if TYPE_CHECKING:
34
+ from entrygraph.detect.taint import SinkRegistry
35
+
32
36
  # expand_relative re-exported for backward compatibility with callers/tests.
33
37
  __all__ = ["FileResolver", "ResolvedEdge", "expand_relative"]
34
38
 
@@ -66,12 +70,14 @@ class FileResolver:
66
70
  table: SymbolTable,
67
71
  externals: ExternalRegistry,
68
72
  is_package: bool = False,
73
+ sink_registry: SinkRegistry | None = None,
69
74
  ) -> None:
70
75
  self.x = extraction
71
76
  self.module_symbol_id = module_symbol_id
72
77
  self.table = table
73
78
  self.externals = externals
74
79
  self.is_package = is_package
80
+ self.sink_registry = sink_registry
75
81
  self.prefix = LANG_PREFIX.get(extraction.language, extraction.language)
76
82
  self.import_map, self.wildcard_modules = build_import_map(extraction, is_package)
77
83
 
@@ -217,6 +223,14 @@ class FileResolver:
217
223
  return self.table.by_fqn[target]
218
224
  return self.table.unique_by_name(name, (SymbolKind.FUNCTION, SymbolKind.METHOD))
219
225
 
226
+ def _guess_is_sink(self, ref: RawReference) -> bool:
227
+ """Whether the receiver-agnostic guess for this attribute call matches a
228
+ registered sink — checked with the same call the index-time stamper uses."""
229
+ if self.sink_registry is None:
230
+ return False
231
+ guess = f"{self.prefix}:*.{ref.callee_name}"
232
+ return self.sink_registry.match(guess, ref.arg_preview) is not None
233
+
220
234
  def _bind(self, ref: RawReference) -> tuple[int | None, str, Confidence, str | None]:
221
235
  # 1. import-map expansion (chase re-export chains for project targets)
222
236
  first_seg = ref.callee_text.split(".", 1)[0].split("(", 1)[0]
@@ -270,14 +284,21 @@ class FileResolver:
270
284
  # where runner is a local variable) this is the only way to recover the
271
285
  # call-graph edge without local type inference — it's the documented
272
286
  # FUZZY tradeoff.
273
- kinds = (
274
- (SymbolKind.METHOD,)
275
- if ref.receiver_text is not None
276
- else (SymbolKind.FUNCTION, SymbolKind.CLASS)
277
- )
278
- dst_id = self.table.unique_by_name(ref.callee_name, kinds)
279
- if dst_id is not None:
280
- return dst_id, self.table.qname_of[dst_id], Confidence.FUZZY, None
287
+ #
288
+ # But if the receiver-agnostic guess (`py:*.execute`) is itself a
289
+ # registered sink, do NOT fuzzy-bind: rewriting dst_qname to a unique
290
+ # project method named `execute` would erase the sink stamp and hide the
291
+ # vulnerability. Prefer the guess so the sink survives (step 5); CHA still
292
+ # adds the project method as a via="cha" candidate, so no edge is lost.
293
+ if not (ref.receiver_text is not None and self._guess_is_sink(ref)):
294
+ kinds = (
295
+ (SymbolKind.METHOD,)
296
+ if ref.receiver_text is not None
297
+ else (SymbolKind.FUNCTION, SymbolKind.CLASS)
298
+ )
299
+ dst_id = self.table.unique_by_name(ref.callee_name, kinds)
300
+ if dst_id is not None:
301
+ return dst_id, self.table.qname_of[dst_id], Confidence.FUZZY, None
281
302
 
282
303
  # 5. unresolved: language-prefixed guess, still a real node for sinks
283
304
  if ref.receiver_text is not None:
@@ -0,0 +1,15 @@
1
+ const cp = require('child_process');
2
+ const { exec } = require('child_process');
3
+ const express = require('express');
4
+
5
+ function runDefault(input) {
6
+ cp.execSync(input);
7
+ }
8
+
9
+ function runNamed(input) {
10
+ exec(input);
11
+ }
12
+
13
+ const app = express();
14
+ app.get('/a', (req, res) => runDefault(req.query.cmd));
15
+ app.get('/b', (req, res) => runNamed(req.query.cmd));
@@ -0,0 +1,14 @@
1
+ import sqlite3
2
+
3
+
4
+ class Runner:
5
+ # the ONLY project method named "execute" — the collision that used to
6
+ # swallow cursor.execute via unique-name fuzzy binding
7
+ def execute(self):
8
+ return 1
9
+
10
+
11
+ def query(db_path, user_id):
12
+ conn = sqlite3.connect(db_path)
13
+ cur = conn.cursor()
14
+ cur.execute("SELECT * FROM users WHERE id = " + user_id)
@@ -0,0 +1,41 @@
1
+ """End-to-end CommonJS `require()` support (Phase 1.3).
2
+
3
+ Regression: the JS extractor dropped the `require()` captures entirely, so a
4
+ pure-CommonJS app produced no imports — no framework detection and no
5
+ canonicalization of `cp.execSync` to a `child_process` sink.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from entrygraph import CodeGraph
15
+
16
+ COMMONJS_APP = Path(__file__).parent / "fixtures" / "javascript" / "commonjs_app"
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def graph(tmp_path_factory) -> CodeGraph:
21
+ db = tmp_path_factory.mktemp("db") / "graph.db"
22
+ g = CodeGraph.index(COMMONJS_APP, db=db)
23
+ yield g
24
+ g.close()
25
+
26
+
27
+ def test_commonjs_require_enables_framework_detection(graph):
28
+ names = {f.name for f in graph.detect().frameworks}
29
+ assert "express" in names
30
+
31
+
32
+ def test_commonjs_require_canonicalizes_command_sinks(graph):
33
+ # both the default-bound (cp.execSync) and destructured (exec) forms must
34
+ # resolve to child_process command_exec sinks
35
+ assert graph.stats().sink_edges >= 2
36
+ sinks = {
37
+ row["dst_qname"]
38
+ for row in graph.sql("SELECT dst_qname FROM edges WHERE sink_id IS NOT NULL")
39
+ }
40
+ assert "js:child_process.execSync" in sinks
41
+ assert "js:child_process.exec" in sinks
@@ -72,6 +72,32 @@ def test_relative_import_resolution():
72
72
  assert any(i.module == "db.conn" for i in x.imports)
73
73
 
74
74
 
75
+ def test_commonjs_require_bindings():
76
+ x = extract(
77
+ "const cp = require('child_process');\n"
78
+ "const { exec, spawn: launch } = require('child_process');\n"
79
+ "const express = require('express');\n"
80
+ "require('./side-effect');\n"
81
+ "const rel = require('../db/conn');\n",
82
+ path="src/routes/users.js",
83
+ )
84
+ imports = {(i.module, i.imported_name, i.alias) for i in x.imports}
85
+ assert ("child_process", None, "cp") in imports # default -> module binding
86
+ assert ("child_process", "exec", "exec") in imports # destructured
87
+ assert ("child_process", "spawn", "launch") in imports # destructured + rename
88
+ assert ("express", None, "express") in imports
89
+ assert ("db.conn", None, "rel") in imports # relative resolved to dotted, bound to `rel`
90
+ # side-effect require still registers the module (framework signal source)
91
+ assert any(m == "routes.side-effect" and n is None for m, n, _ in imports)
92
+
93
+
94
+ def test_commonjs_require_emits_framework_signal():
95
+ # regression: CommonJS `require('express')` used to yield no import at all,
96
+ # so express was undetectable in pure-CJS apps.
97
+ x = extract("const express = require('express');\n")
98
+ assert ("import", "express") in x.framework_signals
99
+
100
+
75
101
  def test_calls_with_receivers():
76
102
  x = extract(
77
103
  "import cp from 'child_process';\nfunction run() { cp.execSync('ls'); helper(); }\n"
@@ -0,0 +1,36 @@
1
+ """Sink preservation vs unique-name fuzzy binding (Phase 1.4).
2
+
3
+ Regression: an unknown-receiver call like `cursor.execute(sql)` fuzzy-bound to a
4
+ unique project method named `execute`, rewriting dst_qname so the `py:*.execute`
5
+ SQL sink never got stamped — the vulnerability silently vanished from the graph.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ from entrygraph import CodeGraph
15
+
16
+ FUZZY_SINK_APP = Path(__file__).parent / "fixtures" / "python" / "fuzzy_sink"
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def graph(tmp_path_factory) -> CodeGraph:
21
+ db = tmp_path_factory.mktemp("db") / "graph.db"
22
+ g = CodeGraph.index(FUZZY_SINK_APP, db=db)
23
+ yield g
24
+ g.close()
25
+
26
+
27
+ def test_sql_sink_survives_unique_name_collision(graph):
28
+ sinks = graph.sql("SELECT dst_qname, sink_id FROM edges WHERE sink_id IS NOT NULL")
29
+ by_qname = {r["dst_qname"]: r["sink_id"] for r in sinks}
30
+ assert by_qname.get("py:*.execute") == "py.sql-execute"
31
+
32
+
33
+ def test_sql_path_reachable_despite_collision(graph):
34
+ paths = graph.paths(source="app.query", sink_category="sql", include_unresolved=True)
35
+ assert paths
36
+ assert paths[0].symbols[-1].qname == "py:*.execute"
@@ -20,7 +20,7 @@ def make_table() -> SymbolTable:
20
20
  return table
21
21
 
22
22
 
23
- def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2):
23
+ def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2, sink_registry=None):
24
24
  x = FileExtraction(
25
25
  path="app/routes.py",
26
26
  language="python",
@@ -31,7 +31,7 @@ def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2):
31
31
  references=list(refs),
32
32
  )
33
33
  externals = ExternalRegistry(iter(range(100, 200)).__next__)
34
- return FileResolver(x, module_id, table, externals), externals
34
+ return FileResolver(x, module_id, table, externals, sink_registry=sink_registry), externals
35
35
 
36
36
 
37
37
  def ref(callee_text, callee_name=None, receiver=None, caller=None, kind="call"):
@@ -180,6 +180,36 @@ def test_unresolved_gets_prefixed_placeholder():
180
180
  assert by_qname["py:eval"].confidence is Confidence.UNRESOLVED
181
181
 
182
182
 
183
+ def test_sink_named_attribute_call_is_not_fuzzy_bound():
184
+ # `cursor.execute(...)` guesses to the sink `py:*.execute`. Even though a
185
+ # unique project method `Runner.execute` (id 12) exists, we must NOT fuzzy-bind
186
+ # to it — that would rewrite dst_qname and erase the SQL sink. Keep the guess.
187
+ from entrygraph.detect.taint import builtin_registry
188
+
189
+ table = make_table()
190
+ resolver, _ = make_resolver(
191
+ table, [ref("cursor.execute", receiver="cursor")], sink_registry=builtin_registry()
192
+ )
193
+ call = next(e for e in resolver.resolve() if e.kind is EdgeKind.CALLS and e.via != "cha")
194
+ assert call.dst_qname == "py:*.execute"
195
+ assert call.confidence is Confidence.UNRESOLVED
196
+ assert call.dst_symbol_id != 12
197
+
198
+
199
+ def test_non_sink_attribute_call_still_fuzzy_binds_with_registry():
200
+ # `obj.render` is not a sink, so unique-name fuzzy binding still applies even
201
+ # with a sink registry present — the fix is targeted to sink names only.
202
+ from entrygraph.detect.taint import builtin_registry
203
+
204
+ table = make_table()
205
+ resolver, _ = make_resolver(
206
+ table, [ref("obj.render", receiver="obj")], sink_registry=builtin_registry()
207
+ )
208
+ call = next(e for e in resolver.resolve() if e.kind is EdgeKind.CALLS)
209
+ assert call.dst_symbol_id == 13 # Runner.render
210
+ assert call.confidence is Confidence.FUZZY
211
+
212
+
183
213
  def test_relative_import_expansion():
184
214
  table = make_table()
185
215
  table.add_symbol(40, "app.utils", 40 and "utils", SymbolKind.MODULE)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes