entrygraph 0.1.4__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.4 → entrygraph-0.1.6}/PKG-INFO +4 -1
- {entrygraph-0.1.4 → entrygraph-0.1.6}/README.md +3 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/_version.py +2 -2
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/api.py +40 -6
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/cli/main.py +13 -2
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/db/meta.py +1 -1
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/db/models.py +4 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/python.py +25 -14
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/frameworks.py +10 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/manifests.py +3 -1
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/taint.py +15 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/graph/scoring.py +1 -1
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/pipeline/scanner.py +7 -7
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/resolve/resolver.py +4 -1
- {entrygraph-0.1.4 → 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.4 → entrygraph-0.1.6}/tests/test_entrypoint_expansion.py +11 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_frameworks.py +12 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_manifests.py +11 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_resolver.py +32 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_scoring.py +9 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_taint.py +20 -0
- entrygraph-0.1.6/tests/test_taint_sources.py +50 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/.github/workflows/ci.yml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/.github/workflows/release.yml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/.gitignore +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/LICENSE +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/RELEASING.md +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/pyproject.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/__main__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/cli/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/cli/render.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/csharp.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/go.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/java.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/javascript.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/lib_python.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/php.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/python.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/ruby.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/data/sinks/rust.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/db/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/db/engine.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/db/queries.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/base.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/configs.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/golang.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/java.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/php.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/detect/entrypoints/rust.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/errors.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/base.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/csharp.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/golang.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/ir.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/java.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/javascript.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/php.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/python.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/registry.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/ruby.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/extract/rust.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/fs/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/fs/hashing.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/fs/lang.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/fs/walker.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/graph/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/graph/adjacency.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/graph/cte.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/kinds.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/parsing/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/parsing/parsers.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/parsing/queries.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/pipeline/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/pipeline/worker.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/pipeline/writer.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/py.typed +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/csharp/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/go/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/go/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/go/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/java/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/java/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/java/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/javascript/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/php/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/php/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/php/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/python/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/python/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/python/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/ruby/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/rust/calls.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/rust/definitions.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/queries/rust/imports.scm +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/resolve/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/resolve/externals.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/resolve/hierarchy.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/src/entrygraph/resolve/symbol_table.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/conftest.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/go.mod +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/main.go +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/go/gin_app/service.go +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/pom.xml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/package.json +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/javascript/express_app/src/services.js +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/artisan +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/composer.json +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/db.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/routes.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/app/services.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/cli.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/python/flask_app/requirements.txt +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_api.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_cli.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_engine_pragmas.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_entrypoints.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_csharp.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_go.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_java.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_javascript.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_php.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_python.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_ruby.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_extract_rust.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_hardening.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_incremental.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_indexer.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_lang.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_models.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_pool.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_reachability.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_render.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_sink_catalog.py +0 -0
- {entrygraph-0.1.4 → entrygraph-0.1.6}/tests/test_walker.py +0 -0
- {entrygraph-0.1.4 → 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
|
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
|
|
8
8
|
from entrygraph.detect.entrypoints.base import (
|
|
9
9
|
EntrypointRule,
|
|
10
|
+
Matcher,
|
|
10
11
|
first_string_arg,
|
|
11
12
|
identifier_args,
|
|
12
13
|
methods_kwarg,
|
|
@@ -157,20 +158,30 @@ def _django_urls(x: FileExtraction) -> list[EntrypointHint]:
|
|
|
157
158
|
return hints
|
|
158
159
|
|
|
159
160
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
161
|
+
def _click_like(framework: str, rule_id: str) -> Matcher:
|
|
162
|
+
"""click and typer share decorator shape (`@app.command`); only the
|
|
163
|
+
reported framework/rule_id differ, so bind them per rule."""
|
|
164
|
+
|
|
165
|
+
def _match(x: FileExtraction) -> list[EntrypointHint]:
|
|
166
|
+
hints = []
|
|
167
|
+
for symbol, decorator in _decorated(x):
|
|
168
|
+
if _CLICK_CMD.match(decorator):
|
|
169
|
+
hints.append(
|
|
170
|
+
EntrypointHint(
|
|
171
|
+
rule_id=rule_id,
|
|
172
|
+
kind=EntrypointKind.CLI_COMMAND,
|
|
173
|
+
handler_qualified_name=symbol.qualified_name,
|
|
174
|
+
name=first_string_arg(decorator) or symbol.name,
|
|
175
|
+
framework=framework,
|
|
176
|
+
)
|
|
171
177
|
)
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
return hints
|
|
179
|
+
|
|
180
|
+
return _match
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_click_commands = _click_like("click", "python.click.command")
|
|
184
|
+
_typer_commands = _click_like("typer", "python.typer.command")
|
|
174
185
|
|
|
175
186
|
|
|
176
187
|
def _celery_tasks(x: FileExtraction) -> list[EntrypointHint]:
|
|
@@ -363,7 +374,7 @@ register(
|
|
|
363
374
|
)
|
|
364
375
|
register(
|
|
365
376
|
EntrypointRule(
|
|
366
|
-
"python.typer.command", "python", "typer", EntrypointKind.CLI_COMMAND,
|
|
377
|
+
"python.typer.command", "python", "typer", EntrypointKind.CLI_COMMAND, _typer_commands
|
|
367
378
|
)
|
|
368
379
|
)
|
|
369
380
|
register(
|
|
@@ -104,6 +104,16 @@ FRAMEWORKS: list[FrameworkSpec] = [
|
|
|
104
104
|
),
|
|
105
105
|
),
|
|
106
106
|
# JS/TS, Go, Java, Ruby specs land with their extractors (M8)
|
|
107
|
+
FrameworkSpec(
|
|
108
|
+
"aws-lambda-js",
|
|
109
|
+
"javascript",
|
|
110
|
+
(
|
|
111
|
+
FrameworkSignal("file_presence", "serverless.y*ml", 0.5),
|
|
112
|
+
FrameworkSignal("file_presence", "template.y*ml", 0.3),
|
|
113
|
+
FrameworkSignal("manifest_dep", "@types/aws-lambda", 0.6),
|
|
114
|
+
FrameworkSignal("manifest_dep", "aws-lambda", 0.5),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
107
117
|
FrameworkSpec(
|
|
108
118
|
"express",
|
|
109
119
|
"javascript",
|
|
@@ -223,9 +223,11 @@ def parse_manifests(root: str | Path) -> ManifestDeps:
|
|
|
223
223
|
if not manifest.is_file():
|
|
224
224
|
continue
|
|
225
225
|
if any(
|
|
226
|
-
part in ("node_modules", "vendor", ".venv", "venv", "target"
|
|
226
|
+
part in ("node_modules", "vendor", ".venv", "venv", "target")
|
|
227
227
|
for part in manifest.parts
|
|
228
228
|
):
|
|
229
|
+
# note: "packages" is intentionally NOT excluded — it is the
|
|
230
|
+
# standard JS workspace layout (packages/<name>/package.json).
|
|
229
231
|
continue
|
|
230
232
|
try:
|
|
231
233
|
text = manifest.read_text(encoding="utf-8", errors="replace")
|
|
@@ -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
|
|
@@ -33,7 +33,7 @@ _LENGTH_DECAY = 0.97
|
|
|
33
33
|
_CONST_TOKEN = re.compile(
|
|
34
34
|
r"""^(
|
|
35
35
|
\s | , | = | \( | \) | \[ | \] | \{ | \} | : |
|
|
36
|
-
'[^']*' | "[^"]*" | `[
|
|
36
|
+
'[^']*' | "[^"]*" | `(?:[^`$]|\$(?!\{))*` | # backtick: no ${...} interpolation
|
|
37
37
|
\d[\d_.eExXaAbBcCdDfF]* |
|
|
38
38
|
True|False|None|null|true|false|nil |
|
|
39
39
|
[A-Za-z_]\w*\s*= # kwarg name before '='
|
|
@@ -305,12 +305,13 @@ def _write_files(
|
|
|
305
305
|
for path, fid in session.execute(select(File.path, File.id).where(File.repo_id == repo.id)):
|
|
306
306
|
file_id_by_path[path] = fid
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
to_index_paths = {w.path for w in diff.to_index}
|
|
309
|
+
reindexed = to_index_paths | {
|
|
309
310
|
w.path for w in walked if w.skip_reason and w.path not in file_id_by_path
|
|
310
311
|
}
|
|
311
312
|
new_rows = []
|
|
312
313
|
for wf in walked:
|
|
313
|
-
if wf.path in file_id_by_path and wf.path not in
|
|
314
|
+
if wf.path in file_id_by_path and wf.path not in to_index_paths:
|
|
314
315
|
continue # unchanged; row already present
|
|
315
316
|
if wf.path not in reindexed and not wf.skip_reason:
|
|
316
317
|
continue
|
|
@@ -434,11 +435,9 @@ def _write_edges_and_entrypoints(
|
|
|
434
435
|
resolver = FileResolver(x, module_ids[path], table, externals, is_package)
|
|
435
436
|
file_id = file_id_by_path[path]
|
|
436
437
|
for edge in resolver.resolve():
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
else None
|
|
441
|
-
)
|
|
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
|
|
442
441
|
edge_rows.append(
|
|
443
442
|
{
|
|
444
443
|
"id": alloc.take(Edge),
|
|
@@ -451,6 +450,7 @@ def _write_edges_and_entrypoints(
|
|
|
451
450
|
"confidence": int(edge.confidence),
|
|
452
451
|
"arg_preview": edge.arg_preview,
|
|
453
452
|
"sink_id": sink_id,
|
|
453
|
+
"source_id": source_id,
|
|
454
454
|
"via": edge.via,
|
|
455
455
|
}
|
|
456
456
|
)
|
|
@@ -71,6 +71,7 @@ class FileResolver:
|
|
|
71
71
|
self.module_symbol_id = module_symbol_id
|
|
72
72
|
self.table = table
|
|
73
73
|
self.externals = externals
|
|
74
|
+
self.is_package = is_package
|
|
74
75
|
self.prefix = LANG_PREFIX.get(extraction.language, extraction.language)
|
|
75
76
|
self.import_map, self.wildcard_modules = build_import_map(extraction, is_package)
|
|
76
77
|
|
|
@@ -122,7 +123,9 @@ class FileResolver:
|
|
|
122
123
|
seen: set[str] = set()
|
|
123
124
|
for imp in self.x.imports:
|
|
124
125
|
module = (
|
|
125
|
-
expand_relative(imp, self.x.module_path,
|
|
126
|
+
expand_relative(imp, self.x.module_path, self.is_package)
|
|
127
|
+
if imp.is_relative
|
|
128
|
+
else imp.module
|
|
126
129
|
)
|
|
127
130
|
if not module or module in seen:
|
|
128
131
|
continue
|
|
@@ -160,6 +160,17 @@ def test_lambda_js_handler_rule():
|
|
|
160
160
|
assert hints and hints[0].kind is EntrypointKind.LAMBDA_HANDLER
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
def test_typer_command_reports_typer_not_click():
|
|
164
|
+
# typer reuses click's decorator shape; the hint must carry framework/rule_id
|
|
165
|
+
# for typer, not be mislabeled as click.
|
|
166
|
+
sym = _sym("serve", "cli.serve")
|
|
167
|
+
sym.decorators = ["@app.command()"]
|
|
168
|
+
hints = _run("python.typer.command", _extraction([sym]), frameworks=frozenset({"typer"}))
|
|
169
|
+
assert hints
|
|
170
|
+
assert hints[0].framework == "typer"
|
|
171
|
+
assert hints[0].rule_id == "python.typer.command"
|
|
172
|
+
|
|
173
|
+
|
|
163
174
|
def test_sidekiq_worker_rule():
|
|
164
175
|
from entrygraph.detect.entrypoints import rules_for
|
|
165
176
|
|
|
@@ -33,6 +33,18 @@ def test_language_gating():
|
|
|
33
33
|
assert not any(d.name == "rails" for d in detected)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def test_aws_lambda_js_detected_from_serverless_config():
|
|
37
|
+
# the javascript.aws-lambda.handler entrypoint rule is gated on this spec;
|
|
38
|
+
# regression: the spec was missing, so the rule could never fire.
|
|
39
|
+
detected = detect_frameworks(
|
|
40
|
+
ManifestDeps(javascript={"@types/aws-lambda"}),
|
|
41
|
+
import_signals=set(),
|
|
42
|
+
file_paths=["serverless.yml"],
|
|
43
|
+
languages_present={"javascript"},
|
|
44
|
+
)
|
|
45
|
+
assert any(d.name == "aws-lambda-js" for d in detected)
|
|
46
|
+
|
|
47
|
+
|
|
36
48
|
def test_typescript_counts_for_js_frameworks():
|
|
37
49
|
manifests = ManifestDeps(javascript={"express"})
|
|
38
50
|
detected = detect_frameworks(
|
|
@@ -78,6 +78,17 @@ def test_parse_manifests_fixture():
|
|
|
78
78
|
assert "requirements.txt" in deps.sources
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def test_parse_manifests_reads_js_workspace_packages(tmp_path: Path):
|
|
82
|
+
# `packages/<name>/package.json` is the standard JS workspace layout and must
|
|
83
|
+
# not be excluded (regression: "packages" was in the skip list).
|
|
84
|
+
pkg = tmp_path / "packages" / "api"
|
|
85
|
+
pkg.mkdir(parents=True)
|
|
86
|
+
(pkg / "package.json").write_text('{"dependencies": {"express": "^4"}}')
|
|
87
|
+
deps = parse_manifests(tmp_path)
|
|
88
|
+
assert "express" in deps.javascript
|
|
89
|
+
assert "packages/api/package.json" in deps.sources
|
|
90
|
+
|
|
91
|
+
|
|
81
92
|
# ---------------- C1: C#, PHP, Rust manifests ----------------
|
|
82
93
|
from entrygraph.detect.manifests import ( # noqa: E402
|
|
83
94
|
parse_cargo_toml,
|
|
@@ -57,6 +57,38 @@ def test_import_based_project_resolution():
|
|
|
57
57
|
assert call.confidence is Confidence.IMPORT
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def test_relative_import_from_package_init_resolves_submodule():
|
|
61
|
+
# `from .sub import thing` in the package's own __init__.py (module_path
|
|
62
|
+
# "app", is_package=True) must resolve to the project module "app.sub".
|
|
63
|
+
# Regression: is_package was hardcoded False, dropping "app" and yielding a
|
|
64
|
+
# bogus ".sub" external placeholder.
|
|
65
|
+
table = make_table()
|
|
66
|
+
table.add_module("app.sub", 20)
|
|
67
|
+
x = FileExtraction(
|
|
68
|
+
path="app/__init__.py",
|
|
69
|
+
language="python",
|
|
70
|
+
module_path="app",
|
|
71
|
+
parse_ok=True,
|
|
72
|
+
error_count=0,
|
|
73
|
+
imports=[
|
|
74
|
+
RawImport(
|
|
75
|
+
module="sub",
|
|
76
|
+
imported_name="thing",
|
|
77
|
+
alias="thing",
|
|
78
|
+
span=SPAN,
|
|
79
|
+
is_relative=True,
|
|
80
|
+
relative_level=1,
|
|
81
|
+
)
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
externals = ExternalRegistry(iter(range(100, 200)).__next__)
|
|
85
|
+
resolver = FileResolver(x, 1, table, externals, is_package=True)
|
|
86
|
+
edges = resolver.resolve()
|
|
87
|
+
imp = next(e for e in edges if e.kind is EdgeKind.IMPORTS)
|
|
88
|
+
assert imp.dst_qname == "app.sub"
|
|
89
|
+
assert imp.dst_symbol_id == 20
|
|
90
|
+
|
|
91
|
+
|
|
60
92
|
def test_external_import_creates_placeholder():
|
|
61
93
|
table = make_table()
|
|
62
94
|
imports = [RawImport(module="subprocess", imported_name=None, alias="sub", span=SPAN)]
|
|
@@ -17,6 +17,15 @@ def test_is_constant_args():
|
|
|
17
17
|
assert not is_constant_args("(a, b, c, d, e, f, ...")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def test_js_template_literal_interpolation_is_not_constant():
|
|
21
|
+
# a plain backtick template with no interpolation is a literal
|
|
22
|
+
assert is_constant_args("(`ls -la`)")
|
|
23
|
+
# `${...}` carries a variable into the sink -> the archetypal JS command
|
|
24
|
+
# injection must NOT be scored as constant/safe
|
|
25
|
+
assert not is_constant_args("(`ls ${req.query.dir}`)")
|
|
26
|
+
assert not is_constant_args("(`${userInput}`)")
|
|
27
|
+
|
|
28
|
+
|
|
20
29
|
def test_score_path_ordering_by_severity():
|
|
21
30
|
common = {
|
|
22
31
|
"hop_confidences": [int(Confidence.EXACT)],
|
|
@@ -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
|