apisec-code-bolt 0.1.1__tar.gz → 0.1.3__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.1 → apisec_code_bolt-0.1.3}/PKG-INFO +2 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/pyproject.toml +4 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/__init__.py +6 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/analyzer.py +314 -2
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_graph.py +24 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/data_flow.py +26 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/dependency_extractor.py +44 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/integration_detector.py +401 -11
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/read_site_detector.py +15 -3
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +136 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/client.py +11 -4
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/discovery.py +2 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/manifest.py +35 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/types.py +7 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/_graphql_common.py +45 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/base.py +6 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2 -19
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +12 -19
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/js/_route_helpers.py +30 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/express_plugin.py +2 -18
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +2 -16
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +11 -18
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +2 -15
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +26 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +2 -19
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/__init__.py +8 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +16 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +486 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +358 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +1050 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +459 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +640 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/visitors.py +24 -3
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/__init__.py +5 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/language_services.py +101 -0
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/parser.py +342 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/ast_cache.py +33 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/executor.py +27 -1
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/handlers.py +413 -13
- apisec_code_bolt-0.1.3/src/apisec_code_bolt/query/manifest_context.py +313 -0
- apisec_code_bolt-0.1.3/tests/fixtures/dep_files/Gemfile.lock +21 -0
- apisec_code_bolt-0.1.3/tests/fixtures/sample_flask_pydantic/app.py +50 -0
- apisec_code_bolt-0.1.3/tests/fixtures/sample_grape/api.rb +68 -0
- apisec_code_bolt-0.1.3/tests/fixtures/sample_sinatra/app.rb +38 -0
- apisec_code_bolt-0.1.3/tests/test_query/test_auth_indicator_matching.py +106 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/test_query/test_handlers.py +92 -0
- apisec_code_bolt-0.1.3/tests/test_query/test_read_site_origin_types.py +47 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_benchmark_run.py +46 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_dependency_parsing.py +57 -0
- apisec_code_bolt-0.1.3/tests/unit/test_grape_plugin.py +369 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_graphql_plugin.py +42 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_integration_detection.py +115 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_java_parser.py +34 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_js_graphql_plugin.py +55 -0
- apisec_code_bolt-0.1.3/tests/unit/test_pydantic_body_inference.py +188 -0
- apisec_code_bolt-0.1.3/tests/unit/test_rails_plugin.py +722 -0
- apisec_code_bolt-0.1.3/tests/unit/test_ruby_graphql_plugin.py +541 -0
- apisec_code_bolt-0.1.3/tests/unit/test_sinatra_plugin.py +315 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/.gitignore +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/README.md +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/__main__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/main.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/apisec_client.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/config.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/credentials.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/log_format.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/repo.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/state.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/telemetry.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/base.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/services.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/conftest.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/Cargo.toml +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/Pipfile +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/build.gradle +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/build.gradle.kts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/go.mod +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/package.json +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pom.xml +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/requirements.txt +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/setup.cfg +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/setup.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_django/urls.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_django/views.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/app.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/routes/auth.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/routes/users.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/main.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_flask/app.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_cli.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_django_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_express_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_fastapi_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_flask_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_nestjs_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/integration/test_spring_boot_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/test_query/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/test_query/fixtures/sample_app.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/test_query/test_executor.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/__init__.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_benchmark_audit.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_call_graph.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_config.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_csharp_literals.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_csharp_type_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_data_flow.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_discovery.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_django_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_dynamic_routes.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_express_nestjs_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_fastapi_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_fastify_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_instrumentation.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_jwt_config_extraction.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_path_resolver.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_python_parser.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_repo_canonical.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_router_registry.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_sensitivity_capability.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_telemetry.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_transformation_tracking.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_types.py +0 -0
- {apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/tests/unit/test_wcf_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.3
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
31
31
|
Requires-Dist: rich>=13.7.0
|
|
32
32
|
Requires-Dist: tree-sitter-c-sharp>=0.23
|
|
33
33
|
Requires-Dist: tree-sitter-javascript>=0.23
|
|
34
|
+
Requires-Dist: tree-sitter-ruby>=0.23
|
|
34
35
|
Requires-Dist: tree-sitter-typescript>=0.23
|
|
35
36
|
Requires-Dist: tree-sitter>=0.23
|
|
36
37
|
Requires-Dist: typing-extensions>=4.9.0
|
|
@@ -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.3"
|
|
8
8
|
description = "Static analysis probe for extracting architectural metadata from codebases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Proprietary"}
|
|
@@ -41,6 +41,9 @@ dependencies = [
|
|
|
41
41
|
"tree-sitter-javascript>=0.23", # JS grammar for tree-sitter
|
|
42
42
|
"tree-sitter-typescript>=0.23", # TypeScript grammar for tree-sitter
|
|
43
43
|
|
|
44
|
+
# Ruby parsing
|
|
45
|
+
"tree-sitter-ruby>=0.23", # Ruby grammar for tree-sitter
|
|
46
|
+
|
|
44
47
|
# Graph operations
|
|
45
48
|
"networkx>=3.2", # Call graph, data flow graphs
|
|
46
49
|
|
|
@@ -6,7 +6,12 @@ information (routes, data flows, authentication patterns) without sending
|
|
|
6
6
|
raw source code outside the customer's environment.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
try:
|
|
10
|
+
from importlib.metadata import version as _pkg_version
|
|
11
|
+
|
|
12
|
+
__version__ = _pkg_version("apisec-code-bolt")
|
|
13
|
+
except Exception:
|
|
14
|
+
__version__ = "0.0.0"
|
|
10
15
|
__author__ = "APIsec"
|
|
11
16
|
|
|
12
17
|
from .core.config import CodeBoltConfig, load_config
|
|
@@ -61,6 +61,7 @@ from ..frameworks.base import FrameworkPluginRegistry
|
|
|
61
61
|
from ..parsing import csharp as _csharp_pkg # noqa: F401 — registers CSharpParser
|
|
62
62
|
from ..parsing import js as _js_pkg # noqa: F401 — registers JavaScriptParser
|
|
63
63
|
from ..parsing import jvm as _jvm_pkg # noqa: F401 — registers JavaParser
|
|
64
|
+
from ..parsing import ruby as _ruby_pkg # noqa: F401 — registers RubyParser
|
|
64
65
|
from ..parsing.base import ParsedFile, ParserRegistry
|
|
65
66
|
from ..parsing.csharp.language_services import CSharpLanguageServices
|
|
66
67
|
from ..parsing.js.language_services import JavaScriptLanguageServices
|
|
@@ -69,6 +70,7 @@ from ..parsing.jvm.language_services import JavaLanguageServices
|
|
|
69
70
|
from ..parsing.python.language_services import PythonLanguageServices
|
|
70
71
|
from ..parsing.python.parser import PythonProjectParser
|
|
71
72
|
from ..parsing.python.type_resolver import SchemaBuilder, TypeResolver
|
|
73
|
+
from ..parsing.ruby.language_services import RubyLanguageServices
|
|
72
74
|
from ..parsing.services import AnalysisContext, LanguageServices
|
|
73
75
|
from .call_graph import CallGraph, build_call_graph_with_context
|
|
74
76
|
from .data_flow import DataFlowAnalyzer
|
|
@@ -201,11 +203,17 @@ class ProjectAnalyzer:
|
|
|
201
203
|
Language.JAVA: JavaLanguageServices(),
|
|
202
204
|
Language.CSHARP: CSharpLanguageServices(),
|
|
203
205
|
Language.JAVASCRIPT: JavaScriptLanguageServices(),
|
|
206
|
+
Language.RUBY: RubyLanguageServices(),
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
# Analysis contexts per language (built during analysis)
|
|
207
210
|
self._analysis_contexts: dict[Language, AnalysisContext] = {}
|
|
208
211
|
|
|
212
|
+
# Express parent-router auth middleware map, built during context setup.
|
|
213
|
+
# Maps resolved file path str → list of auth middleware variable names
|
|
214
|
+
# that apply to routes in that file (inherited from parent router mounts).
|
|
215
|
+
self._express_parent_auth_map: dict[str, list[str]] = {}
|
|
216
|
+
|
|
209
217
|
# Parsed data
|
|
210
218
|
self._parsed_files: dict[Path, ParsedFile] = {}
|
|
211
219
|
self._discovery_result: DiscoveryResult | None = None
|
|
@@ -369,7 +377,11 @@ class ProjectAnalyzer:
|
|
|
369
377
|
self._populate_route_auth_mapping(ctx["auth"], ctx["routes"], ctx["middleware"])
|
|
370
378
|
self._link_routes_to_auth_schemes(ctx["routes"], ctx["auth"])
|
|
371
379
|
self._propagate_file_auth_to_routes(ctx["routes"], ctx["auth"])
|
|
380
|
+
self._propagate_express_parent_auth_to_routes(ctx["routes"])
|
|
381
|
+
self._propagate_django_class_auth_to_routes(ctx["routes"], ctx["auth"])
|
|
382
|
+
self._propagate_rails_auth_to_routes(ctx["routes"], ctx["auth"])
|
|
372
383
|
self._apply_spring_filter_chain_policy(ctx["routes"])
|
|
384
|
+
self._infer_pydantic_route_bodies(ctx["routes"])
|
|
373
385
|
ctx["calls"] = self._extract_all_calls()
|
|
374
386
|
|
|
375
387
|
def _stage_analyze_flows(self, ctx: dict[str, Any]) -> None:
|
|
@@ -485,13 +497,20 @@ class ProjectAnalyzer:
|
|
|
485
497
|
logger.debug("Django prefix map failed: %s", e)
|
|
486
498
|
elif language == Language.JAVASCRIPT:
|
|
487
499
|
try:
|
|
488
|
-
from .url_prefix_resolver import
|
|
500
|
+
from .url_prefix_resolver import (
|
|
501
|
+
build_express_auth_middleware_map,
|
|
502
|
+
build_express_prefix_map,
|
|
503
|
+
)
|
|
489
504
|
|
|
490
505
|
pm = build_express_prefix_map(files, project_root=self.config.project_root)
|
|
491
506
|
context.language_services["_url_prefix_map"] = pm
|
|
492
507
|
logger.debug("Express prefix map built: %d entries", len(pm))
|
|
508
|
+
|
|
509
|
+
am = build_express_auth_middleware_map(files)
|
|
510
|
+
self._express_parent_auth_map = am
|
|
511
|
+
logger.debug("Express auth middleware map built: %d entries", len(am))
|
|
493
512
|
except Exception as e:
|
|
494
|
-
logger.debug("Express prefix map failed: %s", e)
|
|
513
|
+
logger.debug("Express prefix/auth map failed: %s", e)
|
|
495
514
|
|
|
496
515
|
self._analysis_contexts[language] = context
|
|
497
516
|
|
|
@@ -1286,6 +1305,138 @@ class ProjectAnalyzer:
|
|
|
1286
1305
|
# matches the suffix of any qualified name.
|
|
1287
1306
|
return any(qn.endswith("." + type_name) for qn in known_models)
|
|
1288
1307
|
|
|
1308
|
+
# -------------------------------------------------------------------------
|
|
1309
|
+
# Pydantic body inference
|
|
1310
|
+
# -------------------------------------------------------------------------
|
|
1311
|
+
|
|
1312
|
+
# Frameworks whose plugins don't natively parse Pydantic request bodies.
|
|
1313
|
+
# FastAPI and Django Ninja are excluded because their plugins already set
|
|
1314
|
+
# route.body from route signature annotations.
|
|
1315
|
+
_PYDANTIC_INFER_FRAMEWORKS: frozenset[str] = frozenset(
|
|
1316
|
+
{"flask", "django", "generic", "webhook", ""}
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# Pydantic instantiation patterns we recognise in call sites.
|
|
1320
|
+
# A call site contributes a candidate model name when:
|
|
1321
|
+
# (a) its callee_name exactly matches a known Pydantic model class name, OR
|
|
1322
|
+
# (b) its callee_name is "ModelName.parse_obj", "ModelName.model_validate",
|
|
1323
|
+
# or "ModelName.from_orm".
|
|
1324
|
+
_PYDANTIC_CLASS_METHODS: frozenset[str] = frozenset(
|
|
1325
|
+
{"parse_obj", "model_validate", "from_orm", "model_validate_json"}
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
# Receiver expressions that indicate the argument came from the HTTP request.
|
|
1329
|
+
_REQUEST_RECEIVERS: frozenset[str] = frozenset({"request", "req"})
|
|
1330
|
+
|
|
1331
|
+
def _infer_pydantic_route_bodies(self, routes: list[RouteModel]) -> None:
|
|
1332
|
+
"""Set body.model_name for Python routes that instantiate a Pydantic model.
|
|
1333
|
+
|
|
1334
|
+
Flask, Django, and similar frameworks don't emit Pydantic body metadata
|
|
1335
|
+
from their plugins — the handler just calls ``Model(**request.get_json())``
|
|
1336
|
+
directly. This pass scans call sites to recover those model references,
|
|
1337
|
+
so _collect_schemas can assign usage="request_body" instead of "domain".
|
|
1338
|
+
"""
|
|
1339
|
+
py_ctx = self._analysis_contexts.get(Language.PYTHON)
|
|
1340
|
+
if not py_ctx or not py_ctx.type_resolver:
|
|
1341
|
+
return
|
|
1342
|
+
|
|
1343
|
+
all_models = py_ctx.type_resolver.get_all_models()
|
|
1344
|
+
if not all_models:
|
|
1345
|
+
return
|
|
1346
|
+
|
|
1347
|
+
# simple class name → first matching qualified name
|
|
1348
|
+
simple_to_qname: dict[str, str] = {}
|
|
1349
|
+
for qname in all_models:
|
|
1350
|
+
simple = qname.rsplit(".", 1)[-1]
|
|
1351
|
+
if simple not in simple_to_qname:
|
|
1352
|
+
simple_to_qname[simple] = qname
|
|
1353
|
+
model_simple_names: frozenset[str] = frozenset(simple_to_qname)
|
|
1354
|
+
|
|
1355
|
+
# caller qualified name → list of ParsedCallSite
|
|
1356
|
+
caller_calls: dict[str, list] = {}
|
|
1357
|
+
for parsed in self._parsed_files.values():
|
|
1358
|
+
if not parsed.success or parsed.language != Language.PYTHON:
|
|
1359
|
+
continue
|
|
1360
|
+
for cs in parsed.call_sites or []:
|
|
1361
|
+
if cs.caller_function:
|
|
1362
|
+
key = cs.caller_function.full
|
|
1363
|
+
caller_calls.setdefault(key, []).append(cs)
|
|
1364
|
+
|
|
1365
|
+
for i, route in enumerate(routes):
|
|
1366
|
+
if route.body is not None:
|
|
1367
|
+
continue
|
|
1368
|
+
if route.method not in ("POST", "PUT", "PATCH"):
|
|
1369
|
+
continue
|
|
1370
|
+
if route.framework not in self._PYDANTIC_INFER_FRAMEWORKS:
|
|
1371
|
+
continue
|
|
1372
|
+
|
|
1373
|
+
call_sites = caller_calls.get(route.handler_function, [])
|
|
1374
|
+
if not call_sites:
|
|
1375
|
+
continue
|
|
1376
|
+
|
|
1377
|
+
model_name = self._pick_pydantic_body_model(call_sites, model_simple_names)
|
|
1378
|
+
if model_name:
|
|
1379
|
+
routes[i] = route.model_copy(
|
|
1380
|
+
update={
|
|
1381
|
+
"body": HttpBodyModel(
|
|
1382
|
+
content_type="application/json",
|
|
1383
|
+
model_name=model_name,
|
|
1384
|
+
body_fields=[],
|
|
1385
|
+
required=True,
|
|
1386
|
+
)
|
|
1387
|
+
}
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
def _pick_pydantic_body_model(
|
|
1391
|
+
self,
|
|
1392
|
+
call_sites: list,
|
|
1393
|
+
model_simple_names: frozenset[str],
|
|
1394
|
+
) -> str | None:
|
|
1395
|
+
"""Return the first Pydantic model instantiated with what looks like request data.
|
|
1396
|
+
|
|
1397
|
+
Prefers calls that directly spread request data (**request.json, **data where
|
|
1398
|
+
data came from get_json). Falls back to any Pydantic model instantiation found
|
|
1399
|
+
in the handler — these are almost always the request schema for POST handlers.
|
|
1400
|
+
"""
|
|
1401
|
+
# Check if this handler reads from an HTTP request object at all
|
|
1402
|
+
has_request_read = any(
|
|
1403
|
+
cs.receiver_expression in self._REQUEST_RECEIVERS
|
|
1404
|
+
or "get_json" in cs.callee_name
|
|
1405
|
+
or "request.json" in cs.callee_name
|
|
1406
|
+
or "request.data" in cs.callee_name
|
|
1407
|
+
or "request.body" in cs.callee_name
|
|
1408
|
+
for cs in call_sites
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
candidates: list[tuple[int, str]] = [] # (priority, model_name)
|
|
1412
|
+
|
|
1413
|
+
for cs in call_sites:
|
|
1414
|
+
callee = cs.callee_name
|
|
1415
|
+
|
|
1416
|
+
# Direct instantiation: UserCreate(**data)
|
|
1417
|
+
if callee in model_simple_names:
|
|
1418
|
+
has_spread = any(getattr(arg, "is_keyword_spread", False) for arg in cs.arguments)
|
|
1419
|
+
priority = 0 if has_spread else 1
|
|
1420
|
+
candidates.append((priority, callee))
|
|
1421
|
+
|
|
1422
|
+
# Class-method call: UserCreate.parse_obj(data) / UserCreate.model_validate(data)
|
|
1423
|
+
elif "." in callee:
|
|
1424
|
+
obj, _, method = callee.rpartition(".")
|
|
1425
|
+
if obj in model_simple_names and method in self._PYDANTIC_CLASS_METHODS:
|
|
1426
|
+
candidates.append((0, obj))
|
|
1427
|
+
|
|
1428
|
+
if not candidates:
|
|
1429
|
+
return None
|
|
1430
|
+
|
|
1431
|
+
# If no request read was found, be conservative: only infer if there's a spread call
|
|
1432
|
+
if not has_request_read:
|
|
1433
|
+
candidates = [(p, m) for p, m in candidates if p == 0]
|
|
1434
|
+
if not candidates:
|
|
1435
|
+
return None
|
|
1436
|
+
|
|
1437
|
+
candidates.sort(key=lambda x: x[0])
|
|
1438
|
+
return candidates[0][1]
|
|
1439
|
+
|
|
1289
1440
|
def _get_type_resolver(self):
|
|
1290
1441
|
"""Get the type resolver from whichever analysis context is available."""
|
|
1291
1442
|
ctx = (
|
|
@@ -1426,6 +1577,11 @@ class ProjectAnalyzer:
|
|
|
1426
1577
|
extracts=dep.extracts_fields,
|
|
1427
1578
|
validates=dep.validates,
|
|
1428
1579
|
jwt_operations=dep.jwt_operations,
|
|
1580
|
+
controller_module=dep.qualified_name.module
|
|
1581
|
+
if dep.qualified_name
|
|
1582
|
+
else "",
|
|
1583
|
+
only_actions=list(dep.only_actions),
|
|
1584
|
+
except_actions=list(dep.except_actions),
|
|
1429
1585
|
)
|
|
1430
1586
|
)
|
|
1431
1587
|
except ValidationError as exc:
|
|
@@ -1550,6 +1706,148 @@ class ProjectAnalyzer:
|
|
|
1550
1706
|
if dep_name:
|
|
1551
1707
|
route.router_name = dep_name
|
|
1552
1708
|
|
|
1709
|
+
def _propagate_express_parent_auth_to_routes(self, routes: list[RouteModel]) -> None:
|
|
1710
|
+
"""Propagate auth middleware from parent Express router files to child routes.
|
|
1711
|
+
|
|
1712
|
+
A parent router file may call router.use(apikey) before mounting sub-routers
|
|
1713
|
+
via router.use('/path', subRouter). Routes in those sub-router files inherit
|
|
1714
|
+
the middleware even though it doesn't appear in their own file.
|
|
1715
|
+
|
|
1716
|
+
The parent auth map is built by build_express_auth_middleware_map() and stored
|
|
1717
|
+
on self._express_parent_auth_map during context setup. Each entry maps a
|
|
1718
|
+
resolved file path to the list of auth middleware variable names accumulated
|
|
1719
|
+
from all ancestor router.use() calls that precede the mount of that file.
|
|
1720
|
+
"""
|
|
1721
|
+
if not self._express_parent_auth_map:
|
|
1722
|
+
return
|
|
1723
|
+
for route in routes:
|
|
1724
|
+
if route.router_name is not None:
|
|
1725
|
+
continue
|
|
1726
|
+
if not route.handler_location or not route.handler_location.file:
|
|
1727
|
+
continue
|
|
1728
|
+
auth_middlewares = self._express_parent_auth_map.get(route.handler_location.file, [])
|
|
1729
|
+
if auth_middlewares:
|
|
1730
|
+
route.router_name = "+".join(auth_middlewares)
|
|
1731
|
+
|
|
1732
|
+
def _propagate_django_class_auth_to_routes(
|
|
1733
|
+
self,
|
|
1734
|
+
routes: list[RouteModel],
|
|
1735
|
+
auth_model: AuthModel,
|
|
1736
|
+
) -> None:
|
|
1737
|
+
"""Link Django CBV CLASS-type auth dependencies to their routes.
|
|
1738
|
+
|
|
1739
|
+
DRF APIView subclasses that inherit from an auth mixin (e.g. ApiAuthMixin)
|
|
1740
|
+
are emitted as CLASS-type auth dependencies by the Django plugin. The
|
|
1741
|
+
generic _propagate_file_auth_to_routes only handles MIDDLEWARE-type deps,
|
|
1742
|
+
so this pass closes the gap for class-based auth.
|
|
1743
|
+
|
|
1744
|
+
Matching logic: route handler_function strings look like
|
|
1745
|
+
"urls.FileStandardUploadApi.post" (module.ClassName.method). We split on "."
|
|
1746
|
+
and check every component against a global class-name → dep-name map so
|
|
1747
|
+
the lookup works regardless of module prefix.
|
|
1748
|
+
|
|
1749
|
+
File-key matching is intentionally skipped — Django view classes are defined
|
|
1750
|
+
in apis.py but routes are extracted from urls.py, so the handler_location
|
|
1751
|
+
file and the dep location file differ.
|
|
1752
|
+
"""
|
|
1753
|
+
class_to_dep: dict[str, str] = {}
|
|
1754
|
+
for dep in auth_model.auth_dependencies:
|
|
1755
|
+
if dep.type != "CLASS":
|
|
1756
|
+
continue
|
|
1757
|
+
class_to_dep[dep.name] = dep.name
|
|
1758
|
+
|
|
1759
|
+
if not class_to_dep:
|
|
1760
|
+
return
|
|
1761
|
+
|
|
1762
|
+
for route in routes:
|
|
1763
|
+
if route.router_name is not None:
|
|
1764
|
+
continue
|
|
1765
|
+
handler = str(route.handler_function or "")
|
|
1766
|
+
for part in handler.split("."):
|
|
1767
|
+
dep_name = class_to_dep.get(part)
|
|
1768
|
+
if dep_name:
|
|
1769
|
+
route.router_name = dep_name
|
|
1770
|
+
break
|
|
1771
|
+
|
|
1772
|
+
def _propagate_rails_auth_to_routes(
|
|
1773
|
+
self,
|
|
1774
|
+
routes: list[RouteModel],
|
|
1775
|
+
auth_model: AuthModel,
|
|
1776
|
+
) -> None:
|
|
1777
|
+
"""Link Rails before_action guards to routes using controller + action matching.
|
|
1778
|
+
|
|
1779
|
+
Deps extracted by the Rails plugin carry qualified_name.module = controller
|
|
1780
|
+
resource name (e.g. 'articles') and only_actions/except_actions filters.
|
|
1781
|
+
Routes carry handler_function.module = same controller name and
|
|
1782
|
+
handler_function.name = action name.
|
|
1783
|
+
|
|
1784
|
+
A route is covered by a dep when:
|
|
1785
|
+
- dep.qualified_name.module == route.handler_function.module (same controller)
|
|
1786
|
+
- action is in dep.only_actions (if only_actions is set), OR
|
|
1787
|
+
action is NOT in dep.except_actions (if except_actions is set), OR
|
|
1788
|
+
no filter is set (applies to all actions)
|
|
1789
|
+
|
|
1790
|
+
Special case — Ruby GraphQL routes (path starts with /graphql:):
|
|
1791
|
+
graphql-ruby routes are served by a single GraphqlController whose
|
|
1792
|
+
controller_module is "graphql". Their handler_function.module is the
|
|
1793
|
+
type-file stem ("query_type", "mutation_type"), NOT "graphql", so the
|
|
1794
|
+
regular controller matching never fires. We match them unconditionally
|
|
1795
|
+
against any dep whose controller_module is "graphql" — action-level
|
|
1796
|
+
only:/except: filters on that dep target controller actions ("execute"),
|
|
1797
|
+
not individual GraphQL field names, so coverage is treated as global.
|
|
1798
|
+
"""
|
|
1799
|
+
rails_deps = [
|
|
1800
|
+
dep
|
|
1801
|
+
for dep in auth_model.auth_dependencies
|
|
1802
|
+
if dep.type == "FUNCTION" and dep.controller_module
|
|
1803
|
+
]
|
|
1804
|
+
if not rails_deps:
|
|
1805
|
+
return
|
|
1806
|
+
|
|
1807
|
+
graphql_ctrl_deps = [d for d in rails_deps if d.controller_module == "graphql"]
|
|
1808
|
+
|
|
1809
|
+
for route in routes:
|
|
1810
|
+
if route.router_name is not None:
|
|
1811
|
+
continue
|
|
1812
|
+
hf = route.handler_function
|
|
1813
|
+
if not hf:
|
|
1814
|
+
continue
|
|
1815
|
+
|
|
1816
|
+
# Ruby GraphQL routes: match against GraphqlController deps only.
|
|
1817
|
+
if route.path.startswith("/graphql:") and graphql_ctrl_deps:
|
|
1818
|
+
route.router_name = graphql_ctrl_deps[0].name
|
|
1819
|
+
continue
|
|
1820
|
+
|
|
1821
|
+
# handler_function stored as "controller.action" string in RouteModel
|
|
1822
|
+
hf_str = str(hf)
|
|
1823
|
+
parts = hf_str.split(".")
|
|
1824
|
+
ctrl_module = parts[0] if len(parts) > 1 else ""
|
|
1825
|
+
action = parts[-1]
|
|
1826
|
+
|
|
1827
|
+
for dep in rails_deps:
|
|
1828
|
+
# Rails singular `resource :user` stores module "user" but
|
|
1829
|
+
# the controller class is UsersController → dep module "users".
|
|
1830
|
+
# Try exact match first, then simple +s pluralization as fallback.
|
|
1831
|
+
# Namespaced controllers (Api::V1::AnnouncementsController →
|
|
1832
|
+
# "api/v1/announcements") only store the resource name in the
|
|
1833
|
+
# routes file ("announcements"), so also match on the last segment.
|
|
1834
|
+
dep_mod = dep.controller_module
|
|
1835
|
+
dep_suffix = dep_mod.rsplit("/", 1)[-1]
|
|
1836
|
+
candidates = {ctrl_module, ctrl_module + "s"}
|
|
1837
|
+
if dep_mod not in candidates and dep_suffix not in candidates:
|
|
1838
|
+
continue
|
|
1839
|
+
only = dep.only_actions
|
|
1840
|
+
excl = dep.except_actions
|
|
1841
|
+
if only:
|
|
1842
|
+
covered = action in only
|
|
1843
|
+
elif excl:
|
|
1844
|
+
covered = action not in excl
|
|
1845
|
+
else:
|
|
1846
|
+
covered = True
|
|
1847
|
+
if covered:
|
|
1848
|
+
route.router_name = dep.name
|
|
1849
|
+
break
|
|
1850
|
+
|
|
1553
1851
|
def _apply_spring_filter_chain_policy(self, routes: list[RouteModel]) -> None:
|
|
1554
1852
|
"""Apply a Spring SecurityFilterChain policy to Spring/GraphQL routes.
|
|
1555
1853
|
|
|
@@ -1844,6 +2142,20 @@ class ProjectAnalyzer:
|
|
|
1844
2142
|
"truncated_flows": 0,
|
|
1845
2143
|
}
|
|
1846
2144
|
|
|
2145
|
+
# ── Ruby intra-method taint (runs without a call graph) ──────────────
|
|
2146
|
+
ruby_files = [
|
|
2147
|
+
pf for pf in self._parsed_files.values() if pf.success and pf.language == Language.RUBY
|
|
2148
|
+
]
|
|
2149
|
+
if ruby_files:
|
|
2150
|
+
from ..frameworks.ruby.taint_scanner import RubyTaintScanner
|
|
2151
|
+
|
|
2152
|
+
for flow_dict in RubyTaintScanner(ruby_files).scan():
|
|
2153
|
+
try:
|
|
2154
|
+
flows.append(DataFlowModel(**flow_dict))
|
|
2155
|
+
self._data_flow_stats["flows_discovered"] += 1
|
|
2156
|
+
except (KeyError, ValueError, TypeError) as exc:
|
|
2157
|
+
logger.warning("Ruby taint flow model error: %s", exc)
|
|
2158
|
+
|
|
1847
2159
|
if not self._call_graph:
|
|
1848
2160
|
return flows, self._data_flow_stats
|
|
1849
2161
|
|
{apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_graph.py
RENAMED
|
@@ -400,6 +400,30 @@ class CallGraphBuilder:
|
|
|
400
400
|
if len(parts) > 1:
|
|
401
401
|
scope_function = ".".join(parts[:-1])
|
|
402
402
|
|
|
403
|
+
# Statically-typed languages (Java, C#) record a local variable's
|
|
404
|
+
# DECLARED type, which is authoritative for method-receiver
|
|
405
|
+
# resolution regardless of the initializer -- e.g. for
|
|
406
|
+
# ``LoaderOptions options = new LoaderOptions()`` the initializer is
|
|
407
|
+
# an object-creation expression (not a "call"), but the declared
|
|
408
|
+
# type tells us ``options`` is a ``LoaderOptions``. Binding it lets
|
|
409
|
+
# ``options.method()`` resolve to the library FQCN (needed for
|
|
410
|
+
# class-level reachability). Resolve the declared type through the
|
|
411
|
+
# import map; skip ``var``/``val`` inference keywords and primitives
|
|
412
|
+
# (lower-cased type names like ``int``/``boolean`` are never call
|
|
413
|
+
# receivers).
|
|
414
|
+
decl_type = assign.type_annotation
|
|
415
|
+
if decl_type and decl_type not in ("var", "val", "dynamic"):
|
|
416
|
+
base = decl_type.split("<", 1)[0].replace("[]", "").strip()
|
|
417
|
+
if base and base[0].isupper():
|
|
418
|
+
resolved = self._imports.get((file_path, base), base)
|
|
419
|
+
self._bindings.add_assignment(
|
|
420
|
+
assign.target,
|
|
421
|
+
resolved,
|
|
422
|
+
file_path,
|
|
423
|
+
assign.location.line,
|
|
424
|
+
scope_function=scope_function,
|
|
425
|
+
)
|
|
426
|
+
|
|
403
427
|
if assign.source_type == "call" and assign.source_call:
|
|
404
428
|
# x = SomeClass() -> x is bound to SomeClass
|
|
405
429
|
called = assign.source_call
|
{apisec_code_bolt-0.1.1 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/data_flow.py
RENAMED
|
@@ -47,6 +47,21 @@ if TYPE_CHECKING:
|
|
|
47
47
|
logger = logging.getLogger(__name__)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
# Maps an OriginType member name to its wire placement (the OpenAPI-style
|
|
51
|
+
# ``in`` taxonomy: query/path/header/cookie/body). Only HTTP request inputs
|
|
52
|
+
# have a wire identity; non-request origins (env vars, DB reads, etc.) are
|
|
53
|
+
# absent here and produce no WireIdentityModel.
|
|
54
|
+
_ORIGIN_TYPE_TO_WIRE_IN: dict[str, str] = {
|
|
55
|
+
"HTTP_PATH_PARAM": "path",
|
|
56
|
+
"HTTP_QUERY_PARAM": "query",
|
|
57
|
+
"HTTP_HEADER": "header",
|
|
58
|
+
"HTTP_COOKIE": "cookie",
|
|
59
|
+
"HTTP_BODY": "body",
|
|
60
|
+
"HTTP_FORM": "body",
|
|
61
|
+
"HTTP_FILE": "body",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
50
65
|
# =============================================================================
|
|
51
66
|
# Core Data Structures
|
|
52
67
|
# =============================================================================
|
|
@@ -1720,6 +1735,7 @@ class DataFlowAnalyzer:
|
|
|
1720
1735
|
FlowStepModel,
|
|
1721
1736
|
LocationModel,
|
|
1722
1737
|
TransformationModel,
|
|
1738
|
+
WireIdentityModel,
|
|
1723
1739
|
)
|
|
1724
1740
|
|
|
1725
1741
|
result = []
|
|
@@ -1763,16 +1779,25 @@ class DataFlowAnalyzer:
|
|
|
1763
1779
|
|
|
1764
1780
|
sink_evidence_model = self._evidence_to_model(flow.sink_evidence)
|
|
1765
1781
|
|
|
1782
|
+
origin_type_name = flow.origin_type.name
|
|
1783
|
+
wire_in = _ORIGIN_TYPE_TO_WIRE_IN.get(origin_type_name)
|
|
1784
|
+
wire_identity = (
|
|
1785
|
+
WireIdentityModel(name=flow.origin_name, location=wire_in)
|
|
1786
|
+
if wire_in and flow.origin_name
|
|
1787
|
+
else None
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1766
1790
|
flow_model = DataFlowModel(
|
|
1767
1791
|
id=flow.id,
|
|
1768
1792
|
origin=DataOriginModel(
|
|
1769
|
-
type=
|
|
1793
|
+
type=origin_type_name,
|
|
1770
1794
|
name=flow.origin_name,
|
|
1771
1795
|
location=LocationModel(
|
|
1772
1796
|
file=str(flow.origin_location[0]),
|
|
1773
1797
|
line=flow.origin_location[1],
|
|
1774
1798
|
),
|
|
1775
1799
|
entry_point_ref=flow.entry_point_id,
|
|
1800
|
+
wire=wire_identity,
|
|
1776
1801
|
),
|
|
1777
1802
|
sink=DataSinkModel(
|
|
1778
1803
|
function=flow.sink_function,
|
|
@@ -37,6 +37,7 @@ class DependencyExtractor:
|
|
|
37
37
|
"package.json": "_parse_package_json",
|
|
38
38
|
"go.mod": "_parse_go_mod",
|
|
39
39
|
"Cargo.toml": "_parse_cargo_toml",
|
|
40
|
+
"Gemfile.lock": "_parse_gemfile_lock",
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
# Each parser handles exactly one ecosystem; tag packages authoritatively at
|
|
@@ -54,6 +55,7 @@ class DependencyExtractor:
|
|
|
54
55
|
"_parse_csproj": "NuGet",
|
|
55
56
|
"_parse_pom": "Maven",
|
|
56
57
|
"_parse_gradle": "Maven",
|
|
58
|
+
"_parse_gemfile_lock": "RubyGems",
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
_SKIP_DIRS = frozenset(
|
|
@@ -354,6 +356,48 @@ class DependencyExtractor:
|
|
|
354
356
|
|
|
355
357
|
return packages
|
|
356
358
|
|
|
359
|
+
# -- Gemfile.lock (Ruby) ---------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def _parse_gemfile_lock(self, path: Path) -> list[PackageDependencyModel]:
|
|
362
|
+
"""Parse Gemfile.lock — resolved gems under each GEM/GIT/PATH ``specs:``.
|
|
363
|
+
|
|
364
|
+
A lockfile pins exact versions, so every gem yields a confident version.
|
|
365
|
+
Resolved gems sit at a 4-space indent as ``name (version)``; deeper
|
|
366
|
+
(6-space) lines are sub-dependency constraints and are skipped, as is the
|
|
367
|
+
2-space DEPENDENCIES section (ranges, not resolved). Platform suffixes
|
|
368
|
+
(``1.13.9-x86_64-linux``) are stripped — Ruby prereleases use dots
|
|
369
|
+
(``1.2.3.rc1``), so a hyphen in a lockfile version is always the platform.
|
|
370
|
+
"""
|
|
371
|
+
packages: list[PackageDependencyModel] = []
|
|
372
|
+
spec_re = re.compile(r"^ ([A-Za-z0-9._-]+) \(([^)]+)\)$")
|
|
373
|
+
try:
|
|
374
|
+
in_specs = False
|
|
375
|
+
for raw in path.read_text(errors="replace").splitlines():
|
|
376
|
+
if raw and not raw[0].isspace():
|
|
377
|
+
in_specs = False # top-level section (GEM, PLATFORMS, DEPENDENCIES, …)
|
|
378
|
+
continue
|
|
379
|
+
if raw.strip() == "specs:":
|
|
380
|
+
in_specs = True
|
|
381
|
+
continue
|
|
382
|
+
if not in_specs:
|
|
383
|
+
continue
|
|
384
|
+
m = spec_re.match(raw)
|
|
385
|
+
if not m:
|
|
386
|
+
continue # 6-space sub-dependency constraints, etc.
|
|
387
|
+
packages.append(
|
|
388
|
+
PackageDependencyModel(
|
|
389
|
+
name=m.group(1),
|
|
390
|
+
version=m.group(2).split("-", 1)[0].strip(),
|
|
391
|
+
version_constraint=None,
|
|
392
|
+
is_dev=False,
|
|
393
|
+
source_file=path.name,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.warning("Failed to parse %s: %s", path.name, e)
|
|
398
|
+
|
|
399
|
+
return packages
|
|
400
|
+
|
|
357
401
|
# -- Pipfile ---------------------------------------------------------------
|
|
358
402
|
|
|
359
403
|
def _parse_pipfile(self, path: Path) -> list[PackageDependencyModel]:
|