apisec-code-bolt 0.1.2__tar.gz → 0.1.3__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 (235) hide show
  1. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/PKG-INFO +2 -1
  2. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/pyproject.toml +4 -1
  3. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/__init__.py +1 -0
  4. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/analyzer.py +314 -2
  5. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_graph.py +24 -0
  6. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/data_flow.py +26 -1
  7. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/dependency_extractor.py +44 -0
  8. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/integration_detector.py +401 -11
  9. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/read_site_detector.py +15 -3
  10. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/url_prefix_resolver.py +136 -0
  11. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/client.py +11 -4
  12. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/discovery.py +2 -0
  13. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/manifest.py +35 -0
  14. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/types.py +7 -0
  15. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/_graphql_common.py +45 -0
  16. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/base.py +6 -0
  17. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2 -19
  18. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/graphql_plugin.py +12 -19
  19. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/js/_route_helpers.py +30 -0
  20. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/express_plugin.py +2 -18
  21. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/fastify_plugin.py +2 -16
  22. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/graphql_plugin.py +11 -18
  23. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/nestjs_plugin.py +2 -15
  24. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/flask_plugin.py +26 -1
  25. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/graphql_plugin.py +2 -19
  26. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/__init__.py +8 -0
  27. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/_ts_helpers.py +16 -0
  28. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/grape_plugin.py +486 -0
  29. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/graphql_plugin.py +358 -0
  30. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/rails_plugin.py +1050 -0
  31. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/sinatra_plugin.py +459 -0
  32. apisec_code_bolt-0.1.3/src/apisec_code_bolt/frameworks/ruby/taint_scanner.py +640 -0
  33. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/visitors.py +24 -3
  34. apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/__init__.py +5 -0
  35. apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/language_services.py +101 -0
  36. apisec_code_bolt-0.1.3/src/apisec_code_bolt/parsing/ruby/parser.py +342 -0
  37. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/ast_cache.py +33 -0
  38. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/executor.py +27 -1
  39. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/handlers.py +413 -13
  40. apisec_code_bolt-0.1.3/src/apisec_code_bolt/query/manifest_context.py +313 -0
  41. apisec_code_bolt-0.1.3/tests/fixtures/dep_files/Gemfile.lock +21 -0
  42. apisec_code_bolt-0.1.3/tests/fixtures/sample_flask_pydantic/app.py +50 -0
  43. apisec_code_bolt-0.1.3/tests/fixtures/sample_grape/api.rb +68 -0
  44. apisec_code_bolt-0.1.3/tests/fixtures/sample_sinatra/app.rb +38 -0
  45. apisec_code_bolt-0.1.3/tests/test_query/test_auth_indicator_matching.py +106 -0
  46. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/test_query/test_handlers.py +92 -0
  47. apisec_code_bolt-0.1.3/tests/test_query/test_read_site_origin_types.py +47 -0
  48. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_benchmark_run.py +46 -0
  49. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_dependency_parsing.py +57 -0
  50. apisec_code_bolt-0.1.3/tests/unit/test_grape_plugin.py +369 -0
  51. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_graphql_plugin.py +42 -0
  52. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_integration_detection.py +115 -0
  53. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_java_parser.py +34 -0
  54. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_js_graphql_plugin.py +55 -0
  55. apisec_code_bolt-0.1.3/tests/unit/test_pydantic_body_inference.py +188 -0
  56. apisec_code_bolt-0.1.3/tests/unit/test_rails_plugin.py +722 -0
  57. apisec_code_bolt-0.1.3/tests/unit/test_ruby_graphql_plugin.py +541 -0
  58. apisec_code_bolt-0.1.3/tests/unit/test_sinatra_plugin.py +315 -0
  59. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/.gitignore +0 -0
  60. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/README.md +0 -0
  61. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/__main__.py +0 -0
  62. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/__init__.py +0 -0
  63. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/binding_tracker.py +0 -0
  64. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_graph_types.py +0 -0
  65. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/call_resolver.py +0 -0
  66. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/capability_tagger.py +0 -0
  67. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/config_scanner.py +0 -0
  68. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/flow_analysis.py +0 -0
  69. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/hof_catalog.py +0 -0
  70. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/literal_scanner.py +0 -0
  71. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/path_normalizer.py +0 -0
  72. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/request_patterns.py +0 -0
  73. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/sensitivity_classifier.py +0 -0
  74. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/analysis/sink_evidence.py +0 -0
  75. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/__init__.py +0 -0
  76. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/exit_codes.py +0 -0
  77. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cli/main.py +0 -0
  78. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/__init__.py +0 -0
  79. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/cloud/apisec_client.py +0 -0
  80. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/__init__.py +0 -0
  81. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/config.py +0 -0
  82. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/credentials.py +0 -0
  83. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/log_format.py +0 -0
  84. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/repo.py +0 -0
  85. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/state.py +0 -0
  86. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/core/telemetry.py +0 -0
  87. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/fingerprinting/__init__.py +0 -0
  88. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/__init__.py +0 -0
  89. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/_jwt_common.py +0 -0
  90. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/auth_helpers.py +0 -0
  91. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/__init__.py +0 -0
  92. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/_path_helpers.py +0 -0
  93. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +0 -0
  94. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +0 -0
  95. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +0 -0
  96. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/refit_plugin.py +0 -0
  97. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +0 -0
  98. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/__init__.py +0 -0
  99. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/_annotations.py +0 -0
  100. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/_constraints.py +0 -0
  101. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/jaxrs_plugin.py +0 -0
  102. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/jwt_config_extractor.py +0 -0
  103. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/micronaut_plugin.py +0 -0
  104. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/java/spring_plugin.py +0 -0
  105. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/js/__init__.py +0 -0
  106. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/__init__.py +0 -0
  107. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/celery_plugin.py +0 -0
  108. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/click_plugin.py +0 -0
  109. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/django_plugin.py +0 -0
  110. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/fastapi/__init__.py +0 -0
  111. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/fastapi/plugin.py +0 -0
  112. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/prefect_plugin.py +0 -0
  113. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/frameworks/python/webhook_plugin.py +0 -0
  114. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/__init__.py +0 -0
  115. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/base.py +0 -0
  116. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/__init__.py +0 -0
  117. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/language_services.py +0 -0
  118. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/literals.py +0 -0
  119. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/parser.py +0 -0
  120. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/csharp/type_resolver.py +0 -0
  121. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/__init__.py +0 -0
  122. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/language_services.py +0 -0
  123. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/js/parser.py +0 -0
  124. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/__init__.py +0 -0
  125. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/language_services.py +0 -0
  126. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/parser.py +0 -0
  127. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/jvm/type_resolver.py +0 -0
  128. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/__init__.py +0 -0
  129. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/cbv_extractor.py +0 -0
  130. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/constant_resolver.py +0 -0
  131. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/cross_file_resolver.py +0 -0
  132. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/dynamic_route_detector.py +0 -0
  133. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/expression_utils.py +0 -0
  134. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/extraction_types.py +0 -0
  135. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/language_services.py +0 -0
  136. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/parameter_analyzer.py +0 -0
  137. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/parser.py +0 -0
  138. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/path_resolver.py +0 -0
  139. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/router_registry.py +0 -0
  140. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/python/type_resolver.py +0 -0
  141. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/parsing/services.py +0 -0
  142. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/src/apisec_code_bolt/query/__init__.py +0 -0
  143. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/conftest.py +0 -0
  144. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/Cargo.toml +0 -0
  145. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/Pipfile +0 -0
  146. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/build.gradle +0 -0
  147. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/build.gradle.kts +0 -0
  148. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/go.mod +0 -0
  149. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/package.json +0 -0
  150. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pom.xml +0 -0
  151. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pyproject_pep621.toml +0 -0
  152. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/pyproject_poetry.toml +0 -0
  153. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/requirements-dev.txt +0 -0
  154. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/requirements.txt +0 -0
  155. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/setup.cfg +0 -0
  156. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/dep_files/setup.py +0 -0
  157. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_django/urls.py +0 -0
  158. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_django/views.py +0 -0
  159. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/app.ts +0 -0
  160. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/middleware/authenticate.ts +0 -0
  161. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/routes/auth.ts +0 -0
  162. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_express/routes/users.ts +0 -0
  163. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/main.py +0 -0
  164. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/routes/__init__.py +0 -0
  165. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/routes/users.py +0 -0
  166. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/services/__init__.py +0 -0
  167. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_fastapi/services/user_service.py +0 -0
  168. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_flask/app.py +0 -0
  169. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/auth.controller.ts +0 -0
  170. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/roles.decorator.ts +0 -0
  171. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/roles.guard.ts +0 -0
  172. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_nestjs/users.controller.ts +0 -0
  173. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/config/SecurityConfig.java +0 -0
  174. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/controller/UserController.java +0 -0
  175. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/model/User.java +0 -0
  176. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/java/com/example/demo/service/UserService.java +0 -0
  177. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/sample_spring_boot/src/main/resources/application.properties +0 -0
  178. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/VULNERABILITIES.md +0 -0
  179. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/config/SecurityConfig.java +0 -0
  180. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/AdminController.java +0 -0
  181. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/OrderController.java +0 -0
  182. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/controller/UserController.java +0 -0
  183. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/Order.java +0 -0
  184. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/model/User.java +0 -0
  185. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/OrderService.java +0 -0
  186. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/java/com/example/vulnapi/service/UserService.java +0 -0
  187. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/fixtures/vuln_spring_boot/src/main/resources/application.properties +0 -0
  188. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/__init__.py +0 -0
  189. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_cli.py +0 -0
  190. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_django_analysis.py +0 -0
  191. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_express_analysis.py +0 -0
  192. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_fastapi_analysis.py +0 -0
  193. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_flask_analysis.py +0 -0
  194. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_nestjs_analysis.py +0 -0
  195. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/integration/test_spring_boot_analysis.py +0 -0
  196. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/test_query/__init__.py +0 -0
  197. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/test_query/fixtures/sample_app.py +0 -0
  198. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/test_query/test_executor.py +0 -0
  199. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/__init__.py +0 -0
  200. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_aspnet_plugin.py +0 -0
  201. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_benchmark_audit.py +0 -0
  202. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_call_graph.py +0 -0
  203. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_cbv_extractor.py +0 -0
  204. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_config.py +0 -0
  205. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_constant_resolver.py +0 -0
  206. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_cross_file_resolver.py +0 -0
  207. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_csharp_literals.py +0 -0
  208. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_csharp_type_resolver.py +0 -0
  209. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_data_flow.py +0 -0
  210. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_discovery.py +0 -0
  211. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_django_plugin.py +0 -0
  212. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_dynamic_routes.py +0 -0
  213. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_express_nestjs_plugin.py +0 -0
  214. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_fastapi_plugin.py +0 -0
  215. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_fastify_plugin.py +0 -0
  216. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_flow_analysis.py +0 -0
  217. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_grpc_plugin.py +0 -0
  218. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_instrumentation.py +0 -0
  219. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_jaxrs_plugin.py +0 -0
  220. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_jwt_config_extraction.py +0 -0
  221. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_legacy_aspnet_plugin.py +0 -0
  222. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_micronaut_plugin.py +0 -0
  223. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_parameter_analyzer.py +0 -0
  224. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_path_resolver.py +0 -0
  225. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_python_parser.py +0 -0
  226. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_refit_plugin.py +0 -0
  227. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_repo_canonical.py +0 -0
  228. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_router_registry.py +0 -0
  229. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_sensitivity_capability.py +0 -0
  230. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_sink_evidence.py +0 -0
  231. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_spring_plugin.py +0 -0
  232. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_telemetry.py +0 -0
  233. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_transformation_tracking.py +0 -0
  234. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_types.py +0 -0
  235. {apisec_code_bolt-0.1.2 → apisec_code_bolt-0.1.3}/tests/unit/test_wcf_plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apisec-code-bolt
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -31,6 +31,7 @@ Requires-Dist: pyyaml>=6.0
31
31
  Requires-Dist: rich>=13.7.0
32
32
  Requires-Dist: tree-sitter-c-sharp>=0.23
33
33
  Requires-Dist: tree-sitter-javascript>=0.23
34
+ Requires-Dist: tree-sitter-ruby>=0.23
34
35
  Requires-Dist: tree-sitter-typescript>=0.23
35
36
  Requires-Dist: tree-sitter>=0.23
36
37
  Requires-Dist: typing-extensions>=4.9.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apisec-code-bolt"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Static analysis probe for extracting architectural metadata from codebases"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -41,6 +41,9 @@ dependencies = [
41
41
  "tree-sitter-javascript>=0.23", # JS grammar for tree-sitter
42
42
  "tree-sitter-typescript>=0.23", # TypeScript grammar for tree-sitter
43
43
 
44
+ # Ruby parsing
45
+ "tree-sitter-ruby>=0.23", # Ruby grammar for tree-sitter
46
+
44
47
  # Graph operations
45
48
  "networkx>=3.2", # Call graph, data flow graphs
46
49
 
@@ -8,6 +8,7 @@ raw source code outside the customer's environment.
8
8
 
9
9
  try:
10
10
  from importlib.metadata import version as _pkg_version
11
+
11
12
  __version__ = _pkg_version("apisec-code-bolt")
12
13
  except Exception:
13
14
  __version__ = "0.0.0"
@@ -61,6 +61,7 @@ from ..frameworks.base import FrameworkPluginRegistry
61
61
  from ..parsing import csharp as _csharp_pkg # noqa: F401 — registers CSharpParser
62
62
  from ..parsing import js as _js_pkg # noqa: F401 — registers JavaScriptParser
63
63
  from ..parsing import jvm as _jvm_pkg # noqa: F401 — registers JavaParser
64
+ from ..parsing import ruby as _ruby_pkg # noqa: F401 — registers RubyParser
64
65
  from ..parsing.base import ParsedFile, ParserRegistry
65
66
  from ..parsing.csharp.language_services import CSharpLanguageServices
66
67
  from ..parsing.js.language_services import JavaScriptLanguageServices
@@ -69,6 +70,7 @@ from ..parsing.jvm.language_services import JavaLanguageServices
69
70
  from ..parsing.python.language_services import PythonLanguageServices
70
71
  from ..parsing.python.parser import PythonProjectParser
71
72
  from ..parsing.python.type_resolver import SchemaBuilder, TypeResolver
73
+ from ..parsing.ruby.language_services import RubyLanguageServices
72
74
  from ..parsing.services import AnalysisContext, LanguageServices
73
75
  from .call_graph import CallGraph, build_call_graph_with_context
74
76
  from .data_flow import DataFlowAnalyzer
@@ -201,11 +203,17 @@ class ProjectAnalyzer:
201
203
  Language.JAVA: JavaLanguageServices(),
202
204
  Language.CSHARP: CSharpLanguageServices(),
203
205
  Language.JAVASCRIPT: JavaScriptLanguageServices(),
206
+ Language.RUBY: RubyLanguageServices(),
204
207
  }
