apisec-code-bolt 0.1.6__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.6 → apisec_code_bolt-0.1.8}/PKG-INFO +3 -3
  2. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/README.md +2 -2
  3. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/pyproject.toml +1 -1
  4. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/analyzer.py +10 -10
  5. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/dependency_extractor.py +199 -89
  6. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/main.py +43 -5
  7. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/apisec_client.py +9 -2
  8. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/telemetry.py +6 -3
  9. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -1
  10. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/executor.py +1 -1
  11. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_cli.py +45 -1
  12. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_dependency_parsing.py +178 -2
  13. apisec_code_bolt-0.1.8/tests/unit/test_upload_verify_timeout.py +69 -0
  14. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/.gitignore +0 -0
  15. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__init__.py +0 -0
  16. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/__main__.py +0 -0
  17. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/__init__.py +0 -0
  18. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
  19. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
  20. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
  21. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
  22. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
  23. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
  24. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/data_flow.py +0 -0
  25. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
  26. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
  27. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
  28. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
  29. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
  30. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
  31. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
  32. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
  33. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
  34. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
  35. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
  36. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/__init__.py +0 -0
  37. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
  38. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/__init__.py +0 -0
  39. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/cloud/client.py +0 -0
  40. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/__init__.py +0 -0
  41. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/config.py +0 -0
  42. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/credentials.py +0 -0
  43. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/discovery.py +0 -0
  44. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/log_format.py +0 -0
  45. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/manifest.py +0 -0
  46. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/repo.py +0 -0
  47. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/state.py +0 -0
  48. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/core/types.py +0 -0
  49. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
  50. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
  51. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
  52. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
  53. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
  54. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/base.py +0 -0
  55. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
  56. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
  57. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
  58. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
  59. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
  60. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
  61. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
  62. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
  63. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
  64. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
  65. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
  66. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
  67. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
  68. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
  69. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
  70. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
  71. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
  72. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
  73. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
  74. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
  75. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
  76. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
  77. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
  78. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
  79. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
  80. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
  81. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
  82. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
  83. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
  84. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
  85. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
  86. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
  87. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
  88. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
  89. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
  90. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
  91. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
  92. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
  93. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
  94. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +0 -0
  95. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/__init__.py +0 -0
  96. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/base.py +0 -0
  97. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
  98. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
  99. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
  100. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
  101. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
  102. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
  103. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
  104. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
  105. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
  106. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
  107. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
  108. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
  109. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
  110. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
  111. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
  112. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
  113. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
  114. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
  115. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
  116. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
  117. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
  118. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
  119. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
  120. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
  121. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
  122. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
  123. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
  124. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
  125. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/parsing/services.py +0 -0
  126. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/__init__.py +0 -0
  127. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/ast_cache.py +0 -0
  128. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/handlers.py +0 -0
  129. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/src/apisec_code_bolt/query/manifest_context.py +0 -0
  130. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/conftest.py +0 -0
  131. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Cargo.toml +0 -0
  132. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Gemfile.lock +0 -0
  133. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/Pipfile +0 -0
  134. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle +0 -0
  135. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/build.gradle.kts +0 -0
  136. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/go.mod +0 -0
  137. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/package.json +0 -0
  138. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pom.xml +0 -0
  139. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
  140. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
  141. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
  142. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/requirements.txt +0 -0
  143. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.cfg +0 -0
  144. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/dep_files/setup.py +0 -0
  145. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/urls.py +0 -0
  146. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_django/views.py +0 -0
  147. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/app.ts +0 -0
  148. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
  149. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/auth.ts +0 -0
  150. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_express/routes/users.ts +0 -0
  151. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/main.py +0 -0
  152. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
  153. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
  154. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
  155. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
  156. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask/app.py +0 -0
  157. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
  158. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_grape/api.rb +0 -0
  159. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
  160. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
  161. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
  162. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
  163. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_sinatra/app.rb +0 -0
  164. {apisec_code_bolt-0.1.6 → 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.6 → 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.6 → 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.6 → 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.6 → apisec_code_bolt-0.1.8}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
  169. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
  170. {apisec_code_bolt-0.1.6 → 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.6 → 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.6 → 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.6 → 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.6 → 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.6 → 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.6 → 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.6 → 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.6 → apisec_code_bolt-0.1.8}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
  179. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/fixtures/vulnerable_app/pom.xml +0 -0
  180. {apisec_code_bolt-0.1.6 → 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.6 → apisec_code_bolt-0.1.8}/tests/integration/__init__.py +0 -0
  182. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_django_analysis.py +0 -0
  183. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_express_analysis.py +0 -0
  184. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_fastapi_analysis.py +0 -0
  185. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_flask_analysis.py +0 -0
  186. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_nestjs_analysis.py +0 -0
  187. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_spring_boot_analysis.py +0 -0
  188. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
  189. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/__init__.py +0 -0
  190. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/fixtures/sample_app.py +0 -0
  191. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/test_auth_indicator_matching.py +0 -0
  192. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/test_executor.py +0 -0
  193. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/test_handlers.py +0 -0
  194. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/test_query/test_read_site_origin_types.py +0 -0
  195. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/__init__.py +0 -0
  196. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_apisec_client.py +0 -0
  197. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_aspnet_plugin.py +0 -0
  198. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_audit.py +0 -0
  199. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_benchmark_run.py +0 -0
  200. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_call_graph.py +0 -0
  201. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_cbv_extractor.py +0 -0
  202. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_cli_console_url.py +0 -0
  203. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_config.py +0 -0
  204. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_config_scanner_framework.py +0 -0
  205. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_constant_resolver.py +0 -0
  206. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_cross_file_resolver.py +0 -0
  207. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_literals.py +0 -0
  208. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_csharp_type_resolver.py +0 -0
  209. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_data_flow.py +0 -0
  210. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_discovery.py +0 -0
  211. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_django_plugin.py +0 -0
  212. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_dynamic_routes.py +0 -0
  213. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_express_nestjs_plugin.py +0 -0
  214. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_fastapi_plugin.py +0 -0
  215. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_fastify_plugin.py +0 -0
  216. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_flow_analysis.py +0 -0
  217. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_grape_plugin.py +0 -0
  218. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_graphql_plugin.py +0 -0
  219. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_grpc_plugin.py +0 -0
  220. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_instrumentation.py +0 -0
  221. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_integration_detection.py +0 -0
  222. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_java_parser.py +0 -0
  223. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_jaxrs_plugin.py +0 -0
  224. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_js_graphql_plugin.py +0 -0
  225. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_jwt_config_extraction.py +0 -0
  226. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
  227. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_manifest_upload_gzip.py +0 -0
  228. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_micronaut_plugin.py +0 -0
  229. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_parameter_analyzer.py +0 -0
  230. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_path_resolver.py +0 -0
  231. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_pydantic_body_inference.py +0 -0
  232. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_python_parser.py +0 -0
  233. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_rails_plugin.py +0 -0
  234. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_refit_plugin.py +0 -0
  235. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_repo_canonical.py +0 -0
  236. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_router_registry.py +0 -0
  237. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_ruby_graphql_plugin.py +0 -0
  238. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_sensitivity_capability.py +0 -0
  239. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_sinatra_plugin.py +0 -0
  240. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_sink_evidence.py +0 -0
  241. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_spring_plugin.py +0 -0
  242. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_static_route_catchup.py +0 -0
  243. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_enrichment.py +0 -0
  244. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_surface_extraction_client.py +0 -0
  245. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_telemetry.py +0 -0
  246. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_transformation_tracking.py +0 -0
  247. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_types.py +0 -0
  248. {apisec_code_bolt-0.1.6 → apisec_code_bolt-0.1.8}/tests/unit/test_wcf_plugin.py +0 -0
  249. {apisec_code_bolt-0.1.6 → 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.6
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.6"
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]
@@ -882,87 +949,129 @@ class DependencyExtractor:
882
949
  """Resolve versions pinned by parent/imported BOMs by walking the POM
883
950
  ancestry (parents + ``<scope>import</scope>``) on Maven Central.
884
951
 
885
- Accumulates ``<properties>`` (nearest wins) and ``<dependencyManagement>``
886
- pins across the chain, then dereferences ``${...}`` refs. Bounded depth +
887
- cycle guard; any fetch/parse miss simply contributes nothing. Returns
888
- ``{groupId:artifactId -> version}``.
952
+ Maven properties are **POM-scoped, not global**: two BOMs can define the
953
+ same property name with different values (e.g. spring-boot-dependencies
954
+ carries a Jackson-2 version while ``tools.jackson:jackson-bom`` carries a
955
+ Jackson-3 one). So each BOM's ``<dependencyManagement>`` pins are resolved
956
+ against THAT BOM's own scope (its ``<properties>`` + its parent chain,
957
+ child overriding parent) — never a shared soup — and imported BOMs are
958
+ resolved recursively in their own scope. First-seen pin wins (nearest to
959
+ the app). An imported-BOM coordinate whose version is itself a ``${...}``
960
+ ref is dereferenced before fetching. Bounded depth + cycle guards; any
961
+ fetch/parse miss contributes nothing. ``seed_props`` is intentionally
962
+ unused — seed coordinates arrive already version-resolved from the app pom.
963
+ Returns ``{groupId:artifactId -> version}``.
889
964
  """
