codejury 0.10.0__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.10.0 → codejury-0.10.1}/PKG-INFO +1 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/debate.py +6 -3
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/mock.py +1 -2
- codejury-0.10.1/codejury/agents/parsing.py +45 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/verifier.py +7 -3
- {codejury-0.10.0 → codejury-0.10.1}/codejury/assembly.py +1 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/baseline.py +5 -9
- {codejury-0.10.0 → codejury-0.10.1}/codejury/cli.py +22 -14
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/capability.py +15 -2
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/observation.py +25 -4
- {codejury-0.10.0 → codejury-0.10.1}/codejury/evaluation.py +8 -2
- {codejury-0.10.0 → codejury-0.10.1}/codejury/infrastructure/json_parse.py +8 -3
- {codejury-0.10.0 → codejury-0.10.1}/codejury/integrations/github.py +24 -9
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/base.py +9 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/challenge.py +11 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/retry.py +8 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/reporting.py +15 -10
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/callers.py +3 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/diff.py +2 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/function.py +12 -3
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/repo.py +4 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/suppression.py +7 -9
- {codejury-0.10.0 → codejury-0.10.1}/codejury/tasks/base.py +2 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/PKG-INFO +1 -1
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/SOURCES.txt +2 -0
- {codejury-0.10.0 → codejury-0.10.1}/pyproject.toml +1 -1
- {codejury-0.10.0 → 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.10.0/codejury/agents/parsing.py +0 -42
- {codejury-0.10.0 → codejury-0.10.1}/LICENSE +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/README.md +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/base.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/agents/refuter.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/analysis/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/analysis/provenance.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/analysis/taint.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/authentication.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/authorization.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/business_logic.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/crypto.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/data_protection.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/dependency_config.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/error_logging.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/excessive_agency.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/input_validation.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/insecure_output_handling.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/output_encoding.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/prompt_injection.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/secrets.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/capabilities/session.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_allowlist_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_arbitrary_tool_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_destructive_no_confirm_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_fixed_enum_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_human_approval_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ag_model_confirmed_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_bcrypt_password.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_jwt_noverify_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_jwt_verified_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_sha256_checksum_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_sha256_password.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authn_weak_hash_indirect_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authz_idor_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/authz_owner_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/business_logic_price_tamper_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/business_logic_server_checked_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/cmdi_fixed_argv_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/cmdi_ossystem_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/cmdi_subprocess_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/crypto_aesgcm_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/crypto_ecb_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/data_protection_plaintext_pii_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/data_protection_tokenized_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/dependency_config_tls_verify_off_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/dependency_config_tls_verify_on_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/deserialize_json_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/deserialize_pickle_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/error_logging_redacted_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/error_logging_secret_leak_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_escaped_output_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_exec_output_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_innerhtml_output_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_json_response_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_output_to_sql_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ioh_schema_validated_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/literal_eval_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/path_basename_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/path_contained_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/path_traversal_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_delimited_data_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_format_role_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_indirect_rag_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_system_concat_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_user_content_concat_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/pi_user_role_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/secrets_env_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/secrets_hardcoded_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/session_fixation_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/session_secure_cookie_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/sql_constant_concat_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/sqli_format_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/sqli_fstring_query.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/sqli_indirect_var_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/sqli_parameterized_query.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ssrf_allowlist_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ssrf_constant_url_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ssrf_substring_allowlist_bypass_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/ssrf_user_url_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xfile_idor_no_check_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xfile_idor_owner_checked_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xfile_path_sanitized_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xfile_path_tainted_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xss_innerhtml_constant_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xss_innerhtml_vuln.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/golden/xss_textcontent_safe.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/suppressions.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/taint.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/tasks/audit_diff_debate.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/data/tasks/quick_scan_single.yaml +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/artifact.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/context.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/domain/result.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/infrastructure/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/infrastructure/cache.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/integrations/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/adaptive.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/debate.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/pipeline.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/reflexion.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/single.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/orchestrators/taint_gate.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/anthropic.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/base.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/litellm.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/mock.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/openai.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/providers/openai_format.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/resources.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/base.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/chunker.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/sources/mock.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/tasks/__init__.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury/tasks/registry.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/dependency_links.txt +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/entry_points.txt +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/requires.txt +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/codejury.egg-info/top_level.txt +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/setup.cfg +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_adaptive.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_anthropic_provider.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_assembly.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_audit_pipeline.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_baseline.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_cache.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_callers.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_capability.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_challenge.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_cli_audit.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_context.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_debate_agents.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_debate_orchestrator.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_diff_source.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_function_source.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_integrations.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_json_parse.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_litellm_provider.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_openai_provider.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_orchestrator.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_pipeline_orchestrator.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_provenance.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_reflexion_orchestrator.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_repo_source.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_reporting.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_retry_provider.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_sarif.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_suppression.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_taint.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_taint_crossfile.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_taint_gate.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_tasks.py +0 -0
- {codejury-0.10.0 → codejury-0.10.1}/tests/test_verifier.py +0 -0
|
@@ -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]:
|
|
@@ -88,7 +88,7 @@ def build_orchestration(
|
|
|
88
88
|
def provider_tag(provider: Provider) -> str:
|
|
89
89
|
"""A stable short name for a provider (unwrapping RetryProvider) for cache keys,
|
|
90
90
|
so two providers that accept the same model string do not share cached verdicts."""
|
|
91
|
-
return type(getattr(provider, "
|
|
91
|
+
return type(getattr(provider, "inner", provider)).__name__
|
|
92
92
|
|
|
93
93
|
|
|
94
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]:
|
|
@@ -14,6 +14,7 @@ across subclass inheritance.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import dataclasses
|
|
17
18
|
from dataclasses import asdict, dataclass, field
|
|
18
19
|
from typing import Any, ClassVar, Literal
|
|
19
20
|
|
|
@@ -112,13 +113,33 @@ _OBSERVATION_CLASSES: dict[ObservationKind, type[Observation]] = {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
|
|
116
|
+
_PROBLEM_STATUSES = ("VULNERABLE", "PARTIAL")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_problem(o: Observation) -> bool:
|
|
120
|
+
"""A reportable problem: a Finding, or a VULNERABLE/PARTIAL Verdict. The single
|
|
121
|
+
definition shared by reporting, suppression, and the diff baseline."""
|
|
122
|
+
return isinstance(o, Finding) or (isinstance(o, Verdict) and o.status in _PROBLEM_STATUSES)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _only_known(cls: type, data: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
fields = {f.name for f in dataclasses.fields(cls)}
|
|
127
|
+
return {k: v for k, v in data.items() if k in fields}
|
|
128
|
+
|
|
129
|
+
|
|
115
130
|
def observation_from_dict(data: dict[str, Any]) -> Observation:
|
|
116
131
|
"""Reconstruct an observation from its ``to_dict`` form (inverse of it).
|
|
117
132
|
|
|
118
|
-
Used
|
|
133
|
+
Used to round-trip results through JSON (the verdict cache and the diff
|
|
134
|
+
baseline). On-disk reports are external input, so this fails with a clear
|
|
135
|
+
error on a missing/unknown ``kind`` and tolerates unknown extra keys (forward
|
|
136
|
+
compatibility) rather than crashing with KeyError/TypeError.
|
|
119
137
|
"""
|
|
120
138
|
data = dict(data)
|
|
121
|
-
|
|
139
|
+
kind = data.pop("kind", None)
|
|
140
|
+
cls = _OBSERVATION_CLASSES.get(kind)
|
|
141
|
+
if cls is None:
|
|
142
|
+
raise ValueError(f"unknown observation kind: {kind!r}")
|
|
122
143
|
if "evidence" in data:
|
|
123
|
-
data["evidence"] = [Evidence(**e) for e in data["evidence"]]
|
|
124
|
-
return cls(**data)
|
|
144
|
+
data["evidence"] = [Evidence(**_only_known(Evidence, e)) for e in data["evidence"]]
|
|
145
|
+
return cls(**_only_known(cls, data))
|
|
@@ -30,6 +30,7 @@ import yaml
|
|
|
30
30
|
from codejury.domain.artifact import CodeArtifact
|
|
31
31
|
from codejury.domain.capability import Capability
|
|
32
32
|
from codejury.domain.context import AnalysisContext
|
|
33
|
+
from codejury.domain.observation import Observation
|
|
33
34
|
from codejury.providers.base import Provider
|
|
34
35
|
|
|
35
36
|
|
|
@@ -126,7 +127,12 @@ def load_cases(directory: str | Path, *, split: str | None = None) -> list[Golde
|
|
|
126
127
|
for path in sorted(Path(directory).glob("*.yaml")):
|
|
127
128
|
with open(path, encoding="utf-8") as f:
|
|
128
129
|
data = yaml.safe_load(f)
|
|
129
|
-
|
|
130
|
+
if not isinstance(data, dict):
|
|
131
|
+
raise ValueError(f"{path}: expected a YAML mapping, got {type(data).__name__}")
|
|
132
|
+
try:
|
|
133
|
+
case = GoldenCase.from_dict(path.stem, data)
|
|
134
|
+
except KeyError as exc:
|
|
135
|
+
raise ValueError(f"{path}: golden case missing required key {exc}") from exc
|
|
130
136
|
if split is None or case.split == split:
|
|
131
137
|
cases.append(case)
|
|
132
138
|
return cases
|
|
@@ -166,7 +172,7 @@ def evaluate(
|
|
|
166
172
|
return report
|
|
167
173
|
|
|
168
174
|
|
|
169
|
-
def _predicted_vulnerable(observations: list) -> bool:
|
|
175
|
+
def _predicted_vulnerable(observations: list[Observation]) -> bool:
|
|
170
176
|
"""Did the run flag a problem? A Finding (debate/reflexion) or a VULNERABLE
|
|
171
177
|
Verdict (single/pipeline/taint) counts; SECURE verdicts and dismissed
|
|
172
178
|
concessions do not. (A bare Finding has no ``status``, so checking only
|
|
@@ -10,13 +10,18 @@ from __future__ import annotations
|
|
|
10
10
|
import json
|
|
11
11
|
import re
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
|
|
13
|
+
# Non-greedy so a second code fence later in the text doesn't get swallowed into the
|
|
14
|
+
# captured span; nested braces inside one block are still matched by the {...} anchors.
|
|
15
|
+
_FENCE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL)
|
|
16
|
+
|
|
17
|
+
# Defensive ceiling: model output is bounded by max_tokens, but never scan an
|
|
18
|
+
# unbounded string (the balanced-brace pass is superlinear on pathological input).
|
|
19
|
+
_MAX_SCAN = 1_000_000
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
def extract_json_object(text: str) -> dict | None:
|
|
18
23
|
"""Return the first JSON object found in `text`, or None if there is none."""
|
|
19
|
-
text = text.strip()
|
|
24
|
+
text = text.strip()[:_MAX_SCAN]
|
|
20
25
|
|
|
21
26
|
try:
|
|
22
27
|
obj = json.loads(text)
|
|
@@ -10,6 +10,7 @@ review body. The review requests changes when any problem is found.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import urllib.error
|
|
13
14
|
import urllib.request
|
|
14
15
|
from typing import Any, Callable
|
|
15
16
|
|
|
@@ -18,18 +19,21 @@ from codejury.domain.result import AnalysisResult
|
|
|
18
19
|
|
|
19
20
|
Results = list[tuple[str, AnalysisResult]]
|
|
20
21
|
|
|
22
|
+
# rank for ordering inline comments most-severe first before truncating to max_comments
|
|
23
|
+
_COMMENT_RANK = {"CRITICAL": 0, "HIGH": 1, "VULNERABLE": 1, "MEDIUM": 2, "PARTIAL": 2, "LOW": 3, "INFO": 4}
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
def build_review(results: Results, *, max_comments: int = 50) -> dict:
|
|
23
|
-
|
|
24
|
-
problems = 0
|
|
27
|
+
ranked: list[tuple[int, dict]] = []
|
|
25
28
|
for _path, result in results:
|
|
26
29
|
for o in result.observations:
|
|
27
30
|
comment = _inline_comment(o)
|
|
28
|
-
if comment is None:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if comment is not None:
|
|
32
|
+
ranked.append((_comment_rank(o), comment))
|
|
33
|
+
problems = len(ranked)
|
|
34
|
+
# surface the most severe inline when there are more problems than the cap
|
|
35
|
+
ranked.sort(key=lambda rc: rc[0])
|
|
36
|
+
comments = [c for _, c in ranked[:max_comments]]
|
|
33
37
|
|
|
34
38
|
body = (
|
|
35
39
|
f"codejury found {problems} issue(s)." if problems else "codejury found no issues."
|
|
@@ -43,6 +47,11 @@ def build_review(results: Results, *, max_comments: int = 50) -> dict:
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
|
|
50
|
+
def _comment_rank(o: Observation) -> int:
|
|
51
|
+
key = o.severity if o.kind == "finding" else getattr(o, "status", "")
|
|
52
|
+
return _COMMENT_RANK.get(key, 5)
|
|
53
|
+
|
|
54
|
+
|
|
46
55
|
def _inline_comment(o: Observation) -> dict | None:
|
|
47
56
|
if o.kind == "finding":
|
|
48
57
|
evidence = o.evidence[0] if o.evidence else None
|
|
@@ -75,8 +84,14 @@ def post_review(
|
|
|
75
84
|
if transport is not None:
|
|
76
85
|
return transport(url, data, headers)
|
|
77
86
|
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
try:
|
|
88
|
+
with urllib.request.urlopen(request) as response:
|
|
89
|
+
return response.status
|
|
90
|
+
except urllib.error.HTTPError as exc:
|
|
91
|
+
# surface GitHub's error detail (auth 401, repo/PR 404, out-of-diff line 422);
|
|
92
|
+
# the token is in the request, not this response body, so it is safe to include.
|
|
93
|
+
detail = exc.read().decode("utf-8", "replace")[:500]
|
|
94
|
+
raise RuntimeError(f"GitHub review POST failed: {exc.code} {detail}") from exc
|
|
80
95
|
|
|
81
96
|
|
|
82
97
|
def parse_pr_ref(ref: str) -> tuple[str, str, int]:
|
|
@@ -16,4 +16,12 @@ from codejury.domain.result import AnalysisResult
|
|
|
16
16
|
|
|
17
17
|
class Orchestrator(ABC):
|
|
18
18
|
@abstractmethod
|
|
19
|
-
def run(self, agents: dict[str, Agent], context: AnalysisContext) -> AnalysisResult:
|
|
19
|
+
def run(self, agents: dict[str, Agent], context: AnalysisContext) -> AnalysisResult:
|
|
20
|
+
"""Run the orchestration and return a result.
|
|
21
|
+
|
|
22
|
+
Contract: never raise for an agent/provider failure -- record it in
|
|
23
|
+
``AnalysisResult.error`` and return the observations produced so far.
|
|
24
|
+
``single`` deliberately stops at the first failure; the multi-agent
|
|
25
|
+
strategies record the error and return their partial ruling.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
@@ -17,6 +17,7 @@ taint checks over-report.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import dataclasses
|
|
20
|
+
from collections import Counter
|
|
20
21
|
|
|
21
22
|
from codejury.agents.base import Agent
|
|
22
23
|
from codejury.domain.context import AnalysisContext
|
|
@@ -50,10 +51,19 @@ class ChallengeOrchestrator(Orchestrator):
|
|
|
50
51
|
|
|
51
52
|
refutations = agents["refuter"].run(dataclasses.replace(context, history=flagged))
|
|
52
53
|
reasons = {c.target: c.reason for c in refutations if isinstance(c, Concession)}
|
|
54
|
+
# the refuter targets a capability, not an individual verdict; if a capability
|
|
55
|
+
# has more than one flagged verdict the refutation is ambiguous, so leave them
|
|
56
|
+
# all standing rather than risk dismissing one the refuter did not concede.
|
|
57
|
+
flagged_per_cap = Counter(v.capability for v in flagged)
|
|
53
58
|
|
|
54
59
|
observations: list[Observation] = []
|
|
55
60
|
for v in verdicts:
|
|
56
|
-
if
|
|
61
|
+
if (
|
|
62
|
+
isinstance(v, Verdict)
|
|
63
|
+
and v.status == "VULNERABLE"
|
|
64
|
+
and v.capability in reasons
|
|
65
|
+
and flagged_per_cap[v.capability] == 1
|
|
66
|
+
):
|
|
57
67
|
observations.append(
|
|
58
68
|
Concession(
|
|
59
69
|
capability=v.capability,
|
|
@@ -21,11 +21,18 @@ class RetryProvider(Provider):
|
|
|
21
21
|
max_attempts: int = 3,
|
|
22
22
|
base_delay: float = 1.0,
|
|
23
23
|
sleep: Callable[[float], None] = time.sleep,
|
|
24
|
+
retryable: tuple[type[BaseException], ...] = (Exception,),
|
|
24
25
|
) -> None:
|
|
25
26
|
self._inner = inner
|
|
26
27
|
self._max_attempts = max_attempts
|
|
27
28
|
self._base_delay = base_delay
|
|
28
29
|
self._sleep = sleep
|
|
30
|
+
self._retryable = retryable
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def inner(self) -> Provider:
|
|
34
|
+
"""The wrapped provider (so callers need not reach into a private field)."""
|
|
35
|
+
return self._inner
|
|
29
36
|
|
|
30
37
|
def complete(
|
|
31
38
|
self,
|
|
@@ -41,7 +48,7 @@ class RetryProvider(Provider):
|
|
|
41
48
|
return self._inner.complete(
|
|
42
49
|
system=system, messages=messages, model=model, max_tokens=max_tokens, cache=cache
|
|
43
50
|
)
|
|
44
|
-
except
|
|
51
|
+
except self._retryable:
|
|
45
52
|
if attempt == self._max_attempts:
|
|
46
53
|
raise
|
|
47
54
|
self._sleep(self._base_delay * attempt)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
|
|
13
13
|
from codejury import __version__ as _tool_version
|
|
14
|
-
from codejury.domain.observation import Observation, observation_from_dict
|
|
14
|
+
from codejury.domain.observation import Observation, is_problem, observation_from_dict
|
|
15
15
|
from codejury.domain.result import AnalysisResult
|
|
16
16
|
|
|
17
17
|
Results = list[tuple[str, AnalysisResult]]
|
|
@@ -39,8 +39,17 @@ def to_json(results: Results) -> str:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def from_json(text: str) -> Results:
|
|
42
|
-
"""Parse a ``to_json`` report back into results (used to load a diff baseline).
|
|
43
|
-
|
|
42
|
+
"""Parse a ``to_json`` report back into results (used to load a diff baseline).
|
|
43
|
+
|
|
44
|
+
Raises ValueError on a malformed report (it is external, possibly hand-edited,
|
|
45
|
+
input) rather than an opaque KeyError/TypeError.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
payload = json.loads(text)
|
|
49
|
+
except json.JSONDecodeError as exc:
|
|
50
|
+
raise ValueError(f"baseline report is not valid JSON: {exc}") from exc
|
|
51
|
+
if not isinstance(payload, dict):
|
|
52
|
+
raise ValueError("baseline report must be a JSON object with a 'files' list")
|
|
44
53
|
return [
|
|
45
54
|
(
|
|
46
55
|
f.get("path", ""),
|
|
@@ -61,7 +70,7 @@ def to_markdown(results: Results) -> str:
|
|
|
61
70
|
if result.error:
|
|
62
71
|
lines.append(f"> error: {result.error}")
|
|
63
72
|
|
|
64
|
-
problems = sorted((o for o in result.observations if
|
|
73
|
+
problems = sorted((o for o in result.observations if is_problem(o)), key=_rank)
|
|
65
74
|
cleared = [o for o in result.observations if o.kind == "verdict" and o.status in _CLEARED]
|
|
66
75
|
dismissed = [o for o in result.observations if o.kind == "concession"]
|
|
67
76
|
|
|
@@ -93,16 +102,12 @@ def _summary(results: Results) -> list[str]:
|
|
|
93
102
|
dismissed += 1
|
|
94
103
|
return [
|
|
95
104
|
f"- files audited: {len(results)}",
|
|
96
|
-
f"- issues: {vulnerable}
|
|
105
|
+
f"- issues: {vulnerable} problem verdict(s) (VULNERABLE/PARTIAL), {findings} finding(s)",
|
|
97
106
|
f"- checked and clear: {cleared}",
|
|
98
107
|
f"- dismissed: {dismissed}",
|
|
99
108
|
]
|
|
100
109
|
|
|
101
110
|
|
|
102
|
-
def _is_problem(o: Observation) -> bool:
|
|
103
|
-
return o.kind == "finding" or (o.kind == "verdict" and o.status in _PROBLEM_STATUSES)
|
|
104
|
-
|
|
105
|
-
|
|
106
111
|
def _rank(o: Observation) -> int:
|
|
107
112
|
if o.kind == "finding":
|
|
108
113
|
return _SEVERITY_ORDER.get(o.severity, 5)
|
|
@@ -146,7 +151,7 @@ def to_sarif(results: Results) -> str:
|
|
|
146
151
|
|
|
147
152
|
for _, result in results:
|
|
148
153
|
for o in result.observations:
|
|
149
|
-
if not
|
|
154
|
+
if not is_problem(o):
|
|
150
155
|
continue
|
|
151
156
|
locations = _sarif_locations(o)
|
|
152
157
|
if not locations:
|
|
@@ -4,7 +4,9 @@ For a file under review, find where the functions and classes it defines are
|
|
|
4
4
|
called elsewhere in the repository. Showing those call sites lets the verifier
|
|
5
5
|
trace where an argument comes from -- which is exactly what single-file review
|
|
6
6
|
lacks for taint-style issues (a path/command that is operator-supplied vs
|
|
7
|
-
attacker-controlled). This is a textual usage finder, not a full call graph
|
|
7
|
+
attacker-controlled). This is a textual usage finder, not a full call graph: the
|
|
8
|
+
call regex also matches a name inside a string, a comment, or a `def`/`class`
|
|
9
|
+
line elsewhere, so treat the result as a hint, not a precise caller set.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
from __future__ import annotations
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Splits on ``diff --git`` headers (falling back to a single section for a plain
|
|
4
4
|
diff with no git header). The path is taken from the +++ line, then the ---
|
|
5
5
|
line, then the header -- skipping /dev/null so adds and deletes still resolve to
|
|
6
|
-
the real file.
|
|
6
|
+
the real file. Combined/merge diffs (``diff --cc``) are not split and would fold
|
|
7
|
+
into the preceding section; the intended input is an ordinary two-way PR diff.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""FunctionSource -- split Python source into one CodeArtifact per function.
|
|
2
2
|
|
|
3
|
-
Parses the AST and emits an artifact for every function and method
|
|
4
|
-
async
|
|
5
|
-
|
|
3
|
+
Parses the AST and emits an artifact for every top-level function and method
|
|
4
|
+
(async included), in source order. Functions nested inside another function are
|
|
5
|
+
skipped: their source already lives inside the enclosing function's artifact, so
|
|
6
|
+
emitting them again would send the model overlapping, duplicated content. The
|
|
7
|
+
content must be valid Python; a parse failure raises SyntaxError.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -23,6 +25,7 @@ class FunctionSource(Source):
|
|
|
23
25
|
functions = [
|
|
24
26
|
node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
25
27
|
]
|
|
28
|
+
functions = [n for n in functions if not _nested_in_function(n, functions)]
|
|
26
29
|
functions.sort(key=lambda n: n.lineno)
|
|
27
30
|
|
|
28
31
|
artifacts = []
|
|
@@ -33,3 +36,9 @@ class FunctionSource(Source):
|
|
|
33
36
|
CodeArtifact(kind="function", path=f"{self._path}::{node.name}", content=segment)
|
|
34
37
|
)
|
|
35
38
|
return artifacts
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _nested_in_function(node: ast.AST, functions: list[ast.AST]) -> bool:
|
|
42
|
+
"""True if ``node`` is defined inside another function (a method in a class is
|
|
43
|
+
not -- its enclosing scope is the class, not a function)."""
|
|
44
|
+
return any(other is not node and any(d is node for d in ast.walk(other)) for other in functions)
|