codejury 0.9.0__tar.gz → 0.9.1__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 (183) hide show
  1. {codejury-0.9.0 → codejury-0.9.1}/PKG-INFO +1 -1
  2. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/verifier.py +13 -4
  3. {codejury-0.9.0 → codejury-0.9.1}/codejury/analysis/taint.py +39 -17
  4. {codejury-0.9.0 → codejury-0.9.1}/codejury/assembly.py +8 -2
  5. {codejury-0.9.0 → codejury-0.9.1}/codejury/cli.py +2 -2
  6. {codejury-0.9.0 → codejury-0.9.1}/codejury/evaluation.py +14 -3
  7. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/taint_gate.py +1 -1
  8. {codejury-0.9.0 → codejury-0.9.1}/codejury/reporting.py +2 -9
  9. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/PKG-INFO +1 -1
  10. {codejury-0.9.0 → codejury-0.9.1}/pyproject.toml +1 -1
  11. {codejury-0.9.0 → codejury-0.9.1}/tests/test_assembly.py +20 -1
  12. {codejury-0.9.0 → codejury-0.9.1}/tests/test_evaluation.py +11 -0
  13. {codejury-0.9.0 → codejury-0.9.1}/tests/test_taint.py +27 -1
  14. {codejury-0.9.0 → codejury-0.9.1}/tests/test_taint_crossfile.py +10 -0
  15. {codejury-0.9.0 → codejury-0.9.1}/tests/test_taint_gate.py +17 -0
  16. {codejury-0.9.0 → codejury-0.9.1}/LICENSE +0 -0
  17. {codejury-0.9.0 → codejury-0.9.1}/README.md +0 -0
  18. {codejury-0.9.0 → codejury-0.9.1}/codejury/__init__.py +0 -0
  19. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/__init__.py +0 -0
  20. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/base.py +0 -0
  21. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/debate.py +0 -0
  22. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/mock.py +0 -0
  23. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/parsing.py +0 -0
  24. {codejury-0.9.0 → codejury-0.9.1}/codejury/agents/refuter.py +0 -0
  25. {codejury-0.9.0 → codejury-0.9.1}/codejury/analysis/__init__.py +0 -0
  26. {codejury-0.9.0 → codejury-0.9.1}/codejury/analysis/provenance.py +0 -0
  27. {codejury-0.9.0 → codejury-0.9.1}/codejury/baseline.py +0 -0
  28. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/authentication.yaml +0 -0
  29. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/authorization.yaml +0 -0
  30. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/business_logic.yaml +0 -0
  31. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/crypto.yaml +0 -0
  32. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/data_protection.yaml +0 -0
  33. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/dependency_config.yaml +0 -0
  34. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/error_logging.yaml +0 -0
  35. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/excessive_agency.yaml +0 -0
  36. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/input_validation.yaml +0 -0
  37. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
  38. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/output_encoding.yaml +0 -0
  39. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/prompt_injection.yaml +0 -0
  40. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/secrets.yaml +0 -0
  41. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/capabilities/session.yaml +0 -0
  42. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
  43. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
  44. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
  45. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
  46. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
  47. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
  48. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
  49. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
  50. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
  51. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
  52. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_sha256_password.yaml +0 -0
  53. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
  54. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
  55. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/authz_owner_safe.yaml +0 -0
  56. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
  57. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
  58. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
  59. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
  60. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
  61. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
  62. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
  63. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
  64. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
  65. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
  66. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
  67. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
  68. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
  69. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
  70. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
  71. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
  72. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
  73. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
  74. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
  75. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
  76. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
  77. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/literal_eval_safe.yaml +0 -0
  78. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/path_basename_safe.yaml +0 -0
  79. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/path_contained_safe.yaml +0 -0
  80. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
  81. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
  82. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
  83. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
  84. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
  85. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
  86. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
  87. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/secrets_env_safe.yaml +0 -0
  88. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
  89. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
  90. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
  91. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
  92. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
  93. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
  94. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
  95. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
  96. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
  97. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
  98. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
  99. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
  100. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
  101. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
  102. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
  103. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
  104. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
  105. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
  106. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
  107. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/suppressions.yaml +0 -0
  108. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/taint.yaml +0 -0
  109. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
  110. {codejury-0.9.0 → codejury-0.9.1}/codejury/data/tasks/quick_scan_single.yaml +0 -0
  111. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/__init__.py +0 -0
  112. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/artifact.py +0 -0
  113. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/capability.py +0 -0
  114. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/context.py +0 -0
  115. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/observation.py +0 -0
  116. {codejury-0.9.0 → codejury-0.9.1}/codejury/domain/result.py +0 -0
  117. {codejury-0.9.0 → codejury-0.9.1}/codejury/infrastructure/__init__.py +0 -0
  118. {codejury-0.9.0 → codejury-0.9.1}/codejury/infrastructure/cache.py +0 -0
  119. {codejury-0.9.0 → codejury-0.9.1}/codejury/infrastructure/json_parse.py +0 -0
  120. {codejury-0.9.0 → codejury-0.9.1}/codejury/integrations/__init__.py +0 -0
  121. {codejury-0.9.0 → codejury-0.9.1}/codejury/integrations/github.py +0 -0
  122. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/__init__.py +0 -0
  123. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/base.py +0 -0
  124. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/challenge.py +0 -0
  125. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/debate.py +0 -0
  126. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/pipeline.py +0 -0
  127. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/reflexion.py +0 -0
  128. {codejury-0.9.0 → codejury-0.9.1}/codejury/orchestrators/single.py +0 -0
  129. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/__init__.py +0 -0
  130. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/anthropic.py +0 -0
  131. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/base.py +0 -0
  132. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/litellm.py +0 -0
  133. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/mock.py +0 -0
  134. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/openai.py +0 -0
  135. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/openai_format.py +0 -0
  136. {codejury-0.9.0 → codejury-0.9.1}/codejury/providers/retry.py +0 -0
  137. {codejury-0.9.0 → codejury-0.9.1}/codejury/resources.py +0 -0
  138. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/__init__.py +0 -0
  139. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/base.py +0 -0
  140. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/callers.py +0 -0
  141. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/chunker.py +0 -0
  142. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/diff.py +0 -0
  143. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/function.py +0 -0
  144. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/mock.py +0 -0
  145. {codejury-0.9.0 → codejury-0.9.1}/codejury/sources/repo.py +0 -0
  146. {codejury-0.9.0 → codejury-0.9.1}/codejury/suppression.py +0 -0
  147. {codejury-0.9.0 → codejury-0.9.1}/codejury/tasks/__init__.py +0 -0
  148. {codejury-0.9.0 → codejury-0.9.1}/codejury/tasks/base.py +0 -0
  149. {codejury-0.9.0 → codejury-0.9.1}/codejury/tasks/registry.py +0 -0
  150. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/SOURCES.txt +0 -0
  151. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/dependency_links.txt +0 -0
  152. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/entry_points.txt +0 -0
  153. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/requires.txt +0 -0
  154. {codejury-0.9.0 → codejury-0.9.1}/codejury.egg-info/top_level.txt +0 -0
  155. {codejury-0.9.0 → codejury-0.9.1}/setup.cfg +0 -0
  156. {codejury-0.9.0 → codejury-0.9.1}/tests/test_anthropic_provider.py +0 -0
  157. {codejury-0.9.0 → codejury-0.9.1}/tests/test_audit_pipeline.py +0 -0
  158. {codejury-0.9.0 → codejury-0.9.1}/tests/test_baseline.py +0 -0
  159. {codejury-0.9.0 → codejury-0.9.1}/tests/test_cache.py +0 -0
  160. {codejury-0.9.0 → codejury-0.9.1}/tests/test_callers.py +0 -0
  161. {codejury-0.9.0 → codejury-0.9.1}/tests/test_capability.py +0 -0
  162. {codejury-0.9.0 → codejury-0.9.1}/tests/test_challenge.py +0 -0
  163. {codejury-0.9.0 → codejury-0.9.1}/tests/test_cli_audit.py +0 -0
  164. {codejury-0.9.0 → codejury-0.9.1}/tests/test_context.py +0 -0
  165. {codejury-0.9.0 → codejury-0.9.1}/tests/test_debate_agents.py +0 -0
  166. {codejury-0.9.0 → codejury-0.9.1}/tests/test_debate_orchestrator.py +0 -0
  167. {codejury-0.9.0 → codejury-0.9.1}/tests/test_diff_source.py +0 -0
  168. {codejury-0.9.0 → codejury-0.9.1}/tests/test_function_source.py +0 -0
  169. {codejury-0.9.0 → codejury-0.9.1}/tests/test_integrations.py +0 -0
  170. {codejury-0.9.0 → codejury-0.9.1}/tests/test_json_parse.py +0 -0
  171. {codejury-0.9.0 → codejury-0.9.1}/tests/test_litellm_provider.py +0 -0
  172. {codejury-0.9.0 → codejury-0.9.1}/tests/test_openai_provider.py +0 -0
  173. {codejury-0.9.0 → codejury-0.9.1}/tests/test_orchestrator.py +0 -0
  174. {codejury-0.9.0 → codejury-0.9.1}/tests/test_pipeline_orchestrator.py +0 -0
  175. {codejury-0.9.0 → codejury-0.9.1}/tests/test_provenance.py +0 -0
  176. {codejury-0.9.0 → codejury-0.9.1}/tests/test_reflexion_orchestrator.py +0 -0
  177. {codejury-0.9.0 → codejury-0.9.1}/tests/test_repo_source.py +0 -0
  178. {codejury-0.9.0 → codejury-0.9.1}/tests/test_reporting.py +0 -0
  179. {codejury-0.9.0 → codejury-0.9.1}/tests/test_retry_provider.py +0 -0
  180. {codejury-0.9.0 → codejury-0.9.1}/tests/test_sarif.py +0 -0
  181. {codejury-0.9.0 → codejury-0.9.1}/tests/test_suppression.py +0 -0
  182. {codejury-0.9.0 → codejury-0.9.1}/tests/test_tasks.py +0 -0
  183. {codejury-0.9.0 → codejury-0.9.1}/tests/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: General-purpose Application Security AI audit framework -- five-layer architecture, capabilities as first-class data
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -93,16 +93,25 @@ def _build_prompt(path: str, content: str, cap: Capability, context: str = "") -
93
93
  )