890
965
  import xml.etree.ElementTree as ET
891
966
 
892
- props: dict[str, str] = dict(seed_props)
893
- raw_pins: dict[str, str] = {}
894
- seen: set[str] = set()
895
- worklist = list(seeds)
896
- depth = 0
897
- while worklist and depth < 8:
898
- depth += 1
899
- nxt: list[tuple[str, str, str]] = []
900
- for g, a, v in worklist:
901
- if not (g and a and v):
902
- continue
903
- key = f"{g}:{a}:{v}"
904
- if key in seen:
905
- continue
906
- seen.add(key)
907
- data = self._fetch_pom(g, a, v)
908
- if not data:
909
- continue
910
- try:
911
- el = ET.fromstring(data)
912
- except ET.ParseError:
913
- continue
914
- bns = el.tag[: el.tag.index("}") + 1] if el.tag.startswith("{") else ""
915
-
916
- def bq(t: str, _ns: str = bns) -> str:
917
- return f"{_ns}{t}"
918
-
919
- pe = el.find(bq("properties"))
920
- if pe is not None:
921
- for c in pe:
922
- k = c.tag[len(bns):] if bns else c.tag
923
- if c.text and k not in props: # nearest (child) wins
924
- props[k] = c.text.strip()
925
- dm = el.find(bq("dependencyManagement"))
926
- dmd = dm.find(bq("dependencies")) if dm is not None else None
927
- if dmd is not None:
928
- for d in dmd.findall(bq("dependency")):
929
- dg = (d.findtext(bq("groupId")) or "").strip()
930
- da = (d.findtext(bq("artifactId")) or "").strip()
931
- dv = (d.findtext(bq("version")) or "").strip()
932
- scope = (d.findtext(bq("scope")) or "").strip().lower()
933
- if scope == "import" and dg and da and dv:
934
- nxt.append((dg, da, dv))
935
- elif dg and da and dv and f"{dg}:{da}" not in raw_pins:
936
- raw_pins[f"{dg}:{da}"] = dv
937
- par = el.find(bq("parent"))
938
- if par is not None:
939
- pg = (par.findtext(bq("groupId")) or "").strip()
940
- pa = (par.findtext(bq("artifactId")) or "").strip()
941
- pv = (par.findtext(bq("version")) or "").strip()
942
- if pg and pa and pv:
943
- nxt.append((pg, pa, pv))
944
- worklist = nxt
945
-
946
- def deref(val: str | None) -> str | None:
947
- if val is None:
967
+ parsed: dict[str, tuple | None] = {}
968
+
969
+ def parse(g: str, a: str, v: str):
970
+ """(own_props, dm_pins, imports, parent) for a POM; cached; None if unfetchable."""
971
+ key = f"{g}:{a}:{v}"
972
+ if key in parsed:
973
+ return parsed[key]
974
+ parsed[key] = None # guard re-entry / unfetchable
975
+ data = self._fetch_pom(g, a, v)
976
+ if not data:
948
977
  return None
