entrygraph 0.1.8__tar.gz → 0.1.10__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 (177) hide show
  1. {entrygraph-0.1.8 → entrygraph-0.1.10}/PKG-INFO +1 -1
  2. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/_version.py +2 -2
  3. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/pipeline/scanner.py +13 -6
  4. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/resolve/resolver.py +29 -8
  5. entrygraph-0.1.10/tests/fixtures/python/fuzzy_sink/app.py +14 -0
  6. entrygraph-0.1.10/tests/fixtures/python/heal_fidelity/caller.py +4 -0
  7. entrygraph-0.1.10/tests/fixtures/python/heal_fidelity/worker.py +3 -0
  8. entrygraph-0.1.10/tests/test_fuzzy_sink.py +36 -0
  9. entrygraph-0.1.10/tests/test_heal_fidelity.py +66 -0
  10. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_resolver.py +32 -2
  11. {entrygraph-0.1.8 → entrygraph-0.1.10}/.github/workflows/ci.yml +0 -0
  12. {entrygraph-0.1.8 → entrygraph-0.1.10}/.github/workflows/release.yml +0 -0
  13. {entrygraph-0.1.8 → entrygraph-0.1.10}/.gitignore +0 -0
  14. {entrygraph-0.1.8 → entrygraph-0.1.10}/LICENSE +0 -0
  15. {entrygraph-0.1.8 → entrygraph-0.1.10}/README.md +0 -0
  16. {entrygraph-0.1.8 → entrygraph-0.1.10}/RELEASING.md +0 -0
  17. {entrygraph-0.1.8 → entrygraph-0.1.10}/pyproject.toml +0 -0
  18. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/__init__.py +0 -0
  19. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/__main__.py +0 -0
  20. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/api.py +0 -0
  21. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/cli/__init__.py +0 -0
  22. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/cli/main.py +0 -0
  23. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/cli/render.py +0 -0
  24. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/csharp.toml +0 -0
  25. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/go.toml +0 -0
  26. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/java.toml +0 -0
  27. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/javascript.toml +0 -0
  28. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  29. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  30. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/php.toml +0 -0
  31. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/python.toml +0 -0
  32. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/ruby.toml +0 -0
  33. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/data/sinks/rust.toml +0 -0
  34. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/db/__init__.py +0 -0
  35. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/db/engine.py +0 -0
  36. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/db/meta.py +0 -0
  37. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/db/models.py +0 -0
  38. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/db/queries.py +0 -0
  39. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/__init__.py +0 -0
  40. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  41. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/base.py +0 -0
  42. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  43. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
  44. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/golang.py +0 -0
  45. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/java.py +0 -0
  46. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
  47. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/php.py +0 -0
  48. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/python.py +0 -0
  49. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  50. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  51. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/frameworks.py +0 -0
  52. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/manifests.py +0 -0
  53. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/detect/taint.py +0 -0
  54. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/errors.py +0 -0
  55. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/__init__.py +0 -0
  56. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/base.py +0 -0
  57. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/csharp.py +0 -0
  58. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/golang.py +0 -0
  59. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/ir.py +0 -0
  60. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/java.py +0 -0
  61. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/javascript.py +0 -0
  62. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/php.py +0 -0
  63. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/python.py +0 -0
  64. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/registry.py +0 -0
  65. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/ruby.py +0 -0
  66. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/extract/rust.py +0 -0
  67. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/fs/__init__.py +0 -0
  68. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/fs/hashing.py +0 -0
  69. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/fs/lang.py +0 -0
  70. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/fs/walker.py +0 -0
  71. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/graph/__init__.py +0 -0
  72. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/graph/adjacency.py +0 -0
  73. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/graph/cte.py +0 -0
  74. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/graph/scoring.py +0 -0
  75. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/kinds.py +0 -0
  76. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/parsing/__init__.py +0 -0
  77. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/parsing/parsers.py +0 -0
  78. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/parsing/queries.py +0 -0
  79. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/pipeline/__init__.py +0 -0
  80. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/pipeline/worker.py +0 -0
  81. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/pipeline/writer.py +0 -0
  82. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/py.typed +0 -0
  83. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/csharp/calls.scm +0 -0
  84. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  85. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/csharp/imports.scm +0 -0
  86. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/go/calls.scm +0 -0
  87. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/go/definitions.scm +0 -0
  88. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/go/imports.scm +0 -0
  89. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/java/calls.scm +0 -0
  90. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/java/definitions.scm +0 -0
  91. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/java/imports.scm +0 -0
  92. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/javascript/calls.scm +0 -0
  93. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  94. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/javascript/imports.scm +0 -0
  95. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/php/calls.scm +0 -0
  96. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/php/definitions.scm +0 -0
  97. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/php/imports.scm +0 -0
  98. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/python/calls.scm +0 -0
  99. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/python/definitions.scm +0 -0
  100. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/python/imports.scm +0 -0
  101. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/ruby/calls.scm +0 -0
  102. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  103. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/ruby/imports.scm +0 -0
  104. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/rust/calls.scm +0 -0
  105. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/rust/definitions.scm +0 -0
  106. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/queries/rust/imports.scm +0 -0
  107. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/resolve/__init__.py +0 -0
  108. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/resolve/externals.py +0 -0
  109. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/resolve/hierarchy.py +0 -0
  110. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/resolve/symbol_table.py +0 -0
  111. {entrygraph-0.1.8 → entrygraph-0.1.10}/src/entrygraph/results.py +0 -0
  112. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/conftest.py +0 -0
  113. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  114. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  115. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  116. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  117. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/go/gin_app/go.mod +0 -0
  118. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/go/gin_app/main.go +0 -0
  119. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/go/gin_app/service.go +0 -0
  120. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/java/spring_app/pom.xml +0 -0
  121. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  122. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  123. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  124. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  125. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/javascript/commonjs_app/server.js +0 -0
  126. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/javascript/express_app/package.json +0 -0
  127. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  128. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  129. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  130. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/php/laravel_app/artisan +0 -0
  131. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/php/laravel_app/composer.json +0 -0
  132. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  133. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  134. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/app/db.py +0 -0
  135. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  136. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/app/services.py +0 -0
  137. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/cli.py +0 -0
  138. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  139. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/sanitizer/app.py +0 -0
  140. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/python/taint_source/handler.py +0 -0
  141. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  142. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  143. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  144. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  145. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  146. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  147. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_api.py +0 -0
  148. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_cli.py +0 -0
  149. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_commonjs.py +0 -0
  150. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_engine_pragmas.py +0 -0
  151. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_entrypoint_expansion.py +0 -0
  152. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_entrypoints.py +0 -0
  153. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_csharp.py +0 -0
  154. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_go.py +0 -0
  155. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_java.py +0 -0
  156. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_javascript.py +0 -0
  157. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_php.py +0 -0
  158. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_python.py +0 -0
  159. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_ruby.py +0 -0
  160. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_extract_rust.py +0 -0
  161. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_frameworks.py +0 -0
  162. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_hardening.py +0 -0
  163. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_incremental.py +0 -0
  164. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_indexer.py +0 -0
  165. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_lang.py +0 -0
  166. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_manifests.py +0 -0
  167. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_models.py +0 -0
  168. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_pool.py +0 -0
  169. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_reachability.py +0 -0
  170. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_render.py +0 -0
  171. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_scoring.py +0 -0
  172. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_sink_catalog.py +0 -0
  173. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_taint.py +0 -0
  174. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_taint_sanitizers.py +0 -0
  175. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_taint_sources.py +0 -0
  176. {entrygraph-0.1.8 → entrygraph-0.1.10}/tests/test_walker.py +0 -0
  177. {entrygraph-0.1.8 → entrygraph-0.1.10}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.8