94
94
 
95
95
 
96
- def _anti_pattern_cwes(cap: Capability) -> dict[str, str]:
97
- """Map anti_pattern id -> CWE, so a verdict can inherit the CWE it matched."""
96
+ _SEVERITY_RANK = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0}
97
+
98
+
99
+ def _anti_pattern_cwes(cap: Capability) -> dict[str, tuple[int, str]]:
100
+ """Map anti_pattern id -> (severity rank, CWE), so a verdict can inherit the CWE
101
+ of the most severe anti-pattern it matched (deterministic, not first-seen)."""
98
102
  return {
99
- p.id: p.cwe
103
+ p.id: (_SEVERITY_RANK.get(p.severity, 2), p.cwe)
100
104
  for sub in cap.sub_capabilities.values()
101
105
  for p in sub.anti_patterns
102
106
  if p.cwe
103
107
  }
104
108
 
105
109
 
110
+ def _resolve_cwe(matched_anti: list[str], cwe_by_id: dict[str, tuple[int, str]]) -> str:
111
+ matched = [cwe_by_id[a] for a in matched_anti if a in cwe_by_id]
112
+ return max(matched, key=lambda rank_cwe: rank_cwe[0])[1] if matched else ""
113
+
114
+
106
115
  def _parse_verdicts(text: str, cap: Capability) -> list[Verdict]:
107
116
  obj = extract_json_object(text)
108
117
  if not obj:
@@ -122,7 +131,7 @@ def _parse_verdicts(text: str, cap: Capability) -> list[Verdict]:
122
131
  reasoning=str(v.get("reasoning", "")),
123
132
  matched_correct=str_list(v.get("matched_correct")),
124
133
  matched_anti=matched_anti,
125
- cwe=next((cwe_by_id[a] for a in matched_anti if a in cwe_by_id), ""),
134
+ cwe=_resolve_cwe(matched_anti, cwe_by_id),
126
135
  evidence=to_evidence(v.get("evidence")),
127
136
  confidence=to_float(v.get("confidence"), 0.5),
128
137
  )
@@ -21,6 +21,7 @@ PARAM, for the cross-file caller hop (next) to resolve.
21
21
  from __future__ import annotations
22
22
 
23
23
  import ast
24
+ import functools
24
25
  from dataclasses import dataclass
25
26
  from enum import Enum
26
27
  from pathlib import Path
@@ -133,6 +134,12 @@ def _walk(func, expr, vocab, assigns, params, seen, resolve) -> Taint:
133
134
  return Taint.SANITIZED # a sanitizer cleans its result regardless of input
134
135
  if _callee_in(expr, vocab.sources):
135
136
  return Taint.EXTERNAL # e.g. input()
