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.
- {entrygraph-0.1.5 → entrygraph-0.1.6}/PKG-INFO +4 -1
- {entrygraph-0.1.5 → entrygraph-0.1.6}/README.md +3 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/_version.py +2 -2
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/api.py +40 -6
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/main.py +13 -2
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/meta.py +1 -1
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/models.py +4 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/taint.py +15 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/scanner.py +4 -5
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/results.py +1 -0
- entrygraph-0.1.6/tests/fixtures/python/taint_source/handler.py +12 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_taint.py +20 -0
- entrygraph-0.1.6/tests/test_taint_sources.py +50 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/.github/workflows/ci.yml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/.github/workflows/release.yml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/.gitignore +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/LICENSE +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/RELEASING.md +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/pyproject.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/__main__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/cli/render.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/csharp.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/go.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/java.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/javascript.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_python.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/php.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/python.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/ruby.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/data/sinks/rust.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/engine.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/db/queries.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/base.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/configs.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/golang.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/java.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/php.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/python.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/rust.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/frameworks.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/detect/manifests.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/errors.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/base.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/csharp.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/golang.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/ir.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/java.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/javascript.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/php.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/python.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/registry.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/ruby.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/extract/rust.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/hashing.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/lang.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/fs/walker.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/adjacency.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/cte.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/graph/scoring.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/kinds.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/parsers.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/parsing/queries.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/worker.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/pipeline/writer.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/py.typed +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/go/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/java/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/php/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/python/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/calls.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/definitions.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/queries/rust/imports.scm +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/externals.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/hierarchy.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/resolver.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/src/entrygraph/resolve/symbol_table.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/conftest.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/go.mod +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/main.go +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/service.go +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/pom.xml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/package.json +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/services.js +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/artisan +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/composer.json +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/db.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/routes.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/services.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/cli.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/requirements.txt +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_api.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_cli.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_engine_pragmas.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_entrypoint_expansion.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_entrypoints.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_csharp.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_go.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_java.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_javascript.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_php.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_python.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_ruby.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_extract_rust.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_frameworks.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_hardening.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_incremental.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_indexer.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_lang.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_manifests.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_models.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_pool.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_reachability.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_render.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_resolver.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_scoring.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_sink_catalog.py +0 -0
- {entrygraph-0.1.5 → entrygraph-0.1.6}/tests/test_walker.py +0 -0
- {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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
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.
|
|
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.
|
|
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
|
|
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]{
|
|
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",
|
|
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)"
|
|
@@ -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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
)
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
{entrygraph-0.1.5 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|