entrygraph 0.1.6__tar.gz → 0.1.8__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.6 → entrygraph-0.1.8}/PKG-INFO +12 -7
- {entrygraph-0.1.6 → entrygraph-0.1.8}/README.md +11 -6
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/_version.py +2 -2
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/api.py +67 -14
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/cli/main.py +1 -1
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/javascript.py +65 -0
- entrygraph-0.1.8/tests/fixtures/javascript/commonjs_app/server.js +15 -0
- entrygraph-0.1.8/tests/fixtures/python/sanitizer/app.py +12 -0
- entrygraph-0.1.8/tests/test_commonjs.py +41 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_javascript.py +26 -0
- entrygraph-0.1.8/tests/test_taint_sanitizers.py +57 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/.github/workflows/ci.yml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/.github/workflows/release.yml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/.gitignore +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/LICENSE +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/RELEASING.md +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/pyproject.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/__main__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/cli/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/cli/render.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/csharp.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/go.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/java.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/javascript.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/lib_python.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/php.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/python.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/ruby.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/data/sinks/rust.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/db/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/db/engine.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/db/meta.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/db/models.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/db/queries.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/base.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/configs.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/golang.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/java.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/php.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/python.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/entrypoints/rust.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/frameworks.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/manifests.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/detect/taint.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/errors.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/base.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/csharp.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/golang.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/ir.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/java.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/php.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/python.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/registry.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/ruby.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/extract/rust.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/fs/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/fs/hashing.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/fs/lang.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/fs/walker.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/graph/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/graph/adjacency.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/graph/cte.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/graph/scoring.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/kinds.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/parsing/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/parsing/parsers.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/parsing/queries.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/pipeline/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/pipeline/scanner.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/pipeline/worker.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/pipeline/writer.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/py.typed +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/csharp/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/csharp/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/csharp/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/go/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/go/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/go/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/java/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/java/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/java/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/javascript/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/javascript/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/javascript/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/php/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/php/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/php/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/python/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/python/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/python/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/ruby/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/ruby/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/ruby/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/rust/calls.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/rust/definitions.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/queries/rust/imports.scm +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/resolve/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/resolve/externals.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/resolve/hierarchy.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/resolve/resolver.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/resolve/symbol_table.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/src/entrygraph/results.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/conftest.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/go/gin_app/go.mod +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/go/gin_app/main.go +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/go/gin_app/service.go +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/java/spring_app/pom.xml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/javascript/express_app/package.json +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/javascript/express_app/src/services.js +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/php/laravel_app/artisan +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/php/laravel_app/composer.json +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/app/db.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/app/routes.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/app/services.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/cli.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/flask_app/requirements.txt +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/python/taint_source/handler.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_api.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_cli.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_engine_pragmas.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_entrypoint_expansion.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_entrypoints.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_csharp.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_go.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_java.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_php.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_python.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_ruby.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_extract_rust.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_frameworks.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_hardening.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_incremental.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_indexer.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_lang.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_manifests.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_models.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_pool.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_reachability.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_render.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_resolver.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_scoring.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_sink_catalog.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_taint.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_taint_sources.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/tests/test_walker.py +0 -0
- {entrygraph-0.1.6 → entrygraph-0.1.8}/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.8
|
|
4
4
|
Summary: Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index
|
|
5
5
|
Project-URL: Repository, https://github.com/brettbergin/entrygraph
|
|
6
6
|
Author-email: Brett Bergin <brettberginbc@yahoo.com>
|
|
@@ -202,8 +202,11 @@ entrygraph paths --source '*' --sink-category command_exec
|
|
|
202
202
|
`--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
|
|
203
203
|
- **Precision/recall dial**: by default only high-confidence edges are traversed.
|
|
204
204
|
Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
|
|
205
|
-
calls) or `--include-fuzzy` (speculative class-hierarchy edges)
|
|
206
|
-
|
|
205
|
+
calls) or `--include-fuzzy` (speculative class-hierarchy edges).
|
|
206
|
+
- **Sanitizers**: a registered sanitizer for the sink's category called on a
|
|
207
|
+
path (e.g. `shlex.quote`) *discounts* its risk score — heuristically, since
|
|
208
|
+
there is no dataflow, so it never zeroes the risk or hides the path.
|
|
209
|
+
`--prune-sanitized` opts into dropping those paths entirely.
|
|
207
210
|
- Target an exact sink with `--sink py:subprocess.run` instead of a category.
|
|
208
211
|
|
|
209
212
|
### `stats` & `--json`
|
|
@@ -294,7 +297,7 @@ graph.paths(source="app.routes.*", sink_category="sql",
|
|
|
294
297
|
graph.paths(source="app.routes.*", sink_category="command_exec",
|
|
295
298
|
include_fuzzy=True) # follow speculative class-hierarchy (CHA) edges
|
|
296
299
|
graph.paths(source="app.routes.*", sink_category="command_exec",
|
|
297
|
-
prune_sanitized=True) # drop paths
|
|
300
|
+
prune_sanitized=True) # drop paths where a shlex.quote etc. is called
|
|
298
301
|
|
|
299
302
|
# Incremental re-index (only changed/added/deleted files are reparsed)
|
|
300
303
|
graph.refresh()
|
|
@@ -335,9 +338,11 @@ results are safe to hold and trivial to serialize.
|
|
|
335
338
|
root with `[[sink]]` / `[[source]]` / `[[sanitizer]]` tables (same schema as
|
|
336
339
|
the built-in `data/sinks/*.toml`), or call
|
|
337
340
|
`entrygraph.detect.taint.register_sink(...)` / `register_sanitizer(...)`. A
|
|
338
|
-
`[[sanitizer]]`
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
`[[sanitizer]]` called on a path discounts its risk score for that category;
|
|
342
|
+
since reachability has no dataflow, the discount is capped (a match never
|
|
343
|
+
zeroes risk or hides a path — use `--prune-sanitized` to drop them explicitly).
|
|
344
|
+
Third-party wrapper libraries that reach a sink internally are covered by
|
|
345
|
+
`data/sinks/lib_*.toml`
|
|
341
346
|
"library summaries" (same schema, with a `library = "..."` tag).
|
|
342
347
|
- **New frameworks / entrypoints** — register a `FrameworkSpec` and an
|
|
343
348
|
`EntrypointRule`; adding a framework is usually a few lines.
|
|
@@ -151,8 +151,11 @@ entrygraph paths --source '*' --sink-category command_exec
|
|
|
151
151
|
`--source-category http_input` / `env`) — `entrygraph paths --source-category http_input --sink-category sql`. Combine with `--source` to union both.
|
|
152
152
|
- **Precision/recall dial**: by default only high-confidence edges are traversed.
|
|
153
153
|
Widen with `--include-unresolved` (wildcard `py:*.execute` sinks + dynamic
|
|
154
|
-
calls) or `--include-fuzzy` (speculative class-hierarchy edges)
|
|
155
|
-
|
|
154
|
+
calls) or `--include-fuzzy` (speculative class-hierarchy edges).
|
|
155
|
+
- **Sanitizers**: a registered sanitizer for the sink's category called on a
|
|
156
|
+
path (e.g. `shlex.quote`) *discounts* its risk score — heuristically, since
|
|
157
|
+
there is no dataflow, so it never zeroes the risk or hides the path.
|
|
158
|
+
`--prune-sanitized` opts into dropping those paths entirely.
|
|
156
159
|
- Target an exact sink with `--sink py:subprocess.run` instead of a category.
|
|
157
160
|
|
|
158
161
|
### `stats` & `--json`
|
|
@@ -243,7 +246,7 @@ graph.paths(source="app.routes.*", sink_category="sql",
|
|
|
243
246
|
graph.paths(source="app.routes.*", sink_category="command_exec",
|
|
244
247
|
include_fuzzy=True) # follow speculative class-hierarchy (CHA) edges
|
|
245
248
|
graph.paths(source="app.routes.*", sink_category="command_exec",
|
|
246
|
-
prune_sanitized=True) # drop paths
|
|
249
|
+
prune_sanitized=True) # drop paths where a shlex.quote etc. is called
|
|
247
250
|
|
|
248
251
|
# Incremental re-index (only changed/added/deleted files are reparsed)
|
|
249
252
|
graph.refresh()
|
|
@@ -284,9 +287,11 @@ results are safe to hold and trivial to serialize.
|
|
|
284
287
|
root with `[[sink]]` / `[[source]]` / `[[sanitizer]]` tables (same schema as
|
|
285
288
|
the built-in `data/sinks/*.toml`), or call
|
|
286
289
|
`entrygraph.detect.taint.register_sink(...)` / `register_sanitizer(...)`. A
|
|
287
|
-
`[[sanitizer]]`
|
|
288
|
-
|
|
289
|
-
|
|
290
|
+
`[[sanitizer]]` called on a path discounts its risk score for that category;
|
|
291
|
+
since reachability has no dataflow, the discount is capped (a match never
|
|
292
|
+
zeroes risk or hides a path — use `--prune-sanitized` to drop them explicitly).
|
|
293
|
+
Third-party wrapper libraries that reach a sink internally are covered by
|
|
294
|
+
`data/sinks/lib_*.toml`
|
|
290
295
|
"library summaries" (same schema, with a `library = "..."` tag).
|
|
291
296
|
- **New frameworks / entrypoints** — register a `FrameworkSpec` and an
|
|
292
297
|
`EntrypointRule`; adding a framework is usually a few lines.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.8'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 8)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -66,9 +66,13 @@ def _traversal_params(
|
|
|
66
66
|
return floor, include_fuzzy
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
70
|
-
"""True if
|
|
71
|
-
|
|
69
|
+
def _has_sanitizer(path: CallPath) -> bool:
|
|
70
|
+
"""True if a category sanitizer is called on the path (heuristic, no dataflow).
|
|
71
|
+
|
|
72
|
+
Drives ``--prune-sanitized``. Sanitizer matches only discount risk (never zero
|
|
73
|
+
it), so pruning is an explicit opt-in that trades recall for noise reduction
|
|
74
|
+
rather than a silent, certain "this path is safe" claim."""
|
|
75
|
+
return any(e.sanitized_by for e in path.edges)
|
|
72
76
|
|
|
73
77
|
|
|
74
78
|
class CodeGraph:
|
|
@@ -296,13 +300,20 @@ class CodeGraph:
|
|
|
296
300
|
edge_map = self._edge_rows(session, raw_paths)
|
|
297
301
|
tainted_sources = self._tainted_source_ids(session, sources)
|
|
298
302
|
excluded_nodes = self._nodes_with_open_frontier(session, all_ids, kinds, floor)
|
|
303
|
+
sibling_calls = self._out_call_qnames(session, all_ids)
|
|
299
304
|
built = [
|
|
300
305
|
self._materialize_path(
|
|
301
|
-
path,
|
|
306
|
+
path,
|
|
307
|
+
symbol_map,
|
|
308
|
+
edge_map,
|
|
309
|
+
registry,
|
|
310
|
+
tainted_sources,
|
|
311
|
+
excluded_nodes,
|
|
312
|
+
sibling_calls,
|
|
302
313
|
)
|
|
303
314
|
for path in raw_paths
|
|
304
315
|
]
|
|
305
|
-
results = [cp for cp in built if not (prune_sanitized and
|
|
316
|
+
results = [cp for cp in built if not (prune_sanitized and _has_sanitizer(cp))]
|
|
306
317
|
results.sort(
|
|
307
318
|
key=lambda p: (-(p.risk_score or 0.0), len(p.symbols), [s.id for s in p.symbols])
|
|
308
319
|
)
|
|
@@ -543,8 +554,38 @@ class CodeGraph:
|
|
|
543
554
|
).scalars()
|
|
544
555
|
return set(rows)
|
|
545
556
|
|
|
557
|
+
@staticmethod
|
|
558
|
+
def _out_call_qnames(session: Session, node_ids: set[int]) -> dict[int, list[str]]:
|
|
559
|
+
"""For each node, the callee qnames of its outgoing CALLS edges.
|
|
560
|
+
|
|
561
|
+
Used for sanitizer detection: a sanitizer is usually a *sibling* call of
|
|
562
|
+
an on-path function (`shlex.quote(x)` then `subprocess.run(...)`), not a
|
|
563
|
+
node on the source->sink path itself, so it never appears among the path's
|
|
564
|
+
own hops."""
|
|
565
|
+
if not node_ids:
|
|
566
|
+
return {}
|
|
567
|
+
from entrygraph.kinds import EdgeKind
|
|
568
|
+
|
|
569
|
+
rows = session.execute(
|
|
570
|
+
select(models.Edge.src_symbol_id, models.Edge.dst_qname).where(
|
|
571
|
+
models.Edge.src_symbol_id.in_(node_ids),
|
|
572
|
+
models.Edge.kind == EdgeKind.CALLS,
|
|
573
|
+
)
|
|
574
|
+
).all()
|
|
575
|
+
out: dict[int, list[str]] = {}
|
|
576
|
+
for sid, dst_qname in rows:
|
|
577
|
+
out.setdefault(sid, []).append(dst_qname)
|
|
578
|
+
return out
|
|
579
|
+
|
|
546
580
|
def _materialize_path(
|
|
547
|
-
self,
|
|
581
|
+
self,
|
|
582
|
+
path,
|
|
583
|
+
symbol_map,
|
|
584
|
+
edge_map,
|
|
585
|
+
registry,
|
|
586
|
+
tainted_sources,
|
|
587
|
+
excluded_nodes,
|
|
588
|
+
sibling_calls,
|
|
548
589
|
) -> CallPath:
|
|
549
590
|
symbols = tuple(symbol_map[node] for node, _ in path)
|
|
550
591
|
hops = [hop for _, hop in path[1:]]
|
|
@@ -556,10 +597,11 @@ class CodeGraph:
|
|
|
556
597
|
sink_category = registry.sinks[sink_id].category if sink_id in registry.sinks else None
|
|
557
598
|
terminal_const = is_constant_args(terminal.arg_preview) if terminal else False
|
|
558
599
|
|
|
559
|
-
# sanitizer detection:
|
|
560
|
-
#
|
|
600
|
+
# sanitizer detection: a sanitizer for the sink's category called by any
|
|
601
|
+
# on-path node (its own hops OR a sibling call)
|
|
602
|
+
sibling_qnames = {q for node, _ in path for q in sibling_calls.get(node, ())}
|
|
561
603
|
sanitized_ids, sanitized_effect = self._path_sanitizers(
|
|
562
|
-
symbols, rows, registry, sink_category
|
|
604
|
+
symbols, rows, sibling_qnames, registry, sink_category
|
|
563
605
|
)
|
|
564
606
|
|
|
565
607
|
path_edges: list[PathEdge] = []
|
|
@@ -593,17 +635,28 @@ class CodeGraph:
|
|
|
593
635
|
)
|
|
594
636
|
|
|
595
637
|
@staticmethod
|
|
596
|
-
def _path_sanitizers(symbols, rows, registry, sink_category):
|
|
597
|
-
"""Return (matched sanitizer ids,
|
|
638
|
+
def _path_sanitizers(symbols, rows, sibling_qnames, registry, sink_category):
|
|
639
|
+
"""Return (matched sanitizer ids, effect) for the sink category.
|
|
640
|
+
|
|
641
|
+
Candidates are every callee reachable from an on-path node: the path's own
|
|
642
|
+
hops plus sibling calls (`sibling_qnames`). The effect is capped at
|
|
643
|
+
``"reduces"`` regardless of the catalog's ``effect`` — with no dataflow we
|
|
644
|
+
cannot prove the sanitized value is the one reaching the sink, so a match
|
|
645
|
+
discounts risk but never drives it to zero (which would silently hide a
|
|
646
|
+
real path). ``--prune-sanitized`` is the explicit, heuristic opt-in to
|
|
647
|
+
drop these."""
|
|
598
648
|
if not sink_category:
|
|
599
649
|
return [], None
|
|
600
650
|
matched: list = []
|
|
601
|
-
candidates =
|
|
651
|
+
candidates = (
|
|
652
|
+
[s.qname for s in symbols]
|
|
653
|
+
+ [r.dst_qname for r in rows if r is not None]
|
|
654
|
+
+ list(sibling_qnames)
|
|
655
|
+
)
|
|
602
656
|
for qname in candidates:
|
|
603
657
|
for san in registry.match_sanitizers(qname):
|
|
604
658
|
if san.category == sink_category:
|
|
605
659
|
matched.append(san)
|
|
606
660
|
if not matched:
|
|
607
661
|
return [], None
|
|
608
|
-
|
|
609
|
-
return [s.id for s in matched], effect
|
|
662
|
+
return [s.id for s in matched], "reduces"
|
|
@@ -415,7 +415,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
415
415
|
"--prune-sanitized",
|
|
416
416
|
dest="prune_sanitized",
|
|
417
417
|
action="store_true",
|
|
418
|
-
help="drop paths
|
|
418
|
+
help="drop paths where a category sanitizer is called (heuristic, no dataflow)",
|
|
419
419
|
)
|
|
420
420
|
p.set_defaults(func=cmd_paths)
|
|
421
421
|
|
|
@@ -238,12 +238,77 @@ class JavaScriptExtractor:
|
|
|
238
238
|
)
|
|
239
239
|
for node in caps.get("export.from", []):
|
|
240
240
|
self._reexport(node, ctx, out)
|
|
241
|
+
for node in caps.get("require", []):
|
|
242
|
+
self._commonjs_require(node, ctx, out)
|
|
241
243
|
|
|
242
244
|
for imp in out.imports:
|
|
243
245
|
out.framework_signals.append(
|
|
244
246
|
("import", imp.module.split(".")[0] if "." in imp.module else imp.module)
|
|
245
247
|
)
|
|
246
248
|
|
|
249
|
+
def _commonjs_require(self, call, ctx, out) -> None:
|
|
250
|
+
"""CommonJS `require('m')` bindings -> RawImport, mirroring ESM handling.
|
|
251
|
+
|
|
252
|
+
``const cp = require('m')`` -> whole module bound to ``cp``
|
|
253
|
+
``const { x, y: z } = require('m')`` -> named imports ``x`` and ``y`` (as ``z``)
|
|
254
|
+
bare / chained / reassigned -> module-only (side-effect) import,
|
|
255
|
+
which still yields the framework signal even when no alias can be bound.
|
|
256
|
+
"""
|
|
257
|
+
args = call.child_by_field_name("arguments")
|
|
258
|
+
spec = None
|
|
259
|
+
if args is not None:
|
|
260
|
+
for child in args.named_children:
|
|
261
|
+
if child.type == "string":
|
|
262
|
+
spec = node_text(child).strip("'\"`")
|
|
263
|
+
break
|
|
264
|
+
if not spec:
|
|
265
|
+
return # dynamic require (variable/template arg) — target unknowable
|
|
266
|
+
module = self._resolve_module(spec, ctx)
|
|
267
|
+
span = span_of(call)
|
|
268
|
+
|
|
269
|
+
parent = call.parent
|
|
270
|
+
name_node = None
|
|
271
|
+
if parent is not None and parent.type == "variable_declarator":
|
|
272
|
+
name_node = parent.child_by_field_name("name")
|
|
273
|
+
elif parent is not None and parent.type == "assignment_expression":
|
|
274
|
+
name_node = parent.child_by_field_name("left")
|
|
275
|
+
|
|
276
|
+
if name_node is not None and name_node.type == "identifier":
|
|
277
|
+
# Bind the alias to the module itself (same as an ESM default import),
|
|
278
|
+
# so `const cp = require('child_process'); cp.exec()` canonicalizes to
|
|
279
|
+
# child_process.exec.
|
|
280
|
+
out.imports.append(
|
|
281
|
+
RawImport(module=module, imported_name=None, alias=node_text(name_node), span=span)
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
if name_node is not None and name_node.type == "object_pattern":
|
|
285
|
+
for child in name_node.named_children:
|
|
286
|
+
if child.type == "shorthand_property_identifier_pattern": # { exec }
|
|
287
|
+
nm = node_text(child)
|
|
288
|
+
out.imports.append(
|
|
289
|
+
RawImport(module=module, imported_name=nm, alias=nm, span=span)
|
|
290
|
+
)
|
|
291
|
+
elif child.type == "pair_pattern": # { exec: run }
|
|
292
|
+
key = child.child_by_field_name("key")
|
|
293
|
+
value = child.child_by_field_name("value")
|
|
294
|
+
if key is None:
|
|
295
|
+
continue
|
|
296
|
+
alias = (
|
|
297
|
+
node_text(value)
|
|
298
|
+
if value is not None and value.type == "identifier"
|
|
299
|
+
else node_text(key)
|
|
300
|
+
)
|
|
301
|
+
out.imports.append(
|
|
302
|
+
RawImport(
|
|
303
|
+
module=module, imported_name=node_text(key), alias=alias, span=span
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
return
|
|
307
|
+
# bare `require('m')`, `require('m').foo`, `module.exports = require('m')`, etc.
|
|
308
|
+
out.imports.append(
|
|
309
|
+
RawImport(module=module, imported_name=None, alias=module.split(".")[-1], span=span)
|
|
310
|
+
)
|
|
311
|
+
|
|
247
312
|
def _reexport(self, node, ctx, out) -> None:
|
|
248
313
|
"""`export { X as Y } from "./m"` / `export * from "./m"` -> RawReexport."""
|
|
249
314
|
source = node.child_by_field_name("source")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const cp = require('child_process');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const express = require('express');
|
|
4
|
+
|
|
5
|
+
function runDefault(input) {
|
|
6
|
+
cp.execSync(input);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function runNamed(input) {
|
|
10
|
+
exec(input);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const app = express();
|
|
14
|
+
app.get('/a', (req, res) => runDefault(req.query.cmd));
|
|
15
|
+
app.get('/b', (req, res) => runNamed(req.query.cmd));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sanitized(cmd):
|
|
6
|
+
# shlex.quote is a registered command_exec sanitizer, called on the same
|
|
7
|
+
# function as the sink (a sibling call, not a node on the source->sink path).
|
|
8
|
+
subprocess.run(shlex.quote(cmd))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unsanitized(cmd):
|
|
12
|
+
subprocess.run(cmd)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""End-to-end CommonJS `require()` support (Phase 1.3).
|
|
2
|
+
|
|
3
|
+
Regression: the JS extractor dropped the `require()` captures entirely, so a
|
|
4
|
+
pure-CommonJS app produced no imports — no framework detection and no
|
|
5
|
+
canonicalization of `cp.execSync` to a `child_process` sink.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from entrygraph import CodeGraph
|
|
15
|
+
|
|
16
|
+
COMMONJS_APP = Path(__file__).parent / "fixtures" / "javascript" / "commonjs_app"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="module")
|
|
20
|
+
def graph(tmp_path_factory) -> CodeGraph:
|
|
21
|
+
db = tmp_path_factory.mktemp("db") / "graph.db"
|
|
22
|
+
g = CodeGraph.index(COMMONJS_APP, db=db)
|
|
23
|
+
yield g
|
|
24
|
+
g.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_commonjs_require_enables_framework_detection(graph):
|
|
28
|
+
names = {f.name for f in graph.detect().frameworks}
|
|
29
|
+
assert "express" in names
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_commonjs_require_canonicalizes_command_sinks(graph):
|
|
33
|
+
# both the default-bound (cp.execSync) and destructured (exec) forms must
|
|
34
|
+
# resolve to child_process command_exec sinks
|
|
35
|
+
assert graph.stats().sink_edges >= 2
|
|
36
|
+
sinks = {
|
|
37
|
+
row["dst_qname"]
|
|
38
|
+
for row in graph.sql("SELECT dst_qname FROM edges WHERE sink_id IS NOT NULL")
|
|
39
|
+
}
|
|
40
|
+
assert "js:child_process.execSync" in sinks
|
|
41
|
+
assert "js:child_process.exec" in sinks
|
|
@@ -72,6 +72,32 @@ def test_relative_import_resolution():
|
|
|
72
72
|
assert any(i.module == "db.conn" for i in x.imports)
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def test_commonjs_require_bindings():
|
|
76
|
+
x = extract(
|
|
77
|
+
"const cp = require('child_process');\n"
|
|
78
|
+
"const { exec, spawn: launch } = require('child_process');\n"
|
|
79
|
+
"const express = require('express');\n"
|
|
80
|
+
"require('./side-effect');\n"
|
|
81
|
+
"const rel = require('../db/conn');\n",
|
|
82
|
+
path="src/routes/users.js",
|
|
83
|
+
)
|
|
84
|
+
imports = {(i.module, i.imported_name, i.alias) for i in x.imports}
|
|
85
|
+
assert ("child_process", None, "cp") in imports # default -> module binding
|
|
86
|
+
assert ("child_process", "exec", "exec") in imports # destructured
|
|
87
|
+
assert ("child_process", "spawn", "launch") in imports # destructured + rename
|
|
88
|
+
assert ("express", None, "express") in imports
|
|
89
|
+
assert ("db.conn", None, "rel") in imports # relative resolved to dotted, bound to `rel`
|
|
90
|
+
# side-effect require still registers the module (framework signal source)
|
|
91
|
+
assert any(m == "routes.side-effect" and n is None for m, n, _ in imports)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_commonjs_require_emits_framework_signal():
|
|
95
|
+
# regression: CommonJS `require('express')` used to yield no import at all,
|
|
96
|
+
# so express was undetectable in pure-CJS apps.
|
|
97
|
+
x = extract("const express = require('express');\n")
|
|
98
|
+
assert ("import", "express") in x.framework_signals
|
|
99
|
+
|
|
100
|
+
|
|
75
101
|
def test_calls_with_receivers():
|
|
76
102
|
x = extract(
|
|
77
103
|
"import cp from 'child_process';\nfunction run() { cp.execSync('ls'); helper(); }\n"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Sanitizer detection over sibling call edges (Phase 1.2).
|
|
2
|
+
|
|
3
|
+
Regression: builtin sanitizers (external qnames like `py:shlex.quote`) could
|
|
4
|
+
never fire — detection only looked at nodes on the source->sink path, but a
|
|
5
|
+
sanitizer is a *sibling* call of an on-path function. And a heuristic match must
|
|
6
|
+
only discount risk, never drive it to zero (which would hide a real path).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from entrygraph import CodeGraph
|
|
16
|
+
|
|
17
|
+
SANITIZER_APP = Path(__file__).parent / "fixtures" / "python" / "sanitizer"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(scope="module")
|
|
21
|
+
def graph(tmp_path_factory) -> CodeGraph:
|
|
22
|
+
db = tmp_path_factory.mktemp("db") / "graph.db"
|
|
23
|
+
g = CodeGraph.index(SANITIZER_APP, db=db)
|
|
24
|
+
yield g
|
|
25
|
+
g.close()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _path(graph, source):
|
|
29
|
+
paths = graph.paths(source=source, sink_category="command_exec")
|
|
30
|
+
assert paths, f"expected a command_exec path from {source}"
|
|
31
|
+
return paths[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_sibling_sanitizer_is_detected(graph):
|
|
35
|
+
# shlex.quote is a sibling call of app.sanitized, not a node on the path
|
|
36
|
+
path = _path(graph, "app.sanitized")
|
|
37
|
+
assert any("py.sanitize.shlex" in e.sanitized_by for e in path.edges)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_unsanitized_path_has_no_sanitizer(graph):
|
|
41
|
+
path = _path(graph, "app.unsanitized")
|
|
42
|
+
assert all(not e.sanitized_by for e in path.edges)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_sanitizer_discounts_but_does_not_zero_risk(graph):
|
|
46
|
+
# the sanitized path must still carry meaningful (nonzero) risk, and be
|
|
47
|
+
# strictly lower-risk than the identical unsanitized path
|
|
48
|
+
sanitized = _path(graph, "app.sanitized")
|
|
49
|
+
unsanitized = _path(graph, "app.unsanitized")
|
|
50
|
+
assert 0.0 < sanitized.risk_score < unsanitized.risk_score
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_prune_sanitized_drops_only_sanitized_paths(graph):
|
|
54
|
+
kept = graph.paths(source="app.*", sink_category="command_exec", prune_sanitized=True)
|
|
55
|
+
sources = {p.symbols[0].qname for p in kept}
|
|
56
|
+
assert "app.unsanitized" in sources
|
|
57
|
+
assert "app.sanitized" not in sources
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.6 → entrygraph-0.1.8}/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
|
|
File without changes
|
|
File without changes
|