205
208
 
206
209
  # Analysis contexts per language (built during analysis)
207
210
  self._analysis_contexts: dict[Language, AnalysisContext] = {}
208
211
 
212
+ # Express parent-router auth middleware map, built during context setup.
213
+ # Maps resolved file path str → list of auth middleware variable names
214
+ # that apply to routes in that file (inherited from parent router mounts).
215
+ self._express_parent_auth_map: dict[str, list[str]] = {}
216
+
209
217
  # Parsed data
210
218
  self._parsed_files: dict[Path, ParsedFile] = {}
211
219
  self._discovery_result: DiscoveryResult | None = None
@@ -369,7 +377,11 @@ class ProjectAnalyzer:
369
377
  self._populate_route_auth_mapping(ctx["auth"], ctx["routes"], ctx["middleware"])
370
378
  self._link_routes_to_auth_schemes(ctx["routes"], ctx["auth"])
371
379
  self._propagate_file_auth_to_routes(ctx["routes"], ctx["auth"])
380
+ self._propagate_express_parent_auth_to_routes(ctx["routes"])
381
+ self._propagate_django_class_auth_to_routes(ctx["routes"], ctx["auth"])
382
+ self._propagate_rails_auth_to_routes(ctx["routes"], ctx["auth"])
372
383
  self._apply_spring_filter_chain_policy(ctx["routes"])
