apisec-code-bolt 0.1.8__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.
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/PKG-INFO +43 -1
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/README.md +42 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/pyproject.toml +1 -1
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/analyzer.py +86 -25
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/data_flow.py +12 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/telemetry.py +2 -1
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +218 -18
- apisec_code_bolt-0.1.9/tests/unit/test_entry_point_interface_resolution.py +149 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_rails_plugin.py +597 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/.gitignore +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__main__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/dependency_extractor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/main.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/apisec_client.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/client.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/config.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/credentials.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/discovery.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/log_format.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/manifest.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/repo.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/state.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/types.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/base.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/base.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/services.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/ast_cache.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/executor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/handlers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/manifest_context.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/conftest.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Cargo.toml +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Gemfile.lock +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Pipfile +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle.kts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/go.mod +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/package.json +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pom.xml +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements.txt +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.cfg +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/urls.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/views.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/app.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/auth.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/users.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/main.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask/app.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_grape/api.rb +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_sinatra/app.rb +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/pom.xml +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/src/main/java/com/demo/DemoController.java +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_cli.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_django_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_express_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_fastapi_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_flask_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_nestjs_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_spring_boot_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/fixtures/sample_app.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_auth_indicator_matching.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_executor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_handlers.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_read_site_origin_types.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/__init__.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_apisec_client.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_audit.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_run.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_call_graph.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cli_console_url.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_config.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_config_scanner_framework.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_literals.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_type_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_data_flow.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_dependency_parsing.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_discovery.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_django_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_dynamic_routes.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_express_nestjs_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_fastapi_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_fastify_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_grape_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_instrumentation.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_integration_detection.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_java_parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_js_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_jwt_config_extraction.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_manifest_upload_gzip.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_path_resolver.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_pydantic_body_inference.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_python_parser.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_repo_canonical.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_router_registry.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_ruby_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sensitivity_capability.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sinatra_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_static_route_catchup.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_enrichment.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_extraction_client.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_telemetry.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_transformation_tracking.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_types.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_upload_verify_timeout.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_wcf_plugin.py +0 -0
- {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_webforms_plugin.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apisec-code-bolt
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Static analysis probe for extracting architectural metadata from codebases
|
|
5
5
|
Project-URL: Homepage, https://apisec.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.apisec.ai/code-bolt
|
|
@@ -349,6 +349,48 @@ ruff check .
|
|
|
349
349
|
ruff format --check .
|
|
350
350
|
```
|
|
351
351
|
|
|
352
|
+
### Publishing a release (maintainers)
|
|
353
|
+
|
|
354
|
+
The CLI is published to [PyPI](https://pypi.org/project/apisec-code-bolt/) by the
|
|
355
|
+
**Publish to PyPI** GitHub Action. **Merging to `build` does NOT publish** — the
|
|
356
|
+
workflow only runs on `workflow_dispatch` or a published GitHub Release, and it
|
|
357
|
+
builds from whatever ref it runs on.
|
|
358
|
+
|
|
359
|
+
Prerequisites (one-time): the `PYPI_API_TOKEN` repository secret must be set
|
|
360
|
+
(Settings → Secrets and variables → Actions).
|
|
361
|
+
|
|
362
|
+
1. **Bump the version.** Edit `version` in `pyproject.toml` (single source of
|
|
363
|
+
truth; `--version` reads it via package metadata). PyPI **rejects re-uploads**
|
|
364
|
+
of an existing version, so the number must be higher than the current PyPI
|
|
365
|
+
release. Open a PR and merge it to `build`.
|
|
366
|
+
|
|
367
|
+
2. **Publish** — dispatch the workflow (the standard path we use):
|
|
368
|
+
Actions → *Publish to PyPI* → **Run workflow** → select branch **`build`**.
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
gh workflow run "Publish to PyPI" --ref build
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
<details><summary>Alternative: publish via a GitHub Release</summary>
|
|
375
|
+
|
|
376
|
+
New release → create tag `vX.Y.Z` → set **Target: `build`** (it defaults to
|
|
377
|
+
the default branch; only `build` has the bumped version) → Publish. This
|
|
378
|
+
fires the same workflow via `release: [published]`.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
|
|
382
|
+
```
|
|
383
|
+
</details>
|
|
384
|
+
|
|
385
|
+
3. **Verify.** Confirm the new version appears on
|
|
386
|
+
[PyPI](https://pypi.org/project/apisec-code-bolt/), then upgrade an install:
|
|
387
|
+
```bash
|
|
388
|
+
uv tool upgrade apisec-code-bolt # or: pipx upgrade apisec-code-bolt
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
> Always release from `build` after the version bump has merged there — releasing
|
|
392
|
+
> from a ref that still carries the old version will fail the PyPI upload.
|
|
393
|
+
|
|
352
394
|
## Privacy
|
|
353
395
|
|
|
354
396
|
apisec-code-bolt is designed with privacy as a core principle:
|
|
@@ -302,6 +302,48 @@ ruff check .
|
|
|
302
302
|
ruff format --check .
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
+
### Publishing a release (maintainers)
|
|
306
|
+
|
|
307
|
+
The CLI is published to [PyPI](https://pypi.org/project/apisec-code-bolt/) by the
|
|
308
|
+
**Publish to PyPI** GitHub Action. **Merging to `build` does NOT publish** — the
|
|
309
|
+
workflow only runs on `workflow_dispatch` or a published GitHub Release, and it
|
|
310
|
+
builds from whatever ref it runs on.
|
|
311
|
+
|
|
312
|
+
Prerequisites (one-time): the `PYPI_API_TOKEN` repository secret must be set
|
|
313
|
+
(Settings → Secrets and variables → Actions).
|
|
314
|
+
|
|
315
|
+
1. **Bump the version.** Edit `version` in `pyproject.toml` (single source of
|
|
316
|
+
truth; `--version` reads it via package metadata). PyPI **rejects re-uploads**
|
|
317
|
+
of an existing version, so the number must be higher than the current PyPI
|
|
318
|
+
release. Open a PR and merge it to `build`.
|
|
319
|
+
|
|
320
|
+
2. **Publish** — dispatch the workflow (the standard path we use):
|
|
321
|
+
Actions → *Publish to PyPI* → **Run workflow** → select branch **`build`**.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
gh workflow run "Publish to PyPI" --ref build
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
<details><summary>Alternative: publish via a GitHub Release</summary>
|
|
328
|
+
|
|
329
|
+
New release → create tag `vX.Y.Z` → set **Target: `build`** (it defaults to
|
|
330
|
+
the default branch; only `build` has the bumped version) → Publish. This
|
|
331
|
+
fires the same workflow via `release: [published]`.
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
|
|
335
|
+
```
|
|
336
|
+
</details>
|
|
337
|
+
|
|
338
|
+
3. **Verify.** Confirm the new version appears on
|
|
339
|
+
[PyPI](https://pypi.org/project/apisec-code-bolt/), then upgrade an install:
|
|
340
|
+
```bash
|
|
341
|
+
uv tool upgrade apisec-code-bolt # or: pipx upgrade apisec-code-bolt
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
> Always release from `build` after the version bump has merged there — releasing
|
|
345
|
+
> from a ref that still carries the old version will fail the PyPI upload.
|
|
346
|
+
|
|
305
347
|
## Privacy
|
|
306
348
|
|
|
307
349
|
apisec-code-bolt is designed with privacy as a core principle:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apisec-code-bolt"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Static analysis probe for extracting architectural metadata from codebases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Proprietary"}
|
|
@@ -2234,46 +2234,107 @@ class ProjectAnalyzer:
|
|
|
2234
2234
|
Resolve handler qualified names to call graph symbols.
|
|
2235
2235
|
|
|
2236
2236
|
Handles mismatches between route extraction and call graph naming
|
|
2237
|
-
(e.g., module path variations)
|
|
2237
|
+
(e.g., module path variations), and the interface-handler pattern where
|
|
2238
|
+
the route annotations live on an interface method but the body lives in
|
|
2239
|
+
an implementing class (e.g. Micronaut operation interfaces: routes are
|
|
2240
|
+
declared on ``*Operations`` interfaces and the ``@Controller`` class
|
|
2241
|
+
implements them). Taint must seed from the concrete implementation —
|
|
2242
|
+
the interface method has no body, so a manifest handler pointing at it
|
|
2243
|
+
yields zero data flows.
|
|
2238
2244
|
"""
|
|
2239
2245
|
if not self._call_graph:
|
|
2240
2246
|
return entry_points
|
|
2241
2247
|
|
|
2242
|
-
|
|
2243
|
-
|
|
2248
|
+
cg = self._call_graph
|
|
2249
|
+
symbols = set(cg._symbols.keys())
|
|
2250
|
+
|
|
2251
|
+
# interface simple-name -> {implementing class qualified names}. Sourced
|
|
2252
|
+
# from parsed classes (base_classes includes Java `implements`); the
|
|
2253
|
+
# built CallGraph does not expose the builder's class-base map.
|
|
2254
|
+
iface_impls: dict[str, set[str]] = {}
|
|
2255
|
+
for pf in self._parsed_files.values():
|
|
2256
|
+
if not getattr(pf, "success", False):
|
|
2257
|
+
continue
|
|
2258
|
+
for cls in getattr(pf, "classes", None) or []:
|
|
2259
|
+
try:
|
|
2260
|
+
cls_qname = cls.qualified_name.full
|
|
2261
|
+
except AttributeError:
|
|
2262
|
+
continue
|
|
2263
|
+
for base in getattr(cls, "base_classes", None) or []:
|
|
2264
|
+
iface_impls.setdefault(base.rsplit(".", 1)[-1], set()).add(cls_qname)
|
|
2265
|
+
|
|
2266
|
+
def _redirect_to_impl(handler_qn: str) -> str:
|
|
2267
|
+
"""If handler is declared on an interface a concrete class implements,
|
|
2268
|
+
redirect to that class's method (where the body/params live)."""
|
|
2269
|
+
if "." not in handler_qn:
|
|
2270
|
+
return handler_qn
|
|
2271
|
+
owner, method = handler_qn.rsplit(".", 1)
|
|
2272
|
+
impls = iface_impls.get(owner.rsplit(".", 1)[-1])
|
|
2273
|
+
if not impls:
|
|
2274
|
+
return handler_qn
|
|
2275
|
+
fallback = None
|
|
2276
|
+
for impl_cls in impls:
|
|
2277
|
+
cand = f"{impl_cls}.{method}"
|
|
2278
|
+
if cand == handler_qn or cand not in symbols:
|
|
2279
|
+
continue
|
|
2280
|
+
node = cg.get_node(cand)
|
|
2281
|
+
# Prefer a concrete implementation with a body (call edges) or one
|
|
2282
|
+
# not flagged abstract; keep the interface handler otherwise.
|
|
2283
|
+
if cg.get_callees(cand) or not getattr(node, "is_abstract", False):
|
|
2284
|
+
return cand
|
|
2285
|
+
fallback = fallback or cand
|
|
2286
|
+
return fallback or handler_qn
|
|
2244
2287
|
|
|
2288
|
+
resolved = []
|
|
2245
2289
|
for ep in entry_points:
|
|
2246
2290
|
handler = ep.get("handler_qualified_name")
|
|
2247
2291
|
if not handler:
|
|
2248
2292
|
resolved.append(ep)
|
|
2249
2293
|
continue
|
|
2250
2294
|
|
|
2251
|
-
if handler in symbols
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2295
|
+
chosen = handler if handler in symbols else None
|
|
2296
|
+
if chosen is None:
|
|
2297
|
+
# Try common variations (module-path prefix stripping, __init__).
|
|
2298
|
+
candidates = [handler, handler.replace(".__init__", "")]
|
|
2299
|
+
if "." in handler:
|
|
2300
|
+
parts = handler.split(".")
|
|
2301
|
+
candidates += [".".join(parts[i:]) for i in range(1, len(parts))]
|
|
2302
|
+
for cand in candidates:
|
|
2303
|
+
if cand in symbols:
|
|
2304
|
+
chosen = cand
|
|
2305
|
+
break
|
|
2306
|
+
# Suffix match on the handler tail. Some plugins record the
|
|
2307
|
+
# handler module as the *routing* file rather than the view's
|
|
2308
|
+
# module (e.g. Django: `urls.UserSessionLoginApi.post` or the
|
|
2309
|
+
# function-based `api_router.favorite_post`, while the symbols
|
|
2310
|
+
# are `app.authentication.apis.UserSessionLoginApi.post` and
|
|
2311
|
+
# `app.api.views.favorite_post`). Prefix stripping can't bridge
|
|
2312
|
+
# that. Try the `Class.method` tail first, then the bare name,
|
|
2313
|
+
# accepting a match only when it is unambiguous.
|
|
2314
|
+
if chosen is None and "." in handler:
|
|
2315
|
+
parts = handler.split(".")
|
|
2316
|
+
for depth in (2, 1):
|
|
2317
|
+
if depth > len(parts):
|
|
2318
|
+
continue
|
|
2319
|
+
tail = "." + ".".join(parts[-depth:])
|
|
2320
|
+
tail_matches = [s for s in symbols if s.endswith(tail)]
|
|
2321
|
+
if len(tail_matches) == 1:
|
|
2322
|
+
chosen = tail_matches[0]
|
|
2323
|
+
break
|
|
2324
|
+
chosen = chosen or handler
|
|
2325
|
+
|
|
2326
|
+
# Interface-handler → concrete implementation (Micronaut operation
|
|
2327
|
+
# interfaces, and any handler declared on an implemented interface).
|
|
2328
|
+
redirected = _redirect_to_impl(chosen)
|
|
2329
|
+
if redirected != handler:
|
|
2330
|
+
ep = dict(ep)
|
|
2331
|
+
ep["handler_qualified_name"] = redirected
|
|
2273
2332
|
resolved.append(ep)
|
|
2274
2333
|
|
|
2275
2334
|
return resolved
|
|
2276
2335
|
|
|
2336
|
+
return resolved
|
|
2337
|
+
|
|
2277
2338
|
# =========================================================================
|
|
2278
2339
|
# Integration Detection
|
|
2279
2340
|
# =========================================================================
|
{apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/data_flow.py
RENAMED
|
@@ -1646,6 +1646,18 @@ class DataFlowAnalyzer:
|
|
|
1646
1646
|
if pname and ploc:
|
|
1647
1647
|
param_locations[pname] = ploc
|
|
1648
1648
|
|
|
1649
|
+
# Django/DRF views receive all user input via the `request` object
|
|
1650
|
+
# (request.data / .GET / .POST / .query_params) rather than typed
|
|
1651
|
+
# per-field route params, so there is nothing in param_locations to
|
|
1652
|
+
# seed. Treat the handler's `request` parameter itself as an HTTP
|
|
1653
|
+
# origin and let propagation carry it into request.* reads, serializer
|
|
1654
|
+
# construction, and downstream sinks.
|
|
1655
|
+
if framework.startswith("django"):
|
|
1656
|
+
for pobj in getattr(func, "parameters", None) or []:
|
|
1657
|
+
pname = getattr(pobj, "name", None)
|
|
1658
|
+
if pname == "request" and pname not in param_locations:
|
|
1659
|
+
param_locations[pname] = "HTTP_BODY"
|
|
1660
|
+
|
|
1649
1661
|
route_path = ep.get("path", "")
|
|
1650
1662
|
origins = source_id.identify_from_function(
|
|
1651
1663
|
func,
|
|
@@ -337,7 +337,8 @@ def emit_install_event(*, probe_version: str = "") -> None:
|
|
|
337
337
|
# Overridable for non-prod testing (staging/dev receivers), mirroring the
|
|
338
338
|
# analyze path's --api-url. Defaults to prod, so unset means no change.
|
|
339
339
|
_TELEMETRY_ENDPOINT = os.environ.get(
|
|
340
|
-
"APISEC_TELEMETRY_ENDPOINT",
|
|
340
|
+
"APISEC_TELEMETRY_ENDPOINT",
|
|
341
|
+
"https://api.apisec.ai/v1/applications/surface/telemetry/events",
|
|
341
342
|
)
|
|
342
343
|
|
|
343
344
|
|
|
@@ -52,21 +52,47 @@ _SOURCE_CALLEES: frozenset[str] = frozenset(
|
|
|
52
52
|
{"params", "cookies", "session", "request", "headers", "env"}
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"request.raw_post",
|
|
63
|
-
"request.query_string",
|
|
64
|
-
"request.query_parameters",
|
|
65
|
-
"request.path_parameters",
|
|
66
|
-
"request.headers[",
|
|
67
|
-
"request.env[",
|
|
55
|
+
# HTTP-verb DSL methods used by rack-style frameworks (Grape, Sinatra) that
|
|
56
|
+
# declare a route handler as a block rather than a ``def`` method:
|
|
57
|
+
# get '/users/:id' do ... end post do ... end
|
|
58
|
+
# These blocks never appear as ``method`` nodes, so the method-based passes
|
|
59
|
+
# below miss them entirely; Pass C scans their block bodies directly.
|
|
60
|
+
_ROUTE_VERBS: frozenset[str] = frozenset(
|
|
61
|
+
{"get", "post", "put", "patch", "delete", "options", "head"}
|
|
68
62
|
)
|
|
69
63
|
|
|
64
|
+
# Inherently-tainted source identifiers: `params`, `cookies`, `session`,
|
|
65
|
+
# `request`, `headers`, `env`. Matched by NAME (not substring) inside
|
|
66
|
+
# ``_is_tainted_node`` so that ``group_params`` / ``webcal_params`` (which
|
|
67
|
+
# merely *contain* the substring ``params``) and ``Model.find(params[:id])``
|
|
68
|
+
# (whose call text contains ``params[``) are NOT mis-read as sources. This
|
|
69
|
+
# replaces the removed ``params[``-substring fallback, which was the P0
|
|
70
|
+
# over-taint root cause: it marked every expression embedding ``params[...]``
|
|
71
|
+
# as tainted and poisoned all derived values (see RUBY_SUPPORT_ROADMAP.md P0).
|
|
72
|
+
# Method chains on these (``request.body``, ``params.permit``) and indexing
|
|
73
|
+
# (``params[:id]``, ``cookies[:sid]``) are covered structurally by the
|
|
74
|
+
# element_reference / source-call rules, so no substring fallback is needed.
|
|
75
|
+
_SOURCE_IDENTIFIERS: frozenset[str] = _SOURCE_CALLEES
|
|
76
|
+
|
|
77
|
+
# Sanitizer calls (P3) — a call whose RESULT is not tainted even when its
|
|
78
|
+
# receiver / arguments are. Prevents the "receiver is tainted => the call is
|
|
79
|
+
# a source" rule from propagating taint through coercions and escaping.
|
|
80
|
+
# - numeric coercion (receiver-agnostic): params[:x].to_i / .to_f / .to_d
|
|
81
|
+
# - numeric constructors (no receiver): Integer(x) / Float(x) / BigDecimal(x)
|
|
82
|
+
# - Shellwords escaping: Shellwords.escape / .shellescape
|
|
83
|
+
_NUMERIC_COERCIONS: frozenset[str] = frozenset({"to_i", "to_f", "to_d", "to_r"})
|
|
84
|
+
_SANITIZER_CONSTRUCTORS: frozenset[str] = frozenset({"Integer", "Float", "BigDecimal"})
|
|
85
|
+
_SHELLWORDS_SANITIZERS: frozenset[str] = frozenset({"escape", "shellescape"})
|
|
86
|
+
|
|
87
|
+
# ActiveRecord query-builder sinks that support parameterized forms
|
|
88
|
+
# (``?``, ``:named``, hash). For these, a tainted value passed as a BOUND
|
|
89
|
+
# argument — after a plain SQL template string, or as a hash / keyword
|
|
90
|
+
# value — is SAFE; only taint interpolated *into* a SQL string argument is
|
|
91
|
+
# dangerous. Raw-execution SQL sinks (``execute``, ``find_by_sql``,
|
|
92
|
+
# ``update_all``, ...) take a single SQL string and keep the normal
|
|
93
|
+
# tainted-arg treatment.
|
|
94
|
+
_QUERY_BUILDER_SINKS: frozenset[str] = frozenset({"where", "having", "order", "group"})
|
|
95
|
+
|
|
70
96
|
# ── Sink catalogue ────────────────────────────────────────────────────────────
|
|
71
97
|
|
|
72
98
|
|
|
@@ -76,11 +102,22 @@ class _Sink:
|
|
|
76
102
|
receiver_fragment: str | None # receiver text must contain this (None = any)
|
|
77
103
|
sink_type: str
|
|
78
104
|
description: str
|
|
105
|
+
receiver_excludes: tuple[str, ...] | None = None # skip if receiver contains any
|
|
79
106
|
|
|
80
107
|
|
|
81
108
|
_SINKS: list[_Sink] = [
|
|
82
109
|
# ── SQL injection ─────────────────────────────────────────────────────────
|
|
83
|
-
|
|
110
|
+
# `execute` is raw-SQL only on a DB connection (e.g. `connection.execute(sql)`).
|
|
111
|
+
# GraphQL `*Schema.execute(query_string)` runs a GraphQL query, NOT SQL —
|
|
112
|
+
# exclude receivers whose name ends in `Schema` (ApiSchema, MySchema,
|
|
113
|
+
# GraphQL::Schema) so a `Schema.execute(params[:query])` controller isn't SQLi.
|
|
114
|
+
_Sink(
|
|
115
|
+
"execute",
|
|
116
|
+
None,
|
|
117
|
+
"sql_injection",
|
|
118
|
+
"Raw SQL execution",
|
|
119
|
+
receiver_excludes=("Schema",),
|
|
120
|
+
),
|
|
84
121
|
_Sink("exec_query", None, "sql_injection", "Raw SQL execution"),
|
|
85
122
|
_Sink("find_by_sql", None, "sql_injection", "Raw SQL query"),
|
|
86
123
|
_Sink("select_all", None, "sql_injection", "Raw SQL select"),
|
|
@@ -100,7 +137,20 @@ _SINKS: list[_Sink] = [
|
|
|
100
137
|
_Sink("capture2e", None, "command_injection", "Open3.capture2e"),
|
|
101
138
|
_Sink("capture3", None, "command_injection", "Open3.capture3"),
|
|
102
139
|
_Sink("pipeline", "Open3", "command_injection", "Open3.pipeline"),
|
|
103
|
-
# ──
|
|
140
|
+
# ── Code / template injection (eval family) ──────────────────────────────
|
|
141
|
+
# `eval` / `instance_eval` / `class_eval` / `module_eval` with a tainted
|
|
142
|
+
# STRING argument execute attacker-controlled Ruby. The block form
|
|
143
|
+
# (`class_eval { ... }`) has no string arg and naturally does not fire.
|
|
144
|
+
_Sink("eval", None, "code_injection", "Kernel#eval with user-controlled string"),
|
|
145
|
+
_Sink("instance_eval", None, "code_injection", "instance_eval with user-controlled string"),
|
|
146
|
+
_Sink("class_eval", None, "code_injection", "class_eval with user-controlled string"),
|
|
147
|
+
_Sink("module_eval", None, "code_injection", "module_eval with user-controlled string"),
|
|
148
|
+
# ── SSTI ──────────────────────────────────────────────────────────────────
|
|
149
|
+
# ERB.new with a tainted template renders attacker-controlled ERB.
|
|
150
|
+
# Receiver-gated to the ERB class so the common `x.new` callee doesn't
|
|
151
|
+
# over-fire.
|
|
152
|
+
_Sink("new", "ERB", "ssti", "ERB.new with user-controlled template"),
|
|
153
|
+
# ── Open redirect ─────────────────────────────────────────────────────────────
|
|
104
154
|
_Sink("redirect_to", None, "open_redirect", "HTTP redirect with user-controlled URL"),
|
|
105
155
|
# ── Path traversal ────────────────────────────────────────────────────────
|
|
106
156
|
_Sink("send_file", None, "path_traversal", "File serving with user-supplied path"),
|
|
@@ -267,6 +317,32 @@ class _MethodScanner:
|
|
|
267
317
|
return True
|
|
268
318
|
return False
|
|
269
319
|
|
|
320
|
+
def _is_sanitizer_call(self, node: Node) -> bool:
|
|
321
|
+
"""True if the call's RESULT is guaranteed non-tainted (P3 sanitizers).
|
|
322
|
+
|
|
323
|
+
Checked before the source-call rule so that ``params[:x].to_i`` and
|
|
324
|
+
``Shellwords.escape(...)`` clear taint instead of propagating it.
|
|
325
|
+
"""
|
|
326
|
+
if node.type != "call":
|
|
327
|
+
return False
|
|
328
|
+
method_node = node.child_by_field_name("method")
|
|
329
|
+
if method_node is None:
|
|
330
|
+
return False
|
|
331
|
+
method = self._text(method_node)
|
|
332
|
+
# Numeric coercion — a number cannot inject into any sink.
|
|
333
|
+
if method in _NUMERIC_COERCIONS:
|
|
334
|
+
return True
|
|
335
|
+
receiver_node = node.child_by_field_name("receiver")
|
|
336
|
+
# Numeric constructors: Integer(x) / Float(x) / BigDecimal(x) (no receiver)
|
|
337
|
+
if receiver_node is None and method in _SANITIZER_CONSTRUCTORS:
|
|
338
|
+
return True
|
|
339
|
+
# Shellwords.escape / Shellwords.shellescape
|
|
340
|
+
if receiver_node is not None and method in _SHELLWORDS_SANITIZERS:
|
|
341
|
+
recv = self._text(receiver_node)
|
|
342
|
+
if recv == "Shellwords" or recv.endswith("::Shellwords"):
|
|
343
|
+
return True
|
|
344
|
+
return False
|
|
345
|
+
|
|
270
346
|
def _is_tainted_node(self, node: Node) -> bool:
|
|
271
347
|
"""
|
|
272
348
|
Recursively determine whether a subtree is tainted.
|
|
@@ -283,6 +359,22 @@ class _MethodScanner:
|
|
|
283
359
|
if t in ("identifier", "instance_variable") and self._text(node) in self._tainted_vars:
|
|
284
360
|
return True
|
|
285
361
|
|
|
362
|
+
# Inherently-tainted source identifiers: `params`, `cookies`,
|
|
363
|
+
# `session`, `request`, `headers`, `env`. Matched by NAME, not by
|
|
364
|
+
# substring, so `group_params` / `webcal_params` (which merely
|
|
365
|
+
# contain `params`) are NOT treated as sources. This replaces the
|
|
366
|
+
# removed `params[`-substring fallback (the P0 over-taint root cause:
|
|
367
|
+
# it marked `Model.find(params[:id])` and every derived value as
|
|
368
|
+
# tainted).
|
|
369
|
+
if t == "identifier" and self._text(node) in _SOURCE_IDENTIFIERS:
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
# Sanitizer call — result is NOT tainted (P3). Checked before the
|
|
373
|
+
# source-call rule so `params[:x].to_i` / `Shellwords.escape(...)`
|
|
374
|
+
# clear taint instead of propagating it.
|
|
375
|
+
if t == "call" and self._is_sanitizer_call(node):
|
|
376
|
+
return False
|
|
377
|
+
|
|
286
378
|
# Direct source call or method chain on tainted receiver
|
|
287
379
|
if t == "call" and self._is_source_call(node):
|
|
288
380
|
return True
|
|
@@ -290,6 +382,27 @@ class _MethodScanner:
|
|
|
290
382
|
# params[:id], cookies[:session], tainted_var[:x]
|
|
291
383
|
if t == "element_reference":
|
|
292
384
|
obj = node.child_by_field_name("object")
|
|
385
|
+
# Rack multipart Tempfile accessor: `params[:file][:tempfile]`
|
|
386
|
+
# yields a server-managed Tempfile (a server-assigned temp path),
|
|
387
|
+
# not a user-supplied string. Only the NESTED multipart-part
|
|
388
|
+
# shape is cleared — a top-level `params[:tempfile]` is a plain
|
|
389
|
+
# user param named `tempfile` and stays tainted (see
|
|
390
|
+
# test_toplevel_param_named_tempfile_still_fires).
|
|
391
|
+
if (
|
|
392
|
+
obj is not None
|
|
393
|
+
and obj.type == "element_reference"
|
|
394
|
+
and any(
|
|
395
|
+
c.type in ("simple_symbol", "symbol") and self._text(c) == ":tempfile"
|
|
396
|
+
for c in node.children
|
|
397
|
+
)
|
|
398
|
+
):
|
|
399
|
+
inner = obj.child_by_field_name("object")
|
|
400
|
+
if (
|
|
401
|
+
inner is not None
|
|
402
|
+
and inner.type == "identifier"
|
|
403
|
+
and self._text(inner) in _SOURCE_IDENTIFIERS
|
|
404
|
+
):
|
|
405
|
+
return False
|
|
293
406
|
if obj and self._is_tainted_node(obj):
|
|
294
407
|
return True
|
|
295
408
|
|
|
@@ -318,9 +431,7 @@ class _MethodScanner:
|
|
|
318
431
|
if self._local_call_returns_tainted(node):
|
|
319
432
|
return True
|
|
320
433
|
|
|
321
|
-
|
|
322
|
-
raw = self._text(node)
|
|
323
|
-
return any(sub in raw for sub in _SOURCE_SUBSTRINGS)
|
|
434
|
+
return False
|
|
324
435
|
|
|
325
436
|
def _local_call_returns_tainted(self, call_node: Node) -> bool:
|
|
326
437
|
"""Return True if a local method call with tainted args returns tainted."""
|
|
@@ -398,9 +509,24 @@ class _MethodScanner:
|
|
|
398
509
|
for sink in candidates:
|
|
399
510
|
if sink.receiver_fragment and sink.receiver_fragment not in receiver_text:
|
|
400
511
|
continue
|
|
512
|
+
if sink.receiver_excludes and any(
|
|
513
|
+
ex in receiver_text for ex in sink.receiver_excludes
|
|
514
|
+
):
|
|
515
|
+
continue
|
|
401
516
|
args_node = node.child_by_field_name("arguments")
|
|
402
517
|
if not args_node:
|
|
403
518
|
continue
|
|
519
|
+
# Query-builder sinks (where/having/order/group)
|
|
520
|
+
# support parameterized forms. If the first argument
|
|
521
|
+
# is a plain SQL template (a string literal with NO
|
|
522
|
+
# interpolation) or a hash / keyword pair, tainted values
|
|
523
|
+
# in the BOUND arguments are SAFE — only taint
|
|
524
|
+
# interpolated *into* a SQL string is dangerous. Raw
|
|
525
|
+
# SQL-execution sinks skip this guard.
|
|
526
|
+
if sink.callee in _QUERY_BUILDER_SINKS and self._is_parameterized_call(
|
|
527
|
+
node
|
|
528
|
+
):
|
|
529
|
+
continue
|
|
404
530
|
arg_idx = 0
|
|
405
531
|
for arg_node in args_node.children:
|
|
406
532
|
if arg_node.type in (",", "(", ")", "argument_list"):
|
|
@@ -461,6 +587,35 @@ class _MethodScanner:
|
|
|
461
587
|
f["path"] = [{"caller": self._method_name, "callee": callee_name}] + f.get("path", [])
|
|
462
588
|
return callee_flows
|
|
463
589
|
|
|
590
|
+
def _is_parameterized_call(self, call_node: Node) -> bool:
|
|
591
|
+
"""True if a query-builder call uses a bound / parameterized form.
|
|
592
|
+
|
|
593
|
+
Recognises the three safe first-argument shapes (the rest fall through
|
|
594
|
+
to the normal tainted-arg check):
|
|
595
|
+
- hash / keyword pair: ``where(name: x)`` / ``where({name: x})``
|
|
596
|
+
- plain SQL template: ``where("col = ?", x)`` /
|
|
597
|
+
``where("col = :k", k: x)`` — a string literal with NO
|
|
598
|
+
interpolation; the ``?``/``:k`` are bound placeholders.
|
|
599
|
+
Interpolated strings, concatenations, and bare identifiers are NOT
|
|
600
|
+
parameterized (the taint is in the SQL string itself).
|
|
601
|
+
"""
|
|
602
|
+
args_node = call_node.child_by_field_name("arguments")
|
|
603
|
+
if args_node is None:
|
|
604
|
+
return False
|
|
605
|
+
first = None
|
|
606
|
+
for arg in args_node.children:
|
|
607
|
+
if arg.type in (",", "(", ")", "argument_list"):
|
|
608
|
+
continue
|
|
609
|
+
first = arg
|
|
610
|
+
break
|
|
611
|
+
if first is None:
|
|
612
|
+
return False
|
|
613
|
+
if first.type in ("pair", "hash"):
|
|
614
|
+
return True
|
|
615
|
+
if first.type == "string":
|
|
616
|
+
return not any(c.type == "interpolation" for c in first.children)
|
|
617
|
+
return False
|
|
618
|
+
|
|
464
619
|
# ── Flow construction ─────────────────────────────────────────────────────
|
|
465
620
|
|
|
466
621
|
def _make_flow(
|
|
@@ -546,6 +701,35 @@ def _collect_methods(node: Node, src: bytes) -> list[tuple[Node, str]]:
|
|
|
546
701
|
return results
|
|
547
702
|
|
|
548
703
|
|
|
704
|
+
def _collect_route_blocks(node: Node, src: bytes) -> list[tuple[Node, str]]:
|
|
705
|
+
"""Find HTTP-verb DSL route handler blocks (Grape/Sinatra).
|
|
706
|
+
|
|
707
|
+
``get '/users/:id' do ... end`` parses as a ``call`` whose method is an
|
|
708
|
+
HTTP verb with an attached ``do_block``/``block``. Returns
|
|
709
|
+
``(block_node, verb)`` for each; the block exposes a ``body`` field, so a
|
|
710
|
+
``_MethodScanner`` can scan it exactly like a method body. Non-verb DSL
|
|
711
|
+
blocks (``resource``, ``namespace``, ``before``) are skipped, but we
|
|
712
|
+
recurse through them to reach the verb blocks nested inside.
|
|
713
|
+
"""
|
|
714
|
+
results: list[tuple[Node, str]] = []
|
|
715
|
+
if node.type == "call":
|
|
716
|
+
mnode = node.child_by_field_name("method")
|
|
717
|
+
if mnode is not None:
|
|
718
|
+
verb = src[mnode.start_byte : mnode.end_byte].decode("utf-8", errors="replace")
|
|
719
|
+
if verb in _ROUTE_VERBS:
|
|
720
|
+
blk = node.child_by_field_name("block")
|
|
721
|
+
if blk is None:
|
|
722
|
+
for c in node.children:
|
|
723
|
+
if c.type in ("do_block", "block"):
|
|
724
|
+
blk = c
|
|
725
|
+
break
|
|
726
|
+
if blk is not None and blk.child_by_field_name("body") is not None:
|
|
727
|
+
results.append((blk, verb))
|
|
728
|
+
for child in node.children:
|
|
729
|
+
results.extend(_collect_route_blocks(child, src))
|
|
730
|
+
return results
|
|
731
|
+
|
|
732
|
+
|
|
549
733
|
def _collect_classes(node: Node, src: bytes) -> list[tuple[Node, str]]:
|
|
550
734
|
results: list[tuple[Node, str]] = []
|
|
551
735
|
if node.type == "class":
|
|
@@ -608,8 +792,10 @@ class RubyTaintScanner:
|
|
|
608
792
|
scanned_ids: set[int] = set()
|
|
609
793
|
|
|
610
794
|
# Pass A: methods within classes — cross-method @ivar context + call graph
|
|
795
|
+
file_ivar_taint: set[str] = set()
|
|
611
796
|
for class_node, _ in _collect_classes(tree.root_node, src):
|
|
612
797
|
ivar_taint = _prescan_class_instance_vars(class_node, src, pf.path)
|
|
798
|
+
file_ivar_taint |= ivar_taint
|
|
613
799
|
for method_node, method_name in _collect_methods(class_node, src):
|
|
614
800
|
scanned_ids.add(method_node.id)
|
|
615
801
|
scanner = _MethodScanner(
|
|
@@ -636,5 +822,19 @@ class RubyTaintScanner:
|
|
|
636
822
|
)
|
|
637
823
|
flows.extend(scanner.scan())
|
|
638
824
|
|
|
825
|
+
# Pass C: HTTP-verb DSL route blocks (Grape/Sinatra). Their handler
|
|
826
|
+
# bodies are blocks, not ``def`` methods, so Passes A/B skip them.
|
|
827
|
+
for block_node, verb in _collect_route_blocks(tree.root_node, src):
|
|
828
|
+
scanner = _MethodScanner(
|
|
829
|
+
src=src,
|
|
830
|
+
path=pf.path,
|
|
831
|
+
method_node=block_node,
|
|
832
|
+
method_name=verb,
|
|
833
|
+
extra_tainted_vars=file_ivar_taint,
|
|
834
|
+
method_index=method_index,
|
|
835
|
+
depth=0,
|
|
836
|
+
)
|
|
837
|
+
flows.extend(scanner.scan())
|
|
838
|
+
|
|
639
839
|
logger.debug("RubyTaintScanner: %d flows across %d files", len(flows), len(self._files))
|
|
640
840
|
return flows
|