apisec-code-bolt 0.1.8__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.8 → apisec_code_bolt-0.1.9}/PKG-INFO +43 -1
  2. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/README.md +42 -0
  3. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/pyproject.toml +1 -1
  4. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/analyzer.py +86 -25
  5. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/data_flow.py +12 -0
  6. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/telemetry.py +2 -1
  7. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +218 -18
  8. apisec_code_bolt-0.1.9/tests/unit/test_entry_point_interface_resolution.py +149 -0
  9. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_rails_plugin.py +597 -0
  10. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/.gitignore +0 -0
  11. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__init__.py +0 -0
  12. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/__main__.py +0 -0
  13. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/__init__.py +0 -0
  14. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
  15. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph.py +0 -0
  16. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
  17. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
  18. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
  19. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
  20. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/dependency_extractor.py +0 -0
  21. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
  22. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
  23. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/integration_detector.py +0 -0
  24. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
  25. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
  26. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/read_site_detector.py +0 -0
  27. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
  28. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
  29. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
  30. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/surface_enrichment.py +0 -0
  31. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +0 -0
  32. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/__init__.py +0 -0
  33. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
  34. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cli/main.py +0 -0
  35. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/__init__.py +0 -0
  36. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/apisec_client.py +0 -0
  37. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/cloud/client.py +0 -0
  38. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/__init__.py +0 -0
  39. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/config.py +0 -0
  40. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/credentials.py +0 -0
  41. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/discovery.py +0 -0
  42. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/log_format.py +0 -0
  43. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/manifest.py +0 -0
  44. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/repo.py +0 -0
  45. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/state.py +0 -0
  46. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/core/types.py +0 -0
  47. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
  48. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
  49. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_graphql_common.py +0 -0
  50. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
  51. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
  52. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/base.py +0 -0
  53. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
  54. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
  55. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +0 -0
  56. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
  57. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
  58. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
  59. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
  60. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
  61. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/dotnet/webforms_plugin.py +0 -0
  62. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
  63. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
  64. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
  65. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +0 -0
  66. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
  67. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
  68. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
  69. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
  70. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
  71. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/_route_helpers.py +0 -0
  72. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/express_plugin.py +0 -0
  73. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +0 -0
  74. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +0 -0
  75. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +0 -0
  76. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
  77. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
  78. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
  79. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
  80. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
  81. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
  82. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +0 -0
  83. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +0 -0
  84. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
  85. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
  86. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/__init__.py +0 -0
  87. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +0 -0
  88. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +0 -0
  89. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +0 -0
  90. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +0 -0
  91. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +0 -0
  92. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/__init__.py +0 -0
  93. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/base.py +0 -0
  94. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
  95. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
  96. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
  97. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
  98. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
  99. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
  100. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
  101. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
  102. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
  103. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
  104. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -0
  105. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
  106. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
  107. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
  108. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
  109. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
  110. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
  111. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
  112. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
  113. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
  114. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
  115. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
  116. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
  117. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
  118. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
  119. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/python/visitors.py +0 -0
  120. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/__init__.py +0 -0
  121. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/language_services.py +0 -0
  122. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/ruby/parser.py +0 -0
  123. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/parsing/services.py +0 -0
  124. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/__init__.py +0 -0
  125. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/ast_cache.py +0 -0
  126. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/executor.py +0 -0
  127. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/handlers.py +0 -0
  128. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/src/apisec_code_bolt/query/manifest_context.py +0 -0
  129. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/conftest.py +0 -0
  130. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Cargo.toml +0 -0
  131. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Gemfile.lock +0 -0
  132. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/Pipfile +0 -0
  133. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle +0 -0
  134. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/build.gradle.kts +0 -0
  135. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/go.mod +0 -0
  136. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/package.json +0 -0
  137. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pom.xml +0 -0
  138. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
  139. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
  140. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
  141. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/requirements.txt +0 -0
  142. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.cfg +0 -0
  143. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/dep_files/setup.py +0 -0
  144. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/urls.py +0 -0
  145. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_django/views.py +0 -0
  146. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/app.ts +0 -0
  147. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
  148. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/auth.ts +0 -0
  149. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_express/routes/users.ts +0 -0
  150. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/main.py +0 -0
  151. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
  152. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
  153. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
  154. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
  155. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask/app.py +0 -0
  156. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_flask_pydantic/app.py +0 -0
  157. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_grape/api.rb +0 -0
  158. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
  159. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
  160. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
  161. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
  162. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_sinatra/app.rb +0 -0
  163. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
  164. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
  165. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
  166. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
  167. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
  168. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
  169. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
  170. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
  171. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
  172. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
  173. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
  174. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
  175. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
  176. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
  177. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
  178. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/pom.xml +0 -0
  179. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/fixtures/vulnerable_app/src/main/java/com/demo/DemoController.java +0 -0
  180. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/__init__.py +0 -0
  181. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_cli.py +0 -0
  182. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_django_analysis.py +0 -0
  183. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_express_analysis.py +0 -0
  184. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_fastapi_analysis.py +0 -0
  185. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_flask_analysis.py +0 -0
  186. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_nestjs_analysis.py +0 -0
  187. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_spring_boot_analysis.py +0 -0
  188. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/integration/test_vulnerable_app_reachability_guard.py +0 -0
  189. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/__init__.py +0 -0
  190. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/fixtures/sample_app.py +0 -0
  191. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_auth_indicator_matching.py +0 -0
  192. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_executor.py +0 -0
  193. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_handlers.py +0 -0
  194. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/test_query/test_read_site_origin_types.py +0 -0
  195. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/__init__.py +0 -0
  196. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_apisec_client.py +0 -0
  197. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_aspnet_plugin.py +0 -0
  198. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_audit.py +0 -0
  199. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_benchmark_run.py +0 -0
  200. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_call_graph.py +0 -0
  201. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cbv_extractor.py +0 -0
  202. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cli_console_url.py +0 -0
  203. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_config.py +0 -0
  204. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_config_scanner_framework.py +0 -0
  205. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_constant_resolver.py +0 -0
  206. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_cross_file_resolver.py +0 -0
  207. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_literals.py +0 -0
  208. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_csharp_type_resolver.py +0 -0
  209. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_data_flow.py +0 -0
  210. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_dependency_parsing.py +0 -0
  211. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_discovery.py +0 -0
  212. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_django_plugin.py +0 -0
  213. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_dynamic_routes.py +0 -0
  214. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_express_nestjs_plugin.py +0 -0
  215. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_fastapi_plugin.py +0 -0
  216. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_fastify_plugin.py +0 -0
  217. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_flow_analysis.py +0 -0
  218. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_grape_plugin.py +0 -0
  219. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_graphql_plugin.py +0 -0
  220. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_grpc_plugin.py +0 -0
  221. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_instrumentation.py +0 -0
  222. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_integration_detection.py +0 -0
  223. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_java_parser.py +0 -0
  224. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_jaxrs_plugin.py +0 -0
  225. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_js_graphql_plugin.py +0 -0
  226. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_jwt_config_extraction.py +0 -0
  227. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
  228. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_manifest_upload_gzip.py +0 -0
  229. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_micronaut_plugin.py +0 -0
  230. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_parameter_analyzer.py +0 -0
  231. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_path_resolver.py +0 -0
  232. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_pydantic_body_inference.py +0 -0
  233. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_python_parser.py +0 -0
  234. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_refit_plugin.py +0 -0
  235. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_repo_canonical.py +0 -0
  236. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_router_registry.py +0 -0
  237. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_ruby_graphql_plugin.py +0 -0
  238. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sensitivity_capability.py +0 -0
  239. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sinatra_plugin.py +0 -0
  240. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_sink_evidence.py +0 -0
  241. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_spring_plugin.py +0 -0
  242. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_static_route_catchup.py +0 -0
  243. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_enrichment.py +0 -0
  244. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_surface_extraction_client.py +0 -0
  245. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_telemetry.py +0 -0
  246. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_transformation_tracking.py +0 -0
  247. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_types.py +0 -0
  248. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_upload_verify_timeout.py +0 -0
  249. {apisec_code_bolt-0.1.8 → apisec_code_bolt-0.1.9}/tests/unit/test_wcf_plugin.py +0 -0
  250. {apisec_code_bolt-0.1.8 → 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.8
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
@@ -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:
@@ -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.8"
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"}
@@ -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())
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
2244
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,
@@ -337,7 +337,8 @@ def emit_install_event(*, probe_version: str = "") -> None:
337
337
  # Overridable for non-prod testing (staging/dev receivers), mirroring the
338
338
  # analyze path's --api-url. Defaults to prod, so unset means no change.
339
339
  _TELEMETRY_ENDPOINT = os.environ.get(
340
- "APISEC_TELEMETRY_ENDPOINT", "https://api.apisec.ai/v1/surface/telemetry/events"
340
+ "APISEC_TELEMETRY_ENDPOINT",
341
+ "https://api.apisec.ai/v1/applications/surface/telemetry/events",
341
342
  )
342
343
 
343
344
 
@@ -52,21 +52,47 @@ _SOURCE_CALLEES: frozenset[str] = frozenset(
52
52
  {"params", "cookies", "session", "request", "headers", "env"}
53
53
  )
54
54
 
55
- _SOURCE_SUBSTRINGS: tuple[str, ...] = (
56
- "params[",
57
- "params.require",
58
- "params.permit",
59
- "cookies[",
60
- "session[",
61
- "request.body",
62
- "request.raw_post",
63
- "request.query_string",
64
- "request.query_parameters",
65
- "request.path_parameters",
66
- "request.headers[",
67
- "request.env[",
55
+ # HTTP-verb DSL methods used by rack-style frameworks (Grape, Sinatra) that
56
+ # declare a route handler as a block rather than a ``def`` method:
57
+ # get '/users/:id' do ... end post do ... end
58
+ # These blocks never appear as ``method`` nodes, so the method-based passes
59
+ # below miss them entirely; Pass C scans their block bodies directly.
60
+ _ROUTE_VERBS: frozenset[str] = frozenset(
61
+ {"get", "post", "put", "patch", "delete", "options", "head"}
68
62
  )
69
63
 
64
+ # Inherently-tainted source identifiers: `params`, `cookies`, `session`,
65
+ # `request`, `headers`, `env`. Matched by NAME (not substring) inside
66
+ # ``_is_tainted_node`` so that ``group_params`` / ``webcal_params`` (which
67
+ # merely *contain* the substring ``params``) and ``Model.find(params[:id])``
68
+ # (whose call text contains ``params[``) are NOT mis-read as sources. This
69
+ # replaces the removed ``params[``-substring fallback, which was the P0
70
+ # over-taint root cause: it marked every expression embedding ``params[...]``
71
+ # as tainted and poisoned all derived values (see RUBY_SUPPORT_ROADMAP.md P0).
72
+ # Method chains on these (``request.body``, ``params.permit``) and indexing
73
+ # (``params[:id]``, ``cookies[:sid]``) are covered structurally by the
74
+ # element_reference / source-call rules, so no substring fallback is needed.
75
+ _SOURCE_IDENTIFIERS: frozenset[str] = _SOURCE_CALLEES
76
+
77
+ # Sanitizer calls (P3) — a call whose RESULT is not tainted even when its
78
+ # receiver / arguments are. Prevents the "receiver is tainted => the call is
79
+ # a source" rule from propagating taint through coercions and escaping.
80
+ # - numeric coercion (receiver-agnostic): params[:x].to_i / .to_f / .to_d
81
+ # - numeric constructors (no receiver): Integer(x) / Float(x) / BigDecimal(x)
82
+ # - Shellwords escaping: Shellwords.escape / .shellescape
83
+ _NUMERIC_COERCIONS: frozenset[str] = frozenset({"to_i", "to_f", "to_d", "to_r"})
84
+ _SANITIZER_CONSTRUCTORS: frozenset[str] = frozenset({"Integer", "Float", "BigDecimal"})
85
+ _SHELLWORDS_SANITIZERS: frozenset[str] = frozenset({"escape", "shellescape"})
86
+
87
+ # ActiveRecord query-builder sinks that support parameterized forms
88
+ # (``?``, ``:named``, hash). For these, a tainted value passed as a BOUND
89
+ # argument — after a plain SQL template string, or as a hash / keyword
90
+ # value — is SAFE; only taint interpolated *into* a SQL string argument is
91
+ # dangerous. Raw-execution SQL sinks (``execute``, ``find_by_sql``,
92
+ # ``update_all``, ...) take a single SQL string and keep the normal
93
+ # tainted-arg treatment.
94
+ _QUERY_BUILDER_SINKS: frozenset[str] = frozenset({"where", "having", "order", "group"})
95
+
70
96
  # ── Sink catalogue ────────────────────────────────────────────────────────────
71
97
 
72
98
 
@@ -76,11 +102,22 @@ class _Sink:
76
102
  receiver_fragment: str | None # receiver text must contain this (None = any)
77
103
  sink_type: str
78
104
  description: str
105
+ receiver_excludes: tuple[str, ...] | None = None # skip if receiver contains any
79
106
 
80
107
 
81
108
  _SINKS: list[_Sink] = [
82
109
  # ── SQL injection ─────────────────────────────────────────────────────────
83
- _Sink("execute", None, "sql_injection", "Raw SQL execution"),
110
+ # `execute` is raw-SQL only on a DB connection (e.g. `connection.execute(sql)`).
111
+ # GraphQL `*Schema.execute(query_string)` runs a GraphQL query, NOT SQL —
112
+ # exclude receivers whose name ends in `Schema` (ApiSchema, MySchema,
113
+ # GraphQL::Schema) so a `Schema.execute(params[:query])` controller isn't SQLi.
114
+ _Sink(
115
+ "execute",
116
+ None,
117
+ "sql_injection",
118
+ "Raw SQL execution",
119
+ receiver_excludes=("Schema",),
120
+ ),
84
121
  _Sink("exec_query", None, "sql_injection", "Raw SQL execution"),
85
122
  _Sink("find_by_sql", None, "sql_injection", "Raw SQL query"),
86
123
  _Sink("select_all", None, "sql_injection", "Raw SQL select"),
@@ -100,7 +137,20 @@ _SINKS: list[_Sink] = [
100
137
  _Sink("capture2e", None, "command_injection", "Open3.capture2e"),
101
138
  _Sink("capture3", None, "command_injection", "Open3.capture3"),
102
139
  _Sink("pipeline", "Open3", "command_injection", "Open3.pipeline"),
103
- # ── Open redirect ─────────────────────────────────────────────────────────
140
+ # ── Code / template injection (eval family) ──────────────────────────────
141
+ # `eval` / `instance_eval` / `class_eval` / `module_eval` with a tainted
142
+ # STRING argument execute attacker-controlled Ruby. The block form
143
+ # (`class_eval { ... }`) has no string arg and naturally does not fire.
144
+ _Sink("eval", None, "code_injection", "Kernel#eval with user-controlled string"),
145
+ _Sink("instance_eval", None, "code_injection", "instance_eval with user-controlled string"),
146
+ _Sink("class_eval", None, "code_injection", "class_eval with user-controlled string"),
147
+ _Sink("module_eval", None, "code_injection", "module_eval with user-controlled string"),
148
+ # ── SSTI ──────────────────────────────────────────────────────────────────
149
+ # ERB.new with a tainted template renders attacker-controlled ERB.
150
+ # Receiver-gated to the ERB class so the common `x.new` callee doesn't
151
+ # over-fire.
152
+ _Sink("new", "ERB", "ssti", "ERB.new with user-controlled template"),
153
+ # ── Open redirect ─────────────────────────────────────────────────────────────
104
154
  _Sink("redirect_to", None, "open_redirect", "HTTP redirect with user-controlled URL"),
105
155
  # ── Path traversal ────────────────────────────────────────────────────────
106
156
  _Sink("send_file", None, "path_traversal", "File serving with user-supplied path"),
@@ -267,6 +317,32 @@ class _MethodScanner:
267
317
  return True
268
318
  return False
269
319
 
320
+ def _is_sanitizer_call(self, node: Node) -> bool:
321
+ """True if the call's RESULT is guaranteed non-tainted (P3 sanitizers).
322
+
323
+ Checked before the source-call rule so that ``params[:x].to_i`` and
324
+ ``Shellwords.escape(...)`` clear taint instead of propagating it.
325
+ """
326
+ if node.type != "call":
327
+ return False
328
+ method_node = node.child_by_field_name("method")
329
+ if method_node is None:
330
+ return False
331
+ method = self._text(method_node)
332
+ # Numeric coercion — a number cannot inject into any sink.
333
+ if method in _NUMERIC_COERCIONS:
334
+ return True
335
+ receiver_node = node.child_by_field_name("receiver")
336
+ # Numeric constructors: Integer(x) / Float(x) / BigDecimal(x) (no receiver)
337
+ if receiver_node is None and method in _SANITIZER_CONSTRUCTORS:
338
+ return True
339
+ # Shellwords.escape / Shellwords.shellescape
340
+ if receiver_node is not None and method in _SHELLWORDS_SANITIZERS:
341
+ recv = self._text(receiver_node)
342
+ if recv == "Shellwords" or recv.endswith("::Shellwords"):
343
+ return True
344
+ return False
345
+
270
346
  def _is_tainted_node(self, node: Node) -> bool:
271
347
  """
272
348
  Recursively determine whether a subtree is tainted.
@@ -283,6 +359,22 @@ class _MethodScanner:
283
359
  if t in ("identifier", "instance_variable") and self._text(node) in self._tainted_vars:
284
360
  return True
285
361
 
362
+ # Inherently-tainted source identifiers: `params`, `cookies`,
363
+ # `session`, `request`, `headers`, `env`. Matched by NAME, not by
364
+ # substring, so `group_params` / `webcal_params` (which merely
365
+ # contain `params`) are NOT treated as sources. This replaces the
366
+ # removed `params[`-substring fallback (the P0 over-taint root cause:
367
+ # it marked `Model.find(params[:id])` and every derived value as
368
+ # tainted).
369
+ if t == "identifier" and self._text(node) in _SOURCE_IDENTIFIERS:
370
+ return True
371
+
372
+ # Sanitizer call — result is NOT tainted (P3). Checked before the
373
+ # source-call rule so `params[:x].to_i` / `Shellwords.escape(...)`
374
+ # clear taint instead of propagating it.
375
+ if t == "call" and self._is_sanitizer_call(node):
376
+ return False
377
+
286
378
  # Direct source call or method chain on tainted receiver
287
379
  if t == "call" and self._is_source_call(node):
288
380
  return True
@@ -290,6 +382,27 @@ class _MethodScanner:
290
382
  # params[:id], cookies[:session], tainted_var[:x]
291
383
  if t == "element_reference":
292
384
  obj = node.child_by_field_name("object")
385
+ # Rack multipart Tempfile accessor: `params[:file][:tempfile]`
386
+ # yields a server-managed Tempfile (a server-assigned temp path),
387
+ # not a user-supplied string. Only the NESTED multipart-part
388
+ # shape is cleared — a top-level `params[:tempfile]` is a plain
389
+ # user param named `tempfile` and stays tainted (see
390
+ # test_toplevel_param_named_tempfile_still_fires).
391
+ if (
392
+ obj is not None
393
+ and obj.type == "element_reference"
394
+ and any(
395
+ c.type in ("simple_symbol", "symbol") and self._text(c) == ":tempfile"
396
+ for c in node.children
397
+ )
398
+ ):
399
+ inner = obj.child_by_field_name("object")
400
+ if (
401
+ inner is not None
402
+ and inner.type == "identifier"
403
+ and self._text(inner) in _SOURCE_IDENTIFIERS
404
+ ):
405
+ return False
293
406
  if obj and self._is_tainted_node(obj):
294
407
  return True
295
408
 
@@ -318,9 +431,7 @@ class _MethodScanner:
318
431
  if self._local_call_returns_tainted(node):
319
432
  return True
320
433
 
321
- # Fast text fallback
322
- raw = self._text(node)
323
- return any(sub in raw for sub in _SOURCE_SUBSTRINGS)
434
+ return False
324
435
 
325
436
  def _local_call_returns_tainted(self, call_node: Node) -> bool:
326
437
  """Return True if a local method call with tainted args returns tainted."""
@@ -398,9 +509,24 @@ class _MethodScanner:
398
509
  for sink in candidates:
399
510
  if sink.receiver_fragment and sink.receiver_fragment not in receiver_text:
400
511
  continue
512
+ if sink.receiver_excludes and any(
513
+ ex in receiver_text for ex in sink.receiver_excludes
514
+ ):
515
+ continue
401
516
  args_node = node.child_by_field_name("arguments")
402
517
  if not args_node:
403
518
  continue
519
+ # Query-builder sinks (where/having/order/group)
520
+ # support parameterized forms. If the first argument
521
+ # is a plain SQL template (a string literal with NO
522
+ # interpolation) or a hash / keyword pair, tainted values
523
+ # in the BOUND arguments are SAFE — only taint
524
+ # interpolated *into* a SQL string is dangerous. Raw
525
+ # SQL-execution sinks skip this guard.
526
+ if sink.callee in _QUERY_BUILDER_SINKS and self._is_parameterized_call(
527
+ node
528
+ ):
529
+ continue
404
530
  arg_idx = 0
405
531
  for arg_node in args_node.children:
406
532
  if arg_node.type in (",", "(", ")", "argument_list"):
@@ -461,6 +587,35 @@ class _MethodScanner:
461
587
  f["path"] = [{"caller": self._method_name, "callee": callee_name}] + f.get("path", [])
462
588
  return callee_flows
463
589
 
590
+ def _is_parameterized_call(self, call_node: Node) -> bool:
591
+ """True if a query-builder call uses a bound / parameterized form.
592
+
593
+ Recognises the three safe first-argument shapes (the rest fall through
594
+ to the normal tainted-arg check):
595
+ - hash / keyword pair: ``where(name: x)`` / ``where({name: x})``
596
+ - plain SQL template: ``where("col = ?", x)`` /
597
+ ``where("col = :k", k: x)`` — a string literal with NO
598
+ interpolation; the ``?``/``:k`` are bound placeholders.
599
+ Interpolated strings, concatenations, and bare identifiers are NOT
600
+ parameterized (the taint is in the SQL string itself).
601
+ """
602
+ args_node = call_node.child_by_field_name("arguments")
603
+ if args_node is None:
604
+ return False
605
+ first = None
606
+ for arg in args_node.children:
607
+ if arg.type in (",", "(", ")", "argument_list"):
608
+ continue
609
+ first = arg
610
+ break
611
+ if first is None:
612
+ return False
613
+ if first.type in ("pair", "hash"):
614
+ return True
615
+ if first.type == "string":
616
+ return not any(c.type == "interpolation" for c in first.children)
617
+ return False
618
+
464
619
  # ── Flow construction ─────────────────────────────────────────────────────
465
620
 
466
621
  def _make_flow(
@@ -546,6 +701,35 @@ def _collect_methods(node: Node, src: bytes) -> list[tuple[Node, str]]:
546
701
  return results
547
702
 
548
703
 
704
+ def _collect_route_blocks(node: Node, src: bytes) -> list[tuple[Node, str]]:
705
+ """Find HTTP-verb DSL route handler blocks (Grape/Sinatra).
706
+
707
+ ``get '/users/:id' do ... end`` parses as a ``call`` whose method is an
708
+ HTTP verb with an attached ``do_block``/``block``. Returns
709
+ ``(block_node, verb)`` for each; the block exposes a ``body`` field, so a
710
+ ``_MethodScanner`` can scan it exactly like a method body. Non-verb DSL
711
+ blocks (``resource``, ``namespace``, ``before``) are skipped, but we
712
+ recurse through them to reach the verb blocks nested inside.
713
+ """
714
+ results: list[tuple[Node, str]] = []
715
+ if node.type == "call":
716
+ mnode = node.child_by_field_name("method")
717
+ if mnode is not None:
718
+ verb = src[mnode.start_byte : mnode.end_byte].decode("utf-8", errors="replace")
719
+ if verb in _ROUTE_VERBS:
720
+ blk = node.child_by_field_name("block")
721
+ if blk is None:
722
+ for c in node.children:
723
+ if c.type in ("do_block", "block"):
724
+ blk = c
725
+ break
726
+ if blk is not None and blk.child_by_field_name("body") is not None:
727
+ results.append((blk, verb))
728
+ for child in node.children:
729
+ results.extend(_collect_route_blocks(child, src))
730
+ return results
731
+
732
+
549
733
  def _collect_classes(node: Node, src: bytes) -> list[tuple[Node, str]]:
550
734
  results: list[tuple[Node, str]] = []
551
735
  if node.type == "class":
@@ -608,8 +792,10 @@ class RubyTaintScanner:
608
792
  scanned_ids: set[int] = set()
609
793
 
610
794
  # Pass A: methods within classes — cross-method @ivar context + call graph
795
+ file_ivar_taint: set[str] = set()
611
796
  for class_node, _ in _collect_classes(tree.root_node, src):
612
797
  ivar_taint = _prescan_class_instance_vars(class_node, src, pf.path)
798
+ file_ivar_taint |= ivar_taint
613
799
  for method_node, method_name in _collect_methods(class_node, src):
614
800
  scanned_ids.add(method_node.id)
615
801
  scanner = _MethodScanner(
@@ -636,5 +822,19 @@ class RubyTaintScanner:
636
822
  )
637
823
  flows.extend(scanner.scan())
638
824
 
825
+ # Pass C: HTTP-verb DSL route blocks (Grape/Sinatra). Their handler
826
+ # bodies are blocks, not ``def`` methods, so Passes A/B skip them.
827
+ for block_node, verb in _collect_route_blocks(tree.root_node, src):
828
+ scanner = _MethodScanner(
829
+ src=src,
830
+ path=pf.path,
831
+ method_node=block_node,
832
+ method_name=verb,
833
+ extra_tainted_vars=file_ivar_taint,
834
+ method_index=method_index,
835
+ depth=0,
836
+ )
837
+ flows.extend(scanner.scan())
838
+
639
839
  logger.debug("RubyTaintScanner: %d flows across %d files", len(flows), len(self._files))
640
840
  return flows