apisec-code-bolt 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/PKG-INFO +3 -3
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/README.md +2 -2
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/pyproject.toml +1 -1
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/analyzer.py +10 -10
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/dependency_extractor.py +86 -18
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/main.py +43 -5
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/apisec_client.py +9 -2
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/telemetry.py +6 -3
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -1
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/executor.py +1 -1
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_cli.py +45 -1
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_dependency_parsing.py +125 -4
- apisec_code_bolt-0.1.8/tests/unit/test_upload_verify_timeout.py +69 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/.gitignore +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__main__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/data_flow.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/client.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/config.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/credentials.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/discovery.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/log_format.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/manifest.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/repo.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/state.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/types.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/base.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/base.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/services.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/ast_cache.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/handlers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/manifest_context.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/conftest.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Cargo.toml +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Gemfile.lock +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Pipfile +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle.kts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/go.mod +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/package.json +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pom.xml +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements.txt +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.cfg +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/urls.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/views.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/app.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/auth.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/users.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/main.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask/app.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_grape/api.rb +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_sinatra/app.rb +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vulnerable_app/pom.xml +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vulnerable_app/src/main/java/com/demo/DemoController.java +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_django_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_express_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_fastapi_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_flask_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_nestjs_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_spring_boot_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/fixtures/sample_app.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_auth_indicator_matching.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_executor.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_handlers.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_read_site_origin_types.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/__init__.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_apisec_client.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_audit.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_run.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_call_graph.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cbv_extractor.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cli_console_url.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_config.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_config_scanner_framework.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_constant_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cross_file_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_literals.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_type_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_data_flow.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_discovery.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_django_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_dynamic_routes.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_express_nestjs_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_fastapi_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_fastify_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_flow_analysis.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_grape_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_grpc_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_instrumentation.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_integration_detection.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_java_parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_jaxrs_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_js_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_jwt_config_extraction.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_manifest_upload_gzip.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_micronaut_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_parameter_analyzer.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_path_resolver.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_pydantic_body_inference.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_python_parser.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_rails_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_refit_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_repo_canonical.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_router_registry.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_ruby_graphql_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sensitivity_capability.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sinatra_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sink_evidence.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_spring_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_static_route_catchup.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_enrichment.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_extraction_client.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_telemetry.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_transformation_tracking.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_types.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_wcf_plugin.py +0 -0
- {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/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.8
|
|
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
|
|
@@ -291,8 +291,8 @@ Options:
|
|
|
291
291
|
|
|
292
292
|
### telemetry
|
|
293
293
|
|
|
294
|
-
Manage anonymous
|
|
295
|
-
code, paths, or credentials).
|
|
294
|
+
Manage anonymous usage telemetry (opt-out; on by default, disable any time with
|
|
295
|
+
`telemetry off`; never includes code, paths, or credentials).
|
|
296
296
|
|
|
297
297
|
```bash
|
|
298
298
|
apisec-code-bolt telemetry on|off|status
|
|
@@ -244,8 +244,8 @@ Options:
|
|
|
244
244
|
|
|
245
245
|
### telemetry
|
|
246
246
|
|
|
247
|
-
Manage anonymous
|
|
248
|
-
code, paths, or credentials).
|
|
247
|
+
Manage anonymous usage telemetry (opt-out; on by default, disable any time with
|
|
248
|
+
`telemetry off`; never includes code, paths, or credentials).
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
251
|
apisec-code-bolt telemetry on|off|status
|
|
@@ -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.8"
|
|
8
8
|
description = "Static analysis probe for extracting architectural metadata from codebases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Proprietary"}
|
|
@@ -790,16 +790,16 @@ class ProjectAnalyzer:
|
|
|
790
790
|
changed = True
|
|
791
791
|
|
|
792
792
|
# Group-level auth: RequireAuthorization chained on the group
|
|
793
|
-
has_group_auth =
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
)()
|
|
801
|
-
|
|
802
|
-
|
|
793
|
+
has_group_auth = False
|
|
794
|
+
for call in parsed_file.call_sites:
|
|
795
|
+
if call.callee_name.split(".")[-1] != "RequireAuthorization":
|
|
796
|
+
continue
|
|
797
|
+
recv = call.receiver_expression or ""
|
|
798
|
+
if not recv.startswith("line:"):
|
|
799
|
+
continue
|
|
800
|
+
if recv[5:].isdigit() and int(recv[5:]) in chain_lines:
|
|
801
|
+
has_group_auth = True
|
|
802
|
+
break
|
|
803
803
|
|
|
804
804
|
# Find calls to custom Map* extension methods (non-standard, non-chained).
|
|
805
805
|
# These are calls like ``group.MapCreateBrandEndpoint()`` where the method
|
|
@@ -26,6 +26,25 @@ from ..parsing.base import ParsedFile
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _maven_bom_resolution_enabled() -> bool:
|
|
30
|
+
"""Whether to resolve parent/imported Maven BOM versions from Maven Central.
|
|
31
|
+
|
|
32
|
+
Defaults ON: without it, deps whose versions are managed by a parent POM or
|
|
33
|
+
imported BOM (Spring Boot's jackson-databind, hsqldb, …) come back
|
|
34
|
+
version=None and are invisible to CVE matching. Resolution fetches those POMs
|
|
35
|
+
(network egress) and degrades cleanly to None on any failure.
|
|
36
|
+
|
|
37
|
+
Opt out for air-gapped / no-egress runs with ``--offline`` (sets
|
|
38
|
+
``APISEC_OFFLINE``) or ``APISEC_RESOLVE_MAVEN_BOMS=0``.
|
|
39
|
+
"""
|
|
40
|
+
if os.environ.get("APISEC_OFFLINE", "").strip().lower() in ("1", "true", "yes"):
|
|
41
|
+
return False
|
|
42
|
+
flag = os.environ.get("APISEC_RESOLVE_MAVEN_BOMS")
|
|
43
|
+
if flag is not None:
|
|
44
|
+
return flag.strip().lower() not in ("0", "false", "no", "")
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
29
48
|
class DependencyExtractor:
|
|
30
49
|
"""Extracts package dependencies and internal imports from a project."""
|
|
31
50
|
|
|
@@ -854,27 +873,75 @@ class DependencyExtractor:
|
|
|
854
873
|
|
|
855
874
|
_MAVEN_CENTRAL = "https://repo1.maven.org/maven2"
|
|
856
875
|
_bom_pom_cache: dict[str, bytes | None] = {}
|
|
876
|
+
_BOM_FETCH_ATTEMPTS = 3
|
|
877
|
+
|
|
878
|
+
def _bom_cache_dir(self) -> Path:
|
|
879
|
+
base = os.environ.get("APISEC_BOM_CACHE_DIR") or os.path.join(
|
|
880
|
+
os.path.expanduser("~"), ".cache", "apisec-code-bolt", "maven-boms"
|
|
881
|
+
)
|
|
882
|
+
return Path(base)
|
|
857
883
|
|
|
858
884
|
def _fetch_pom(self, group: str, artifact: str, version: str) -> bytes | None:
|
|
859
|
-
"""Fetch a POM from Maven Central
|
|
885
|
+
"""Fetch a POM from Maven Central, resiliently. None on definitive miss
|
|
886
|
+
(404) or offline; a transient network failure is retried and NOT cached,
|
|
887
|
+
so a blip can't silently null out a managed version for the whole run.
|
|
888
|
+
|
|
889
|
+
Two cache tiers: an in-process dict and an on-disk cache. Version-pinned
|
|
890
|
+
POMs are immutable, so a cached copy is always valid — and it makes
|
|
891
|
+
resolution resilient to a flaky network (once fetched, reused forever).
|
|
892
|
+
"""
|
|
860
893
|
key = f"{group}:{artifact}:{version}"
|
|
861
894
|
if key in self._bom_pom_cache:
|
|
862
895
|
return self._bom_pom_cache[key]
|
|
896
|
+
|
|
897
|
+
cache_file = self._bom_cache_dir() / group.replace(".", "/") / artifact / f"{version}.pom"
|
|
898
|
+
try:
|
|
899
|
+
if cache_file.is_file():
|
|
900
|
+
data = cache_file.read_bytes()
|
|
901
|
+
self._bom_pom_cache[key] = data
|
|
902
|
+
return data
|
|
903
|
+
except Exception: # unreadable cache file → fall through to fetch
|
|
904
|
+
pass
|
|
905
|
+
|
|
863
906
|
url = (
|
|
864
907
|
f"{self._MAVEN_CENTRAL}/{group.replace('.', '/')}/"
|
|
865
908
|
f"{artifact}/{version}/{artifact}-{version}.pom"
|
|
866
909
|
)
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
910
|
+
import time
|
|
911
|
+
|
|
912
|
+
for attempt in range(1, self._BOM_FETCH_ATTEMPTS + 1):
|
|
913
|
+
try:
|
|
914
|
+
import httpx
|
|
915
|
+
|
|
916
|
+
resp = httpx.get(url, timeout=8.0, follow_redirects=True)
|
|
917
|
+
if resp.status_code == 200:
|
|
918
|
+
data = resp.content
|
|
919
|
+
self._bom_pom_cache[key] = data
|
|
920
|
+
try: # best-effort persist; immutable so safe to keep
|
|
921
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
922
|
+
cache_file.write_bytes(data)
|
|
923
|
+
except Exception:
|
|
924
|
+
pass
|
|
925
|
+
return data
|
|
926
|
+
if resp.status_code == 404:
|
|
927
|
+
self._bom_pom_cache[key] = None # definitive miss; don't retry
|
|
928
|
+
return None
|
|
929
|
+
# other statuses (5xx, 429, …) are transient → retry
|
|
930
|
+
except Exception as exc: # network/transport → retry
|
|
931
|
+
logger.debug(
|
|
932
|
+
"BOM POM fetch attempt %d/%d failed for %s: %s",
|
|
933
|
+
attempt,
|
|
934
|
+
self._BOM_FETCH_ATTEMPTS,
|
|
935
|
+
key,
|
|
936
|
+
exc,
|
|
937
|
+
)
|
|
938
|
+
if attempt < self._BOM_FETCH_ATTEMPTS:
|
|
939
|
+
time.sleep(0.5 * attempt) # linear backoff
|
|
870
940
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
logger.debug("BOM POM fetch failed for %s: %s", key, exc)
|
|
876
|
-
self._bom_pom_cache[key] = data
|
|
877
|
-
return data
|
|
941
|
+
# Transient failure exhausted retries. Degrade to None but do NOT cache it,
|
|
942
|
+
# so a later call (or run) can retry rather than being poisoned to null.
|
|
943
|
+
logger.debug("BOM POM fetch exhausted retries for %s", key)
|
|
944
|
+
return None
|
|
878
945
|
|
|
879
946
|
def _remote_managed_versions(
|
|
880
947
|
self, seeds: list[tuple[str, str, str]], seed_props: dict[str, str]
|
|
@@ -921,7 +988,7 @@ class DependencyExtractor:
|
|
|
921
988
|
pe = el.find(q("properties"))
|
|
922
989
|
if pe is not None:
|
|
923
990
|
for c in pe:
|
|
924
|
-
k = c.tag[len(ns):] if ns else c.tag
|
|
991
|
+
k = c.tag[len(ns) :] if ns else c.tag
|
|
925
992
|
if c.text:
|
|
926
993
|
own[k] = c.text.strip()
|
|
927
994
|
own.setdefault("project.version", v)
|
|
@@ -1100,15 +1167,16 @@ class DependencyExtractor:
|
|
|
1100
1167
|
if parent_group and parent_artifact and parent_version:
|
|
1101
1168
|
parent_coord = (parent_group, parent_artifact, parent_version)
|
|
1102
1169
|
|
|
1103
|
-
# Imported-BOM resolution (
|
|
1170
|
+
# Imported-BOM resolution (default ON): third-party deps (h2, mysql, jackson…)
|
|
1104
1171
|
# get their versions from the parent chain / imported BOMs (e.g.
|
|
1105
1172
|
# spring-boot-dependencies), NOT this file, so they'd otherwise be
|
|
1106
|
-
# version=None → silently treated downstream as "not affected"
|
|
1107
|
-
#
|
|
1108
|
-
#
|
|
1109
|
-
# Degrades cleanly to None on any
|
|
1173
|
+
# version=None → silently treated downstream as "not affected", hiding
|
|
1174
|
+
# real CVEs. Fetch those POMs from Maven Central and resolve their pins;
|
|
1175
|
+
# opt out for air-gapped runs via --offline (see
|
|
1176
|
+
# _maven_bom_resolution_enabled). Degrades cleanly to None on any
|
|
1177
|
+
# fetch/parse failure.
|
|
1110
1178
|
remote_managed: dict[str, str] = {}
|
|
1111
|
-
if
|
|
1179
|
+
if _maven_bom_resolution_enabled():
|
|
1112
1180
|
seeds = ([parent_coord] if parent_coord else []) + import_boms
|
|
1113
1181
|
if seeds:
|
|
1114
1182
|
remote_managed = self._remote_managed_versions(seeds, props)
|
|
@@ -6,6 +6,8 @@ This module defines the CLI commands using Click.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import contextlib
|
|
10
|
+
import os
|
|
9
11
|
import sys
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Any
|
|
@@ -150,7 +152,11 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, log_format:
|
|
|
150
152
|
@click.option(
|
|
151
153
|
"--timeout",
|
|
152
154
|
type=int,
|
|
153
|
-
help=
|
|
155
|
+
help=(
|
|
156
|
+
"Timeout in seconds for local analysis and, on upload, for waiting on "
|
|
157
|
+
"cloud verification. Large apps may need a higher value; a client "
|
|
158
|
+
"timeout never stops the analysis, which continues server-side."
|
|
159
|
+
),
|
|
154
160
|
)
|
|
155
161
|
@click.option(
|
|
156
162
|
"--dry-run",
|
|
@@ -163,6 +169,13 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, log_format:
|
|
|
163
169
|
is_flag=True,
|
|
164
170
|
help="Write manifest JSON to stdout instead of a file (for pipeline use).",
|
|
165
171
|
)
|
|
172
|
+
@click.option(
|
|
173
|
+
"--offline",
|
|
174
|
+
is_flag=True,
|
|
175
|
+
help="No network egress during extraction. Disables Maven BOM version "
|
|
176
|
+
"resolution (managed deps stay version=None, so their CVEs can't be matched). "
|
|
177
|
+
"Use for air-gapped runs.",
|
|
178
|
+
)
|
|
166
179
|
@click.pass_context
|
|
167
180
|
def analyze(
|
|
168
181
|
ctx: click.Context,
|
|
@@ -181,8 +194,14 @@ def analyze(
|
|
|
181
194
|
timeout: int | None,
|
|
182
195
|
dry_run: bool = False,
|
|
183
196
|
output_stdout: bool = False,
|
|
197
|
+
offline: bool = False,
|
|
184
198
|
) -> None:
|
|
185
199
|
log_format: str = ctx.obj.get("log_format", "text")
|
|
200
|
+
# Maven BOM version resolution is ON by default (resolves parent/BOM-managed
|
|
201
|
+
# dep versions so their CVEs are matchable). --offline opts out for
|
|
202
|
+
# no-egress environments; the extractor reads APISEC_OFFLINE.
|
|
203
|
+
if offline:
|
|
204
|
+
os.environ["APISEC_OFFLINE"] = "1"
|
|
186
205
|
"""
|
|
187
206
|
Analyze a codebase and generate a manifest.
|
|
188
207
|
|
|
@@ -250,8 +269,6 @@ def analyze(
|
|
|
250
269
|
)
|
|
251
270
|
|
|
252
271
|
if needs_registration():
|
|
253
|
-
import os
|
|
254
|
-
|
|
255
272
|
env_code = os.environ.get("APISEC_REGISTRATION_CODE", "").strip()
|
|
256
273
|
if env_code:
|
|
257
274
|
code = env_code
|
|
@@ -364,7 +381,7 @@ def analyze(
|
|
|
364
381
|
output_path = apisec_dir / "manifest.json"
|
|
365
382
|
manifest_json = result.manifest.to_json(pretty=True)
|
|
366
383
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
367
|
-
output_path.write_text(manifest_json)
|
|
384
|
+
output_path.write_text(manifest_json, encoding="utf-8")
|
|
368
385
|
|
|
369
386
|
if log_format == "json":
|
|
370
387
|
_emit_json_summary(path, result, output_path=output_path)
|
|
@@ -428,6 +445,7 @@ def analyze(
|
|
|
428
445
|
api_url_override,
|
|
429
446
|
reasoning_url,
|
|
430
447
|
quiet,
|
|
448
|
+
verify_timeout=timeout,
|
|
431
449
|
)
|
|
432
450
|
elif cloud_url:
|
|
433
451
|
# Legacy: direct connection to reasoning engine (for local dev)
|
|
@@ -517,6 +535,7 @@ def _run_authenticated_upload(
|
|
|
517
535
|
api_url_override: str | None,
|
|
518
536
|
reasoning_url_override: str | None,
|
|
519
537
|
quiet: bool,
|
|
538
|
+
verify_timeout: int | None = None,
|
|
520
539
|
) -> None:
|
|
521
540
|
"""Register surface with applicationsservice (PAT), then upload manifest to the reasoning engine (same PAT + app_id)."""
|
|
522
541
|
import json
|
|
@@ -580,11 +599,14 @@ def _run_authenticated_upload(
|
|
|
580
599
|
write_state(project_path, RepoState(application_id=registration.app_id))
|
|
581
600
|
|
|
582
601
|
manifest_data = json.loads(result.manifest.to_json())
|
|
602
|
+
upload_kwargs: dict[str, Any] = {"reasoning_engine_url": reasoning_url_override}
|
|
603
|
+
if verify_timeout:
|
|
604
|
+
upload_kwargs["max_wait"] = verify_timeout
|
|
583
605
|
upload_result = client.upload_and_verify(
|
|
584
606
|
registration,
|
|
585
607
|
manifest_data,
|
|
586
608
|
project_path,
|
|
587
|
-
|
|
609
|
+
**upload_kwargs,
|
|
588
610
|
)
|
|
589
611
|
|
|
590
612
|
if not quiet:
|
|
@@ -601,6 +623,14 @@ def _run_authenticated_upload(
|
|
|
601
623
|
vtable.add_row("Error", f"[red]{upload_result['error']}[/red]")
|
|
602
624
|
console.print(vtable)
|
|
603
625
|
|
|
626
|
+
if upload_result.get("status") == "timeout":
|
|
627
|
+
console.print(
|
|
628
|
+
"[yellow]Stopped waiting for verification, but the analysis is "
|
|
629
|
+
"still running server-side[/yellow] — results will appear at the "
|
|
630
|
+
"console link below shortly. This is common for large apps; pass "
|
|
631
|
+
"[cyan]--timeout <seconds>[/cyan] to wait longer."
|
|
632
|
+
)
|
|
633
|
+
|
|
604
634
|
console.print()
|
|
605
635
|
view_url = _resolve_console_url(registration.console_url, registration.app_id)
|
|
606
636
|
panel_body = view_url
|
|
@@ -1097,6 +1127,14 @@ cloud:
|
|
|
1097
1127
|
|
|
1098
1128
|
def main() -> None:
|
|
1099
1129
|
"""Main entry point."""
|
|
1130
|
+
# Force UTF-8 on stdio so non-ASCII manifest/JSON output (e.g. the U+2192
|
|
1131
|
+
# arrow in data-flow output) does not crash on Windows consoles that
|
|
1132
|
+
# default to cp1252.
|
|
1133
|
+
for stream in (sys.stdout, sys.stderr):
|
|
1134
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
1135
|
+
if reconfigure is not None:
|
|
1136
|
+
with contextlib.suppress(ValueError, OSError):
|
|
1137
|
+
reconfigure(encoding="utf-8")
|
|
1100
1138
|
cli(obj={})
|
|
1101
1139
|
|
|
1102
1140
|
|
{apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/apisec_client.py
RENAMED
|
@@ -106,8 +106,15 @@ class ApisecClient:
|
|
|
106
106
|
manifest_data: dict[str, Any],
|
|
107
107
|
project_root: Path,
|
|
108
108
|
reasoning_engine_url: str | None = None,
|
|
109
|
+
max_wait: int = 900,
|
|
109
110
|
) -> dict[str, Any]:
|
|
110
|
-
"""Upload manifest to reasoning engine with Bearer PAT and app_id; run verification.
|
|
111
|
+
"""Upload manifest to reasoning engine with Bearer PAT and app_id; run verification.
|
|
112
|
+
|
|
113
|
+
``max_wait`` bounds how long the client polls for verification to
|
|
114
|
+
finish. Large apps (many endpoints / LLM verification) can legitimately
|
|
115
|
+
take several minutes, so this defaults generously; a client-side
|
|
116
|
+
timeout does not stop the analysis, which continues server-side.
|
|
117
|
+
"""
|
|
111
118
|
engine_url = reasoning_engine_url or self._api_url
|
|
112
119
|
with CloudClient(
|
|
113
120
|
api_url=engine_url,
|
|
@@ -120,7 +127,7 @@ class ApisecClient:
|
|
|
120
127
|
|
|
121
128
|
maybe_enrich_manifest(manifest_data, project_root, client)
|
|
122
129
|
|
|
123
|
-
executor = QueryExecutor(project_root=project_root)
|
|
130
|
+
executor = QueryExecutor(project_root=project_root, max_wait=max_wait)
|
|
124
131
|
result = executor.run_connected(client, manifest_data)
|
|
125
132
|
|
|
126
133
|
return {
|
|
@@ -17,6 +17,7 @@ PII GUARD:
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
+
import os
|
|
20
21
|
import threading
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any
|
|
@@ -267,8 +268,6 @@ def get_registration_code() -> str:
|
|
|
267
268
|
Checks APISEC_REGISTRATION_CODE env var first so CI/automated environments
|
|
268
269
|
can bypass the interactive first-run prompt without writing a config file.
|
|
269
270
|
"""
|
|
270
|
-
import os
|
|
271
|
-
|
|
272
271
|
env_code = os.environ.get("APISEC_REGISTRATION_CODE", "").strip()
|
|
273
272
|
if env_code:
|
|
274
273
|
return env_code
|
|
@@ -335,7 +334,11 @@ def emit_install_event(*, probe_version: str = "") -> None:
|
|
|
335
334
|
threading.Thread(target=_send, daemon=True).start()
|
|
336
335
|
|
|
337
336
|
|
|
338
|
-
|
|
337
|
+
# Overridable for non-prod testing (staging/dev receivers), mirroring the
|
|
338
|
+
# analyze path's --api-url. Defaults to prod, so unset means no change.
|
|
339
|
+
_TELEMETRY_ENDPOINT = os.environ.get(
|
|
340
|
+
"APISEC_TELEMETRY_ENDPOINT", "https://api.apisec.ai/v1/surface/telemetry/events"
|
|
341
|
+
)
|
|
339
342
|
|
|
340
343
|
|
|
341
344
|
def emit_analyze_event(
|
|
@@ -246,7 +246,7 @@ class QueryExecutor:
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
249
|
-
output_file.write_text(json.dumps(output, indent=2, default=str))
|
|
249
|
+
output_file.write_text(json.dumps(output, indent=2, default=str), encoding="utf-8")
|
|
250
250
|
|
|
251
251
|
result.final_status = "complete"
|
|
252
252
|
logger.info(
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""CLI smoke tests using Click's CliRunner."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
from unittest.mock import patch
|
|
4
6
|
|
|
5
7
|
import pytest
|
|
6
8
|
from click.testing import CliRunner
|
|
7
9
|
|
|
8
10
|
from apisec_code_bolt.cli.main import cli
|
|
11
|
+
from apisec_code_bolt.core.manifest import Manifest
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@pytest.fixture
|
|
@@ -64,11 +67,52 @@ class TestAnalyzeCommand:
|
|
|
64
67
|
)
|
|
65
68
|
assert result.exit_code == 0
|
|
66
69
|
assert output.exists()
|
|
67
|
-
import json
|
|
68
70
|
|
|
69
71
|
data = json.loads(output.read_text())
|
|
70
72
|
assert "entry_points" in data
|
|
71
73
|
|
|
74
|
+
def test_analyze_writes_non_ascii_manifest_on_cp1252_locale(
|
|
75
|
+
self, runner, sample_project, tmp_path, monkeypatch
|
|
76
|
+
):
|
|
77
|
+
"""Regression: the manifest must be written UTF-8 even when the OS
|
|
78
|
+
locale default is cp1252 (Windows), so non-ASCII data-flow output such
|
|
79
|
+
as the U+2192 arrow does not crash `analyze`. See BMO support issue.
|
|
80
|
+
"""
|
|
81
|
+
# The tool emits U+2192 in data-flow output; force it into the manifest
|
|
82
|
+
# so the test does not depend on what the analyzer naturally produces.
|
|
83
|
+
monkeypatch.setattr(
|
|
84
|
+
Manifest,
|
|
85
|
+
"to_json",
|
|
86
|
+
lambda self, pretty=False: '{"note": "source \u2192 sink"}',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Reproduce the Windows behaviour on any platform: a write_text() call
|
|
90
|
+
# with no explicit encoding falls back to cp1252, which cannot encode
|
|
91
|
+
# the arrow. The fix passes encoding="utf-8" explicitly, bypassing this.
|
|
92
|
+
real_write_text = Path.write_text
|
|
93
|
+
|
|
94
|
+
def cp1252_default_write_text(self, data, encoding=None, **kwargs):
|
|
95
|
+
if encoding is None:
|
|
96
|
+
encoding = "cp1252"
|
|
97
|
+
return real_write_text(self, data, encoding=encoding, **kwargs)
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(Path, "write_text", cp1252_default_write_text)
|
|
100
|
+
|
|
101
|
+
output = tmp_path / "out.json"
|
|
102
|
+
result = runner.invoke(
|
|
103
|
+
cli,
|
|
104
|
+
[
|
|
105
|
+
"analyze",
|
|
106
|
+
str(sample_project),
|
|
107
|
+
"--no-upload",
|
|
108
|
+
"--output",
|
|
109
|
+
str(output),
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
assert result.exit_code == 0, result.output
|
|
113
|
+
assert output.exists()
|
|
114
|
+
assert "\u2192" in output.read_text(encoding="utf-8")
|
|
115
|
+
|
|
72
116
|
def test_analyze_nonexistent_path(self, runner, tmp_path):
|
|
73
117
|
result = runner.invoke(
|
|
74
118
|
cli,
|
|
@@ -404,13 +404,16 @@ class TestPomXml:
|
|
|
404
404
|
assert by_name["org.springframework.boot:spring-boot-starter-web"].version is None
|
|
405
405
|
|
|
406
406
|
def test_parent_bom_version_inherited_for_parent_groupid(
|
|
407
|
-
self, analyzer: DependencyExtractor, tmp_path: Path
|
|
407
|
+
self, analyzer: DependencyExtractor, tmp_path: Path, monkeypatch
|
|
408
408
|
) -> None:
|
|
409
409
|
# Spring Boot starters omit <version> and inherit it from the <parent>
|
|
410
410
|
# (spring-boot-starter-parent). The parent's own-groupId artifacts must
|
|
411
411
|
# resolve to the parent version; third-party deps must NOT (their versions
|
|
412
412
|
# come from the imported BOM, not the parent). Regression for the
|
|
413
413
|
# null-version → "VERSION_NOT_AFFECTED" false-clean.
|
|
414
|
+
# Offline so this isolates the LOCAL parent-group logic; with BOM
|
|
415
|
+
# resolution on (the default) h2 would resolve from the remote BOM.
|
|
416
|
+
monkeypatch.setenv("APISEC_OFFLINE", "1")
|
|
414
417
|
pom = tmp_path / "pom.xml"
|
|
415
418
|
pom.write_text(
|
|
416
419
|
"""<?xml version="1.0"?>
|
|
@@ -501,8 +504,9 @@ class TestPomXml:
|
|
|
501
504
|
def test_imported_bom_not_resolved_when_opted_out(
|
|
502
505
|
self, analyzer: DependencyExtractor, tmp_path: Path, monkeypatch
|
|
503
506
|
) -> None:
|
|
504
|
-
#
|
|
507
|
+
# Opted out (--offline / APISEC_OFFLINE): stays offline, no fetch, version None.
|
|
505
508
|
monkeypatch.delenv("APISEC_RESOLVE_MAVEN_BOMS", raising=False)
|
|
509
|
+
monkeypatch.setenv("APISEC_OFFLINE", "1")
|
|
506
510
|
|
|
507
511
|
def boom(self, *a, **k): # _fetch_pom must NOT be called when opted out
|
|
508
512
|
raise AssertionError("network fetch attempted while opted out")
|
|
@@ -558,8 +562,9 @@ class TestPomXml:
|
|
|
558
562
|
</project>"""
|
|
559
563
|
|
|
560
564
|
def fake_fetch(self, group, artifact, version): # noqa: ANN001
|
|
561
|
-
return {"starter-parent": parent_pom, "deps-bom": deps_bom,
|
|
562
|
-
|
|
565
|
+
return {"starter-parent": parent_pom, "deps-bom": deps_bom, "jackson-bom": j3_bom}.get(
|
|
566
|
+
artifact
|
|
567
|
+
)
|
|
563
568
|
|
|
564
569
|
monkeypatch.setattr(DependencyExtractor, "_fetch_pom", fake_fetch)
|
|
565
570
|
monkeypatch.setenv("APISEC_RESOLVE_MAVEN_BOMS", "1")
|
|
@@ -762,3 +767,119 @@ class TestGemfileLock:
|
|
|
762
767
|
by_name = {p.name: p for p in DependencyExtractor(tmp_path, {})._parse_gemfile_lock(lock)}
|
|
763
768
|
assert by_name["cool-gem"].version == "1.2.3"
|
|
764
769
|
assert by_name["my-local"].version == "0.1.0"
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# --- Maven BOM resolution default (--offline opt-out) -----------------------
|
|
773
|
+
|
|
774
|
+
from apisec_code_bolt.analysis.dependency_extractor import ( # noqa: E402
|
|
775
|
+
_maven_bom_resolution_enabled,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
class TestMavenBomResolutionGate:
|
|
780
|
+
"""BOM version resolution defaults ON; opt out via --offline / env."""
|
|
781
|
+
|
|
782
|
+
def test_default_on(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
783
|
+
monkeypatch.delenv("APISEC_OFFLINE", raising=False)
|
|
784
|
+
monkeypatch.delenv("APISEC_RESOLVE_MAVEN_BOMS", raising=False)
|
|
785
|
+
assert _maven_bom_resolution_enabled() is True
|
|
786
|
+
|
|
787
|
+
def test_offline_disables(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
788
|
+
monkeypatch.delenv("APISEC_RESOLVE_MAVEN_BOMS", raising=False)
|
|
789
|
+
for val in ("1", "true", "YES"):
|
|
790
|
+
monkeypatch.setenv("APISEC_OFFLINE", val)
|
|
791
|
+
assert _maven_bom_resolution_enabled() is False
|
|
792
|
+
|
|
793
|
+
def test_env_explicit_disable(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
794
|
+
monkeypatch.delenv("APISEC_OFFLINE", raising=False)
|
|
795
|
+
for val in ("0", "false", "no", ""):
|
|
796
|
+
monkeypatch.setenv("APISEC_RESOLVE_MAVEN_BOMS", val)
|
|
797
|
+
assert _maven_bom_resolution_enabled() is False
|
|
798
|
+
|
|
799
|
+
def test_env_explicit_enable(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
800
|
+
monkeypatch.delenv("APISEC_OFFLINE", raising=False)
|
|
801
|
+
monkeypatch.setenv("APISEC_RESOLVE_MAVEN_BOMS", "1")
|
|
802
|
+
assert _maven_bom_resolution_enabled() is True
|
|
803
|
+
|
|
804
|
+
def test_offline_wins_over_resolve_flag(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
805
|
+
monkeypatch.setenv("APISEC_OFFLINE", "1")
|
|
806
|
+
monkeypatch.setenv("APISEC_RESOLVE_MAVEN_BOMS", "1")
|
|
807
|
+
assert _maven_bom_resolution_enabled() is False
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# --- BOM POM fetch: retry + cache resilience --------------------------------
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
class TestBomPomFetchResilience:
|
|
814
|
+
def _extractor(self, tmp_path, monkeypatch):
|
|
815
|
+
monkeypatch.setenv("APISEC_BOM_CACHE_DIR", str(tmp_path / "bomcache"))
|
|
816
|
+
e = DependencyExtractor(tmp_path, {})
|
|
817
|
+
e._bom_pom_cache.clear() # class-level; isolate per test
|
|
818
|
+
return e
|
|
819
|
+
|
|
820
|
+
def test_retries_transient_failure_then_succeeds(self, tmp_path, monkeypatch):
|
|
821
|
+
e = self._extractor(tmp_path, monkeypatch)
|
|
822
|
+
calls = {"n": 0}
|
|
823
|
+
|
|
824
|
+
class Resp:
|
|
825
|
+
status_code = 200
|
|
826
|
+
content = b"<project/>"
|
|
827
|
+
|
|
828
|
+
def flaky_get(url, **kw):
|
|
829
|
+
calls["n"] += 1
|
|
830
|
+
if calls["n"] < 3:
|
|
831
|
+
raise RuntimeError("connection reset")
|
|
832
|
+
return Resp()
|
|
833
|
+
|
|
834
|
+
import httpx
|
|
835
|
+
|
|
836
|
+
monkeypatch.setattr(httpx, "get", flaky_get)
|
|
837
|
+
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
838
|
+
assert e._fetch_pom("g", "a", "1.0") == b"<project/>"
|
|
839
|
+
assert calls["n"] == 3 # retried twice, succeeded on the third
|
|
840
|
+
|
|
841
|
+
def test_transient_failure_not_cached(self, tmp_path, monkeypatch):
|
|
842
|
+
e = self._extractor(tmp_path, monkeypatch)
|
|
843
|
+
import httpx
|
|
844
|
+
|
|
845
|
+
monkeypatch.setattr(
|
|
846
|
+
httpx, "get", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("down"))
|
|
847
|
+
)
|
|
848
|
+
monkeypatch.setattr("time.sleep", lambda *_: None)
|
|
849
|
+
assert e._fetch_pom("g", "a", "1.0") is None
|
|
850
|
+
assert "g:a:1.0" not in e._bom_pom_cache # not poisoned → retriable later
|
|
851
|
+
|
|
852
|
+
def test_404_is_definitive_and_not_retried(self, tmp_path, monkeypatch):
|
|
853
|
+
e = self._extractor(tmp_path, monkeypatch)
|
|
854
|
+
calls = {"n": 0}
|
|
855
|
+
|
|
856
|
+
class Resp404:
|
|
857
|
+
status_code = 404
|
|
858
|
+
content = b""
|
|
859
|
+
|
|
860
|
+
def get(url, **kw):
|
|
861
|
+
calls["n"] += 1
|
|
862
|
+
return Resp404()
|
|
863
|
+
|
|
864
|
+
import httpx
|
|
865
|
+
|
|
866
|
+
monkeypatch.setattr(httpx, "get", get)
|
|
867
|
+
assert e._fetch_pom("g", "a", "1.0") is None
|
|
868
|
+
assert calls["n"] == 1 # not retried
|
|
869
|
+
assert e._bom_pom_cache["g:a:1.0"] is None # cached as definitive miss
|
|
870
|
+
|
|
871
|
+
def test_disk_cache_avoids_refetch(self, tmp_path, monkeypatch):
|
|
872
|
+
e = self._extractor(tmp_path, monkeypatch)
|
|
873
|
+
import httpx
|
|
874
|
+
|
|
875
|
+
monkeypatch.setattr(
|
|
876
|
+
httpx, "get", lambda *a, **k: type("R", (), {"status_code": 200, "content": b"<p/>"})()
|
|
877
|
+
)
|
|
878
|
+
assert e._fetch_pom("g", "a", "1.0") == b"<p/>"
|
|
879
|
+
# new extractor (fresh in-mem cache), network now hard-fails → must serve from disk
|
|
880
|
+
e2 = DependencyExtractor(tmp_path, {})
|
|
881
|
+
e2._bom_pom_cache.clear()
|
|
882
|
+
monkeypatch.setattr(
|
|
883
|
+
httpx, "get", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not fetch"))
|
|
884
|
+
)
|
|
885
|
+
assert e2._fetch_pom("g", "a", "1.0") == b"<p/>"
|