384
+ self._infer_pydantic_route_bodies(ctx["routes"])
373
385
  ctx["calls"] = self._extract_all_calls()
374
386
 
375
387
  def _stage_analyze_flows(self, ctx: dict[str, Any]) -> None:
@@ -485,13 +497,20 @@ class ProjectAnalyzer:
485
497
  logger.debug("Django prefix map failed: %s", e)
486
498
  elif language == Language.JAVASCRIPT:
487
499
  try:
488
- from .url_prefix_resolver import build_express_prefix_map
500
+ from .url_prefix_resolver import (
501
+ build_express_auth_middleware_map,
502
+ build_express_prefix_map,
503
+ )
489
504
 
490
505
  pm = build_express_prefix_map(files, project_root=self.config.project_root)
491
506
  context.language_services["_url_prefix_map"] = pm
492
507
  logger.debug("Express prefix map built: %d entries", len(pm))
508
+
509
+ am = build_express_auth_middleware_map(files)
510
+ self._express_parent_auth_map = am
511
+ logger.debug("Express auth middleware map built: %d entries", len(am))
493
512
  except Exception as e:
494
- logger.debug("Express prefix map failed: %s", e)
513
+ logger.debug("Express prefix/auth map failed: %s", e)
495
514
 
496
515
  self._analysis_contexts[language] = context
497
516
 
@@ -1286,6 +1305,138 @@ class ProjectAnalyzer:
1286
1305
  # matches the suffix of any qualified name.
1287
1306
  return any(qn.endswith("." + type_name) for qn in known_models)
1288
1307
 
