entrygraph 0.1.6__tar.gz → 0.1.7__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 (170) hide show
  1. {entrygraph-0.1.6 → entrygraph-0.1.7}/PKG-INFO +12 -7
  2. {entrygraph-0.1.6 → entrygraph-0.1.7}/README.md +11 -6
  3. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/_version.py +2 -2
  4. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/api.py +67 -14
  5. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/cli/main.py +1 -1
  6. entrygraph-0.1.7/tests/fixtures/python/sanitizer/app.py +12 -0
  7. entrygraph-0.1.7/tests/test_taint_sanitizers.py +57 -0
  8. {entrygraph-0.1.6 → entrygraph-0.1.7}/.github/workflows/ci.yml +0 -0
  9. {entrygraph-0.1.6 → entrygraph-0.1.7}/.github/workflows/release.yml +0 -0
  10. {entrygraph-0.1.6 → entrygraph-0.1.7}/.gitignore +0 -0
  11. {entrygraph-0.1.6 → entrygraph-0.1.7}/LICENSE +0 -0
  12. {entrygraph-0.1.6 → entrygraph-0.1.7}/RELEASING.md +0 -0
  13. {entrygraph-0.1.6 → entrygraph-0.1.7}/pyproject.toml +0 -0
  14. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/__init__.py +0 -0
  15. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/__main__.py +0 -0
  16. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/cli/__init__.py +0 -0
  17. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/cli/render.py +0 -0
  18. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/csharp.toml +0 -0
  19. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/go.toml +0 -0
  20. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/java.toml +0 -0
  21. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/javascript.toml +0 -0
  22. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  23. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  24. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/php.toml +0 -0
  25. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/python.toml +0 -0
  26. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/ruby.toml +0 -0
  27. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/data/sinks/rust.toml +0 -0
  28. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/db/__init__.py +0 -0
  29. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/db/engine.py +0 -0
  30. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/db/meta.py +0 -0
  31. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/db/models.py +0 -0
  32. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/db/queries.py +0 -0
  33. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/__init__.py +0 -0
  34. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  35. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/base.py +0 -0
  36. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  37. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
  38. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/golang.py +0 -0
  39. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/java.py +0 -0
  40. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
  41. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/php.py +0 -0
  42. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/python.py +0 -0
  43. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  44. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  45. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/frameworks.py +0 -0
  46. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/manifests.py +0 -0
  47. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/detect/taint.py +0 -0
  48. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/errors.py +0 -0
  49. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/__init__.py +0 -0
  50. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/base.py +0 -0
  51. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/csharp.py +0 -0
  52. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/golang.py +0 -0
  53. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/ir.py +0 -0
  54. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/java.py +0 -0
  55. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/javascript.py +0 -0
  56. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/php.py +0 -0
  57. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/python.py +0 -0
  58. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/registry.py +0 -0
  59. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/ruby.py +0 -0
  60. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/extract/rust.py +0 -0
  61. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/fs/__init__.py +0 -0
  62. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/fs/hashing.py +0 -0
  63. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/fs/lang.py +0 -0
  64. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/fs/walker.py +0 -0
  65. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/graph/__init__.py +0 -0
  66. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/graph/adjacency.py +0 -0
  67. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/graph/cte.py +0 -0
  68. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/graph/scoring.py +0 -0
  69. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/kinds.py +0 -0
  70. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/parsing/__init__.py +0 -0
  71. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/parsing/parsers.py +0 -0
  72. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/parsing/queries.py +0 -0
  73. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/pipeline/__init__.py +0 -0
  74. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/pipeline/scanner.py +0 -0
  75. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/pipeline/worker.py +0 -0
  76. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/pipeline/writer.py +0 -0
  77. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/py.typed +0 -0
  78. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/csharp/calls.scm +0 -0
  79. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  80. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/csharp/imports.scm +0 -0
  81. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/go/calls.scm +0 -0
  82. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/go/definitions.scm +0 -0
  83. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/go/imports.scm +0 -0
  84. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/java/calls.scm +0 -0
  85. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/java/definitions.scm +0 -0
  86. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/java/imports.scm +0 -0
  87. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/javascript/calls.scm +0 -0
  88. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  89. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/javascript/imports.scm +0 -0
  90. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/php/calls.scm +0 -0
  91. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/php/definitions.scm +0 -0
  92. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/php/imports.scm +0 -0
  93. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/python/calls.scm +0 -0
  94. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/python/definitions.scm +0 -0
  95. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/python/imports.scm +0 -0
  96. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/ruby/calls.scm +0 -0
  97. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  98. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/ruby/imports.scm +0 -0
  99. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/rust/calls.scm +0 -0
  100. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/rust/definitions.scm +0 -0
  101. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/queries/rust/imports.scm +0 -0
  102. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/resolve/__init__.py +0 -0
  103. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/resolve/externals.py +0 -0
  104. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/resolve/hierarchy.py +0 -0
  105. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/resolve/resolver.py +0 -0
  106. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/resolve/symbol_table.py +0 -0
  107. {entrygraph-0.1.6 → entrygraph-0.1.7}/src/entrygraph/results.py +0 -0
  108. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/conftest.py +0 -0
  109. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  110. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  111. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  112. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  113. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/go/gin_app/go.mod +0 -0
  114. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/go/gin_app/main.go +0 -0
  115. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/go/gin_app/service.go +0 -0
  116. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/java/spring_app/pom.xml +0 -0
  117. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  118. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  119. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  120. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  121. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/javascript/express_app/package.json +0 -0
  122. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  123. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  124. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  125. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/php/laravel_app/artisan +0 -0
  126. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/php/laravel_app/composer.json +0 -0
  127. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  128. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  129. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/app/db.py +0 -0
  130. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  131. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/app/services.py +0 -0
  132. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/cli.py +0 -0
  133. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  134. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/python/taint_source/handler.py +0 -0
  135. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  136. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  137. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  138. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  139. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  140. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  141. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_api.py +0 -0
  142. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_cli.py +0 -0
  143. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_engine_pragmas.py +0 -0
  144. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_entrypoint_expansion.py +0 -0
  145. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_entrypoints.py +0 -0
  146. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_csharp.py +0 -0
  147. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_go.py +0 -0
  148. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_java.py +0 -0
  149. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_javascript.py +0 -0
  150. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_php.py +0 -0
  151. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_python.py +0 -0
  152. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_ruby.py +0 -0
  153. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_extract_rust.py +0 -0
  154. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_frameworks.py +0 -0
  155. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_hardening.py +0 -0
  156. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_incremental.py +0 -0
  157. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_indexer.py +0 -0
  158. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_lang.py +0 -0
  159. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_manifests.py +0 -0
  160. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_models.py +0 -0
  161. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_pool.py +0 -0
  162. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_reachability.py +0 -0
  163. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_render.py +0 -0
  164. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_resolver.py +0 -0
  165. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_scoring.py +0 -0
  166. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_sink_catalog.py +0 -0
  167. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_taint.py +0 -0
  168. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_taint_sources.py +0 -0
  169. {entrygraph-0.1.6 → entrygraph-0.1.7}/tests/test_walker.py +0 -0
  170. {entrygraph-0.1.6 → entrygraph-0.1.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.6
3
+ Version: 0.1.7
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>
@@ -202,8 +202,11 @@ entrygraph paths --source '*' --sink-category command_exec
202
202
  `--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
203
203
  - **Precision/recall dial**: by default only high-confidence edges are traversed.
204
204
  Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
205
- calls) or `--include-fuzzy` (speculative class-hierarchy edges); drop
206
- neutralized paths with `--prune-sanitized`.
205
+ calls) or `--include-fuzzy` (speculative class-hierarchy edges).
206
+ - **Sanitizers**: a registered sanitizer for the sink's category called on a
207
+ path (e.g. `shlex.quote`) *discounts* its risk score — heuristically, since
208
+ there is no dataflow, so it never zeroes the risk or hides the path.
209
+ `--prune-sanitized` opts into dropping those paths entirely.
207
210
  - Target an exact sink with `--sink py:subprocess.run` instead of a category.
208
211
 
209
212
  ### `stats` & `--json`
@@ -294,7 +297,7 @@ graph.paths(source="app.routes.*", sink_category="sql",
294
297
  graph.paths(source="app.routes.*", sink_category="command_exec",
295
298
  include_fuzzy=True) # follow speculative class-hierarchy (CHA) edges
296
299
  graph.paths(source="app.routes.*", sink_category="command_exec",
297
- prune_sanitized=True) # drop paths neutralized by a shlex.quote etc.
300
+ prune_sanitized=True) # drop paths where a shlex.quote etc. is called
298
301
 
299
302
  # Incremental re-index (only changed/added/deleted files are reparsed)
300
303
  graph.refresh()
@@ -335,9 +338,11 @@ results are safe to hold and trivial to serialize.
335
338
  root with `[[sink]]` / `[[source]]` / `[[sanitizer]]` tables (same schema as
336
339
  the built-in `data/sinks/*.toml`), or call
337
340
  `entrygraph.detect.taint.register_sink(...)` / `register_sanitizer(...)`. A
338
- `[[sanitizer]]` with `effect = "neutralizes"` prunes a path for its category;
339
- `effect = "reduces"` only discounts the risk score. Third-party wrapper
340
- libraries that reach a sink internally are covered by `data/sinks/lib_*.toml`
341
+ `[[sanitizer]]` called on a path discounts its risk score for that category;
342
+ since reachability has no dataflow, the discount is capped (a match never
343
+ zeroes risk or hides a path use `--prune-sanitized` to drop them explicitly).
344
+ Third-party wrapper libraries that reach a sink internally are covered by
345
+ `data/sinks/lib_*.toml`
341
346
  "library summaries" (same schema, with a `library = "..."` tag).
342
347
  - **New frameworks / entrypoints** — register a `FrameworkSpec` and an
343
348
  `EntrypointRule`; adding a framework is usually a few lines.
@@ -151,8 +151,11 @@ entrygraph paths --source '*' --sink-category command_exec
151
151
  `--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
152
152
  - **Precision/recall dial**: by default only high-confidence edges are traversed.
153
153
  Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
154
- calls) or `--include-fuzzy` (speculative class-hierarchy edges); drop
155
- neutralized paths with `--prune-sanitized`.
154
+ calls) or `--include-fuzzy` (speculative class-hierarchy edges).
155
+ - **Sanitizers**: a registered sanitizer for the sink's category called on a
156
+ path (e.g. `shlex.quote`) *discounts* its risk score — heuristically, since
157
+ there is no dataflow, so it never zeroes the risk or hides the path.
158
+ `--prune-sanitized` opts into dropping those paths entirely.
156
159
  - Target an exact sink with `--sink py:subprocess.run` instead of a category.
157
160
 
158
161
  ### `stats` & `--json`
@@ -243,7 +246,7 @@ graph.paths(source="app.routes.*", sink_category="sql",
243
246
  graph.paths(source="app.routes.*", sink_category="command_exec",
244
247
  include_fuzzy=True) # follow speculative class-hierarchy (CHA) edges
245
248
  graph.paths(source="app.routes.*", sink_category="command_exec",
246
- prune_sanitized=True) # drop paths neutralized by a shlex.quote etc.
249
+ prune_sanitized=True) # drop paths where a shlex.quote etc. is called
247
250
 
248
251
  # Incremental re-index (only changed/added/deleted files are reparsed)
249
252
  graph.refresh()
@@ -284,9 +287,11 @@ results are safe to hold and trivial to serialize.
284
287
  root with `[[sink]]` / `[[source]]` / `[[sanitizer]]` tables (same schema as
285
288
  the built-in `data/sinks/*.toml`), or call
286
289
  `entrygraph.detect.taint.register_sink(...)` / `register_sanitizer(...)`. A
287
- `[[sanitizer]]` with `effect = "neutralizes"` prunes a path for its category;
288
- `effect = "reduces"` only discounts the risk score. Third-party wrapper
289
- libraries that reach a sink internally are covered by `data/sinks/lib_*.toml`
290
+ `[[sanitizer]]` called on a path discounts its risk score for that category;
291
+ since reachability has no dataflow, the discount is capped (a match never
292
+ zeroes risk or hides a path use `--prune-sanitized` to drop them explicitly).
293
+ Third-party wrapper libraries that reach a sink internally are covered by
294
+ `data/sinks/lib_*.toml`
290
295
  "library summaries" (same schema, with a `library = "..."` tag).
291
296
  - **New frameworks / entrypoints** — register a `FrameworkSpec` and an
292
297
  `EntrypointRule`; adding a framework is usually a few lines.
@@ -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.6'
22
- __version_tuple__ = version_tuple = (0, 1, 6)
21
+ __version__ = version = '0.1.7'
22
+ __version_tuple__ = version_tuple = (0, 1, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -66,9 +66,13 @@ def _traversal_params(
66
66
  return floor, include_fuzzy
67
67
 
68
68
 
69
- def _fully_sanitized(path: CallPath) -> bool:
70
- """True if the terminal hop was neutralized by a sanitizer (risk driven to 0)."""
71
- return path.risk_score == 0.0
69
+ def _has_sanitizer(path: CallPath) -> bool:
70
+ """True if a category sanitizer is called on the path (heuristic, no dataflow).
71
+
72
+ Drives ``--prune-sanitized``. Sanitizer matches only discount risk (never zero
73
+ it), so pruning is an explicit opt-in that trades recall for noise reduction
74
+ rather than a silent, certain "this path is safe" claim."""
75
+ return any(e.sanitized_by for e in path.edges)
72
76
 
73
77
 
74
78
  class CodeGraph:
@@ -296,13 +300,20 @@ class CodeGraph:
296
300
  edge_map = self._edge_rows(session, raw_paths)
297
301
  tainted_sources = self._tainted_source_ids(session, sources)
298
302
  excluded_nodes = self._nodes_with_open_frontier(session, all_ids, kinds, floor)
303
+ sibling_calls = self._out_call_qnames(session, all_ids)
299
304
  built = [
300
305
  self._materialize_path(
301
- path, symbol_map, edge_map, registry, tainted_sources, excluded_nodes
306
+ path,
307
+ symbol_map,
308
+ edge_map,
309
+ registry,
310
+ tainted_sources,
311
+ excluded_nodes,
312
+ sibling_calls,
302
313
  )
303
314
  for path in raw_paths
304
315
  ]
305
- results = [cp for cp in built if not (prune_sanitized and _fully_sanitized(cp))]
316
+ results = [cp for cp in built if not (prune_sanitized and _has_sanitizer(cp))]
306
317
  results.sort(
307
318
  key=lambda p: (-(p.risk_score or 0.0), len(p.symbols), [s.id for s in p.symbols])
308
319
  )
@@ -543,8 +554,38 @@ class CodeGraph:
543
554
  ).scalars()
544
555
  return set(rows)
545
556
 
557
+ @staticmethod
558
+ def _out_call_qnames(session: Session, node_ids: set[int]) -> dict[int, list[str]]:
559
+ """For each node, the callee qnames of its outgoing CALLS edges.
560
+
561
+ Used for sanitizer detection: a sanitizer is usually a *sibling* call of
562
+ an on-path function (`shlex.quote(x)` then `subprocess.run(...)`), not a
563
+ node on the source->sink path itself, so it never appears among the path's
564
+ own hops."""
565
+ if not node_ids:
566
+ return {}
567
+ from entrygraph.kinds import EdgeKind
568
+
569
+ rows = session.execute(
570
+ select(models.Edge.src_symbol_id, models.Edge.dst_qname).where(
571
+ models.Edge.src_symbol_id.in_(node_ids),
572
+ models.Edge.kind == EdgeKind.CALLS,
573
+ )
574
+ ).all()
575
+ out: dict[int, list[str]] = {}
576
+ for sid, dst_qname in rows:
577
+ out.setdefault(sid, []).append(dst_qname)
578
+ return out
579
+
546
580
  def _materialize_path(
547
- self, path, symbol_map, edge_map, registry, tainted_sources, excluded_nodes
581
+ self,
582
+ path,
583
+ symbol_map,
584
+ edge_map,
585
+ registry,
586
+ tainted_sources,
587
+ excluded_nodes,
588
+ sibling_calls,
548
589
  ) -> CallPath:
549
590
  symbols = tuple(symbol_map[node] for node, _ in path)
550
591
  hops = [hop for _, hop in path[1:]]
@@ -556,10 +597,11 @@ class CodeGraph:
556
597
  sink_category = registry.sinks[sink_id].category if sink_id in registry.sinks else None
557
598
  terminal_const = is_constant_args(terminal.arg_preview) if terminal else False
558
599
 
559
- # sanitizer detection: any on-path symbol OR sibling call matching a
560
- # sanitizer for the sink's category
600
+ # sanitizer detection: a sanitizer for the sink's category called by any
601
+ # on-path node (its own hops OR a sibling call)
602
+ sibling_qnames = {q for node, _ in path for q in sibling_calls.get(node, ())}
561
603
  sanitized_ids, sanitized_effect = self._path_sanitizers(
562
- symbols, rows, registry, sink_category
604
+ symbols, rows, sibling_qnames, registry, sink_category
563
605
  )
564
606
 
565
607
  path_edges: list[PathEdge] = []
@@ -593,17 +635,28 @@ class CodeGraph:
593
635
  )
594
636
 
595
637
  @staticmethod
596
- def _path_sanitizers(symbols, rows, registry, sink_category):
597
- """Return (matched sanitizer ids, strongest effect) for the sink category."""
638
+ def _path_sanitizers(symbols, rows, sibling_qnames, registry, sink_category):
639
+ """Return (matched sanitizer ids, effect) for the sink category.
640
+
641
+ Candidates are every callee reachable from an on-path node: the path's own
642
+ hops plus sibling calls (`sibling_qnames`). The effect is capped at
643
+ ``"reduces"`` regardless of the catalog's ``effect`` — with no dataflow we
644
+ cannot prove the sanitized value is the one reaching the sink, so a match
645
+ discounts risk but never drives it to zero (which would silently hide a
646
+ real path). ``--prune-sanitized`` is the explicit, heuristic opt-in to
647
+ drop these."""
598
648
  if not sink_category:
599
649
  return [], None
600
650
  matched: list = []
601
- candidates = [s.qname for s in symbols] + [r.dst_qname for r in rows if r is not None]
651
+ candidates = (
652
+ [s.qname for s in symbols]
653
+ + [r.dst_qname for r in rows if r is not None]
654
+ + list(sibling_qnames)
655
+ )
602
656
  for qname in candidates:
603
657
  for san in registry.match_sanitizers(qname):
604
658
  if san.category == sink_category:
605
659
  matched.append(san)
606
660
  if not matched:
607
661
  return [], None
608
- effect = "neutralizes" if any(s.effect == "neutralizes" for s in matched) else "reduces"
609
- return [s.id for s in matched], effect
662
+ return [s.id for s in matched], "reduces"
@@ -415,7 +415,7 @@ def build_parser() -> argparse.ArgumentParser:
415
415
  "--prune-sanitized",
416
416
  dest="prune_sanitized",
417
417
  action="store_true",
418
- help="drop paths neutralized by a registered sanitizer",
418
+ help="drop paths where a category sanitizer is called (heuristic, no dataflow)",
419
419
  )
420
420
  p.set_defaults(func=cmd_paths)
421
421
 
@@ -0,0 +1,12 @@
1
+ import shlex
2
+ import subprocess
3
+
4
+
5
+ def sanitized(cmd):
6
+ # shlex.quote is a registered command_exec sanitizer, called on the same
7
+ # function as the sink (a sibling call, not a node on the source->sink path).
8
+ subprocess.run(shlex.quote(cmd))
9
+
10
+
11
+ def unsanitized(cmd):
12
+ subprocess.run(cmd)
@@ -0,0 +1,57 @@
1
+ """Sanitizer detection over sibling call edges (Phase 1.2).
2
+
3
+ Regression: builtin sanitizers (external qnames like `py:shlex.quote`) could
4
+ never fire — detection only looked at nodes on the source->sink path, but a
5
+ sanitizer is a *sibling* call of an on-path function. And a heuristic match must
6
+ only discount risk, never drive it to zero (which would hide a real path).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ from entrygraph import CodeGraph
16
+
17
+ SANITIZER_APP = Path(__file__).parent / "fixtures" / "python" / "sanitizer"
18
+
19
+
20
+ @pytest.fixture(scope="module")
21
+ def graph(tmp_path_factory) -> CodeGraph:
22
+ db = tmp_path_factory.mktemp("db") / "graph.db"
23
+ g = CodeGraph.index(SANITIZER_APP, db=db)
24
+ yield g
25
+ g.close()
26
+
27
+
28
+ def _path(graph, source):
29
+ paths = graph.paths(source=source, sink_category="command_exec")
30
+ assert paths, f"expected a command_exec path from {source}"
31
+ return paths[0]
32
+
33
+
34
+ def test_sibling_sanitizer_is_detected(graph):
35
+ # shlex.quote is a sibling call of app.sanitized, not a node on the path
36
+ path = _path(graph, "app.sanitized")
37
+ assert any("py.sanitize.shlex" in e.sanitized_by for e in path.edges)
38
+
39
+
40
+ def test_unsanitized_path_has_no_sanitizer(graph):
41
+ path = _path(graph, "app.unsanitized")
42
+ assert all(not e.sanitized_by for e in path.edges)
43
+
44
+
45
+ def test_sanitizer_discounts_but_does_not_zero_risk(graph):
46
+ # the sanitized path must still carry meaningful (nonzero) risk, and be
47
+ # strictly lower-risk than the identical unsanitized path
48
+ sanitized = _path(graph, "app.sanitized")
49
+ unsanitized = _path(graph, "app.unsanitized")
50
+ assert 0.0 < sanitized.risk_score < unsanitized.risk_score
51
+
52
+
53
+ def test_prune_sanitized_drops_only_sanitized_paths(graph):
54
+ kept = graph.paths(source="app.*", sink_category="command_exec", prune_sanitized=True)
55
+ sources = {p.symbols[0].qname for p in kept}
56
+ assert "app.unsanitized" in sources
57
+ assert "app.sanitized" not in sources
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes