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