949
- seen2: set[str] = set()
978
+ try:
979
+ el = ET.fromstring(data)
980
+ except ET.ParseError:
981
+ return None
982
+ ns = el.tag[: el.tag.index("}") + 1] if el.tag.startswith("{") else ""
983
+
984
+ def q(t: str) -> str:
985
+ return f"{ns}{t}"
986
+
987
+ own: dict[str, str] = {}
988
+ pe = el.find(q("properties"))
989
+ if pe is not None:
990
+ for c in pe:
991
+ k = c.tag[len(ns) :] if ns else c.tag
992
+ if c.text:
993
+ own[k] = c.text.strip()
994
+ own.setdefault("project.version", v)
995
+ own.setdefault("project.groupId", g)
996
+ dm_pins: list[tuple[str, str, str]] = []
997
+ imports: list[tuple[str, str, str]] = []
998
+ dm = el.find(q("dependencyManagement"))
999
+ dmd = dm.find(q("dependencies")) if dm is not None else None
1000
+ if dmd is not None:
1001
+ for d in dmd.findall(q("dependency")):
1002
+ dg = (d.findtext(q("groupId")) or "").strip()
1003
+ da = (d.findtext(q("artifactId")) or "").strip()
1004
+ dv = (d.findtext(q("version")) or "").strip()
1005
+ sc = (d.findtext(q("scope")) or "").strip().lower()
1006
+ if dg and da and dv:
1007
+ (imports if sc == "import" else dm_pins).append((dg, da, dv))
1008
+ parent = None
1009
+ par = el.find(q("parent"))
1010
+ if par is not None:
1011
+ pg = (par.findtext(q("groupId")) or "").strip()
1012
+ pa = (par.findtext(q("artifactId")) or "").strip()
1013
+ pv = (par.findtext(q("version")) or "").strip()
1014
+ if pg and pa and pv:
1015
+ parent = (pg, pa, pv)
1016
+ result = (own, dm_pins, imports, parent)
1017
+ parsed[key] = result
1018
+ return result
1019
+
1020
+ def interp(val: str, scope: dict[str, str]) -> str | None:
1021
+ if not val:
1022
+ return None
1023
+ seen_p: set[str] = set()
950
1024
  m = re.fullmatch(r"\$\{([^}]+)\}", val.strip())