3
+ Version: 0.1.10
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.8'
22
- __version_tuple__ = version_tuple = (0, 1, 8)
21
+ __version__ = version = '0.1.10'
22
+ __version_tuple__ = version_tuple = (0, 1, 10)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -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
@@ -521,17 +523,22 @@ def _heal_dangling_edges(session, table: SymbolTable, new_qnames: set[str]) -> N
521
523
  """Re-bind edges left NULL (degraded on wipe, or targeting a not-yet-existing
522
524
  symbol) whose dst_qname now names a freshly-created symbol."""
523
525
  dangling = session.execute(
524
- select(Edge.id, Edge.dst_qname).where(
526
+ select(Edge.id, Edge.dst_qname, Edge.confidence).where(
525
527
  Edge.dst_symbol_id.is_(None), Edge.dst_qname.in_(new_qnames)
526
528
  )
527
529
  ).all()
528
530
  updates = []
529
- for edge_id, dst_qname in dangling:
531
+ unresolved = int(Confidence.UNRESOLVED)
532
+ for edge_id, dst_qname, confidence in dangling:
530
533
  target = table.by_fqn.get(dst_qname)
531
534
  if target is not None:
532
- updates.append(
533
- {"id": edge_id, "dst_symbol_id": target, "confidence": int(Confidence.IMPORT)}
534
- )
535
+ # Preserve the edge's original tier. Only an edge that was UNRESOLVED
536
+ # (a project import whose target didn't exist yet) upgrades to IMPORT
537
+ # on heal, matching what a full re-index produces. An EXACT/IMPORT/
538
+ # FUZZY/cha edge whose target was merely wiped and recreated keeps its
539
+ # tier — healing must not silently promote a fuzzy or CHA edge.
540
+ healed = int(Confidence.IMPORT) if confidence == unresolved else confidence
541
+ updates.append({"id": edge_id, "dst_symbol_id": target, "confidence": healed})
535
542
  if updates:
536
543
  session.execute(update(Edge), updates)
537
544
 
@@ -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,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,4 @@
1
+ def go(w):
2
+ # receiver `w` is an unknown local -> fuzzy-binds to Worker.process
3
+ # (unique name), producing a CROSS-FILE FUZZY edge into worker.py
4
+ return w.process()
@@ -0,0 +1,3 @@
1
+ class Worker:
2
+ def process(self): # unique method name across the repo
3
+ return 1
@@ -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"
@@ -0,0 +1,66 @@
1
+ """Incremental healing must not alter an edge's confidence tier (Phase 2.4).
2
+
3
+ Regression: `_heal_dangling_edges` re-bound every healed edge at Confidence.IMPORT
4
+ unconditionally. A cross-file FUZZY (or via="cha") edge whose target file was
5
+ merely touched came back as IMPORT, so a min_confidence>=IMPORT query returned
6
+ edges a full re-index would score FUZZY.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shutil
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+ from sqlalchemy import select
17
+ from sqlalchemy.orm import Session
18
+
19
+ from entrygraph.db.engine import make_engine
20
+ from entrygraph.db.models import Edge, Symbol
21
+ from entrygraph.pipeline.scanner import index_repository
22
+
23
+ HEAL_APP = Path(__file__).parent / "fixtures" / "python" / "heal_fidelity"
24
+
25
+
26
+ @pytest.fixture
27
+ def repo(tmp_path) -> Path:
28
+ dst = tmp_path / "repo"
29
+ shutil.copytree(HEAL_APP, dst)
30
+ return dst
31
+
32
+
33
+ def _edge_confidence(engine, src_qname, dst_qname) -> int | None:
34
+ with Session(engine) as s:
35
+ ids = {sym.qname: sym.id for sym in s.execute(select(Symbol)).scalars()}
36
+ src = ids.get(src_qname)
37
+ row = s.execute(
38
+ select(Edge.confidence).where(Edge.src_symbol_id == src, Edge.dst_qname == dst_qname)
39
+ ).first()
40
+ return row[0] if row else None
41
+
42
+
43
+ def test_healed_fuzzy_edge_keeps_its_confidence(repo, tmp_path):
44
+ from entrygraph.kinds import Confidence
45
+
46
+ engine = make_engine(tmp_path / "inc.db")
47
+ index_repository(repo, engine)
48
+ before = _edge_confidence(engine, "caller.go", "worker.Worker.process")
49
+ assert before == int(Confidence.FUZZY) # cross-file unique-name fuzzy edge
50
+
51
+ # touch worker.py: Worker.process gets a new id, the inbound edge is SET NULL
52
+ # then healed. Its tier must stay FUZZY, not be upgraded to IMPORT.
53
+ worker = repo / "worker.py"
54
+ worker.write_text(worker.read_text() + "\n# touch\n")
55
+ os.utime(worker, ns=(0, 10**18))
56
+ index_repository(repo, engine, incremental=True)
57
+
58
+ after = _edge_confidence(engine, "caller.go", "worker.Worker.process")
59
+ assert after == int(Confidence.FUZZY)
60
+
61
+ # and it matches a full re-index
62
+ full = make_engine(tmp_path / "full.db")
63
+ index_repository(repo, full)
64
+ assert after == _edge_confidence(full, "caller.go", "worker.Worker.process")
65
+ full.dispose()
66
+ engine.dispose()
@@ -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