137
+ # a method ON a source/trusted object, e.g. request.args.get("id") or os.environ.get("X")
138
+ func_path = access_path(expr.func)
139
+ if func_path and _access_in(func_path, vocab.sources):
140
+ return Taint.EXTERNAL
141
+ if func_path and _access_in(func_path, vocab.trusted):
142
+ return Taint.TRUSTED
136
143
  if _callee_in(expr, vocab.propagators) or _callee_in(expr, vocab.safe_sinks):
137
144
  return _combine([w(a) for a in expr.args] or [Taint.CONSTANT])
138
145
  return Taint.UNKNOWN # unknown call -- a cross-file hop may resolve it later
@@ -197,10 +204,11 @@ def taint_in_repo(
197
204
 
198
205
 
199
206
  def _caller_resolver(func, files, vocab):
207
+ sites = _call_sites(func.name, files) # computed once, reused across parameters
200
208
  def resolve(param_name: str) -> Taint:
201
209
  index = _param_index(func, param_name)
202
210
  results = []
203
- for scope, call in _call_sites(func.name, files):
211
+ for scope, call in sites:
204
212
  arg = _arg_for_param(call, index, param_name)
205
213
  # one hop only: classify the caller's argument without recursing further
206
214
  results.append(taint_of(scope, arg, vocab) if arg is not None else Taint.UNKNOWN)
@@ -210,6 +218,8 @@ def _caller_resolver(func, files, vocab):
210
218
 
211
219
  def _param_index(func, name: str) -> int | None:
212
220
  positional = [*func.args.posonlyargs, *func.args.args]
221
+ if positional and positional[0].arg in ("self", "cls"):
222
+ positional = positional[1:] # bound-method call sites omit the receiver
213
223
  for i, arg in enumerate(positional):
214
224
  if arg.arg == name:
215
225
  return i
@@ -225,12 +235,21 @@ def _arg_for_param(call: ast.Call, index: int | None, name: str) -> ast.AST | No
225
235
  return None
226
236
 
227
237
 
238
+ @functools.lru_cache(maxsize=256)
239
+ def _parse(source: str) -> ast.Module | None:
240
+ """Parse a source string once and cache it (the same files are walked repeatedly
241
+ during cross-file resolution); None if it does not parse."""
242
+ try:
243
+ return ast.parse(source)
244
+ except SyntaxError:
245
+ return None
246
+
247
+
228
248
  def _call_sites(name: str, files: dict[str, str]) -> list[tuple[ast.AST, ast.Call]]:
229
249
  sites = []
230
250
  for source in files.values():
231
- try:
232
- tree = ast.parse(source)
233
- except SyntaxError:
251
+ tree = _parse(source)
252
+ if tree is None:
234
253
  continue
235
254
  funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
236
255
  for call in find_calls(tree, name):
@@ -250,24 +269,27 @@ def worst_sink_taint(content: str, files: dict[str, str], vocab: TaintVocab) ->
250
269
 
251
270
  A "potential sink" is any call that is not a safe sink, sanitizer, or
252
271
  propagator (those are not where injection happens). Each such call's argument
253
- taint is classified with the cross-file resolver, and the worst is returned.
254
- ``Taint.CONSTANT`` when there is no sink to worry about; ``None`` when the
255
- code does not parse (the caller should then not act).
256
-
257
- Used by the taint gate to downgrade an input_validation finding only when the
258
- whole artifact is provably clean -- so a single tainted sink keeps every
259
- finding (recall preserved).
272
+ taint is classified with the cross-file resolver. A safe sink that consumes
273
+ tainted data (e.g. ``json.loads(request.data)``) contributes SANITIZED -- the
274
+ data was handled safely. The worst contribution is returned.
275
+
276
+ ``Taint.UNKNOWN`` when no inspectable sink is found (the artifact may still be
277
+ unsafe via a return value or an implicit sink, so it is NOT assumed clean);
278
+ ``None`` when the code does not parse. The taint gate downgrades a finding only
279
+ on a SAFE result, so an unproven artifact keeps its findings (recall preserved).
260
280
  """
261
- try:
262
- tree = ast.parse(content)
263
- except SyntaxError:
281
+ tree = _parse(content)
282
+ if tree is None:
264
283
  return None
265
284
  funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
266
285
  taints: list[Taint] = []
267
286
  for call in [n for n in ast.walk(tree) if isinstance(n, ast.Call)]:
268
- if is_safe_sink(call, vocab) or _callee_in(call, vocab.sanitizers) or _callee_in(call, vocab.propagators):
269
- continue # not a place an injection lands
287
+ if is_safe_sink(call, vocab):
288
+ taints.append(Taint.SANITIZED) # tainted data consumed by a safe parser
289
+ continue
290
+ if _callee_in(call, vocab.sanitizers) or _callee_in(call, vocab.propagators):
291
+ continue # not a sink itself; counted via the enclosing sink's argument
270
292
  scope = _enclosing_scope(funcs, call) or tree
271
293
  for arg in (*call.args, *(kw.value for kw in call.keywords)):
272
294
  taints.append(taint_in_repo(scope, arg, vocab, files))
273
- return _combine(taints) if taints else Taint.CONSTANT
295
+ return _combine(taints) if taints else Taint.UNKNOWN
@@ -79,9 +79,15 @@ def build_orchestration(
79
79
  return verifier, SingleOrchestrator()
80
80
 
81
81
 
82
- def orchestration_descriptor(strategy: str, model: str, max_tokens: int) -> str:
82
+ def provider_tag(provider: Provider) -> str:
83
+ """A stable short name for a provider (unwrapping RetryProvider) for cache keys,
84
+ so two providers that accept the same model string do not share cached verdicts."""
85
+ return type(getattr(provider, "_inner", provider)).__name__
86
+
87
+
88
+ def orchestration_descriptor(provider: Provider, strategy: str, model: str, max_tokens: int) -> str:
83
89
  """The non-code, non-capability inputs that affect a verdict, as a cache tag."""
84
- return f"{strategy}|{model}|{max_tokens}"
90
+ return f"{provider_tag(provider)}|{strategy}|{model}|{max_tokens}"
85
91
 
86
92
 
87
93
  def run_over_artifacts(
@@ -79,7 +79,7 @@ def audit(
79
79
  agents, orchestrator = build_orchestration(strategy, provider=provider, model=model, max_tokens=max_tokens)
80
80
  return run_over_source(
81
81
  DiffSource(diff_text), capabilities, agents, orchestrator,
82
- cache=cache, orchestration=orchestration_descriptor(strategy, model, max_tokens),
82
+ cache=cache, orchestration=orchestration_descriptor(provider, strategy, model, max_tokens),
83
83
  )
84
84
 
85
85
 
@@ -114,7 +114,7 @@ def scan(
114
114
  agents, orchestrator = build_orchestration(strategy, provider=provider, model=model, max_tokens=max_tokens)
115
115
  return run_over_artifacts(
116
116
  artifacts, capabilities, agents, orchestrator,
117
- cache=cache, orchestration=orchestration_descriptor(strategy, model, max_tokens),
117
+ cache=cache, orchestration=orchestration_descriptor(provider, strategy, model, max_tokens),
118
118
  )
119
119
 
120
120
 
@@ -159,8 +159,19 @@ def evaluate(
159
159
  capabilities=[capability],
160
160
  )
161
161
  result = orchestrator.run(agents, ctx)
162
- if result.error: # e.g. a provider auth failure -- surface it, don't score blanks
163
- raise RuntimeError(result.error)
164
- predicted = any(getattr(o, "status", None) == "VULNERABLE" for o in result.observations)
162
+ if result.error: # e.g. a provider auth failure -- surface it (with the case), don't score blanks
163
+ raise RuntimeError(f"case {case.name!r}: {result.error}")
164
+ predicted = _predicted_vulnerable(result.observations)
165
165
  report.record(case.capability, actual=case.vulnerable, predicted=predicted)
166
166
  return report
167
+
168
+
169
+ def _predicted_vulnerable(observations: list) -> bool:
170
+ """Did the run flag a problem? A Finding (debate/reflexion) or a VULNERABLE
171
+ Verdict (single/pipeline/taint) counts; SECURE verdicts and dismissed
172
+ concessions do not. (A bare Finding has no ``status``, so checking only
173
+ status would score debate/reflexion as zero recall.)"""
174
+ return any(
175
+ o.kind == "finding" or (o.kind == "verdict" and getattr(o, "status", None) == "VULNERABLE")
176
+ for o in observations
177
+ )
@@ -58,7 +58,7 @@ class TaintGateOrchestrator(Orchestrator):
58
58
  for v in verdicts:
59
59
  if (
60
60
  isinstance(v, Verdict)
61
- and v.status in ("VULNERABLE", "PARTIAL")
61
+ and v.status == "VULNERABLE" # leave PARTIAL ("incomplete validation") signal intact
62
62
  and v.capability.split(".")[0] in self._taint_capabilities
63
63
  ):
64
64
  observations.append(
@@ -9,8 +9,8 @@ dismissed.
9
9
  from __future__ import annotations
10
10
 
11
11
  import json
12
- from importlib.metadata import PackageNotFoundError, version
13
12
 
13
+ from codejury import __version__ as _tool_version
14
14
  from codejury.domain.observation import Observation, observation_from_dict
15
15
  from codejury.domain.result import AnalysisResult
16
16
 
@@ -24,13 +24,6 @@ _PROBLEM_STATUSES = ("VULNERABLE", "PARTIAL")
24
24
  _SARIF_LEVEL = {"CRITICAL": "error", "HIGH": "error", "MEDIUM": "warning", "LOW": "note", "INFO": "note"}
25
25
 
26
26
 
27
- def _tool_version() -> str:
28
- try:
29
- return version("codejury")
30
- except PackageNotFoundError:
31
- return "0"
32
-
33
-
34
27
  def to_json(results: Results) -> str:
35
28
  payload = {
36
29
  "files": [
@@ -183,7 +176,7 @@ def to_sarif(results: Results) -> str:
183
176
  "driver": {
184
177
  "name": "codejury",
185
178
  "informationUri": "https://github.com/aiseclabs/codejury",
186
- "version": _tool_version(),
179
+ "version": _tool_version,
187
180
  "rules": rules,
188
181
  }
189
182
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codejury
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: General-purpose Application Security AI audit framework -- five-layer architecture, capabilities as first-class data
5
5
  Author: AISecLabs
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codejury"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  description = "General-purpose Application Security AI audit framework -- five-layer architecture, capabilities as first-class data"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -2,7 +2,14 @@ from types import SimpleNamespace
2
2
 
3
3
  import pytest
4
4
 
5
- from codejury.assembly import build_orchestration, make_provider, run_over_source
5
+ from codejury.assembly import (
6
+ build_orchestration,
7
+ make_provider,
8
+ orchestration_descriptor,
9
+ provider_tag,
10
+ run_over_source,
11
+ )
12
+ from codejury.providers.retry import RetryProvider
6
13
  from codejury.domain.capability import Capability
7
14
  from codejury.orchestrators.debate import DebateOrchestrator
8
15
  from codejury.orchestrators.pipeline import PipelineOrchestrator
@@ -42,6 +49,18 @@ def test_make_provider_forwards_api_base_and_key():
42
49
  assert captured["api_key"] == "sk-test"
43
50
 
44
51
 
52
+ def test_cache_descriptor_includes_provider():
53
+ # two providers accepting the same model string must not share a cache key
54
+ a = orchestration_descriptor(MockProvider(), "single", "gpt-4o", 8)
55
+ b = orchestration_descriptor(make_provider("openai"), "single", "gpt-4o", 8)
56
+ assert a != b
57
+
58
+
59
+ def test_provider_tag_unwraps_retry():
60
+ assert provider_tag(MockProvider()) == "MockProvider"
61
+ assert provider_tag(RetryProvider(MockProvider())) == "MockProvider"
62
+
63
+
45
64
  def test_run_over_source_runs_each_artifact():
46
65
  provider = MockProvider(default='{"verdicts": [{"sub_capability": "x", "status": "SECURE"}]}')
47
66
  agents, orchestrator = build_orchestration("single", provider=provider, model="m", max_tokens=8)
@@ -155,6 +155,17 @@ def test_evaluate_taint_strategy_gates_constant_sink():
155
155
  assert taint.overall.fp == 0 and taint.overall.tn == 1 # gate cleared it
156
156
 
157
157
 
158
+ def test_predicted_counts_finding_not_just_verdict():
159
+ # debate/reflexion emit Findings (no .status); they must count as a prediction,
160
+ # else eval --orchestrator debate scores recall 0.
161
+ from codejury.domain.observation import Concession, Finding, Verdict
162
+ from codejury.evaluation import _predicted_vulnerable
163
+ assert _predicted_vulnerable([Finding(capability="x", title="t")]) is True
164
+ assert _predicted_vulnerable([Verdict(capability="x", status="VULNERABLE")]) is True
165
+ assert _predicted_vulnerable([Verdict(capability="x", status="SECURE")]) is False
166
+ assert _predicted_vulnerable([Concession(capability="x", target="t")]) is False
167
+
168
+
158
169
  def test_eval_cli_reports_provider_error_without_traceback(monkeypatch, capsys):
159
170
  class _Boom(Provider):
160
171
  def complete(self, **kwargs):
@@ -1,7 +1,7 @@
1
1
  import ast
2
2
 
3
3
  from codejury.analysis.provenance import find_calls, parse_function
4
- from codejury.analysis.taint import SAFE, Taint, is_safe_sink, load_vocab, taint_of
4
+ from codejury.analysis.taint import SAFE, Taint, is_safe_sink, load_vocab, taint_of, worst_sink_taint
5
5
 
6
6
  VOCAB = load_vocab() # the shipped codejury/data/taint.yaml
7
7
 
@@ -95,3 +95,29 @@ def test_is_safe_sink_matches_json_and_literal_eval_not_pickle():
95
95
  def test_safe_set_membership():
96
96
  assert Taint.CONSTANT in SAFE and Taint.SANITIZED in SAFE and Taint.TRUSTED in SAFE
97
97
  assert Taint.EXTERNAL not in SAFE and Taint.UNKNOWN not in SAFE and Taint.PARAM not in SAFE
98
+
99
+
100
+ def test_source_method_call_is_external():
101
+ # request.args.get("id") -- a method ON a source object -- must be EXTERNAL, not UNKNOWN
102
+ src = "def f(request):\n return request.args.get('id')\n"
103
+ func = parse_function(src, "f")
104
+ call = find_calls(func, "get")[0]
105
+ assert taint_of(func, call, VOCAB) is Taint.EXTERNAL
106
+
107
+
108
+ def test_trusted_method_call_is_trusted():
109
+ src = "def f():\n return os.environ.get('X')\n"
110
+ func = parse_function(src, "f")
111
+ assert taint_of(func, find_calls(func, "get")[0], VOCAB) is Taint.TRUSTED
112
+
113
+
114
+ def test_worst_sink_taint_unknown_when_no_sink_call():
115
+ # tainted data escapes via return with no call sink -> NOT provably safe (UNKNOWN)
116
+ code = "def q(request):\n sql = 'SELECT ' + request.args['id']\n return sql\n"
117
+ assert worst_sink_taint(code, {"m.py": code}, VOCAB) is Taint.UNKNOWN
118
+
119
+
120
+ def test_worst_sink_taint_safe_sink_only_is_sanitized():
121
+ # a safe parser consuming external data is provably safe
122
+ code = "def f(request):\n return ast.literal_eval(request.data)\n"
123
+ assert worst_sink_taint(code, {"m.py": code}, VOCAB) is Taint.SANITIZED
@@ -57,6 +57,16 @@ def test_module_level_caller_resolves():
57
57
  assert _sink_arg_taint(caller) is Taint.EXTERNAL
58
58
 
59
59
 
60
+ def test_method_call_site_resolves():
61
+ # serve is a method (has self); a bound call site obj.serve(evil) omits self,
62
+ # so the parameter index must skip self or the caller arg is never matched.
63
+ method = "class S:\n def serve(self, name):\n return open(os.path.join(STATIC_DIR, name)).read()\n"
64
+ caller = "def view(request):\n return s.serve(request.args['name'])\n"
65
+ func = parse_function(method, "serve")
66
+ open_call = find_calls(func, "open")[0]
67
+ assert taint_in_repo(func, open_call.args[0], VOCAB, {"m.py": method, "h.py": caller}) is Taint.EXTERNAL
68
+
69
+
60
70
  def test_multiple_callers_combine_to_worst():
61
71
  caller = (
62
72
  "def a(request):\n"
@@ -87,3 +87,20 @@ def test_unknown_call_is_not_cleared():
87
87
  # a value from an unknown function is not provably safe -> keep the finding.
88
88
  src = "def f(request):\n return run_query(helper(request.args['q']))\n"
89
89
  assert _statuses(_run(src)) == ["VULNERABLE"]
90
+
91
+
92
+ def test_tainted_value_without_call_sink_is_kept():
93
+ # tainted SQL string escapes via return with no call sink -> must NOT be cleared
94
+ src = "def q(request):\n sql = 'SELECT * FROM t WHERE id=' + request.args['id']\n return sql\n"
95
+ assert _statuses(_run(src)) == ["VULNERABLE"]
96
+
97
+
98
+ def test_partial_verdict_is_not_downgraded():
99
+ # the gate downgrades only VULNERABLE; PARTIAL ("incomplete validation") is kept
100
+ partial = json.dumps({"verdicts": [{"sub_capability": "x", "status": "PARTIAL"}]})
101
+ agents = {"verifier": VerifierAgent(provider=MockProvider(default=partial), model="m")}
102
+ ctx = AnalysisContext(
103
+ artifact=CodeArtifact(kind="file", path="m.py", content="def q():\n cursor.execute('SELECT 1')\n"),
104
+ capabilities=[Capability(id="input_validation", name="input_validation")],
105
+ )
106
+ assert _statuses(TaintGateOrchestrator().run(agents, ctx)) == ["PARTIAL"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes