codejury 0.9.1__tar.gz → 0.10.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.
- {codejury-0.9.1 → codejury-0.10.1}/PKG-INFO +7 -2
- {codejury-0.9.1 → codejury-0.10.1}/README.md +6 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/debate.py +6 -3
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/mock.py +1 -2
- codejury-0.10.1/codejury/agents/parsing.py +45 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/verifier.py +7 -3
- {codejury-0.9.1 → codejury-0.10.1}/codejury/analysis/provenance.py +83 -38
- {codejury-0.9.1 → codejury-0.10.1}/codejury/analysis/taint.py +46 -55
- {codejury-0.9.1 → codejury-0.10.1}/codejury/assembly.py +8 -2
- {codejury-0.9.1 → codejury-0.10.1}/codejury/baseline.py +5 -9
- {codejury-0.9.1 → codejury-0.10.1}/codejury/cli.py +22 -14
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/capability.py +15 -2
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/observation.py +25 -4
- {codejury-0.9.1 → codejury-0.10.1}/codejury/evaluation.py +8 -2
- {codejury-0.9.1 → codejury-0.10.1}/codejury/infrastructure/json_parse.py +8 -3
- {codejury-0.9.1 → codejury-0.10.1}/codejury/integrations/github.py +24 -9
- codejury-0.10.1/codejury/orchestrators/adaptive.py +55 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/base.py +9 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/challenge.py +11 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/retry.py +8 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/reporting.py +15 -10
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/callers.py +3 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/diff.py +2 -1
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/function.py +12 -3
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/repo.py +4 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/suppression.py +7 -9
- {codejury-0.9.1 → codejury-0.10.1}/codejury/tasks/base.py +2 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/PKG-INFO +7 -2
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/SOURCES.txt +4 -0
- {codejury-0.9.1 → codejury-0.10.1}/pyproject.toml +1 -1
- codejury-0.10.1/tests/test_adaptive.py +71 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_evaluation.py +1 -1
- codejury-0.10.1/tests/test_parsing.py +32 -0
- codejury-0.10.1/tests/test_review_fixes.py +100 -0
- codejury-0.9.1/codejury/agents/parsing.py +0 -42
- {codejury-0.9.1 → codejury-0.10.1}/LICENSE +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/base.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/agents/refuter.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/analysis/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/authentication.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/authorization.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/business_logic.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/crypto.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/data_protection.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/dependency_config.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/error_logging.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/excessive_agency.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/input_validation.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/output_encoding.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/prompt_injection.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/secrets.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/capabilities/session.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_sha256_password.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/authz_owner_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/literal_eval_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/path_basename_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/path_contained_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/secrets_env_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/suppressions.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/taint.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/data/tasks/quick_scan_single.yaml +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/artifact.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/context.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/domain/result.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/infrastructure/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/infrastructure/cache.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/integrations/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/debate.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/pipeline.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/reflexion.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/single.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/orchestrators/taint_gate.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/anthropic.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/base.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/litellm.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/mock.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/openai.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/providers/openai_format.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/resources.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/base.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/chunker.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/sources/mock.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/tasks/__init__.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury/tasks/registry.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/setup.cfg +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_assembly.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_audit_pipeline.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_baseline.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_cache.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_callers.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_capability.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_challenge.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_cli_audit.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_context.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_debate_agents.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_debate_orchestrator.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_diff_source.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_function_source.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_integrations.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_json_parse.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_litellm_provider.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_openai_provider.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_orchestrator.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_pipeline_orchestrator.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_provenance.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_reflexion_orchestrator.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_repo_source.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_reporting.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_retry_provider.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_sarif.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_suppression.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_taint.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_taint_crossfile.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_taint_gate.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/tests/test_tasks.py +0 -0
- {codejury-0.9.1 → codejury-0.10.1}/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.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
|
|
@@ -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.
|
|
@@ -19,15 +19,18 @@ from codejury.agents.parsing import one_of, str_list, to_evidence, to_float
|
|
|
19
19
|
from codejury.domain.artifact import CodeArtifact
|
|
20
20
|
from codejury.domain.capability import Capability
|
|
21
21
|
from codejury.domain.context import AnalysisContext
|
|
22
|
-
from codejury.domain.observation import Concession, Finding, Observation
|
|
22
|
+
from codejury.domain.observation import Concession, Finding, Observation, Severity
|
|
23
23
|
from codejury.infrastructure.json_parse import extract_json_object
|
|
24
24
|
from codejury.providers.base import Message, Provider
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
from typing import get_args
|
|
27
|
+
|
|
28
|
+
_SEVERITY = set(get_args(Severity)) # single source of truth: the Severity Literal
|
|
27
29
|
|
|
28
30
|
_FINDING_SHAPE = (
|
|
29
31
|
'{"capability": "id.sub", "title": "...", "severity": "HIGH", "cwe": "CWE-89", '
|
|
30
|
-
'"description": "...", "
|
|
32
|
+
'"description": "...", "recommendation": "...", "matched_anti": ["id"], '
|
|
33
|
+
'"evidence": [{"file": "...", "line": 0, "code": "..."}], "confidence": 0.0}'
|
|
31
34
|
)
|
|
32
35
|
|
|
33
36
|
_DEEP_LENS = (
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
It really calls the provider (so the dry-run exercises the agent -> provider
|
|
4
4
|
path), then emits one Verdict per in-scope capability, parking the model's reply
|
|
5
|
-
in ``reasoning``. It does no real parsing or judgement;
|
|
6
|
-
will.
|
|
5
|
+
in ``reasoning``. It does no real parsing or judgement; ``VerifierAgent`` does.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
from __future__ import annotations
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Shared coercion from loosely-typed model JSON into domain values.
|
|
2
|
+
|
|
3
|
+
Agents parse model output that may omit, mistype, or invent fields. These
|
|
4
|
+
helpers coerce defensively -- they never raise on bad input, they fall back.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
from codejury.domain.observation import Evidence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def str_list(value: object) -> list[str]:
|
|
13
|
+
return [str(x) for x in value] if isinstance(value, list) else []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_float(value: object, default: float) -> float:
|
|
17
|
+
# reject bool (it is an int subclass) and non-finite values (json.loads accepts
|
|
18
|
+
# NaN/Infinity); a NaN confidence would silently corrupt every downstream sort.
|
|
19
|
+
if isinstance(value, bool):
|
|
20
|
+
return default
|
|
21
|
+
try:
|
|
22
|
+
result = float(value) # type: ignore[arg-type]
|
|
23
|
+
except (TypeError, ValueError):
|
|
24
|
+
return default
|
|
25
|
+
return result if math.isfinite(result) else default
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def one_of(value: object, allowed: set[str], default: str) -> str:
|
|
29
|
+
# `value in allowed` would raise TypeError on an unhashable model value
|
|
30
|
+
# (e.g. status -> [] or {}); guard so bad input falls back, never raises.
|
|
31
|
+
return value if isinstance(value, str) and value in allowed else default
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def to_evidence(items: object) -> list[Evidence]:
|
|
35
|
+
if not isinstance(items, list):
|
|
36
|
+
return []
|
|
37
|
+
out: list[Evidence] = []
|
|
38
|
+
for e in items:
|
|
39
|
+
if not isinstance(e, dict):
|
|
40
|
+
continue
|
|
41
|
+
line = e.get("line")
|
|
42
|
+
# a location needs a real 1-based line; reject 0, negatives, and bool (an int subclass)
|
|
43
|
+
valid_line = line if isinstance(line, int) and not isinstance(line, bool) and line >= 1 else None
|
|
44
|
+
out.append(Evidence(file=str(e.get("file", "")), line=valid_line, code=str(e.get("code", ""))))
|
|
45
|
+
return out
|
|
@@ -15,11 +15,13 @@ from codejury.agents.base import Agent
|
|
|
15
15
|
from codejury.agents.parsing import one_of, str_list, to_evidence, to_float
|
|
16
16
|
from codejury.domain.capability import Capability
|
|
17
17
|
from codejury.domain.context import AnalysisContext
|
|
18
|
-
from codejury.domain.observation import Observation, Verdict
|
|
18
|
+
from codejury.domain.observation import Observation, Verdict, VerdictStatus
|
|
19
19
|
from codejury.infrastructure.json_parse import extract_json_object
|
|
20
20
|
from codejury.providers.base import Message, Provider
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
from typing import get_args
|
|
23
|
+
|
|
24
|
+
_VALID_STATUS = set(get_args(VerdictStatus)) # single source of truth: the VerdictStatus Literal
|
|
23
25
|
|
|
24
26
|
_SYSTEM = (
|
|
25
27
|
"You are a security verifier. You check code against a checklist of correct and "
|
|
@@ -108,8 +110,10 @@ def _anti_pattern_cwes(cap: Capability) -> dict[str, tuple[int, str]]:
|
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
def _resolve_cwe(matched_anti: list[str], cwe_by_id: dict[str, tuple[int, str]]) -> str:
|
|
113
|
+
# the most severe matched anti-pattern's CWE; ties broken by CWE id so the result
|
|
114
|
+
# is fully determined by the inputs, not by dict/list ordering.
|
|
111
115
|
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 ""
|
|
116
|
+
return max(matched, key=lambda rank_cwe: (rank_cwe[0], rank_cwe[1]))[1] if matched else ""
|
|
113
117
|
|
|
114
118
|
|
|
115
119
|
def _parse_verdicts(text: str, cap: Capability) -> list[Verdict]:
|
|
@@ -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]:
|
|
@@ -35,6 +35,7 @@ from codejury.analysis.provenance import (
|
|
|
35
35
|
callee,
|
|
36
36
|
find_calls,
|
|
37
37
|
parameters,
|
|
38
|
+
reduce_value,
|
|
38
39
|
)
|
|
39
40
|
from codejury.resources import TAINT_FILE
|
|
40
41
|
|
|
@@ -108,65 +109,55 @@ def taint_of(
|
|
|
108
109
|
|
|
109
110
|
``resolve_param`` is an optional ``(name) -> Taint`` callback; when given, a
|
|
110
111
|
value that reaches a parameter is resolved through it (the cross-file caller
|
|
111
|
-
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.
|
|
112
114
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return Taint.UNKNOWN # unknown object attribute (e.g. self.x): not provably safe
|
|
156
|
-
if isinstance(expr, ast.Name):
|
|
157
|
-
if expr.id in seen:
|
|
158
|
-
return Taint.CONSTANT # assignment cycle: no new information
|
|
159
|
-
if expr.id in params:
|
|
160
|
-
return resolve(expr.id) if resolve else Taint.PARAM
|
|
161
|
-
if expr.id in assigns:
|
|
162
|
-
return _combine([_walk(func, r, vocab, assigns, params, seen | {expr.id}, resolve)
|
|
163
|
-
for r in assigns[expr.id]])
|
|
164
|
-
return Taint.TRUSTED # free module global / builtin -- conventionally a constant
|
|
165
|
-
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
|
+
)
|
|
166
157
|
|
|
167
158
|
|
|
168
159
|
def _combine(taints: list[Taint]) -> Taint:
|
|
169
|
-
return max(taints, key=lambda t: _RANK[t])
|
|
160
|
+
return max(taints, key=lambda t: _RANK[t]) if taints else Taint.CONSTANT
|
|
170
161
|
|
|
171
162
|
|
|
172
163
|
def _callee_in(call: ast.Call, names: tuple[str, ...]) -> bool:
|
|
@@ -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),
|
|
@@ -82,7 +88,7 @@ def build_orchestration(
|
|
|
82
88
|
def provider_tag(provider: Provider) -> str:
|
|
83
89
|
"""A stable short name for a provider (unwrapping RetryProvider) for cache keys,
|
|
84
90
|
so two providers that accept the same model string do not share cached verdicts."""
|
|
85
|
-
return type(getattr(provider, "
|
|
91
|
+
return type(getattr(provider, "inner", provider)).__name__
|
|
86
92
|
|
|
87
93
|
|
|
88
94
|
def orchestration_descriptor(provider: Provider, strategy: str, model: str, max_tokens: int) -> str:
|
|
@@ -15,13 +15,11 @@ NOT_PRESENT verdicts and concessions are always kept.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from codejury.domain.observation import Concession, Finding, Observation, Verdict
|
|
18
|
+
from codejury.domain.observation import Concession, Finding, Observation, Verdict, is_problem
|
|
19
19
|
from codejury.domain.result import AnalysisResult
|
|
20
20
|
|
|
21
21
|
Results = list[tuple[str, AnalysisResult]]
|
|
22
22
|
|
|
23
|
-
_PROBLEM_STATUSES = ("VULNERABLE", "PARTIAL")
|
|
24
|
-
|
|
25
23
|
|
|
26
24
|
def finding_key(o: Observation) -> tuple:
|
|
27
25
|
"""A location-tolerant fingerprint for matching a finding across versions."""
|
|
@@ -39,13 +37,13 @@ def filter_new(results: Results, baseline: Results) -> tuple[Results, int]:
|
|
|
39
37
|
|
|
40
38
|
Returns (filtered_results, dropped_count). Non-problem observations are kept.
|
|
41
39
|
"""
|
|
42
|
-
seen = {finding_key(o) for _, r in baseline for o in r.observations if
|
|
40
|
+
seen = {finding_key(o) for _, r in baseline for o in r.observations if is_problem(o)}
|
|
43
41
|
filtered: Results = []
|
|
44
42
|
dropped = 0
|
|
45
43
|
for path, result in results:
|
|
46
44
|
kept: list[Observation] = []
|
|
47
45
|
for o in result.observations:
|
|
48
|
-
if
|
|
46
|
+
if is_problem(o) and finding_key(o) in seen:
|
|
49
47
|
dropped += 1
|
|
50
48
|
else:
|
|
51
49
|
kept.append(o)
|
|
@@ -53,10 +51,8 @@ def filter_new(results: Results, baseline: Results) -> tuple[Results, int]:
|
|
|
53
51
|
return filtered, dropped
|
|
54
52
|
|
|
55
53
|
|
|
56
|
-
def _is_problem(o: Observation) -> bool:
|
|
57
|
-
return isinstance(o, Finding) or (isinstance(o, Verdict) and o.status in _PROBLEM_STATUSES)
|
|
58
|
-
|
|
59
|
-
|
|
60
54
|
def _evidence_sig(o: Observation) -> str:
|
|
55
|
+
# fingerprints only the first evidence snippet -- enough to distinguish findings
|
|
56
|
+
# in practice; two findings differing only in a later snippet would collide.
|
|
61
57
|
evidence = getattr(o, "evidence", [])
|
|
62
58
|
return " ".join(evidence[0].code.split()) if evidence and evidence[0].code else ""
|
|
@@ -280,6 +280,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
280
280
|
scan_p.add_argument("--no-suppress", action="store_true", help="disable the known-noise suppression filter")
|
|
281
281
|
scan_p.add_argument("--no-cache", action="store_true", help="bypass the verdict cache (always re-query the model)")
|
|
282
282
|
scan_p.add_argument("--baseline", default=None, help="a prior JSON report; report only findings new since it")
|
|
283
|
+
scan_p.add_argument("--retries", type=int, default=0, help="provider retry attempts on failure")
|
|
283
284
|
scan_p.add_argument("--fail-on", choices=_FAIL_ON, default=None, dest="fail_on", help="exit 1 if a finding at/above this severity is found")
|
|
284
285
|
|
|
285
286
|
run_p = sub.add_parser("run", help="run a named task preset against a unified diff")
|
|
@@ -301,9 +302,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
301
302
|
eval_p.add_argument("--model", default=DEFAULT_MODEL)
|
|
302
303
|
eval_p.add_argument("--api-base", default=DEFAULT_API_BASE, help="provider base URL (env: CODEJURY_API_BASE)")
|
|
303
304
|
eval_p.add_argument("--api-key", default=DEFAULT_API_KEY, help="provider API key (env: CODEJURY_API_KEY)")
|
|
305
|
+
eval_p.add_argument("--retries", type=int, default=0, help="provider retry attempts on failure")
|
|
304
306
|
|
|
305
307
|
args = parser.parse_args(argv)
|
|
308
|
+
try:
|
|
309
|
+
return _dispatch(args, parser)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
# expected failures (missing diff file, provider auth, malformed input) become
|
|
312
|
+
# one stderr line, not a traceback. eval's own message is preserved by command name.
|
|
313
|
+
print(f"{args.command or 'codejury'} failed: {exc}", file=sys.stderr)
|
|
314
|
+
return 1
|
|
306
315
|
|
|
316
|
+
|
|
317
|
+
def _dispatch(args, parser) -> int:
|
|
307
318
|
if args.command == "audit":
|
|
308
319
|
results = audit(
|
|
309
320
|
_read_diff(args.diff),
|
|
@@ -326,12 +337,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
326
337
|
capabilities = load_capabilities(args.capabilities)
|
|
327
338
|
if args.only:
|
|
328
339
|
wanted = {x.strip() for x in args.only.split(",")}
|
|
340
|
+
unknown = wanted - {c.id for c in capabilities}
|
|
341
|
+
if unknown:
|
|
342
|
+
print(f"warning: --only names unknown capability id(s): {', '.join(sorted(unknown))}", file=sys.stderr)
|
|
329
343
|
capabilities = [c for c in capabilities if c.id in wanted]
|
|
330
344
|
extensions = tuple(e if e.startswith(".") else "." + e for e in args.ext.split(","))
|
|
331
345
|
results = scan(
|
|
332
346
|
args.directory,
|
|
333
347
|
capabilities,
|
|
334
|
-
provider=make_provider(args.provider, api_key=args.api_key, api_base=args.api_base),
|
|
348
|
+
provider=make_provider(args.provider, api_key=args.api_key, api_base=args.api_base, retries=args.retries),
|
|
335
349
|
model=args.model,
|
|
336
350
|
max_tokens=args.max_tokens,
|
|
337
351
|
strategy=args.orchestrator,
|
|
@@ -359,19 +373,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
359
373
|
return _gate_exit(results, args.fail_on)
|
|
360
374
|
|
|
361
375
|
if args.command == "eval":
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
)
|
|
370
|
-
except Exception as exc:
|
|
371
|
-
# e.g. a missing API key surfaces as a provider auth error -- report it
|
|
372
|
-
# as one line, not a traceback (audit gets this via the orchestrator).
|
|
373
|
-
print(f"eval failed: {exc}")
|
|
374
|
-
return 1
|
|
376
|
+
report = evaluate(
|
|
377
|
+
load_cases(args.dataset, split=args.split),
|
|
378
|
+
load_capabilities(args.capabilities),
|
|
379
|
+
provider=make_provider(args.provider, api_key=args.api_key, api_base=args.api_base, retries=args.retries),
|
|
380
|
+
model=args.model,
|
|
381
|
+
strategy=args.orchestrator,
|
|
382
|
+
)
|
|
375
383
|
print(json.dumps(report.to_dict(), indent=2) if args.fmt == "json" else _render_eval(report))
|
|
376
384
|
return 0
|
|
377
385
|
|
|
@@ -20,8 +20,12 @@ from typing import Any
|
|
|
20
20
|
|
|
21
21
|
import yaml
|
|
22
22
|
|
|
23
|
+
from typing import get_args
|
|
24
|
+
|
|
23
25
|
from codejury.domain.observation import Severity
|
|
24
26
|
|
|
27
|
+
_SEVERITIES = frozenset(get_args(Severity))
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
@dataclass(frozen=True, kw_only=True)
|
|
27
31
|
class CorrectPattern:
|
|
@@ -57,12 +61,18 @@ class AntiPattern:
|
|
|
57
61
|
|
|
58
62
|
@classmethod
|
|
59
63
|
def from_dict(cls, data: dict[str, Any]) -> AntiPattern:
|
|
64
|
+
severity = data.get("severity", "MEDIUM")
|
|
65
|
+
if severity not in _SEVERITIES:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"anti_pattern {data.get('id')!r}: invalid severity {severity!r}; "
|
|
68
|
+
f"expected one of {', '.join(_SEVERITIES)}"
|
|
69
|
+
)
|
|
60
70
|
return cls(
|
|
61
71
|
id=data["id"],
|
|
62
72
|
description=data.get("description", ""),
|
|
63
73
|
signals=list(data.get("signals", [])),
|
|
64
74
|
cwe=data.get("cwe", ""),
|
|
65
|
-
severity=
|
|
75
|
+
severity=severity,
|
|
66
76
|
why_bad=data.get("why_bad", ""),
|
|
67
77
|
example_bad=data.get("example_bad", ""),
|
|
68
78
|
example_good=data.get("example_good", ""),
|
|
@@ -130,7 +140,10 @@ def load_capability(path: str | Path) -> Capability:
|
|
|
130
140
|
data = yaml.safe_load(f)
|
|
131
141
|
if not isinstance(data, dict):
|
|
132
142
|
raise ValueError(f"{path}: expected a YAML mapping at the top level, got {type(data).__name__}")
|
|
133
|
-
|
|
143
|
+
try:
|
|
144
|
+
return Capability.from_dict(data)
|
|
145
|
+
except (KeyError, ValueError) as exc:
|
|
146
|
+
raise ValueError(f"{path}: {exc}") from exc
|
|
134
147
|
|
|
135
148
|
|
|
136
149
|
def load_capabilities(directory: str | Path) -> list[Capability]:
|