951
- while m and m.group(1) not in seen2:
952
- seen2.add(m.group(1))
953
- repl = props.get(m.group(1))
1025
+ while m and m.group(1) not in seen_p:
1026
+ seen_p.add(m.group(1))
1027
+ repl = scope.get(m.group(1))
954
1028
  if repl is None:
955
1029
  return None
956
1030
  val = repl.strip()
957
1031
  m = re.fullmatch(r"\$\{([^}]+)\}", val)
958
- return None if (val is None or "${" in val) else val
959
-
960
- out: dict[str, str] = {}
961
- for name, raw in raw_pins.items():
962
- rv = deref(raw)
963
- if rv:
964
- out[name] = rv
965
- return out
1032
+ return None if (not val or "${" in val) else val
1033
+
1034
+ def scope_of(g: str, a: str, v: str, seen: frozenset, depth: int) -> dict[str, str]:
1035
+ """A POM's effective property scope: parent chain, child overrides parent."""
1036
+ key = f"{g}:{a}:{v}"
1037
+ if depth > 10 or key in seen:
1038
+ return {}
1039
+ p = parse(g, a, v)
1040
+ if p is None:
1041
+ return {}
1042
+ own, _dm, _imp, parent = p
1043
+ merged = dict(scope_of(*parent, seen | {key}, depth + 1)) if parent else {}
1044
+ merged.update(own) # own overrides inherited
1045
+ return merged
1046
+
1047
+ pins: dict[str, str] = {}
1048
+ resolved: set[str] = set()
1049
+
1050
+ def resolve_bom(g: str, a: str, v: str, depth: int) -> None:
1051
+ key = f"{g}:{a}:{v}"
1052
+ if depth > 10 or key in resolved:
1053
+ return
1054
+ resolved.add(key)
1055
+ p = parse(g, a, v)
1056
+ if p is None:
1057
+ return
1058
+ _own, dm_pins, imports, parent = p
1059
+ scope = scope_of(g, a, v, frozenset(), 0) # resolve in THIS bom's own scope
1060
+ for dg, da, dv in dm_pins:
1061
+ rv = interp(dv, scope)
1062
+ if rv:
1063
+ pins.setdefault(f"{dg}:{da}", rv) # nearest-to-app wins
1064
+ for dg, da, dv in imports:
1065
+ iv = interp(dv, scope)
1066
+ if iv:
1067
+ resolve_bom(interp(dg, scope) or dg, interp(da, scope) or da, iv, depth + 1)
1068
+ if parent: # inherited dependencyManagement from the parent chain
1069
+ resolve_bom(parent[0], parent[1], parent[2], depth + 1)
1070
+
1071
+ for g, a, v in seeds:
1072
+ if g and a and v:
1073
+ resolve_bom(g, a, v, 0)
1074
+ return pins
966
1075
 
967
1076
  # -- pom.xml (Maven / JVM) -------------------------------------------------
968
1077
 
@@ -1058,15 +1167,16 @@ class DependencyExtractor:
1058
1167
  if parent_group and parent_artifact and parent_version:
1059
1168
  parent_coord = (parent_group, parent_artifact, parent_version)
1060
1169
 
1061
- # Imported-BOM resolution (opt-in): third-party deps (h2, mysql, jackson…)
1170
+ # Imported-BOM resolution (default ON): third-party deps (h2, mysql, jackson…)
1062
1171
  # get their versions from the parent chain / imported BOMs (e.g.
1063
1172
  # spring-boot-dependencies), NOT this file, so they'd otherwise be
1064
- # version=None → silently treated downstream as "not affected". When
1065
- # APISEC_RESOLVE_MAVEN_BOMS is set, fetch those POMs from Maven Central and
1066
- # resolve their pins; default off keeps the extractor offline/no-egress.
1067
- # 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.
1068
1178
  remote_managed: dict[str, str] = {}
1069
- if os.environ.get("APISEC_RESOLVE_MAVEN_BOMS"):
1179
+ if _maven_bom_resolution_enabled():
1070
1180
  seeds = ([parent_coord] if parent_coord else []) + import_boms
1071
1181
  if seeds:
1072
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(