1308
+ # -------------------------------------------------------------------------
1309
+ # Pydantic body inference
1310
+ # -------------------------------------------------------------------------
1311
+
1312
+ # Frameworks whose plugins don't natively parse Pydantic request bodies.
1313
+ # FastAPI and Django Ninja are excluded because their plugins already set
1314
+ # route.body from route signature annotations.
1315
+ _PYDANTIC_INFER_FRAMEWORKS: frozenset[str] = frozenset(
1316
+ {"flask", "django", "generic", "webhook", ""}
1317
+ )
1318
+
1319
+ # Pydantic instantiation patterns we recognise in call sites.
1320
+ # A call site contributes a candidate model name when:
1321
+ # (a) its callee_name exactly matches a known Pydantic model class name, OR
1322
+ # (b) its callee_name is "ModelName.parse_obj", "ModelName.model_validate",
1323
+ # or "ModelName.from_orm".
1324
+ _PYDANTIC_CLASS_METHODS: frozenset[str] = frozenset(
1325
+ {"parse_obj", "model_validate", "from_orm", "model_validate_json"}
1326
+ )
1327
+
1328
+ # Receiver expressions that indicate the argument came from the HTTP request.
1329
+ _REQUEST_RECEIVERS: frozenset[str] = frozenset({"request", "req"})
1330
+
1331
+ def _infer_pydantic_route_bodies(self, routes: list[RouteModel]) -> None:
1332
+ """Set body.model_name for Python routes that instantiate a Pydantic model.
1333
+
1334
+ Flask, Django, and similar frameworks don't emit Pydantic body metadata
1335
+ from their plugins — the handler just calls ``Model(**request.get_json())``
1336
+ directly. This pass scans call sites to recover those model references,
1337
+ so _collect_schemas can assign usage="request_body" instead of "domain".
1338
+ """
1339
+ py_ctx = self._analysis_contexts.get(Language.PYTHON)
1340
+ if not py_ctx or not py_ctx.type_resolver:
1341
+ return
1342
+
1343
+ all_models = py_ctx.type_resolver.get_all_models()
1344
+ if not all_models:
1345
+ return
1346
+
1347
+ # simple class name → first matching qualified name
1348
+ simple_to_qname: dict[str, str] = {}
1349
+ for qname in all_models:
1350
+ simple = qname.rsplit(".", 1)[-1]
1351
+ if simple not in simple_to_qname:
1352
+ simple_to_qname[simple] = qname
1353
+ model_simple_names: frozenset[str] = frozenset(simple_to_qname)
1354
+
1355
+ # caller qualified name → list of ParsedCallSite
1356
+ caller_calls: dict[str, list] = {}
1357
+ for parsed in self._parsed_files.values():
1358
+ if not parsed.success or parsed.language != Language.PYTHON:
1359
+ continue
1360
+ for cs in parsed.call_sites or []:
1361
+ if cs.caller_function:
1362
+ key = cs.caller_function.full
1363
+ caller_calls.setdefault(key, []).append(cs)
1364
+
1365
+ for i, route in enumerate(routes):
1366
+ if route.body is not None:
1367
+ continue
1368
+ if route.method not in ("POST", "PUT", "PATCH"):
1369
+ continue
1370
+ if route.framework not in self._PYDANTIC_INFER_FRAMEWORKS:
1371
+ continue
1372
+
1373
+ call_sites = caller_calls.get(route.handler_function, [])
1374
+ if not call_sites:
1375
+ continue
1376
+
1377
+ model_name = self._pick_pydantic_body_model(call_sites, model_simple_names)
1378
+ if model_name:
1379
+ routes[i] = route.model_copy(
1380
+ update={
1381
+ "body": HttpBodyModel(
1382
+ content_type="application/json",
1383
+ model_name=model_name,
1384
+ body_fields=[],
1385
+ required=True,
1386
+ )
1387
+ }
1388
+ )
1389
+
1390
+ def _pick_pydantic_body_model(
1391
+ self,
1392
+ call_sites: list,
1393
+ model_simple_names: frozenset[str],
1394
+ ) -> str | None:
1395
+ """Return the first Pydantic model instantiated with what looks like request data.
1396
+
1397
+ Prefers calls that directly spread request data (**request.json, **data where
1398
+ data came from get_json). Falls back to any Pydantic model instantiation found
1399
+ in the handler — these are almost always the request schema for POST handlers.
1400
+ """
1401
+ # Check if this handler reads from an HTTP request object at all
1402
+ has_request_read = any(
1403
+ cs.receiver_expression in self._REQUEST_RECEIVERS
1404
+ or "get_json" in cs.callee_name
1405
+ or "request.json" in cs.callee_name
1406
+ or "request.data" in cs.callee_name
1407
+ or "request.body" in cs.callee_name
1408
+ for cs in call_sites
1409
+ )
1410
+
1411
+ candidates: list[tuple[int, str]] = [] # (priority, model_name)
1412
+
1413
+ for cs in call_sites:
1414
+ callee = cs.callee_name
1415
+
1416
+ # Direct instantiation: UserCreate(**data)
1417
+ if callee in model_simple_names:
1418
+ has_spread = any(getattr(arg, "is_keyword_spread", False) for arg in cs.arguments)
1419
+ priority = 0 if has_spread else 1
1420
+ candidates.append((priority, callee))
1421
+
1422
+ # Class-method call: UserCreate.parse_obj(data) / UserCreate.model_validate(data)
1423
+ elif "." in callee:
1424
+ obj, _, method = callee.rpartition(".")
1425
+ if obj in model_simple_names and method in self._PYDANTIC_CLASS_METHODS:
1426
+ candidates.append((0, obj))
1427
+
1428
+ if not candidates:
1429
+ return None
1430
+
1431
+ # If no request read was found, be conservative: only infer if there's a spread call
1432
+ if not has_request_read:
1433
+ candidates = [(p, m) for p, m in candidates if p == 0]
1434
+ if not candidates:
1435
+ return None
1436
+
1437
+ candidates.sort(key=lambda x: x[0])
1438
+ return candidates[0][1]
1439
+
1289
1440
  def _get_type_resolver(self):
1290
1441
  """Get the type resolver from whichever analysis context is available."""
1291
1442
  ctx = (
@@ -1426,6 +1577,11 @@ class ProjectAnalyzer:
1426
1577
  extracts=dep.extracts_fields,
1427
1578
  validates=dep.validates,
1428
1579
  jwt_operations=dep.jwt_operations,
1580
+ controller_module=dep.qualified_name.module
1581
+ if dep.qualified_name
1582
+ else "",
1583
+ only_actions=list(dep.only_actions),
1584
+ except_actions=list(dep.except_actions),
1429
1585
  )
1430
1586
  )
1431
1587
  except ValidationError as exc:
@@ -1550,6 +1706,148 @@ class ProjectAnalyzer:
1550
1706
  if dep_name:
1551
1707
  route.router_name = dep_name
1552
1708
 
1709
+ def _propagate_express_parent_auth_to_routes(self, routes: list[RouteModel]) -> None:
1710
+ """Propagate auth middleware from parent Express router files to child routes.
1711
+
1712
+ A parent router file may call router.use(apikey) before mounting sub-routers
1713
+ via router.use('/path', subRouter). Routes in those sub-router files inherit
1714
+ the middleware even though it doesn't appear in their own file.
1715
+
1716
+ The parent auth map is built by build_express_auth_middleware_map() and stored
1717
+ on self._express_parent_auth_map during context setup. Each entry maps a
1718
+ resolved file path to the list of auth middleware variable names accumulated
1719
+ from all ancestor router.use() calls that precede the mount of that file.
1720
+ """
1721
+ if not self._express_parent_auth_map:
1722
+ return
1723
+ for route in routes:
1724
+ if route.router_name is not None:
1725
+ continue
1726
+ if not route.handler_location or not route.handler_location.file:
1727
+ continue
1728
+ auth_middlewares = self._express_parent_auth_map.get(route.handler_location.file, [])
1729
+ if auth_middlewares:
1730
+ route.router_name = "+".join(auth_middlewares)
1731
+
1732
+ def _propagate_django_class_auth_to_routes(
1733
+ self,
1734
+ routes: list[RouteModel],
1735
+ auth_model: AuthModel,
1736
+ ) -> None:
1737
+ """Link Django CBV CLASS-type auth dependencies to their routes.
1738
+
1739
+ DRF APIView subclasses that inherit from an auth mixin (e.g. ApiAuthMixin)
1740
+ are emitted as CLASS-type auth dependencies by the Django plugin. The
1741
+ generic _propagate_file_auth_to_routes only handles MIDDLEWARE-type deps,
1742
+ so this pass closes the gap for class-based auth.
1743
+
1744
+ Matching logic: route handler_function strings look like
1745
+ "urls.FileStandardUploadApi.post" (module.ClassName.method). We split on "."
1746
+ and check every component against a global class-name → dep-name map so
1747
+ the lookup works regardless of module prefix.
1748
+
1749
+ File-key matching is intentionally skipped — Django view classes are defined
1750
+ in apis.py but routes are extracted from urls.py, so the handler_location
1751
+ file and the dep location file differ.
1752
+ """
1753
+ class_to_dep: dict[str, str] = {}
1754
+ for dep in auth_model.auth_dependencies:
1755
+ if dep.type != "CLASS":
1756
+ continue
1757
+ class_to_dep[dep.name] = dep.name
1758
+
1759
+ if not class_to_dep:
1760
+ return
1761
+
1762
+ for route in routes:
1763
+ if route.router_name is not None:
1764
+ continue
1765
+ handler = str(route.handler_function or "")
1766
+ for part in handler.split("."):
1767
+ dep_name = class_to_dep.get(part)
1768
+ if dep_name:
1769
+ route.router_name = dep_name
1770
+ break
1771
+
1772
+ def _propagate_rails_auth_to_routes(
1773
+ self,
1774
+ routes: list[RouteModel],
1775
+ auth_model: AuthModel,
1776
+ ) -> None:
1777
+ """Link Rails before_action guards to routes using controller + action matching.
1778
+
1779
+ Deps extracted by the Rails plugin carry qualified_name.module = controller
1780
+ resource name (e.g. 'articles') and only_actions/except_actions filters.
1781
+ Routes carry handler_function.module = same controller name and
1782
+ handler_function.name = action name.
1783
+
1784
+ A route is covered by a dep when:
1785
+ - dep.qualified_name.module == route.handler_function.module (same controller)
1786
+ - action is in dep.only_actions (if only_actions is set), OR
1787
+ action is NOT in dep.except_actions (if except_actions is set), OR
1788
+ no filter is set (applies to all actions)
1789
+
1790
+ Special case — Ruby GraphQL routes (path starts with /graphql:):
1791
+ graphql-ruby routes are served by a single GraphqlController whose
1792
+ controller_module is "graphql". Their handler_function.module is the
1793
+ type-file stem ("query_type", "mutation_type"), NOT "graphql", so the
1794
+ regular controller matching never fires. We match them unconditionally
1795
+ against any dep whose controller_module is "graphql" — action-level
1796
+ only:/except: filters on that dep target controller actions ("execute"),
1797
+ not individual GraphQL field names, so coverage is treated as global.
1798
+ """
1799
+ rails_deps = [
1800
+ dep
1801
+ for dep in auth_model.auth_dependencies
1802
+ if dep.type == "FUNCTION" and dep.controller_module
1803
+ ]
1804
+ if not rails_deps:
1805
+ return
1806
+
1807
+ graphql_ctrl_deps = [d for d in rails_deps if d.controller_module == "graphql"]
1808
+
1809
+ for route in routes:
1810
+ if route.router_name is not None:
1811
+ continue
1812
+ hf = route.handler_function
1813
+ if not hf:
1814
+ continue
1815
+
1816
+ # Ruby GraphQL routes: match against GraphqlController deps only.
1817
+ if route.path.startswith("/graphql:") and graphql_ctrl_deps:
1818
+ route.router_name = graphql_ctrl_deps[0].name
1819
+ continue
1820
+
1821
+ # handler_function stored as "controller.action" string in RouteModel
1822
+ hf_str = str(hf)
1823
+ parts = hf_str.split(".")
1824
+ ctrl_module = parts[0] if len(parts) > 1 else ""
1825
+ action = parts[-1]
1826
+
1827
+ for dep in rails_deps:
1828
+ # Rails singular `resource :user` stores module "user" but
1829
+ # the controller class is UsersController → dep module "users".
1830
+ # Try exact match first, then simple +s pluralization as fallback.
1831
+ # Namespaced controllers (Api::V1::AnnouncementsController →
1832
+ # "api/v1/announcements") only store the resource name in the
1833
+ # routes file ("announcements"), so also match on the last segment.
1834
+ dep_mod = dep.controller_module
1835
+ dep_suffix = dep_mod.rsplit("/", 1)[-1]
1836
+ candidates = {ctrl_module, ctrl_module + "s"}
1837
+ if dep_mod not in candidates and dep_suffix not in candidates:
1838
+ continue
1839
+ only = dep.only_actions
1840
+ excl = dep.except_actions
1841
+ if only:
1842
+ covered = action in only
1843
+ elif excl:
1844
+ covered = action not in excl
1845
+ else:
1846
+ covered = True
1847
+ if covered:
1848
+ route.router_name = dep.name
1849
+ break
1850
+
1553
1851
  def _apply_spring_filter_chain_policy(self, routes: list[RouteModel]) -> None:
1554
1852
  """Apply a Spring SecurityFilterChain policy to Spring/GraphQL routes.
1555
1853
 
@@ -1844,6 +2142,20 @@ class ProjectAnalyzer:
1844
2142
  "truncated_flows": 0,
1845
2143
  }
1846
2144
 
2145
+ # ── Ruby intra-method taint (runs without a call graph) ──────────────
2146
+ ruby_files = [
2147
+ pf for pf in self._parsed_files.values() if pf.success and pf.language == Language.RUBY
2148
+ ]
2149
+ if ruby_files:
2150
+ from ..frameworks.ruby.taint_scanner import RubyTaintScanner
2151
+
2152
+ for flow_dict in RubyTaintScanner(ruby_files).scan():
2153
+ try:
2154
+ flows.append(DataFlowModel(**flow_dict))
2155
+ self._data_flow_stats["flows_discovered"] += 1
2156
+ except (KeyError, ValueError, TypeError) as exc:
2157
+ logger.warning("Ruby taint flow model error: %s", exc)
2158
+
1847
2159
  if not self._call_graph:
1848
2160
  return flows, self._data_flow_stats
1849
2161
 
@@ -400,6 +400,30 @@ class CallGraphBuilder:
400
400
  if len(parts) > 1:
401
401
  scope_function = ".".join(parts[:-1])
402
402
 
403
+ # Statically-typed languages (Java, C#) record a local variable's
404
+ # DECLARED type, which is authoritative for method-receiver
405
+ # resolution regardless of the initializer -- e.g. for
406
+ # ``LoaderOptions options = new LoaderOptions()`` the initializer is
407
+ # an object-creation expression (not a "call"), but the declared
408
+ # type tells us ``options`` is a ``LoaderOptions``. Binding it lets
409
+ # ``options.method()`` resolve to the library FQCN (needed for
410
+ # class-level reachability). Resolve the declared type through the
411
+ # import map; skip ``var``/``val`` inference keywords and primitives
412
+ # (lower-cased type names like ``int``/``boolean`` are never call
413
+ # receivers).
414
+ decl_type = assign.type_annotation
415
+ if decl_type and decl_type not in ("var", "val", "dynamic"):
416
+ base = decl_type.split("<", 1)[0].replace("[]", "").strip()
417
+ if base and base[0].isupper():
418
+ resolved = self._imports.get((file_path, base), base)
419
+ self._bindings.add_assignment(
420
+ assign.target,
421
+ resolved,
422
+ file_path,
423
+ assign.location.line,
424
+ scope_function=scope_function,
425
+ )
426
+
403
427
  if assign.source_type == "call" and assign.source_call:
404
428
  # x = SomeClass() -> x is bound to SomeClass
405
429
  called = assign.source_call
@@ -47,6 +47,21 @@ if TYPE_CHECKING:
47
47
  logger = logging.getLogger(__name__)
48
48
 
49
49
 
50
+ # Maps an OriginType member name to its wire placement (the OpenAPI-style
51
+ # ``in`` taxonomy: query/path/header/cookie/body). Only HTTP request inputs
52
+ # have a wire identity; non-request origins (env vars, DB reads, etc.) are
53
+ # absent here and produce no WireIdentityModel.
54
+ _ORIGIN_TYPE_TO_WIRE_IN: dict[str, str] = {
55
+ "HTTP_PATH_PARAM": "path",
56
+ "HTTP_QUERY_PARAM": "query",
57
+ "HTTP_HEADER": "header",
58
+ "HTTP_COOKIE": "cookie",
59
+ "HTTP_BODY": "body",
60
+ "HTTP_FORM": "body",
61
+ "HTTP_FILE": "body",
62
+ }
63
+
64
+
50
65
  # =============================================================================
51
66
  # Core Data Structures
52
67
  # =============================================================================
@@ -1720,6 +1735,7 @@ class DataFlowAnalyzer:
1720
1735
  FlowStepModel,
1721
1736
  LocationModel,
1722
1737
  TransformationModel,
1738
+ WireIdentityModel,
1723
1739
  )
1724
1740
 
1725
1741
  result = []
@@ -1763,16 +1779,25 @@ class DataFlowAnalyzer:
1763
1779
 
1764
1780
  sink_evidence_model = self._evidence_to_model(flow.sink_evidence)
1765
1781
 
1782
+ origin_type_name = flow.origin_type.name
1783
+ wire_in = _ORIGIN_TYPE_TO_WIRE_IN.get(origin_type_name)
1784
+ wire_identity = (
1785
+ WireIdentityModel(name=flow.origin_name, location=wire_in)
1786
+ if wire_in and flow.origin_name
1787
+ else None
1788
+ )
1789
+
1766
1790
  flow_model = DataFlowModel(
1767
1791
  id=flow.id,
1768
1792
  origin=DataOriginModel(
1769
- type=flow.origin_type.name,
1793
+ type=origin_type_name,
1770
1794
  name=flow.origin_name,
1771
1795
  location=LocationModel(
1772
1796
  file=str(flow.origin_location[0]),
1773
1797
  line=flow.origin_location[1],
1774
1798
  ),
1775
1799
  entry_point_ref=flow.entry_point_id,
1800
+ wire=wire_identity,
1776
1801
  ),
1777
1802
  sink=DataSinkModel(
1778
1803
  function=flow.sink_function,
@@ -37,6 +37,7 @@ class DependencyExtractor:
37
37
  "package.json": "_parse_package_json",
38
38
  "go.mod": "_parse_go_mod",
39
39
  "Cargo.toml": "_parse_cargo_toml",
40
+ "Gemfile.lock": "_parse_gemfile_lock",
40
41
  }
41
42
 
42
43
  # Each parser handles exactly one ecosystem; tag packages authoritatively at
@@ -54,6 +55,7 @@ class DependencyExtractor:
54
55
  "_parse_csproj": "NuGet",
55
56
  "_parse_pom": "Maven",
56
57
  "_parse_gradle": "Maven",
58
+ "_parse_gemfile_lock": "RubyGems",
57
59
  }
58
60
 
59
61
  _SKIP_DIRS = frozenset(
@@ -354,6 +356,48 @@ class DependencyExtractor:
354
356
 
355
357
  return packages
356
358
 
359
+ # -- Gemfile.lock (Ruby) ---------------------------------------------------
360
+
361
+ def _parse_gemfile_lock(self, path: Path) -> list[PackageDependencyModel]:
362
+ """Parse Gemfile.lock — resolved gems under each GEM/GIT/PATH ``specs:``.
363
+
364
+ A lockfile pins exact versions, so every gem yields a confident version.
365
+ Resolved gems sit at a 4-space indent as ``name (version)``; deeper
366
+ (6-space) lines are sub-dependency constraints and are skipped, as is the
367
+ 2-space DEPENDENCIES section (ranges, not resolved). Platform suffixes
368
+ (``1.13.9-x86_64-linux``) are stripped — Ruby prereleases use dots
369
+ (``1.2.3.rc1``), so a hyphen in a lockfile version is always the platform.
370
+ """
371
+ packages: list[PackageDependencyModel] = []
372
+ spec_re = re.compile(r"^ ([A-Za-z0-9._-]+) \(([^)]+)\)$")
373
+ try:
374
+ in_specs = False
375
+ for raw in path.read_text(errors="replace").splitlines():
376
+ if raw and not raw[0].isspace():
377
+ in_specs = False # top-level section (GEM, PLATFORMS, DEPENDENCIES, …)
378
+ continue
379
+ if raw.strip() == "specs:":
380
+ in_specs = True
381
+ continue
382
+ if not in_specs:
383
+ continue
384
+ m = spec_re.match(raw)
385
+ if not m:
386
+ continue # 6-space sub-dependency constraints, etc.
387
+ packages.append(
388
+ PackageDependencyModel(
389
+ name=m.group(1),
390
+ version=m.group(2).split("-", 1)[0].strip(),
391
+ version_constraint=None,
392
+ is_dev=False,
393
+ source_file=path.name,
394
+ )
395
+ )
396
+ except Exception as e:
397
+ logger.warning("Failed to parse %s: %s", path.name, e)
398
+
399
+ return packages
400
+
357
401
  # -- Pipfile ---------------------------------------------------------------
358
402
 
359
403
  def _parse_pipfile(self, path: Path) -> list[PackageDependencyModel]: