apisec-code-bolt 0.1.7__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/PKG-INFO +45 -3
  2. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/README.md +44 -2
  3. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/pyproject.toml +1 -1
  4. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/analyzer.py +96 -35
  5. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/data_flow.py +12 -0
  6. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/dependency_extractor.py +86 -18
  7. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/main.py +43 -5
  8. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/apisec_client.py +9 -2
  9. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/telemetry.py +7 -3
  10. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +218 -18
  11. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -1
  12. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/executor.py +1 -1
  13. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_cli.py +45 -1
  14. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_dependency_parsing.py +125 -4
  15. apisec_code_bolt-0.1.9/tests/unit/test_entry_point_interface_resolution.py +149 -0
  16. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_rails_plugin.py +597 -0
  17. apisec_code_bolt-0.1.9/tests/unit/test_upload_verify_timeout.py +69 -0
  18. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/.gitignore +0 -0
  19. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__init__.py +0 -0
  20. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__main__.py +0 -0
  21. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/__init__.py +0 -0
  22. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
  23. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
  24. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
  25. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
  26. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
  27. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
  28. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
  29. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
  30. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
  31. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
  32. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
  33. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
  34. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
  35. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
  36. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
  37. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
  38. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
  39. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/__init__.py +0 -0
  40. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
  41. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/__init__.py +0 -0
  42. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/client.py +0 -0
  43. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/__init__.py +0 -0
  44. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/config.py +0 -0
  45. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/credentials.py +0 -0
  46. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/discovery.py +0 -0
  47. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/log_format.py +0 -0
  48. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/manifest.py +0 -0
  49. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/repo.py +0 -0
  50. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/state.py +0 -0
  51. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/types.py +0 -0
  52. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
  53. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
  54. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
  55. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
  56. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
  57. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/base.py +0 -0
  58. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
  59. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
  60. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
  61. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
  62. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
  63. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
  64. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
  65. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
  66. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
  67. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
  68. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
  69. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
  70. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
  71. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
  72. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
  73. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
  74. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
  75. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
  76. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
  77. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
  78. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
  79. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
  80. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
  81. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
  82. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
  83. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
  84. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
  85. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
  86. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
  87. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
  88. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
  89. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
  90. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
  91. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
  92. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
  93. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
  94. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
  95. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
  96. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
  97. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/__init__.py +0 -0
  98. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/base.py +0 -0
  99. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
  100. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
  101. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
  102. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
  103. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
  104. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
  105. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
  106. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
  107. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
  108. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
  109. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
  110. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
  111. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
  112. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
  113. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
  114. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
  115. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
  116. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
  117. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
  118. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
  119. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
  120. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
  121. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
  122. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
  123. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
  124. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
  125. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
  126. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
  127. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/services.py +0 -0
  128. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/__init__.py +0 -0
  129. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/ast_cache.py +0 -0
  130. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/handlers.py +0 -0
  131. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/manifest_context.py +0 -0
  132. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/conftest.py +0 -0
  133. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Cargo.toml +0 -0
  134. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Gemfile.lock +0 -0
  135. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Pipfile +0 -0
  136. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle +0 -0
  137. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle.kts +0 -0
  138. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/go.mod +0 -0
  139. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/package.json +0 -0
  140. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pom.xml +0 -0
  141. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
  142. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
  143. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
  144. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements.txt +0 -0
  145. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.cfg +0 -0
  146. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.py +0 -0
  147. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/urls.py +0 -0
  148. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/views.py +0 -0
  149. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/app.ts +0 -0
  150. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
  151. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/auth.ts +0 -0
  152. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/users.ts +0 -0
  153. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/main.py +0 -0
  154. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
  155. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
  156. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
  157. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
  158. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask/app.py +0 -0
  159. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
  160. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_grape/api.rb +0 -0
  161. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
  162. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
  163. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
  164. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
  165. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_sinatra/app.rb +0 -0
  166. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
  167. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
  168. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
  169. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
  170. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
  171. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
  172. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
  173. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
  174. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
  175. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
  176. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
  177. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
  178. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
  179. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
  180. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
  181. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/pom.xml +0 -0
  182. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/src/main/java/com/demo/DemoController.java +0 -0
  183. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/__init__.py +0 -0
  184. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_django_analysis.py +0 -0
  185. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_express_analysis.py +0 -0
  186. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_fastapi_analysis.py +0 -0
  187. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_flask_analysis.py +0 -0
  188. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_nestjs_analysis.py +0 -0
  189. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_spring_boot_analysis.py +0 -0
  190. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
  191. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/__init__.py +0 -0
  192. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/fixtures/sample_app.py +0 -0
  193. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/test_auth_indicator_matching.py +0 -0
  194. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/test_executor.py +0 -0
  195. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/test_handlers.py +0 -0
  196. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/test_query/test_read_site_origin_types.py +0 -0
  197. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/__init__.py +0 -0
  198. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_apisec_client.py +0 -0
  199. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_aspnet_plugin.py +0 -0
  200. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_audit.py +0 -0
  201. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_run.py +0 -0
  202. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_call_graph.py +0 -0
  203. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_cbv_extractor.py +0 -0
  204. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_cli_console_url.py +0 -0
  205. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_config.py +0 -0
  206. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_config_scanner_framework.py +0 -0
  207. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_constant_resolver.py +0 -0
  208. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_cross_file_resolver.py +0 -0
  209. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_literals.py +0 -0
  210. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_type_resolver.py +0 -0
  211. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_data_flow.py +0 -0
  212. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_discovery.py +0 -0
  213. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_django_plugin.py +0 -0
  214. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_dynamic_routes.py +0 -0
  215. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_express_nestjs_plugin.py +0 -0
  216. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_fastapi_plugin.py +0 -0
  217. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_fastify_plugin.py +0 -0
  218. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_flow_analysis.py +0 -0
  219. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_grape_plugin.py +0 -0
  220. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_graphql_plugin.py +0 -0
  221. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_grpc_plugin.py +0 -0
  222. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_instrumentation.py +0 -0
  223. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_integration_detection.py +0 -0
  224. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_java_parser.py +0 -0
  225. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_jaxrs_plugin.py +0 -0
  226. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_js_graphql_plugin.py +0 -0
  227. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_jwt_config_extraction.py +0 -0
  228. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
  229. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_manifest_upload_gzip.py +0 -0
  230. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_micronaut_plugin.py +0 -0
  231. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_parameter_analyzer.py +0 -0
  232. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_path_resolver.py +0 -0
  233. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_pydantic_body_inference.py +0 -0
  234. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_python_parser.py +0 -0
  235. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_refit_plugin.py +0 -0
  236. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_repo_canonical.py +0 -0
  237. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_router_registry.py +0 -0
  238. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_ruby_graphql_plugin.py +0 -0
  239. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_sensitivity_capability.py +0 -0
  240. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_sinatra_plugin.py +0 -0
  241. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_sink_evidence.py +0 -0
  242. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_spring_plugin.py +0 -0
  243. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_static_route_catchup.py +0 -0
  244. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_enrichment.py +0 -0
  245. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_extraction_client.py +0 -0
  246. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_telemetry.py +0 -0
  247. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_transformation_tracking.py +0 -0
  248. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_types.py +0 -0
  249. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_wcf_plugin.py +0 -0
  250. {apisec_code_bolt-0.1.7 → apisec_code_bolt-0.1.9}/tests/unit/test_webforms_plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apisec-code-bolt
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Static analysis probe for extracting architectural metadata from codebases
5
5
  Project-URL: Homepage, https://apisec.ai
6
6
  Project-URL: Documentation, https://docs.apisec.ai/code-bolt
@@ -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
@@ -349,6 +349,48 @@ ruff check .
349
349
  ruff format --check .
350
350
  ```
351
351
 
352
+ ### Publishing a release (maintainers)
353
+
354
+ The CLI is published to [PyPI](https://pypi.org/project/apisec-code-bolt/) by the
355
+ **Publish to PyPI** GitHub Action. **Merging to `build` does NOT publish** — the
356
+ workflow only runs on `workflow_dispatch` or a published GitHub Release, and it
357
+ builds from whatever ref it runs on.
358
+
359
+ Prerequisites (one-time): the `PYPI_API_TOKEN` repository secret must be set
360
+ (Settings → Secrets and variables → Actions).
361
+
362
+ 1. **Bump the version.** Edit `version` in `pyproject.toml` (single source of
363
+ truth; `--version` reads it via package metadata). PyPI **rejects re-uploads**
364
+ of an existing version, so the number must be higher than the current PyPI
365
+ release. Open a PR and merge it to `build`.
366
+
367
+ 2. **Publish** — dispatch the workflow (the standard path we use):
368
+ Actions → *Publish to PyPI* → **Run workflow** → select branch **`build`**.
369
+
370
+ ```bash
371
+ gh workflow run "Publish to PyPI" --ref build
372
+ ```
373
+
374
+ <details><summary>Alternative: publish via a GitHub Release</summary>
375
+
376
+ New release → create tag `vX.Y.Z` → set **Target: `build`** (it defaults to
377
+ the default branch; only `build` has the bumped version) → Publish. This
378
+ fires the same workflow via `release: [published]`.
379
+
380
+ ```bash
381
+ gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
382
+ ```
383
+ </details>
384
+
385
+ 3. **Verify.** Confirm the new version appears on
386
+ [PyPI](https://pypi.org/project/apisec-code-bolt/), then upgrade an install:
387
+ ```bash
388
+ uv tool upgrade apisec-code-bolt # or: pipx upgrade apisec-code-bolt
389
+ ```
390
+
391
+ > Always release from `build` after the version bump has merged there — releasing
392
+ > from a ref that still carries the old version will fail the PyPI upload.
393
+
352
394
  ## Privacy
353
395
 
354
396
  apisec-code-bolt is designed with privacy as a core principle:
@@ -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
@@ -302,6 +302,48 @@ ruff check .
302
302
  ruff format --check .
303
303
  ```
304
304
 
305
+ ### Publishing a release (maintainers)
306
+
307
+ The CLI is published to [PyPI](https://pypi.org/project/apisec-code-bolt/) by the
308
+ **Publish to PyPI** GitHub Action. **Merging to `build` does NOT publish** — the
309
+ workflow only runs on `workflow_dispatch` or a published GitHub Release, and it
310
+ builds from whatever ref it runs on.
311
+
312
+ Prerequisites (one-time): the `PYPI_API_TOKEN` repository secret must be set
313
+ (Settings → Secrets and variables → Actions).
314
+
315
+ 1. **Bump the version.** Edit `version` in `pyproject.toml` (single source of
316
+ truth; `--version` reads it via package metadata). PyPI **rejects re-uploads**
317
+ of an existing version, so the number must be higher than the current PyPI
318
+ release. Open a PR and merge it to `build`.
319
+
320
+ 2. **Publish** — dispatch the workflow (the standard path we use):
321
+ Actions → *Publish to PyPI* → **Run workflow** → select branch **`build`**.
322
+
323
+ ```bash
324
+ gh workflow run "Publish to PyPI" --ref build
325
+ ```
326
+
327
+ <details><summary>Alternative: publish via a GitHub Release</summary>
328
+
329
+ New release → create tag `vX.Y.Z` → set **Target: `build`** (it defaults to
330
+ the default branch; only `build` has the bumped version) → Publish. This
331
+ fires the same workflow via `release: [published]`.
332
+
333
+ ```bash
334
+ gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
335
+ ```
336
+ </details>
337
+
338
+ 3. **Verify.** Confirm the new version appears on
339
+ [PyPI](https://pypi.org/project/apisec-code-bolt/), then upgrade an install:
340
+ ```bash
341
+ uv tool upgrade apisec-code-bolt # or: pipx upgrade apisec-code-bolt
342
+ ```
343
+
344
+ > Always release from `build` after the version bump has merged there — releasing
345
+ > from a ref that still carries the old version will fail the PyPI upload.
346
+
305
347
  ## Privacy
306
348
 
307
349
  apisec-code-bolt is designed with privacy as a core principle:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apisec-code-bolt"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "Static analysis probe for extracting architectural metadata from codebases"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -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
@@ -2234,46 +2234,107 @@ class ProjectAnalyzer:
2234
2234
  Resolve handler qualified names to call graph symbols.
2235
2235
 
2236
2236
  Handles mismatches between route extraction and call graph naming
2237
- (e.g., module path variations).
2237
+ (e.g., module path variations), and the interface-handler pattern where
2238
+ the route annotations live on an interface method but the body lives in
2239
+ an implementing class (e.g. Micronaut operation interfaces: routes are
2240
+ declared on ``*Operations`` interfaces and the ``@Controller`` class
2241
+ implements them). Taint must seed from the concrete implementation —
2242
+ the interface method has no body, so a manifest handler pointing at it
2243
+ yields zero data flows.
2238
2244
  """
2239
2245
  if not self._call_graph:
2240
2246
  return entry_points
2241
2247
 
2242
- symbols = set(self._call_graph._symbols.keys())
2243
- resolved = []
2248
+ cg = self._call_graph
2249
+ symbols = set(cg._symbols.keys())
2244
2250
 
2251
+ # interface simple-name -> {implementing class qualified names}. Sourced
2252
+ # from parsed classes (base_classes includes Java `implements`); the
2253
+ # built CallGraph does not expose the builder's class-base map.
2254
+ iface_impls: dict[str, set[str]] = {}
2255
+ for pf in self._parsed_files.values():
2256
+ if not getattr(pf, "success", False):
2257
+ continue
2258
+ for cls in getattr(pf, "classes", None) or []:
2259
+ try:
2260
+ cls_qname = cls.qualified_name.full
2261
+ except AttributeError:
2262
+ continue
2263
+ for base in getattr(cls, "base_classes", None) or []:
2264
+ iface_impls.setdefault(base.rsplit(".", 1)[-1], set()).add(cls_qname)
2265
+
2266
+ def _redirect_to_impl(handler_qn: str) -> str:
2267
+ """If handler is declared on an interface a concrete class implements,
2268
+ redirect to that class's method (where the body/params live)."""
2269
+ if "." not in handler_qn:
2270
+ return handler_qn
2271
+ owner, method = handler_qn.rsplit(".", 1)
2272
+ impls = iface_impls.get(owner.rsplit(".", 1)[-1])
2273
+ if not impls:
2274
+ return handler_qn
2275
+ fallback = None
2276
+ for impl_cls in impls:
2277
+ cand = f"{impl_cls}.{method}"
2278
+ if cand == handler_qn or cand not in symbols:
2279
+ continue
2280
+ node = cg.get_node(cand)
2281
+ # Prefer a concrete implementation with a body (call edges) or one
2282
+ # not flagged abstract; keep the interface handler otherwise.
2283
+ if cg.get_callees(cand) or not getattr(node, "is_abstract", False):
2284
+ return cand
2285
+ fallback = fallback or cand
2286
+ return fallback or handler_qn
2287
+
2288
+ resolved = []
2245
2289
  for ep in entry_points:
2246
2290
  handler = ep.get("handler_qualified_name")
2247
2291
  if not handler:
2248
2292
  resolved.append(ep)
2249
2293
  continue
2250
2294
 
2251
- if handler in symbols:
2252
- resolved.append(ep)
2253
- continue
2254
-
2255
- # Try common variations
2256
- candidates = [
2257
- handler,
2258
- handler.replace(".__init__", ""),
2259
- ]
2260
- # Try with/without package prefix (e.g., "app.main.get_user" -> "main.get_user")
2261
- if "." in handler:
2262
- parts = handler.split(".")
2263
- for i in range(1, len(parts)):
2264
- variant = ".".join(parts[i:])
2265
- if variant not in candidates:
2266
- candidates.append(variant)
2267
-
2268
- for cand in candidates:
2269
- if cand in symbols:
2270
- ep = dict(ep)
2271
- ep["handler_qualified_name"] = cand
2272
- break
2295
+ chosen = handler if handler in symbols else None
2296
+ if chosen is None:
2297
+ # Try common variations (module-path prefix stripping, __init__).
2298
+ candidates = [handler, handler.replace(".__init__", "")]
2299
+ if "." in handler:
2300
+ parts = handler.split(".")
2301
+ candidates += [".".join(parts[i:]) for i in range(1, len(parts))]
2302
+ for cand in candidates:
2303
+ if cand in symbols:
2304
+ chosen = cand
2305
+ break
2306
+ # Suffix match on the handler tail. Some plugins record the
2307
+ # handler module as the *routing* file rather than the view's
2308
+ # module (e.g. Django: `urls.UserSessionLoginApi.post` or the
2309
+ # function-based `api_router.favorite_post`, while the symbols
2310
+ # are `app.authentication.apis.UserSessionLoginApi.post` and
2311
+ # `app.api.views.favorite_post`). Prefix stripping can't bridge
2312
+ # that. Try the `Class.method` tail first, then the bare name,
2313
+ # accepting a match only when it is unambiguous.
2314
+ if chosen is None and "." in handler:
2315
+ parts = handler.split(".")
2316
+ for depth in (2, 1):
2317
+ if depth > len(parts):
2318
+ continue
2319
+ tail = "." + ".".join(parts[-depth:])
2320
+ tail_matches = [s for s in symbols if s.endswith(tail)]
2321
+ if len(tail_matches) == 1:
2322
+ chosen = tail_matches[0]
2323
+ break
2324
+ chosen = chosen or handler
2325
+
2326
+ # Interface-handler → concrete implementation (Micronaut operation
2327
+ # interfaces, and any handler declared on an implemented interface).
2328
+ redirected = _redirect_to_impl(chosen)
2329
+ if redirected != handler:
2330
+ ep = dict(ep)
2331
+ ep["handler_qualified_name"] = redirected
2273
2332
  resolved.append(ep)
2274
2333
 
2275
2334
  return resolved
2276
2335
 
2336
+ return resolved
2337
+
2277
2338
  # =========================================================================
2278
2339
  # Integration Detection
2279
2340
  # =========================================================================
@@ -1646,6 +1646,18 @@ class DataFlowAnalyzer:
1646
1646
  if pname and ploc:
1647
1647
  param_locations[pname] = ploc
1648
1648
 
1649
+ # Django/DRF views receive all user input via the `request` object
1650
+ # (request.data / .GET / .POST / .query_params) rather than typed
1651
+ # per-field route params, so there is nothing in param_locations to
1652
+ # seed. Treat the handler's `request` parameter itself as an HTTP
1653
+ # origin and let propagation carry it into request.* reads, serializer
1654
+ # construction, and downstream sinks.
1655
+ if framework.startswith("django"):
1656
+ for pobj in getattr(func, "parameters", None) or []:
1657
+ pname = getattr(pobj, "name", None)
1658
+ if pname == "request" and pname not in param_locations:
1659
+ param_locations[pname] = "HTTP_BODY"
1660
+
1649
1661
  route_path = ep.get("path", "")
1650
1662
  origins = source_id.identify_from_function(
1651
1663
  func,
@@ -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,12 @@ 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",
341
+ "https://api.apisec.ai/v1/applications/surface/telemetry/events",
342
+ )
339
343
 
340
344
 
341
345
  def emit_analyze_event(