entrygraph 0.1.7__tar.gz → 0.1.9__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.7 → entrygraph-0.1.9}/PKG-INFO +1 -1
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/_version.py +2 -2
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/javascript.py +65 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/scanner.py +3 -1
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/resolver.py +29 -8
- entrygraph-0.1.9/tests/fixtures/javascript/commonjs_app/server.js +15 -0
- entrygraph-0.1.9/tests/fixtures/python/fuzzy_sink/app.py +14 -0
- entrygraph-0.1.9/tests/test_commonjs.py +41 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_javascript.py +26 -0
- entrygraph-0.1.9/tests/test_fuzzy_sink.py +36 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_resolver.py +32 -2
- {entrygraph-0.1.7 → entrygraph-0.1.9}/.github/workflows/ci.yml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/.github/workflows/release.yml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/.gitignore +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/LICENSE +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/README.md +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/RELEASING.md +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/pyproject.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/__main__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/api.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/main.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/cli/render.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/csharp.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/go.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/java.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/javascript.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/lib_python.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/php.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/python.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/ruby.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/data/sinks/rust.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/engine.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/meta.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/models.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/db/queries.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/base.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/configs.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/golang.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/java.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/php.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/python.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/entrypoints/rust.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/frameworks.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/manifests.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/detect/taint.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/errors.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/base.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/csharp.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/golang.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/ir.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/java.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/php.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/python.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/registry.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/ruby.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/extract/rust.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/hashing.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/lang.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/fs/walker.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/adjacency.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/cte.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/graph/scoring.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/kinds.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/parsers.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/parsing/queries.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/worker.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/pipeline/writer.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/py.typed +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/csharp/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/go/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/java/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/javascript/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/php/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/python/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/ruby/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/calls.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/definitions.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/queries/rust/imports.scm +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/externals.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/hierarchy.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/resolve/symbol_table.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/src/entrygraph/results.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/conftest.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/go.mod +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/main.go +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/go/gin_app/service.go +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/pom.xml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/package.json +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/javascript/express_app/src/services.js +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/artisan +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/composer.json +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/db.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/routes.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/app/services.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/cli.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/flask_app/requirements.txt +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/sanitizer/app.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/python/taint_source/handler.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_api.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_cli.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_engine_pragmas.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_entrypoint_expansion.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_entrypoints.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_csharp.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_go.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_java.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_php.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_python.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_ruby.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_extract_rust.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_frameworks.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_hardening.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_incremental.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_indexer.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_lang.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_manifests.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_models.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_pool.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_reachability.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_render.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_scoring.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_sink_catalog.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint_sanitizers.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_taint_sources.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/tests/test_walker.py +0 -0
- {entrygraph-0.1.7 → entrygraph-0.1.9}/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.9
|
|
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>
|
|
@@ -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.9'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 9)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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")
|
|
@@ -432,7 +432,9 @@ def _write_edges_and_entrypoints(
|
|
|
432
432
|
entrypoint_rows: list[dict] = []
|
|
433
433
|
|
|
434
434
|
for path, x, is_package in extractions:
|
|
435
|
-
resolver = FileResolver(
|
|
435
|
+
resolver = FileResolver(
|
|
436
|
+
x, module_ids[path], table, externals, is_package, sink_registry=sink_registry
|
|
437
|
+
)
|
|
436
438
|
file_id = file_id_by_path[path]
|
|
437
439
|
for edge in resolver.resolve():
|
|
438
440
|
is_call = edge.kind is EdgeKind.CALLS
|
|
@@ -17,6 +17,7 @@ incremental re-resolution can heal them when the target appears.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
20
21
|
|
|
21
22
|
from entrygraph.extract.ir import FileExtraction, RawReference
|
|
22
23
|
from entrygraph.kinds import Confidence, EdgeKind, SymbolKind
|
|
@@ -29,6 +30,9 @@ from entrygraph.resolve.hierarchy import (
|
|
|
29
30
|
)
|
|
30
31
|
from entrygraph.resolve.symbol_table import SymbolTable
|
|
31
32
|
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from entrygraph.detect.taint import SinkRegistry
|
|
35
|
+
|
|
32
36
|
# expand_relative re-exported for backward compatibility with callers/tests.
|
|
33
37
|
__all__ = ["FileResolver", "ResolvedEdge", "expand_relative"]
|
|
34
38
|
|
|
@@ -66,12 +70,14 @@ class FileResolver:
|
|
|
66
70
|
table: SymbolTable,
|
|
67
71
|
externals: ExternalRegistry,
|
|
68
72
|
is_package: bool = False,
|
|
73
|
+
sink_registry: SinkRegistry | None = None,
|
|
69
74
|
) -> None:
|
|
70
75
|
self.x = extraction
|
|
71
76
|
self.module_symbol_id = module_symbol_id
|
|
72
77
|
self.table = table
|
|
73
78
|
self.externals = externals
|
|
74
79
|
self.is_package = is_package
|
|
80
|
+
self.sink_registry = sink_registry
|
|
75
81
|
self.prefix = LANG_PREFIX.get(extraction.language, extraction.language)
|
|
76
82
|
self.import_map, self.wildcard_modules = build_import_map(extraction, is_package)
|
|
77
83
|
|
|
@@ -217,6 +223,14 @@ class FileResolver:
|
|
|
217
223
|
return self.table.by_fqn[target]
|
|
218
224
|
return self.table.unique_by_name(name, (SymbolKind.FUNCTION, SymbolKind.METHOD))
|
|
219
225
|
|
|
226
|
+
def _guess_is_sink(self, ref: RawReference) -> bool:
|
|
227
|
+
"""Whether the receiver-agnostic guess for this attribute call matches a
|
|
228
|
+
registered sink — checked with the same call the index-time stamper uses."""
|
|
229
|
+
if self.sink_registry is None:
|
|
230
|
+
return False
|
|
231
|
+
guess = f"{self.prefix}:*.{ref.callee_name}"
|
|
232
|
+
return self.sink_registry.match(guess, ref.arg_preview) is not None
|
|
233
|
+
|
|
220
234
|
def _bind(self, ref: RawReference) -> tuple[int | None, str, Confidence, str | None]:
|
|
221
235
|
# 1. import-map expansion (chase re-export chains for project targets)
|
|
222
236
|
first_seg = ref.callee_text.split(".", 1)[0].split("(", 1)[0]
|
|
@@ -270,14 +284,21 @@ class FileResolver:
|
|
|
270
284
|
# where runner is a local variable) this is the only way to recover the
|
|
271
285
|
# call-graph edge without local type inference — it's the documented
|
|
272
286
|
# FUZZY tradeoff.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
if
|
|
280
|
-
|
|
287
|
+
#
|
|
288
|
+
# But if the receiver-agnostic guess (`py:*.execute`) is itself a
|
|
289
|
+
# registered sink, do NOT fuzzy-bind: rewriting dst_qname to a unique
|
|
290
|
+
# project method named `execute` would erase the sink stamp and hide the
|
|
291
|
+
# vulnerability. Prefer the guess so the sink survives (step 5); CHA still
|
|
292
|
+
# adds the project method as a via="cha" candidate, so no edge is lost.
|
|
293
|
+
if not (ref.receiver_text is not None and self._guess_is_sink(ref)):
|
|
294
|
+
kinds = (
|
|
295
|
+
(SymbolKind.METHOD,)
|
|
296
|
+
if ref.receiver_text is not None
|
|
297
|
+
else (SymbolKind.FUNCTION, SymbolKind.CLASS)
|
|
298
|
+
)
|
|
299
|
+
dst_id = self.table.unique_by_name(ref.callee_name, kinds)
|
|
300
|
+
if dst_id is not None:
|
|
301
|
+
return dst_id, self.table.qname_of[dst_id], Confidence.FUZZY, None
|
|
281
302
|
|
|
282
303
|
# 5. unresolved: language-prefixed guess, still a real node for sinks
|
|
283
304
|
if ref.receiver_text is not None:
|
|
@@ -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,14 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Runner:
|
|
5
|
+
# the ONLY project method named "execute" — the collision that used to
|
|
6
|
+
# swallow cursor.execute via unique-name fuzzy binding
|
|
7
|
+
def execute(self):
|
|
8
|
+
return 1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def query(db_path, user_id):
|
|
12
|
+
conn = sqlite3.connect(db_path)
|
|
13
|
+
cur = conn.cursor()
|
|
14
|
+
cur.execute("SELECT * FROM users WHERE id = " + user_id)
|
|
@@ -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,36 @@
|
|
|
1
|
+
"""Sink preservation vs unique-name fuzzy binding (Phase 1.4).
|
|
2
|
+
|
|
3
|
+
Regression: an unknown-receiver call like `cursor.execute(sql)` fuzzy-bound to a
|
|
4
|
+
unique project method named `execute`, rewriting dst_qname so the `py:*.execute`
|
|
5
|
+
SQL sink never got stamped — the vulnerability silently vanished from the graph.
|
|
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
|
+
FUZZY_SINK_APP = Path(__file__).parent / "fixtures" / "python" / "fuzzy_sink"
|
|
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(FUZZY_SINK_APP, db=db)
|
|
23
|
+
yield g
|
|
24
|
+
g.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_sql_sink_survives_unique_name_collision(graph):
|
|
28
|
+
sinks = graph.sql("SELECT dst_qname, sink_id FROM edges WHERE sink_id IS NOT NULL")
|
|
29
|
+
by_qname = {r["dst_qname"]: r["sink_id"] for r in sinks}
|
|
30
|
+
assert by_qname.get("py:*.execute") == "py.sql-execute"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_sql_path_reachable_despite_collision(graph):
|
|
34
|
+
paths = graph.paths(source="app.query", sink_category="sql", include_unresolved=True)
|
|
35
|
+
assert paths
|
|
36
|
+
assert paths[0].symbols[-1].qname == "py:*.execute"
|
|
@@ -20,7 +20,7 @@ def make_table() -> SymbolTable:
|
|
|
20
20
|
return table
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2):
|
|
23
|
+
def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2, sink_registry=None):
|
|
24
24
|
x = FileExtraction(
|
|
25
25
|
path="app/routes.py",
|
|
26
26
|
language="python",
|
|
@@ -31,7 +31,7 @@ def make_resolver(table, refs=(), imports=(), module="app.routes", module_id=2):
|
|
|
31
31
|
references=list(refs),
|
|
32
32
|
)
|
|
33
33
|
externals = ExternalRegistry(iter(range(100, 200)).__next__)
|
|
34
|
-
return FileResolver(x, module_id, table, externals), externals
|
|
34
|
+
return FileResolver(x, module_id, table, externals, sink_registry=sink_registry), externals
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def ref(callee_text, callee_name=None, receiver=None, caller=None, kind="call"):
|
|
@@ -180,6 +180,36 @@ def test_unresolved_gets_prefixed_placeholder():
|
|
|
180
180
|
assert by_qname["py:eval"].confidence is Confidence.UNRESOLVED
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def test_sink_named_attribute_call_is_not_fuzzy_bound():
|
|
184
|
+
# `cursor.execute(...)` guesses to the sink `py:*.execute`. Even though a
|
|
185
|
+
# unique project method `Runner.execute` (id 12) exists, we must NOT fuzzy-bind
|
|
186
|
+
# to it — that would rewrite dst_qname and erase the SQL sink. Keep the guess.
|
|
187
|
+
from entrygraph.detect.taint import builtin_registry
|
|
188
|
+
|
|
189
|
+
table = make_table()
|
|
190
|
+
resolver, _ = make_resolver(
|
|
191
|
+
table, [ref("cursor.execute", receiver="cursor")], sink_registry=builtin_registry()
|
|
192
|
+
)
|
|
193
|
+
call = next(e for e in resolver.resolve() if e.kind is EdgeKind.CALLS and e.via != "cha")
|
|
194
|
+
assert call.dst_qname == "py:*.execute"
|
|
195
|
+
assert call.confidence is Confidence.UNRESOLVED
|
|
196
|
+
assert call.dst_symbol_id != 12
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_non_sink_attribute_call_still_fuzzy_binds_with_registry():
|
|
200
|
+
# `obj.render` is not a sink, so unique-name fuzzy binding still applies even
|
|
201
|
+
# with a sink registry present — the fix is targeted to sink names only.
|
|
202
|
+
from entrygraph.detect.taint import builtin_registry
|
|
203
|
+
|
|
204
|
+
table = make_table()
|
|
205
|
+
resolver, _ = make_resolver(
|
|
206
|
+
table, [ref("obj.render", receiver="obj")], sink_registry=builtin_registry()
|
|
207
|
+
)
|
|
208
|
+
call = next(e for e in resolver.resolve() if e.kind is EdgeKind.CALLS)
|
|
209
|
+
assert call.dst_symbol_id == 13 # Runner.render
|
|
210
|
+
assert call.confidence is Confidence.FUZZY
|
|
211
|
+
|
|
212
|
+
|
|
183
213
|
def test_relative_import_expansion():
|
|
184
214
|
table = make_table()
|
|
185
215
|
table.add_symbol(40, "app.utils", 40 and "utils", SymbolKind.MODULE)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.7 → entrygraph-0.1.9}/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
|
|
File without changes
|