codejury 0.9.0__tar.gz → 0.10.0__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.
- {codejury-0.9.0 → codejury-0.10.0}/PKG-INFO +7 -2
- {codejury-0.9.0 → codejury-0.10.0}/README.md +6 -1
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/verifier.py +13 -4
- {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/provenance.py +83 -38
- {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/taint.py +79 -66
- {codejury-0.9.0 → codejury-0.10.0}/codejury/assembly.py +15 -3
- {codejury-0.9.0 → codejury-0.10.0}/codejury/cli.py +2 -2
- {codejury-0.9.0 → codejury-0.10.0}/codejury/evaluation.py +14 -3
- codejury-0.10.0/codejury/orchestrators/adaptive.py +55 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/taint_gate.py +1 -1
- {codejury-0.9.0 → codejury-0.10.0}/codejury/reporting.py +2 -9
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/PKG-INFO +7 -2
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/SOURCES.txt +2 -0
- {codejury-0.9.0 → codejury-0.10.0}/pyproject.toml +1 -1
- codejury-0.10.0/tests/test_adaptive.py +71 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_assembly.py +20 -1
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_evaluation.py +11 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint.py +27 -1
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint_crossfile.py +10 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_taint_gate.py +17 -0
- {codejury-0.9.0 → codejury-0.10.0}/LICENSE +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/base.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/debate.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/mock.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/parsing.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/agents/refuter.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/analysis/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/baseline.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/authentication.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/authorization.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/business_logic.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/crypto.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/data_protection.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/dependency_config.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/error_logging.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/excessive_agency.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/input_validation.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/output_encoding.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/prompt_injection.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/secrets.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/capabilities/session.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_sha256_password.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/authz_owner_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/literal_eval_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_basename_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_contained_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/secrets_env_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/suppressions.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/taint.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/data/tasks/quick_scan_single.yaml +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/artifact.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/capability.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/context.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/observation.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/domain/result.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/cache.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/infrastructure/json_parse.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/integrations/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/integrations/github.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/base.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/challenge.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/debate.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/pipeline.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/reflexion.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/orchestrators/single.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/anthropic.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/base.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/litellm.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/mock.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/openai.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/openai_format.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/providers/retry.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/resources.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/base.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/callers.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/chunker.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/diff.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/function.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/mock.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/sources/repo.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/suppression.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/__init__.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/base.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury/tasks/registry.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/setup.cfg +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_audit_pipeline.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_baseline.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_cache.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_callers.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_capability.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_challenge.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_cli_audit.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_context.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_debate_agents.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_debate_orchestrator.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_diff_source.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_function_source.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_integrations.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_json_parse.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_litellm_provider.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_openai_provider.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_orchestrator.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_pipeline_orchestrator.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_provenance.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_reflexion_orchestrator.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_repo_source.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_reporting.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_retry_provider.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_sarif.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_suppression.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_tasks.py +0 -0
- {codejury-0.9.0 → codejury-0.10.0}/tests/test_verifier.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codejury
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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
|
|
@@ -83,7 +83,7 @@ git diff | codejury audit --provider anthropic
|
|
|
83
83
|
| `codejury run <task>` | Run a named task preset (see [Tasks](#tasks)). |
|
|
84
84
|
| `codejury eval` | Score the golden cases; report precision / recall / F1, overall and per capability. |
|
|
85
85
|
|
|
86
|
-
Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint}`,
|
|
86
|
+
Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint,adaptive}`,
|
|
87
87
|
`--provider {anthropic,openai,litellm}`, `--model`,
|
|
88
88
|
`--format {text,markdown,json,sarif}`.
|
|
89
89
|
|
|
@@ -103,6 +103,11 @@ value reaching the sink is constant, sanitized, or trusted (using cross-file
|
|
|
103
103
|
caller/callee context). It downgrades only on positive proof, so it removes false
|
|
104
104
|
positives without dropping real findings.
|
|
105
105
|
|
|
106
|
+
`--orchestrator adaptive` keeps cost down: it runs the single verifier first and
|
|
107
|
+
escalates to a full debate only for artifacts worth it -- any VULNERABLE verdict
|
|
108
|
+
(verify a flag before reporting) or a low-confidence PARTIAL/UNKNOWN one. Clean,
|
|
109
|
+
confident files pay a single call; only the rest pay for debate.
|
|
110
|
+
|
|
106
111
|
`--format sarif` emits a SARIF 2.1.0 log (validates against the official schema)
|
|
107
112
|
for CI and security dashboards: each problem with a code location becomes a
|
|
108
113
|
result carrying its capability (as the rule id), CWE, and a precise location.
|
|
@@ -53,7 +53,7 @@ git diff | codejury audit --provider anthropic
|
|
|
53
53
|
| `codejury run <task>` | Run a named task preset (see [Tasks](#tasks)). |
|
|
54
54
|
| `codejury eval` | Score the golden cases; report precision / recall / F1, overall and per capability. |
|
|
55
55
|
|
|
56
|
-
Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint}`,
|
|
56
|
+
Shared flags: `--orchestrator {single,pipeline,debate,reflexion,challenge,taint,adaptive}`,
|
|
57
57
|
`--provider {anthropic,openai,litellm}`, `--model`,
|
|
58
58
|
`--format {text,markdown,json,sarif}`.
|
|
59
59
|
|
|
@@ -73,6 +73,11 @@ value reaching the sink is constant, sanitized, or trusted (using cross-file
|
|
|
73
73
|
caller/callee context). It downgrades only on positive proof, so it removes false
|
|
74
74
|
positives without dropping real findings.
|
|
75
75
|
|
|
76
|
+
`--orchestrator adaptive` keeps cost down: it runs the single verifier first and
|
|
77
|
+
escalates to a full debate only for artifacts worth it -- any VULNERABLE verdict
|
|
78
|
+
(verify a flag before reporting) or a low-confidence PARTIAL/UNKNOWN one. Clean,
|
|
79
|
+
confident files pay a single call; only the rest pay for debate.
|
|
80
|
+
|
|
76
81
|
`--format sarif` emits a SARIF 2.1.0 log (validates against the official schema)
|
|
77
82
|
for CI and security dashboards: each problem with a code location becomes a
|
|
78
83
|
result carrying its capability (as the rule id), CWE, and a precise location.
|
|
@@ -93,16 +93,25 @@ def _build_prompt(path: str, content: str, cap: Capability, context: str = "") -
|
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
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=
|
|
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
|
)
|
|
@@ -72,7 +72,40 @@ def find_calls(scope: ast.AST, callee: str) -> list[ast.Call]:
|
|
|
72
72
|
|
|
73
73
|
def trace_value(func: ast.FunctionDef | ast.AsyncFunctionDef, expr: ast.AST) -> Origin:
|
|
74
74
|
"""Trace where ``expr`` (an expression inside ``func``) gets its value from."""
|
|
75
|
-
|
|
75
|
+
params = _params(func)
|
|
76
|
+
|
|
77
|
+
def leaf(node: ast.AST, recurse) -> Origin:
|
|
78
|
+
if isinstance(node, ast.Call):
|
|
79
|
+
# the return value's provenance depends on the callee's semantics, which
|
|
80
|
+
# P1-03 decides with the vocabulary; here we just name the callee.
|
|
81
|
+
name = _call_name(node)
|
|
82
|
+
return Origin(calls=frozenset({name})) if name else _UNKNOWN
|
|
83
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
84
|
+
dotted = _dotted(node)
|
|
85
|
+
origin = Origin(attrs=frozenset({dotted})) if dotted else _UNKNOWN
|
|
86
|
+
if _root_name(node) in params: # e.g. request.args[...] where `request` is a parameter
|
|
87
|
+
origin = origin.merge(Origin(params=frozenset({_root_name(node)})))
|
|
88
|
+
return origin
|
|
89
|
+
return _UNKNOWN
|
|
90
|
+
|
|
91
|
+
return reduce_value(
|
|
92
|
+
expr,
|
|
93
|
+
params=params,
|
|
94
|
+
assigns=_assignments(func),
|
|
95
|
+
combine=_merge_origins,
|
|
96
|
+
leaf=leaf,
|
|
97
|
+
on_param=lambda n: Origin(params=frozenset({n})),
|
|
98
|
+
on_global=lambda n: Origin(globals_=frozenset({n})),
|
|
99
|
+
on_constant=lambda: _LITERAL,
|
|
100
|
+
on_cycle=Origin, # assignment cycle -- no new information
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _merge_origins(origins: list[Origin]) -> Origin:
|
|
105
|
+
out = Origin()
|
|
106
|
+
for o in origins:
|
|
107
|
+
out = out.merge(o)
|
|
108
|
+
return out
|
|
76
109
|
|
|
77
110
|
|
|
78
111
|
def parameters(func: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
@@ -100,51 +133,63 @@ def access_root(node: ast.AST) -> str | None:
|
|
|
100
133
|
return _root_name(node)
|
|
101
134
|
|
|
102
135
|
|
|
103
|
-
def
|
|
136
|
+
def reduce_value(
|
|
137
|
+
expr: ast.AST,
|
|
138
|
+
*,
|
|
139
|
+
params: set[str],
|
|
140
|
+
assigns: dict[str, list[ast.AST]],
|
|
141
|
+
combine,
|
|
142
|
+
leaf,
|
|
143
|
+
on_param,
|
|
144
|
+
on_global,
|
|
145
|
+
on_constant,
|
|
146
|
+
on_cycle,
|
|
147
|
+
seen: frozenset[str] = frozenset(),
|
|
148
|
+
):
|
|
149
|
+
"""Walk a value expression, folding it to a single result of the caller's type.
|
|
150
|
+
|
|
151
|
+
This is the structural recursion shared by provenance (P1-01) and taint
|
|
152
|
+
classification (P1-03): the composite forms that pass a value through (binops,
|
|
153
|
+
f-strings, conditionals, collections) and the local-name dispatch (parameter /
|
|
154
|
+
assignment chain / assignment cycle / free global) are handled here once. The
|
|
155
|
+
caller supplies the policy:
|
|
156
|
+
|
|
157
|
+
- ``combine(list)`` -> fold component results (also defines the empty result);
|
|
158
|
+
- ``on_constant() / on_param(name) / on_global(name) / on_cycle()`` -> leaf
|
|
159
|
+
results for a literal, a parameter, a free name, and an assignment cycle;
|
|
160
|
+
- ``leaf(node, recurse)`` -> everything else (calls, attribute/subscript
|
|
161
|
+
access, unmodelled nodes); ``recurse`` re-enters the walk for sub-expressions
|
|
162
|
+
(e.g. a taint propagator recursing into its arguments).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def recurse(e: ast.AST, _seen: frozenset[str] | None = None):
|
|
166
|
+
return reduce_value(
|
|
167
|
+
e, params=params, assigns=assigns, combine=combine, leaf=leaf,
|
|
168
|
+
on_param=on_param, on_global=on_global, on_constant=on_constant,
|
|
169
|
+
on_cycle=on_cycle, seen=seen if _seen is None else _seen,
|
|
170
|
+
)
|
|
171
|
+
|
|
104
172
|
if isinstance(expr, ast.Constant):
|
|
105
|
-
return
|
|
173
|
+
return on_constant()
|
|
106
174
|
if isinstance(expr, ast.JoinedStr): # f-string: literal parts + interpolated exprs
|
|
107
|
-
|
|
108
|
-
for value in expr.values:
|
|
109
|
-
if isinstance(value, ast.FormattedValue):
|
|
110
|
-
origin = origin.merge(_classify(value.value, params, assigns, seen))
|
|
111
|
-
return origin
|
|
175
|
+
return combine([on_constant()] + [recurse(v.value) for v in expr.values if isinstance(v, ast.FormattedValue)])
|
|
112
176
|
if isinstance(expr, ast.BinOp):
|
|
113
|
-
return
|
|
114
|
-
if isinstance(expr,
|
|
115
|
-
return
|
|
116
|
-
if isinstance(expr, ast.IfExp): #
|
|
117
|
-
return
|
|
177
|
+
return combine([recurse(expr.left), recurse(expr.right)])
|
|
178
|
+
if isinstance(expr, ast.BoolOp):
|
|
179
|
+
return combine([recurse(v) for v in expr.values])
|
|
180
|
+
if isinstance(expr, ast.IfExp): # one branch or the other; the test does not flow in
|
|
181
|
+
return combine([recurse(expr.body), recurse(expr.orelse)])
|
|
118
182
|
if isinstance(expr, (ast.List, ast.Tuple, ast.Set)):
|
|
119
|
-
return
|
|
120
|
-
if isinstance(expr, ast.Call):
|
|
121
|
-
# the return value's taint depends on the callee's semantics, which P1-03
|
|
122
|
-
# decides with the sanitizer/propagator catalog; here we just name the callee.
|
|
123
|
-
name = _call_name(expr)
|
|
124
|
-
return Origin(calls=frozenset({name})) if name else _UNKNOWN
|
|
125
|
-
if isinstance(expr, (ast.Attribute, ast.Subscript)):
|
|
126
|
-
dotted = _dotted(expr)
|
|
127
|
-
origin = Origin(attrs=frozenset({dotted})) if dotted else _UNKNOWN
|
|
128
|
-
root = _root_name(expr)
|
|
129
|
-
if root in params: # e.g. request.args[...] where `request` is a parameter
|
|
130
|
-
origin = origin.merge(Origin(params=frozenset({root})))
|
|
131
|
-
return origin
|
|
183
|
+
return combine([recurse(e) for e in expr.elts])
|
|
132
184
|
if isinstance(expr, ast.Name):
|
|
133
185
|
if expr.id in seen: # assignment cycle -- stop
|
|
134
|
-
return
|
|
186
|
+
return on_cycle()
|
|
135
187
|
if expr.id in params:
|
|
136
|
-
return
|
|
188
|
+
return on_param(expr.id)
|
|
137
189
|
if expr.id in assigns:
|
|
138
|
-
return
|
|
139
|
-
return
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _merge_all(exprs: list[ast.AST], params, assigns, seen) -> Origin:
|
|
144
|
-
origin = Origin()
|
|
145
|
-
for e in exprs:
|
|
146
|
-
origin = origin.merge(_classify(e, params, assigns, seen))
|
|
147
|
-
return origin
|
|
190
|
+
return combine([recurse(r, seen | {expr.id}) for r in assigns[expr.id]])
|
|
191
|
+
return on_global(expr.id) # module global, import, or builtin
|
|
192
|
+
return leaf(expr, recurse)
|
|
148
193
|
|
|
149
194
|
|
|
150
195
|
def _params(func: ast.AST) -> set[str]:
|
|
@@ -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
|
|
@@ -34,6 +35,7 @@ from codejury.analysis.provenance import (
|
|
|
34
35
|
callee,
|
|
35
36
|
find_calls,
|
|
36
37
|
parameters,
|
|
38
|
+
reduce_value,
|
|
37
39
|
)
|
|
38
40
|
from codejury.resources import TAINT_FILE
|
|
39
41
|
|
|
@@ -107,59 +109,55 @@ def taint_of(
|
|
|
107
109
|
|
|
108
110
|
``resolve_param`` is an optional ``(name) -> Taint`` callback; when given, a
|
|
109
111
|
value that reaches a parameter is resolved through it (the cross-file caller
|
|
110
|
-
hop, P1-03b) instead of returning PARAM.
|
|
112
|
+
hop, P1-03b) instead of returning PARAM. Shares the structural recursion with
|
|
113
|
+
provenance via ``reduce_value``; this is the vocabulary-aware policy.
|
|
111
114
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if expr.id in assigns:
|
|
155
|
-
return _combine([_walk(func, r, vocab, assigns, params, seen | {expr.id}, resolve)
|
|
156
|
-
for r in assigns[expr.id]])
|
|
157
|
-
return Taint.TRUSTED # free module global / builtin -- conventionally a constant
|
|
158
|
-
return Taint.UNKNOWN
|
|
115
|
+
params = parameters(func)
|
|
116
|
+
|
|
117
|
+
def on_param(name: str) -> Taint:
|
|
118
|
+
return resolve_param(name) if resolve_param else Taint.PARAM
|
|
119
|
+
|
|
120
|
+
def leaf(node: ast.AST, recurse) -> Taint:
|
|
121
|
+
if isinstance(node, ast.Call):
|
|
122
|
+
if _callee_in(node, vocab.sanitizers):
|
|
123
|
+
return Taint.SANITIZED # a sanitizer cleans its result regardless of input
|
|
124
|
+
if _callee_in(node, vocab.sources):
|
|
125
|
+
return Taint.EXTERNAL # e.g. input()
|
|
126
|
+
# a method ON a source/trusted object, e.g. request.args.get("id")
|
|
127
|
+
func_path = access_path(node.func)
|
|
128
|
+
if func_path and _access_in(func_path, vocab.sources):
|
|
129
|
+
return Taint.EXTERNAL
|
|
130
|
+
if func_path and _access_in(func_path, vocab.trusted):
|
|
131
|
+
return Taint.TRUSTED
|
|
132
|
+
if _callee_in(node, vocab.propagators) or _callee_in(node, vocab.safe_sinks):
|
|
133
|
+
return _combine([recurse(a) for a in node.args])
|
|
134
|
+
return Taint.UNKNOWN # unknown call -- a cross-file hop may resolve it later
|
|
135
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
136
|
+
path = access_path(node)
|
|
137
|
+
if path and _access_in(path, vocab.sources):
|
|
138
|
+
return Taint.EXTERNAL
|
|
139
|
+
if path and _access_in(path, vocab.trusted):
|
|
140
|
+
return Taint.TRUSTED
|
|
141
|
+
if access_root(node) in params: # attribute of a parameter -- resolve at the call site
|
|
142
|
+
return on_param(access_root(node))
|
|
143
|
+
return Taint.UNKNOWN # unknown object attribute (e.g. self.x): not provably safe
|
|
144
|
+
return Taint.UNKNOWN
|
|
145
|
+
|
|
146
|
+
return reduce_value(
|
|
147
|
+
expr,
|
|
148
|
+
params=params,
|
|
149
|
+
assigns=assignments(func),
|
|
150
|
+
combine=_combine,
|
|
151
|
+
leaf=leaf,
|
|
152
|
+
on_param=on_param,
|
|
153
|
+
on_global=lambda n: Taint.TRUSTED, # free module global / builtin -- conventionally a constant
|
|
154
|
+
on_constant=lambda: Taint.CONSTANT,
|
|
155
|
+
on_cycle=lambda: Taint.CONSTANT, # assignment cycle: no new information
|
|
156
|
+
)
|
|
159
157
|
|
|
160
158
|
|
|
161
159
|
def _combine(taints: list[Taint]) -> Taint:
|
|
162
|
-
return max(taints, key=lambda t: _RANK[t])
|
|
160
|
+
return max(taints, key=lambda t: _RANK[t]) if taints else Taint.CONSTANT
|
|
163
161
|
|
|
164
162
|
|
|
165
163
|
def _callee_in(call: ast.Call, names: tuple[str, ...]) -> bool:
|
|
@@ -197,10 +195,11 @@ def taint_in_repo(
|
|
|
197
195
|
|
|
198
196
|
|
|
199
197
|
def _caller_resolver(func, files, vocab):
|
|
198
|
+
sites = _call_sites(func.name, files) # computed once, reused across parameters
|
|
200
199
|
def resolve(param_name: str) -> Taint:
|
|
201
200
|
index = _param_index(func, param_name)
|
|
202
201
|
results = []
|
|
203
|
-
for scope, call in
|
|
202
|
+
for scope, call in sites:
|
|
204
203
|
arg = _arg_for_param(call, index, param_name)
|
|
205
204
|
# one hop only: classify the caller's argument without recursing further
|
|
206
205
|
results.append(taint_of(scope, arg, vocab) if arg is not None else Taint.UNKNOWN)
|
|
@@ -210,6 +209,8 @@ def _caller_resolver(func, files, vocab):
|
|
|
210
209
|
|
|
211
210
|
def _param_index(func, name: str) -> int | None:
|
|
212
211
|
positional = [*func.args.posonlyargs, *func.args.args]
|
|
212
|
+
if positional and positional[0].arg in ("self", "cls"):
|
|
213
|
+
positional = positional[1:] # bound-method call sites omit the receiver
|
|
213
214
|
for i, arg in enumerate(positional):
|
|
214
215
|
if arg.arg == name:
|
|
215
216
|
return i
|
|
@@ -225,12 +226,21 @@ def _arg_for_param(call: ast.Call, index: int | None, name: str) -> ast.AST | No
|
|
|
225
226
|
return None
|
|
226
227
|
|
|
227
228
|
|
|
229
|
+
@functools.lru_cache(maxsize=256)
|
|
230
|
+
def _parse(source: str) -> ast.Module | None:
|
|
231
|
+
"""Parse a source string once and cache it (the same files are walked repeatedly
|
|
232
|
+
during cross-file resolution); None if it does not parse."""
|
|
233
|
+
try:
|
|
234
|
+
return ast.parse(source)
|
|
235
|
+
except SyntaxError:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
228
239
|
def _call_sites(name: str, files: dict[str, str]) -> list[tuple[ast.AST, ast.Call]]:
|
|
229
240
|
sites = []
|
|
230
241
|
for source in files.values():
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
except SyntaxError:
|
|
242
|
+
tree = _parse(source)
|
|
243
|
+
if tree is None:
|
|
234
244
|
continue
|
|
235
245
|
funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
236
246
|
for call in find_calls(tree, name):
|
|
@@ -250,24 +260,27 @@ def worst_sink_taint(content: str, files: dict[str, str], vocab: TaintVocab) ->
|
|
|
250
260
|
|
|
251
261
|
A "potential sink" is any call that is not a safe sink, sanitizer, or
|
|
252
262
|
propagator (those are not where injection happens). Each such call's argument
|
|
253
|
-
taint is classified with the cross-file resolver
|
|
254
|
-
``
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
taint is classified with the cross-file resolver. A safe sink that consumes
|
|
264
|
+
tainted data (e.g. ``json.loads(request.data)``) contributes SANITIZED -- the
|
|
265
|
+
data was handled safely. The worst contribution is returned.
|
|
266
|
+
|
|
267
|
+
``Taint.UNKNOWN`` when no inspectable sink is found (the artifact may still be
|
|
268
|
+
unsafe via a return value or an implicit sink, so it is NOT assumed clean);
|
|
269
|
+
``None`` when the code does not parse. The taint gate downgrades a finding only
|
|
270
|
+
on a SAFE result, so an unproven artifact keeps its findings (recall preserved).
|
|
260
271
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
except SyntaxError:
|
|
272
|
+
tree = _parse(content)
|
|
273
|
+
if tree is None:
|
|
264
274
|
return None
|
|
265
275
|
funcs = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
266
276
|
taints: list[Taint] = []
|
|
267
277
|
for call in [n for n in ast.walk(tree) if isinstance(n, ast.Call)]:
|
|
268
|
-
if is_safe_sink(call, vocab)
|
|
269
|
-
|
|
278
|
+
if is_safe_sink(call, vocab):
|
|
279
|
+
taints.append(Taint.SANITIZED) # tainted data consumed by a safe parser
|
|
280
|
+
continue
|
|
281
|
+
if _callee_in(call, vocab.sanitizers) or _callee_in(call, vocab.propagators):
|
|
282
|
+
continue # not a sink itself; counted via the enclosing sink's argument
|
|
270
283
|
scope = _enclosing_scope(funcs, call) or tree
|
|
271
284
|
for arg in (*call.args, *(kw.value for kw in call.keywords)):
|
|
272
285
|
taints.append(taint_in_repo(scope, arg, vocab, files))
|
|
273
|
-
return _combine(taints) if taints else Taint.
|
|
286
|
+
return _combine(taints) if taints else Taint.UNKNOWN
|
|
@@ -18,6 +18,7 @@ from codejury.domain.context import AnalysisContext
|
|
|
18
18
|
from codejury.domain.result import AnalysisResult
|
|
19
19
|
from codejury.infrastructure.cache import VerdictCache, verdict_key
|
|
20
20
|
from codejury.orchestrators.base import Orchestrator
|
|
21
|
+
from codejury.orchestrators.adaptive import AdaptiveOrchestrator
|
|
21
22
|
from codejury.orchestrators.challenge import ChallengeOrchestrator
|
|
22
23
|
from codejury.orchestrators.debate import DebateOrchestrator
|
|
23
24
|
from codejury.orchestrators.pipeline import PipelineOrchestrator
|
|
@@ -31,7 +32,7 @@ from codejury.providers.openai import OpenAIProvider
|
|
|
31
32
|
from codejury.providers.retry import RetryProvider
|
|
32
33
|
from codejury.sources.base import Source
|
|
33
34
|
|
|
34
|
-
STRATEGIES = ("single", "pipeline", "debate", "reflexion", "challenge", "taint")
|
|
35
|
+
STRATEGIES = ("single", "pipeline", "debate", "reflexion", "challenge", "taint", "adaptive")
|
|
35
36
|
PROVIDERS = ("anthropic", "openai", "litellm")
|
|
36
37
|
DEFAULT_MODEL = os.environ.get("CODEJURY_MODEL", "claude-sonnet-4-6")
|
|
37
38
|
DEFAULT_API_BASE = os.environ.get("CODEJURY_API_BASE")
|
|
@@ -59,6 +60,11 @@ def build_orchestration(
|
|
|
59
60
|
roles = (FinderAgent, ChallengerAgent, JudgeAgent)
|
|
60
61
|
agents = {cls.role: cls(provider=provider, model=model, max_tokens=max_tokens) for cls in roles}
|
|
61
62
|
return agents, DebateOrchestrator()
|
|
63
|
+
if strategy == "adaptive":
|
|
64
|
+
roles = (FinderAgent, ChallengerAgent, JudgeAgent)
|
|
65
|
+
agents = {cls.role: cls(provider=provider, model=model, max_tokens=max_tokens) for cls in roles}
|
|
66
|
+
agents["verifier"] = VerifierAgent(provider=provider, model=model, max_tokens=max_tokens)
|
|
67
|
+
return agents, AdaptiveOrchestrator()
|
|
62
68
|
if strategy == "reflexion":
|
|
63
69
|
agents = {
|
|
64
70
|
"actor": FinderAgent(provider=provider, model=model, max_tokens=max_tokens),
|
|
@@ -79,9 +85,15 @@ def build_orchestration(
|
|
|
79
85
|
return verifier, SingleOrchestrator()
|
|
80
86
|
|
|
81
87
|
|
|
82
|
-
def
|
|
88
|
+
def provider_tag(provider: Provider) -> str:
|
|
89
|
+
"""A stable short name for a provider (unwrapping RetryProvider) for cache keys,
|
|
90
|
+
so two providers that accept the same model string do not share cached verdicts."""
|
|
91
|
+
return type(getattr(provider, "_inner", provider)).__name__
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def orchestration_descriptor(provider: Provider, strategy: str, model: str, max_tokens: int) -> str:
|
|
83
95
|
"""The non-code, non-capability inputs that affect a verdict, as a cache tag."""
|
|
84
|
-
return f"{strategy}|{model}|{max_tokens}"
|
|
96
|
+
return f"{provider_tag(provider)}|{strategy}|{model}|{max_tokens}"
|
|
85
97
|
|
|
86
98
|
|
|
87
99
|
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 =
|
|
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
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""AdaptiveOrchestrator -- cheap triage, deep debate only where it pays off.
|
|
2
|
+
|
|
3
|
+
Most files are clean, and debate is several model calls; running it everywhere
|
|
4
|
+
wastes them. So this runs the single verifier first, then escalates to a full
|
|
5
|
+
Finder -> Challenger -> Judge debate only when the cheap pass found something
|
|
6
|
+
worth a second opinion:
|
|
7
|
+
|
|
8
|
+
- **high-risk**: any VULNERABLE verdict -- a flagged vulnerability is verified by
|
|
9
|
+
the adversarial pass before it is reported / gates CI;
|
|
10
|
+
- **disputed**: a PARTIAL or UNKNOWN verdict the verifier is unsure about
|
|
11
|
+
(confidence below the threshold).
|
|
12
|
+
|
|
13
|
+
A clean artifact (only confident SECURE / NOT_PRESENT verdicts) pays one verifier
|
|
14
|
+
call and stops. An escalated artifact is handed to the debate, whose deeper
|
|
15
|
+
ruling supersedes the triage verdicts for that artifact.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from codejury.agents.base import Agent
|
|
21
|
+
from codejury.domain.context import AnalysisContext
|
|
22
|
+
from codejury.domain.observation import Verdict
|
|
23
|
+
from codejury.domain.result import AnalysisResult
|
|
24
|
+
from codejury.orchestrators.base import Orchestrator
|
|
25
|
+
from codejury.orchestrators.debate import DebateOrchestrator
|
|
26
|
+
|
|
27
|
+
_DISPUTABLE = ("PARTIAL", "UNKNOWN")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AdaptiveOrchestrator(Orchestrator):
|
|
31
|
+
def __init__(self, *, confidence_threshold: float = 0.7, debate: DebateOrchestrator | None = None) -> None:
|
|
32
|
+
self._threshold = confidence_threshold
|
|
33
|
+
self._debate = debate if debate is not None else DebateOrchestrator()
|
|
34
|
+
|
|
35
|
+
def run(self, agents: dict[str, Agent], context: AnalysisContext) -> AnalysisResult:
|
|
36
|
+
if "verifier" not in agents:
|
|
37
|
+
return AnalysisResult(error="adaptive requires agents: verifier")
|
|
38
|
+
try:
|
|
39
|
+
verdicts = agents["verifier"].run(context)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
return AnalysisResult(error=f"agent 'verifier' failed: {exc}")
|
|
42
|
+
|
|
43
|
+
if not self._needs_escalation(verdicts):
|
|
44
|
+
return AnalysisResult(observations=verdicts) # cheap path: confident, low-risk
|
|
45
|
+
return self._debate.run(agents, context) # deep path supersedes the triage
|
|
46
|
+
|
|
47
|
+
def _needs_escalation(self, verdicts: list) -> bool:
|
|
48
|
+
for v in verdicts:
|
|
49
|
+
if not isinstance(v, Verdict):
|
|
50
|
+
continue
|
|
51
|
+
if v.status == "VULNERABLE": # high-risk: verify a flagged vuln adversarially
|
|
52
|
+
return True
|
|
53
|
+
if v.status in _DISPUTABLE and v.confidence < self._threshold: # disputed
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
@@ -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
|
|
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
|
},
|