entrygraph 0.1.5__tar.gz → 0.1.6__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 (168) hide show
  1. {entrygraph-0.1.5 → entrygraph-0.1.6}/PKG-INFO +4 -1
  2. {entrygraph-0.1.5 → entrygraph-0.1.6}/README.md +3 -0
  3. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/_version.py +2 -2
  4. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/api.py +40 -6
  5. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/main.py +13 -2
  6. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/meta.py +1 -1
  7. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/models.py +4 -0
  8. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/taint.py +15 -0
  9. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/scanner.py +4 -5
  10. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/results.py +1 -0
  11. entrygraph-0.1.6/tests/fixtures/python/taint_source/handler.py +12 -0
  12. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_taint.py +20 -0
  13. entrygraph-0.1.6/tests/test_taint_sources.py +50 -0
  14. {entrygraph-0.1.5 → entrygraph-0.1.6}/.github/workflows/ci.yml +0 -0
  15. {entrygraph-0.1.5 → entrygraph-0.1.6}/.github/workflows/release.yml +0 -0
  16. {entrygraph-0.1.5 → entrygraph-0.1.6}/.gitignore +0 -0
  17. {entrygraph-0.1.5 → entrygraph-0.1.6}/LICENSE +0 -0
  18. {entrygraph-0.1.5 → entrygraph-0.1.6}/RELEASING.md +0 -0
  19. {entrygraph-0.1.5 → entrygraph-0.1.6}/pyproject.toml +0 -0
  20. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/__init__.py +0 -0
  21. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/__main__.py +0 -0
  22. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/__init__.py +0 -0
  23. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/render.py +0 -0
  24. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/csharp.toml +0 -0
  25. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/go.toml +0 -0
  26. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/java.toml +0 -0
  27. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/javascript.toml +0 -0
  28. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  29. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  30. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/php.toml +0 -0
  31. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/python.toml +0 -0
  32. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/ruby.toml +0 -0
  33. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/rust.toml +0 -0
  34. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/__init__.py +0 -0
  35. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/engine.py +0 -0
  36. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/queries.py +0 -0
  37. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/__init__.py +0 -0
  38. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  39. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/base.py +0 -0
  40. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  41. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
  42. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/golang.py +0 -0
  43. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/java.py +0 -0
  44. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
  45. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/php.py +0 -0
  46. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/python.py +0 -0
  47. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  48. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  49. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/frameworks.py +0 -0
  50. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/manifests.py +0 -0
  51. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/errors.py +0 -0
  52. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/__init__.py +0 -0
  53. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/base.py +0 -0
  54. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/csharp.py +0 -0
  55. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/golang.py +0 -0
  56. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/ir.py +0 -0
  57. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/java.py +0 -0
  58. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/javascript.py +0 -0
  59. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/php.py +0 -0
  60. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/python.py +0 -0
  61. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/registry.py +0 -0
  62. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/ruby.py +0 -0
  63. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/rust.py +0 -0
  64. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/__init__.py +0 -0
  65. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/hashing.py +0 -0
  66. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/lang.py +0 -0
  67. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/walker.py +0 -0
  68. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/__init__.py +0 -0
  69. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/adjacency.py +0 -0
  70. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/cte.py +0 -0
  71. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/scoring.py +0 -0
  72. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/kinds.py +0 -0
  73. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/__init__.py +0 -0
  74. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/parsers.py +0 -0
  75. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/queries.py +0 -0
  76. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/__init__.py +0 -0
  77. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/worker.py +0 -0
  78. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/writer.py +0 -0
  79. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/py.typed +0 -0
  80. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/calls.scm +0 -0
  81. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  82. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/imports.scm +0 -0
  83. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/calls.scm +0 -0
  84. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/definitions.scm +0 -0
  85. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/imports.scm +0 -0
  86. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/calls.scm +0 -0
  87. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/definitions.scm +0 -0
  88. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/imports.scm +0 -0
  89. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/calls.scm +0 -0
  90. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  91. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/imports.scm +0 -0
  92. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/calls.scm +0 -0
  93. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/definitions.scm +0 -0
  94. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/imports.scm +0 -0
  95. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/calls.scm +0 -0
  96. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/definitions.scm +0 -0
  97. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/imports.scm +0 -0
  98. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/calls.scm +0 -0
  99. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  100. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/imports.scm +0 -0
  101. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/calls.scm +0 -0
  102. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/definitions.scm +0 -0
  103. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/imports.scm +0 -0
  104. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/__init__.py +0 -0
  105. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/externals.py +0 -0
  106. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/hierarchy.py +0 -0
  107. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/resolver.py +0 -0
  108. {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/symbol_table.py +0 -0
  109. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/conftest.py +0 -0
  110. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  111. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  112. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  113. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  114. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/go.mod +0 -0
  115. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/main.go +0 -0
  116. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/service.go +0 -0
  117. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/pom.xml +0 -0
  118. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  119. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  120. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  121. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  122. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/package.json +0 -0
  123. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  124. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  125. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  126. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/artisan +0 -0
  127. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/composer.json +0 -0
  128. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  129. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  130. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/db.py +0 -0
  131. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  132. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/services.py +0 -0
  133. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/cli.py +0 -0
  134. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  135. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  136. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  137. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  138. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  139. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  140. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  141. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_api.py +0 -0
  142. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_cli.py +0 -0
  143. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_engine_pragmas.py +0 -0
  144. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_entrypoint_expansion.py +0 -0
  145. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_entrypoints.py +0 -0
  146. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_csharp.py +0 -0
  147. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_go.py +0 -0
  148. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_java.py +0 -0
  149. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_javascript.py +0 -0
  150. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_php.py +0 -0
  151. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_python.py +0 -0
  152. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_ruby.py +0 -0
  153. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_rust.py +0 -0
  154. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_frameworks.py +0 -0
  155. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_hardening.py +0 -0
  156. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_incremental.py +0 -0
  157. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_indexer.py +0 -0
  158. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_lang.py +0 -0
  159. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_manifests.py +0 -0
  160. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_models.py +0 -0
  161. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_pool.py +0 -0
  162. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_reachability.py +0 -0
  163. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_render.py +0 -0
  164. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_resolver.py +0 -0
  165. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_scoring.py +0 -0
  166. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_sink_catalog.py +0 -0
  167. {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_walker.py +0 -0
  168. {entrygraph-0.1.5 → entrygraph-0.1.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.5
3
+ Version: 0.1.6
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>
@@ -197,6 +197,9 @@ entrygraph paths --source '*' --sink-category command_exec
197
197
 
198
198
  - **CI gate**: exits `0` when a path is found, `1` when none —
199
199
  `entrygraph paths --source '*' --sink-category command_exec && echo reachable`.
200
+ - **Catalog sources**: instead of a `--source` glob, use `--source-category` to
201
+ start from every call site of a registered taint source (e.g.
202
+ `--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
200
203
  - **Precision/recall dial**: by default only high-confidence edges are traversed.
201
204
  Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
202
205
  calls) or `--include-fuzzy` (speculative class-hierarchy edges); drop
@@ -146,6 +146,9 @@ entrygraph paths --source '*' --sink-category command_exec
146
146
 
147
147
  - **CI gate**: exits `0` when a path is found, `1` when none —
148
148
  `entrygraph paths --source '*' --sink-category command_exec && echo reachable`.
149
+ - **Catalog sources**: instead of a `--source` glob, use `--source-category` to
150
+ start from every call site of a registered taint source (e.g.
151
+ `--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
149
152
  - **Precision/recall dial**: by default only high-confidence edges are traversed.
150
153
  Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
151
154
  calls) or `--include-fuzzy` (speculative class-hierarchy edges); drop
@@ -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.5'
22
- __version_tuple__ = version_tuple = (0, 1, 5)
21
+ __version__ = version = '0.1.6'
22
+ __version_tuple__ = version_tuple = (0, 1, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -251,7 +251,8 @@ class CodeGraph:
251
251
  def paths(
252
252
  self,
253
253
  *,
254
- source: SourceSpec,
254
+ source: SourceSpec | None = None,
255
+ source_category: str | None = None,
255
256
  sink: SourceSpec | None = None,
256
257
  sink_category: str | None = None,
257
258
  max_depth: int = 25,
@@ -266,6 +267,11 @@ class CodeGraph:
266
267
  ) -> list[CallPath]:
267
268
  """Enumerate source->sink call paths, risk-ranked (highest first).
268
269
 
270
+ Sources are an explicit `source` spec (qname/glob/Symbol/Entrypoint) and/or
271
+ a `source_category` resolved from the taint-source catalog (e.g. every call
272
+ site of an `http_input`/`env` source). Sinks work the same via `sink` /
273
+ `sink_category`.
274
+
269
275
  By default only EXACT/IMPORT edges are traversed; `include_fuzzy` /
270
276
  `include_unresolved` lower the confidence floor (an explicit
271
277
  `min_confidence` int overrides the flags). `include_callbacks` also
@@ -276,7 +282,7 @@ class CodeGraph:
276
282
  floor, include_cha = _traversal_params(min_confidence, include_fuzzy, include_unresolved)
277
283
  kinds = (*edge_kinds, "callback") if include_callbacks else edge_kinds
278
284
  with self._session_factory() as session:
279
- sources = self._spec_to_ids(session, source)
285
+ sources = self._source_ids(session, source, source_category)
280
286
  sinks = self._sink_ids(session, sink, sink_category)
281
287
  if not sources or not sinks:
282
288
  return []
@@ -305,7 +311,8 @@ class CodeGraph:
305
311
  def reachable(
306
312
  self,
307
313
  *,
308
- source: SourceSpec,
314
+ source: SourceSpec | None = None,
315
+ source_category: str | None = None,
309
316
  sink: SourceSpec | None = None,
310
317
  sink_category: str | None = None,
311
318
  max_depth: int = 25,
@@ -319,7 +326,7 @@ class CodeGraph:
319
326
  floor, include_cha = _traversal_params(min_confidence, include_fuzzy, include_unresolved)
320
327
  kinds = (*edge_kinds, "callback") if include_callbacks else edge_kinds
321
328
  with self._session_factory() as session:
322
- sources = self._spec_to_ids(session, source)
329
+ sources = self._source_ids(session, source, source_category)
323
330
  sinks = self._sink_ids(session, sink, sink_category)
324
331
  if not sources or not sinks:
325
332
  return False
@@ -362,6 +369,9 @@ class CodeGraph:
362
369
  sink_edges=count(
363
370
  select(func.count(models.Edge.id)).where(models.Edge.sink_id.is_not(None))
364
371
  ),
372
+ source_edges=count(
373
+ select(func.count(models.Edge.id)).where(models.Edge.source_id.is_not(None))
374
+ ),
365
375
  )
366
376
 
367
377
  def session(self) -> Session:
@@ -426,6 +436,22 @@ class CodeGraph:
426
436
  return {spec.symbol.id}
427
437
  return q.symbol_ids_matching(session, str(spec))
428
438
 
439
+ def _source_ids(self, session: Session, source, source_category: str | None) -> set[int]:
440
+ """Taint origins: explicit `source` spec plus, for `source_category`, every
441
+ symbol that calls a matching catalog taint-source function."""
442
+ ids = self._spec_to_ids(session, source) if source is not None else set()
443
+ if source_category is not None:
444
+ registry = self._registry(session)
445
+ source_pattern_ids = registry.source_ids_for_category(source_category)
446
+ if source_pattern_ids:
447
+ rows = session.execute(
448
+ select(models.Edge.src_symbol_id).where(
449
+ models.Edge.source_id.in_(source_pattern_ids)
450
+ )
451
+ ).scalars()
452
+ ids |= set(rows)
453
+ return ids
454
+
429
455
  def _sink_ids(self, session: Session, sink, sink_category: str | None) -> set[int]:
430
456
  ids = self._spec_to_ids(session, sink) if sink is not None else set()
431
457
  if sink_category is not None:
@@ -474,18 +500,26 @@ class CodeGraph:
474
500
 
475
501
  @staticmethod
476
502
  def _tainted_source_ids(session: Session, sources: set[int]) -> set[int]:
477
- """Source symbol ids that are entrypoints with known user-controlled params."""
503
+ """Source symbol ids known to carry user-controlled data: entrypoints with
504
+ tainted params, plus call sites of catalog taint-source functions."""
478
505
  if not sources:
479
506
  return set()
507
+ tainted: set[int] = set()
480
508
  rows = session.execute(
481
509
  select(models.Entrypoint.symbol_id, models.Entrypoint.extra).where(
482
510
  models.Entrypoint.symbol_id.in_(sources)
483
511
  )
484
512
  ).all()
485
- tainted: set[int] = set()
486
513
  for sid, extra in rows:
487
514
  if extra and json.loads(extra).get("tainted_params"):
488
515
  tainted.add(sid)
516
+ source_callers = session.execute(
517
+ select(models.Edge.src_symbol_id).where(
518
+ models.Edge.source_id.is_not(None),
519
+ models.Edge.src_symbol_id.in_(sources),
520
+ )
521
+ ).scalars()
522
+ tainted |= set(source_callers)
489
523
  return tainted
490
524
 
491
525
  @staticmethod
@@ -261,9 +261,12 @@ def _path_tree(index: int, path) -> Tree:
261
261
 
262
262
 
263
263
  def cmd_paths(args) -> int:
264
+ if not args.source and not args.source_category:
265
+ raise EntrygraphError("provide --source and/or --source-category")
264
266
  with _open(args) as graph:
265
267
  paths = graph.paths(
266
268
  source=args.source,
269
+ source_category=args.source_category,
267
270
  sink=args.sink,
268
271
  sink_category=args.sink_category,
269
272
  max_depth=args.max_depth,
@@ -297,8 +300,11 @@ def cmd_paths(args) -> int:
297
300
  Panel("[yellow]No source → sink paths found.[/]", border_style="yellow", expand=False)
298
301
  )
299
302
  return 1
303
+ origin = (
304
+ args.source or (args.source_category and f"category:{args.source_category}") or "source"
305
+ )
300
306
  target = args.sink or (args.sink_category and f"category:{args.sink_category}") or "sink"
301
- con.print(f"[bold]{len(paths)}[/] path(s) [dim]{args.source} → {target}[/]\n")
307
+ con.print(f"[bold]{len(paths)}[/] path(s) [dim]{origin} → {target}[/]\n")
302
308
  for i, path in enumerate(paths, 1):
303
309
  con.print(_path_tree(i, path))
304
310
  return 0
@@ -374,7 +380,12 @@ def build_parser() -> argparse.ArgumentParser:
374
380
 
375
381
  p = sub.add_parser("paths", help="source -> sink call paths")
376
382
  add_db(p)
377
- p.add_argument("--source", required=True, help="qname or glob")
383
+ p.add_argument("--source", help="qname or glob")
384
+ p.add_argument(
385
+ "--source-category",
386
+ dest="source_category",
387
+ help="named taint-source category (e.g. http_input, env)",
388
+ )
378
389
  p.add_argument("--sink", help="qname or glob (e.g. py:subprocess.run)")
379
390
  p.add_argument(
380
391
  "--sink-category", dest="sink_category", help="named sink category (e.g. command_exec, sql)"
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
15
15
  from entrygraph.db.models import Base, Meta
16
16
  from entrygraph.errors import SchemaMismatchError
17
17
 
18
- SCHEMA_VERSION = 2
18
+ SCHEMA_VERSION = 3
19
19
 
20
20
 
21
21
  def create_schema(engine: Engine) -> None:
@@ -127,6 +127,9 @@ class Edge(Base):
127
127
  confidence: Mapped[int] = mapped_column(Integer, default=0) # kinds.Confidence value
128
128
  arg_preview: Mapped[str | None] = mapped_column(Text)
129
129
  sink_id: Mapped[str | None] = mapped_column(String(64)) # pre-tagged at index time
130
+ # A call to a taint-source function (request/env/stdin/...) tags its calling
131
+ # edge; the src symbol is then a taint origin. Pre-tagged at index time.
132
+ source_id: Mapped[str | None] = mapped_column(String(64))
130
133
  # Edge provenance for edges not resolved by the direct import/scope pass:
131
134
  # "cha" (class-hierarchy candidate), "dynamic" (getattr/computed call),
132
135
  # "reexport" (chased through a barrel file). NULL for directly-resolved edges.
@@ -138,6 +141,7 @@ class Edge(Base):
138
141
  Index("ix_edges_srcfile", "src_file_id"),
139
142
  Index("ix_edges_unresolved", "dst_qname", sqlite_where=text("dst_symbol_id IS NULL")),
140
143
  Index("ix_edges_sink", "sink_id", sqlite_where=text("sink_id IS NOT NULL")),
144
+ Index("ix_edges_source", "source_id", sqlite_where=text("source_id IS NOT NULL")),
141
145
  Index("ix_edges_via", "via", sqlite_where=text("via IS NOT NULL")),
142
146
  )
143
147
 
@@ -85,6 +85,7 @@ class SinkRegistry:
85
85
  self.sources = {s.id: s for s in sources}
86
86
  self.sanitizers = {s.id: s for s in (sanitizers or [])}
87
87
  self._compiled = [(_compile(s.callee), s) for s in sinks]
88
+ self._compiled_sources = [(_compile(s.callee), s) for s in sources]
88
89
  self._compiled_sanitizers = [(_compile(s.callee), s) for s in (sanitizers or [])]
89
90
 
90
91
  def match(self, canonical_callee: str, arg_preview: str | None = None) -> str | None:
@@ -98,6 +99,17 @@ class SinkRegistry:
98
99
  return sink.id
99
100
  return None
100
101
 
102
+ def match_source(self, canonical_callee: str) -> str | None:
103
+ """Return the first matching taint-source id for this callee, or None.
104
+
105
+ A call to a source function (e.g. ``flask.request.args.get``, ``os.getenv``)
106
+ marks its calling site as a taint origin. Stamped on edges at index time,
107
+ symmetric to :meth:`match`."""
108
+ for regex, source in self._compiled_sources:
109
+ if regex.match(canonical_callee):
110
+ return source.id
111
+ return None
112
+
101
113
  def match_sanitizers(self, canonical_callee: str) -> list[SanitizerPattern]:
102
114
  """Sanitizers whose pattern matches this callee qname."""
103
115
  return [s for regex, s in self._compiled_sanitizers if regex.match(canonical_callee)]
@@ -108,6 +120,9 @@ class SinkRegistry:
108
120
  def ids_for_category(self, category: str) -> set[str]:
109
121
  return {s.id for s in self.sinks.values() if s.category == category}
110
122
 
123
+ def source_ids_for_category(self, category: str) -> set[str]:
124
+ return {s.id for s in self.sources.values() if s.category == category}
125
+
111
126
  def severity_of(self, sink_id: str | None) -> str | None:
112
127
  sink = self.sinks.get(sink_id) if sink_id else None
113
128
  return sink.severity if sink else None
@@ -435,11 +435,9 @@ def _write_edges_and_entrypoints(
435
435
  resolver = FileResolver(x, module_ids[path], table, externals, is_package)
436
436
  file_id = file_id_by_path[path]
437
437
  for edge in resolver.resolve():
438
- sink_id = (
439
- sink_registry.match(edge.dst_qname, edge.arg_preview)
440
- if edge.kind is EdgeKind.CALLS
441
- else None
442
- )
438
+ is_call = edge.kind is EdgeKind.CALLS
439
+ sink_id = sink_registry.match(edge.dst_qname, edge.arg_preview) if is_call else None
440
+ source_id = sink_registry.match_source(edge.dst_qname) if is_call else None
443
441
  edge_rows.append(
444
442
  {
445
443
  "id": alloc.take(Edge),
@@ -452,6 +450,7 @@ def _write_edges_and_entrypoints(
452
450
  "confidence": int(edge.confidence),
453
451
  "arg_preview": edge.arg_preview,
454
452
  "sink_id": sink_id,
453
+ "source_id": source_id,
455
454
  "via": edge.via,
456
455
  }
457
456
  )
@@ -136,3 +136,4 @@ class GraphStats:
136
136
  resolved_edges: int
137
137
  entrypoints: int
138
138
  sink_edges: int
139
+ source_edges: int
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+
4
+ def handler():
5
+ # os.getenv is a registered taint source (category "env_input").
6
+ cmd = os.getenv("CMD")
7
+ run(cmd)
8
+
9
+
10
+ def run(cmd):
11
+ # os.system is a registered command_exec sink.
12
+ os.system(cmd)
@@ -4,6 +4,7 @@ from entrygraph.detect.taint import (
4
4
  SanitizerPattern,
5
5
  SinkPattern,
6
6
  SinkRegistry,
7
+ SourcePattern,
7
8
  _load_toml,
8
9
  builtin_registry,
9
10
  )
@@ -56,6 +57,25 @@ def test_sanitizer_matching_and_category_lookup():
56
57
  assert reg.sanitizers_for_category("command_exec")[0].id == "san"
57
58
 
58
59
 
60
+ def test_match_source_and_source_category():
61
+ reg = SinkRegistry(
62
+ sinks=[],
63
+ sources=[
64
+ SourcePattern(id="env", category="env_input", callee="py:{os.getenv,os.environ.get}"),
65
+ SourcePattern(id="http", category="http_input", callee="py:flask.request*"),
66
+ ],
67
+ )
68
+ assert reg.match_source("py:os.getenv") == "env"
69
+ assert reg.match_source("py:flask.request.args.get") == "http"
70
+ assert reg.match_source("py:os.system") is None
71
+ assert reg.source_ids_for_category("env_input") == {"env"}
72
+
73
+
74
+ def test_builtin_registry_matches_env_source():
75
+ reg = builtin_registry()
76
+ assert reg.match_source("py:os.getenv") == "py.env"
77
+
78
+
59
79
  def test_builtin_registry_loads_library_summaries_and_sanitizers():
60
80
  reg = builtin_registry()
61
81
  # library summary: paramiko exec_command mapped to command_exec
@@ -0,0 +1,50 @@
1
+ """End-to-end taint-source catalog wiring (Phase 1.1).
2
+
3
+ Regression: `[[source]]` catalog entries used to be dead configuration — parsed
4
+ but never matched or stamped, so `paths(source_category=...)` was impossible.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+
13
+ from entrygraph import CodeGraph
14
+
15
+ TAINT_SOURCE_APP = Path(__file__).parent / "fixtures" / "python" / "taint_source"
16
+
17
+
18
+ @pytest.fixture(scope="module")
19
+ def graph(tmp_path_factory) -> CodeGraph:
20
+ db = tmp_path_factory.mktemp("db") / "graph.db"
21
+ g = CodeGraph.index(TAINT_SOURCE_APP, db=db)
22
+ yield g
23
+ g.close()
24
+
25
+
26
+ def test_source_edges_stamped(graph):
27
+ # os.getenv call in handler() is stamped as a taint source
28
+ assert graph.stats().source_edges >= 1
29
+
30
+
31
+ def test_paths_from_source_category_to_sink_category(graph):
32
+ # env_input source (os.getenv) reaches a command_exec sink (os.system)
33
+ paths = graph.paths(source_category="env_input", sink_category="command_exec")
34
+ assert paths
35
+ qnames = [s.qname for s in paths[0].symbols]
36
+ assert qnames[0] == "handler.handler"
37
+ assert qnames[-1] == "py:os.system"
38
+
39
+
40
+ def test_reachable_from_source_category(graph):
41
+ assert graph.reachable(source_category="env_input", sink_category="command_exec")
42
+ # a category with no matching source in this repo yields no reach
43
+ assert not graph.reachable(source_category="http_input", sink_category="command_exec")
44
+
45
+
46
+ def test_catalog_source_marks_origin_tainted(graph):
47
+ # the source-tainted risk factor should apply (handler calls os.getenv), so
48
+ # the path risk exceeds the untainted-source baseline discount
49
+ path = graph.paths(source_category="env_input", sink_category="command_exec")[0]
50
+ assert path.risk_score and path.risk_score > 0.5
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes