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.
Files changed (249) hide show
  1. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/PKG-INFO +3 -3
  2. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/README.md +2 -2
  3. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/pyproject.toml +1 -1
  4. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/analyzer.py +10 -10
  5. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/dependency_extractor.py +86 -18
  6. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/main.py +43 -5
  7. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/apisec_client.py +9 -2
  8. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/telemetry.py +6 -3
  9. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -1
  10. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/executor.py +1 -1
  11. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_cli.py +45 -1
  12. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_dependency_parsing.py +125 -4
  13. apisec_code_bolt-0.1.8/tests/unit/test_upload_verify_timeout.py +69 -0
  14. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/.gitignore +0 -0
  15. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__init__.py +0 -0
  16. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__main__.py +0 -0
  17. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/__init__.py +0 -0
  18. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
  19. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
  20. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
  21. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
  22. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
  23. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
  24. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/data_flow.py +0 -0
  25. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
  26. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
  27. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
  28. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
  29. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
  30. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
  31. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
  32. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
  33. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
  34. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
  35. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
  36. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/__init__.py +0 -0
  37. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
  38. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/__init__.py +0 -0
  39. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/client.py +0 -0
  40. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/__init__.py +0 -0
  41. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/config.py +0 -0
  42. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/credentials.py +0 -0
  43. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/discovery.py +0 -0
  44. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/log_format.py +0 -0
  45. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/manifest.py +0 -0
  46. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/repo.py +0 -0
  47. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/state.py +0 -0
  48. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/types.py +0 -0
  49. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
  50. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
  51. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
  52. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
  53. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
  54. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/base.py +0 -0
  55. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
  56. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
  57. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
  58. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
  59. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
  60. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
  61. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
  62. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
  63. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
  64. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
  65. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
  66. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
  67. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
  68. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
  69. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
  70. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
  71. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
  72. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
  73. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
  74. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
  75. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
  76. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
  77. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
  78. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
  79. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
  80. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
  81. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
  82. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
  83. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
  84. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
  85. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
  86. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
  87. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
  88. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
  89. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
  90. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
  91. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
  92. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
  93. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
  94. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +0 -0
  95. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/__init__.py +0 -0
  96. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/base.py +0 -0
  97. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
  98. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
  99. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
  100. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
  101. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
  102. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
  103. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
  104. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
  105. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
  106. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
  107. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
  108. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
  109. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
  110. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
  111. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
  112. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
  113. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
  114. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
  115. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
  116. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
  117. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
  118. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
  119. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
  120. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
  121. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
  122. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
  123. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
  124. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
  125. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/services.py +0 -0
  126. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/__init__.py +0 -0
  127. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/ast_cache.py +0 -0
  128. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/handlers.py +0 -0
  129. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/manifest_context.py +0 -0
  130. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/conftest.py +0 -0
  131. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Cargo.toml +0 -0
  132. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Gemfile.lock +0 -0
  133. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Pipfile +0 -0
  134. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle +0 -0
  135. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle.kts +0 -0
  136. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/go.mod +0 -0
  137. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/package.json +0 -0
  138. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pom.xml +0 -0
  139. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
  140. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
  141. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
  142. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements.txt +0 -0
  143. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.cfg +0 -0
  144. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.py +0 -0
  145. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/urls.py +0 -0
  146. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/views.py +0 -0
  147. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/app.ts +0 -0
  148. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
  149. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/auth.ts +0 -0
  150. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/users.ts +0 -0
  151. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/main.py +0 -0
  152. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
  153. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
  154. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
  155. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
  156. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask/app.py +0 -0
  157. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
  158. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_grape/api.rb +0 -0
  159. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
  160. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
  161. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
  162. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
  163. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_sinatra/app.rb +0 -0
  164. {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
  165. {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
  166. {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
  167. {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
  168. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
  169. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
  170. {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
  171. {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
  172. {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
  173. {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
  174. {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
  175. {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
  176. {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
  177. {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
  178. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
  179. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/fixtures/vulnerable_app/pom.xml +0 -0
  180. {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
  181. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/__init__.py +0 -0
  182. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_django_analysis.py +0 -0
  183. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_express_analysis.py +0 -0
  184. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_fastapi_analysis.py +0 -0
  185. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_flask_analysis.py +0 -0
  186. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_nestjs_analysis.py +0 -0
  187. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_spring_boot_analysis.py +0 -0
  188. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
  189. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/__init__.py +0 -0
  190. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/fixtures/sample_app.py +0 -0
  191. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_auth_indicator_matching.py +0 -0
  192. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_executor.py +0 -0
  193. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_handlers.py +0 -0
  194. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/test_query/test_read_site_origin_types.py +0 -0
  195. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/__init__.py +0 -0
  196. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_apisec_client.py +0 -0
  197. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_aspnet_plugin.py +0 -0
  198. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_audit.py +0 -0
  199. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_run.py +0 -0
  200. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_call_graph.py +0 -0
  201. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cbv_extractor.py +0 -0
  202. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cli_console_url.py +0 -0
  203. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_config.py +0 -0
  204. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_config_scanner_framework.py +0 -0
  205. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_constant_resolver.py +0 -0
  206. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_cross_file_resolver.py +0 -0
  207. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_literals.py +0 -0
  208. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_type_resolver.py +0 -0
  209. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_data_flow.py +0 -0
  210. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_discovery.py +0 -0
  211. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_django_plugin.py +0 -0
  212. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_dynamic_routes.py +0 -0
  213. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_express_nestjs_plugin.py +0 -0
  214. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_fastapi_plugin.py +0 -0
  215. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_fastify_plugin.py +0 -0
  216. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_flow_analysis.py +0 -0
  217. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_grape_plugin.py +0 -0
  218. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_graphql_plugin.py +0 -0
  219. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_grpc_plugin.py +0 -0
  220. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_instrumentation.py +0 -0
  221. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_integration_detection.py +0 -0
  222. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_java_parser.py +0 -0
  223. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_jaxrs_plugin.py +0 -0
  224. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_js_graphql_plugin.py +0 -0
  225. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_jwt_config_extraction.py +0 -0
  226. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
  227. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_manifest_upload_gzip.py +0 -0
  228. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_micronaut_plugin.py +0 -0
  229. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_parameter_analyzer.py +0 -0
  230. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_path_resolver.py +0 -0
  231. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_pydantic_body_inference.py +0 -0
  232. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_python_parser.py +0 -0
  233. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_rails_plugin.py +0 -0
  234. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_refit_plugin.py +0 -0
  235. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_repo_canonical.py +0 -0
  236. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_router_registry.py +0 -0
  237. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_ruby_graphql_plugin.py +0 -0
  238. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sensitivity_capability.py +0 -0
  239. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sinatra_plugin.py +0 -0
  240. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_sink_evidence.py +0 -0
  241. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_spring_plugin.py +0 -0
  242. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_static_route_catchup.py +0 -0
  243. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_enrichment.py +0 -0
  244. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_extraction_client.py +0 -0
  245. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_telemetry.py +0 -0
  246. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_transformation_tracking.py +0 -0
  247. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_types.py +0 -0
  248. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.8}/tests/unit/test_wcf_plugin.py +0 -0
  249. {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.7
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, opt-in usage telemetry (disabled by default; never includes
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, opt-in usage telemetry (disabled by default; never includes
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"
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 = any(
794
- call.callee_name.split(".")[-1] == "RequireAuthorization"
795
- and (call.receiver_expression or "").startswith("line:")
796
- and (
797
- lambda rv=call.receiver_expression: (
798
- rv[5:].isdigit() and int(rv[5:]) in chain_lines
799
- )
800
- )()
801
- for call in parsed_file.call_sites
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 (cached). None on any failure/offline."""
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
- data: bytes | None = None
868
- try:
869
- import httpx
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
- resp = httpx.get(url, timeout=8.0, follow_redirects=True)
872
- if resp.status_code == 200:
873
- data = resp.content
874
- except Exception as exc: # network/transport/anything → degrade to None
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 (opt-in): third-party deps (h2, mysql, jackson…)
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". When
1107
- # APISEC_RESOLVE_MAVEN_BOMS is set, fetch those POMs from Maven Central and
1108
- # resolve their pins; default off keeps the extractor offline/no-egress.
1109
- # Degrades cleanly to None on any fetch/parse failure.
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 os.environ.get("APISEC_RESOLVE_MAVEN_BOMS"):
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="Analysis timeout in seconds.",
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
- reasoning_engine_url=reasoning_url_override,
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
 
@@ -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
- _TELEMETRY_ENDPOINT = "https://api.apisec.ai/v1/surface/telemetry/events"
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(
@@ -52,7 +52,6 @@ class JavaExtractor:
52
52
  self.lines = source.splitlines()
53
53
 
54
54
  def extract(self) -> ParsedFile:
55
-
56
55
  start = time.perf_counter()
57
56
 
58
57
  module_name = self._get_package_name()
@@ -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
- # Default (flag unset): stays offline, no fetch, version None.
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
- "jackson-bom": j3_bom}.get(artifact)
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/>"