patchi 0.7.5__py3-none-any.whl
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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""AttackPlanner — orchestrates all attackers, scores and prioritizes hypotheses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
9
|
+
|
|
10
|
+
from .api_abuse import ApiAbuseAttacker
|
|
11
|
+
from .auth_bypass import AuthBypassAttacker
|
|
12
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
13
|
+
from .business_logic import BusinessLogicAttacker
|
|
14
|
+
from .priv_escalation import PrivEscAttacker
|
|
15
|
+
from .recon import ReconAttacker
|
|
16
|
+
|
|
17
|
+
_log = logging.getLogger("patchi.core.attackers.planner")
|
|
18
|
+
|
|
19
|
+
# Objective priority weights (higher = more important to test)
|
|
20
|
+
_OBJECTIVE_PRIORITY: dict[str, float] = {
|
|
21
|
+
"authentication_bypass": 1.0,
|
|
22
|
+
"privilege_escalation": 0.9,
|
|
23
|
+
"api_abuse": 0.7,
|
|
24
|
+
"reconnaissance": 0.5,
|
|
25
|
+
"business_logic_abuse": 0.6,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_RISK_SCORES: dict[str, float] = {
|
|
29
|
+
"critical": 1.0,
|
|
30
|
+
"high": 0.7,
|
|
31
|
+
"medium": 0.4,
|
|
32
|
+
"low": 0.2,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class AttackPlan:
|
|
38
|
+
"""A prioritized plan of hypotheses to test."""
|
|
39
|
+
|
|
40
|
+
hypotheses: list[Hypothesis]
|
|
41
|
+
total_score: float = 0.0
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"hypotheses": [h.to_dict() for h in self.hypotheses[:20]],
|
|
46
|
+
"total_score": round(self.total_score, 2),
|
|
47
|
+
"count": len(self.hypotheses),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AttackPlanner:
|
|
52
|
+
"""Orchestrate all attackers, score hypotheses, and run the top ones."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, graph: AssuranceGraph, max_run: int = 50):
|
|
55
|
+
self._graph = graph
|
|
56
|
+
self._max_run = max_run
|
|
57
|
+
self._attackers: list[BaseAttacker] = [
|
|
58
|
+
ReconAttacker(graph),
|
|
59
|
+
AuthBypassAttacker(graph),
|
|
60
|
+
PrivEscAttacker(graph),
|
|
61
|
+
BusinessLogicAttacker(graph),
|
|
62
|
+
ApiAbuseAttacker(graph),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def plan(self) -> AttackPlan:
|
|
66
|
+
"""Generate and score all hypotheses."""
|
|
67
|
+
all_hypotheses: list[Hypothesis] = []
|
|
68
|
+
for attacker in self._attackers:
|
|
69
|
+
all_hypotheses.extend(attacker.hypothesize())
|
|
70
|
+
|
|
71
|
+
# Score and sort
|
|
72
|
+
for h in all_hypotheses:
|
|
73
|
+
h._score = self._score(h) # type: ignore[attr-defined]
|
|
74
|
+
|
|
75
|
+
all_hypotheses.sort(key=lambda h: getattr(h, "_score", 0), reverse=True)
|
|
76
|
+
total = sum(getattr(h, "_score", 0) for h in all_hypotheses)
|
|
77
|
+
|
|
78
|
+
return AttackPlan(
|
|
79
|
+
hypotheses=all_hypotheses[: self._max_run],
|
|
80
|
+
total_score=total,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def run_all(self) -> list[AttackResult]:
|
|
84
|
+
"""Run all attackers and collect results."""
|
|
85
|
+
results: list[AttackResult] = []
|
|
86
|
+
for attacker in self._attackers:
|
|
87
|
+
try:
|
|
88
|
+
results.extend(attacker.run())
|
|
89
|
+
except Exception as e:
|
|
90
|
+
_log.warning("Attacker %s failed: %s", attacker.name, e)
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
def run_campaign(self, objective: str) -> list[AttackResult]:
|
|
94
|
+
"""Run attackers matching a specific objective."""
|
|
95
|
+
results: list[AttackResult] = []
|
|
96
|
+
for attacker in self._attackers:
|
|
97
|
+
if attacker.objective == objective:
|
|
98
|
+
try:
|
|
99
|
+
results.extend(attacker.run())
|
|
100
|
+
except Exception as e:
|
|
101
|
+
_log.warning("Attacker %s failed: %s", attacker.name, e)
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
def _score(self, hypothesis: Hypothesis) -> float:
|
|
105
|
+
"""Score a hypothesis by risk × confidence × objective priority."""
|
|
106
|
+
risk = _RISK_SCORES.get(hypothesis.risk, 0.3)
|
|
107
|
+
priority = _OBJECTIVE_PRIORITY.get(hypothesis.objective, 0.3)
|
|
108
|
+
return risk * hypothesis.confidence * priority
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""PrivEscAttacker — test privilege escalation vulnerabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PrivEscAttacker(BaseAttacker):
|
|
9
|
+
"""Tests for illegal state transitions, missing isolation, shared endpoints."""
|
|
10
|
+
|
|
11
|
+
name = "priv_escalation"
|
|
12
|
+
objective = "privilege_escalation"
|
|
13
|
+
|
|
14
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
15
|
+
hypotheses: list[Hypothesis] = []
|
|
16
|
+
|
|
17
|
+
for claim in self._graph.claims.values():
|
|
18
|
+
# Authorization boundary claims
|
|
19
|
+
if "admin" in claim.statement.lower() or "escalat" in claim.statement.lower():
|
|
20
|
+
if claim.verdict.value in ("not_proved", "unproven"):
|
|
21
|
+
hypotheses.append(
|
|
22
|
+
Hypothesis(
|
|
23
|
+
attacker=self.name,
|
|
24
|
+
objective=self.objective,
|
|
25
|
+
target=claim.id,
|
|
26
|
+
risk="critical",
|
|
27
|
+
confidence=0.6,
|
|
28
|
+
description=f"Privilege boundary unverified: {claim.statement}",
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Missing tenant isolation
|
|
33
|
+
if "tenant" in claim.statement.lower() or "isolation" in claim.statement.lower():
|
|
34
|
+
hypotheses.append(
|
|
35
|
+
Hypothesis(
|
|
36
|
+
attacker=self.name,
|
|
37
|
+
objective=self.objective,
|
|
38
|
+
target=claim.id,
|
|
39
|
+
risk="critical",
|
|
40
|
+
confidence=0.7,
|
|
41
|
+
description=f"Tenant isolation unverified: {claim.statement}",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Shared endpoints with different privilege levels
|
|
46
|
+
if "role" in claim.statement.lower() and "check" in claim.statement.lower():
|
|
47
|
+
if claim.verdict.value == "unproven":
|
|
48
|
+
hypotheses.append(
|
|
49
|
+
Hypothesis(
|
|
50
|
+
attacker=self.name,
|
|
51
|
+
objective=self.objective,
|
|
52
|
+
target=claim.id,
|
|
53
|
+
risk="high",
|
|
54
|
+
confidence=0.5,
|
|
55
|
+
description=f"Role-based access unverified: {claim.statement}",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return hypotheses
|
|
60
|
+
|
|
61
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
62
|
+
claim = self._graph.claims.get(hypothesis.target)
|
|
63
|
+
if claim is None:
|
|
64
|
+
return AttackResult(hypothesis=hypothesis, confirmed=False, evidence="Claim not found")
|
|
65
|
+
|
|
66
|
+
refuting = [e for e in claim.evidence if not e.supports]
|
|
67
|
+
if refuting:
|
|
68
|
+
return AttackResult(
|
|
69
|
+
hypothesis=hypothesis,
|
|
70
|
+
confirmed=False,
|
|
71
|
+
evidence=f"Isolation verified: {refuting[0].detail}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
supporting = [e for e in claim.evidence if e.supports]
|
|
75
|
+
if supporting:
|
|
76
|
+
return AttackResult(
|
|
77
|
+
hypothesis=hypothesis,
|
|
78
|
+
confirmed=True,
|
|
79
|
+
evidence=f"Privilege escalation confirmed: {supporting[0].detail}",
|
|
80
|
+
severity="critical",
|
|
81
|
+
remediation="Add role/tenant check before this operation",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return AttackResult(
|
|
85
|
+
hypothesis=hypothesis,
|
|
86
|
+
confirmed=False,
|
|
87
|
+
evidence="Insufficient evidence",
|
|
88
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""ReconAttacker — test reconnaissance vulnerabilities.
|
|
2
|
+
|
|
3
|
+
Looks for unprotected endpoints, excess permissions, info disclosure,
|
|
4
|
+
and unsanitized data flows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReconAttacker(BaseAttacker):
|
|
13
|
+
"""Reconnaissance attacker — finds information disclosure and unprotected endpoints."""
|
|
14
|
+
|
|
15
|
+
name = "recon"
|
|
16
|
+
objective = "reconnaissance"
|
|
17
|
+
|
|
18
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
19
|
+
hypotheses: list[Hypothesis] = []
|
|
20
|
+
|
|
21
|
+
for claim in self._graph.claims.values():
|
|
22
|
+
# Unprotected endpoints
|
|
23
|
+
if "endpoint" in claim.domain.lower() and "auth" in claim.statement.lower():
|
|
24
|
+
if claim.verdict.value in ("not_proved", "unproven"):
|
|
25
|
+
hypotheses.append(
|
|
26
|
+
Hypothesis(
|
|
27
|
+
attacker=self.name,
|
|
28
|
+
objective=self.objective,
|
|
29
|
+
target=claim.id,
|
|
30
|
+
risk="medium",
|
|
31
|
+
confidence=0.6,
|
|
32
|
+
description=f"Endpoint may be unprotected: {claim.statement}",
|
|
33
|
+
evidence_required=["route scan", "auth check"],
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Excess permissions
|
|
38
|
+
if "permission" in claim.statement.lower() or "access" in claim.statement.lower():
|
|
39
|
+
if claim.verdict.value == "unproven":
|
|
40
|
+
hypotheses.append(
|
|
41
|
+
Hypothesis(
|
|
42
|
+
attacker=self.name,
|
|
43
|
+
objective=self.objective,
|
|
44
|
+
target=claim.id,
|
|
45
|
+
risk="medium",
|
|
46
|
+
confidence=0.5,
|
|
47
|
+
description=f"Permission boundary unverified: {claim.statement}",
|
|
48
|
+
evidence_required=["access control check"],
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return hypotheses
|
|
53
|
+
|
|
54
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
55
|
+
claim = self._graph.claims.get(hypothesis.target)
|
|
56
|
+
if claim is None:
|
|
57
|
+
return AttackResult(
|
|
58
|
+
hypothesis=hypothesis,
|
|
59
|
+
confirmed=False,
|
|
60
|
+
evidence="Claim not found in assurance graph",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check if any refuting evidence exists (someone already found it's protected)
|
|
64
|
+
refuting = [e for e in claim.evidence if not e.supports]
|
|
65
|
+
if refuting:
|
|
66
|
+
return AttackResult(
|
|
67
|
+
hypothesis=hypothesis,
|
|
68
|
+
confirmed=False,
|
|
69
|
+
evidence=f"Already disproved: {refuting[0].detail}",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check if any supporting evidence exists (someone confirmed it's unprotected)
|
|
73
|
+
supporting = [e for e in claim.evidence if e.supports]
|
|
74
|
+
if supporting:
|
|
75
|
+
return AttackResult(
|
|
76
|
+
hypothesis=hypothesis,
|
|
77
|
+
confirmed=True,
|
|
78
|
+
evidence=f"Confirmed: {supporting[0].detail}",
|
|
79
|
+
severity=hypothesis.risk,
|
|
80
|
+
remediation="Add authentication/authorization check",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return AttackResult(
|
|
84
|
+
hypothesis=hypothesis,
|
|
85
|
+
confirmed=False,
|
|
86
|
+
evidence="Insufficient evidence to confirm or deny",
|
|
87
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST utilities — language-generic tree-sitter / Python `ast` walking utilities.
|
|
3
|
+
|
|
4
|
+
This package exposes language-generic helpers used by security agents, type
|
|
5
|
+
checkers, and route detectors. The implementation is split into submodules
|
|
6
|
+
but re-exported here so existing imports keep working:
|
|
7
|
+
|
|
8
|
+
from patchi.core.brain.ast_utils import find_calls, find_imports, node_text
|
|
9
|
+
calls = find_calls(content, Lang.PYTHON, ["execute", "eval"])
|
|
10
|
+
|
|
11
|
+
Submodules:
|
|
12
|
+
config.py — per-language node type configs + common sink sets
|
|
13
|
+
helpers.py — node_text, child_by_field, children, get_call_arg, ...
|
|
14
|
+
calls.py — find_calls (+ Python / tree-sitter walkers)
|
|
15
|
+
imports.py — find_imports
|
|
16
|
+
decorators.py — find_decorators
|
|
17
|
+
assignments.py — find_assignments
|
|
18
|
+
control_flow.py — scope / argument walking helpers
|
|
19
|
+
taint.py — source-to-sink taint tracking
|
|
20
|
+
dead_symbols.py — intra-file dead-symbol detection
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from .assignments import find_assignments
|
|
26
|
+
from .calls import find_calls
|
|
27
|
+
|
|
28
|
+
# Re-export public API from submodules
|
|
29
|
+
from .config import (
|
|
30
|
+
CALL_NODE_TYPES,
|
|
31
|
+
DECORATOR_NODE_TYPES,
|
|
32
|
+
EXEC_SINKS,
|
|
33
|
+
HTTP_CLIENTS,
|
|
34
|
+
IMPORT_NODE_TYPES,
|
|
35
|
+
SQL_SINKS,
|
|
36
|
+
)
|
|
37
|
+
from .control_flow import (
|
|
38
|
+
child_by_field_any,
|
|
39
|
+
collect_call_arguments,
|
|
40
|
+
named_children_of_type,
|
|
41
|
+
walk_blocks,
|
|
42
|
+
)
|
|
43
|
+
from .dead_symbols import find_dead_symbols
|
|
44
|
+
from .decorators import find_decorators
|
|
45
|
+
from .helpers import (
|
|
46
|
+
child_by_field,
|
|
47
|
+
children,
|
|
48
|
+
get_call_arg,
|
|
49
|
+
named_children,
|
|
50
|
+
node_text,
|
|
51
|
+
parse_source,
|
|
52
|
+
)
|
|
53
|
+
from .imports import find_imports
|
|
54
|
+
from .taint import SANITIZERS, SOURCES, track_taint
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"CALL_NODE_TYPES",
|
|
58
|
+
"DECORATOR_NODE_TYPES",
|
|
59
|
+
"EXEC_SINKS",
|
|
60
|
+
"HTTP_CLIENTS",
|
|
61
|
+
"IMPORT_NODE_TYPES",
|
|
62
|
+
"SQL_SINKS",
|
|
63
|
+
"child_by_field",
|
|
64
|
+
"children",
|
|
65
|
+
"get_call_arg",
|
|
66
|
+
"named_children",
|
|
67
|
+
"node_text",
|
|
68
|
+
"parse_source",
|
|
69
|
+
"find_calls",
|
|
70
|
+
"find_imports",
|
|
71
|
+
"find_decorators",
|
|
72
|
+
"find_assignments",
|
|
73
|
+
"child_by_field_any",
|
|
74
|
+
"collect_call_arguments",
|
|
75
|
+
"named_children_of_type",
|
|
76
|
+
"walk_blocks",
|
|
77
|
+
"SOURCES",
|
|
78
|
+
"SANITIZERS",
|
|
79
|
+
"track_taint",
|
|
80
|
+
"find_dead_symbols",
|
|
81
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST utilities — variable assignment / declaration finding across languages.
|
|
3
|
+
|
|
4
|
+
Used by env-var analysis, secret scanning, and taint tracking. Returns the
|
|
5
|
+
assigned target name plus the source expression so callers can follow how a
|
|
6
|
+
value (e.g. a secret, a user-controlled string) flows into code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from patchi.core.brain.languages import get_parser
|
|
14
|
+
|
|
15
|
+
from .config import Lang
|
|
16
|
+
from .helpers import child_by_field, children, node_text
|
|
17
|
+
|
|
18
|
+
_log = logging.getLogger("patchi.brain.assignments")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_assignments(content: str, lang: Lang) -> list[dict]:
|
|
22
|
+
"""
|
|
23
|
+
Find all variable assignments in `content`.
|
|
24
|
+
|
|
25
|
+
Returns list of dicts with keys:
|
|
26
|
+
target — assigned name (str)
|
|
27
|
+
value — RHS expression text (str)
|
|
28
|
+
line — 1-based line number (int)
|
|
29
|
+
full_text— full assignment text (str)
|
|
30
|
+
|
|
31
|
+
Python is served from the shared single-pass AST scan (one parse + walk
|
|
32
|
+
per file per process, cached).
|
|
33
|
+
"""
|
|
34
|
+
if lang == Lang.PYTHON:
|
|
35
|
+
return _find_assignments_python(content)
|
|
36
|
+
parser = get_parser(lang)
|
|
37
|
+
if not parser:
|
|
38
|
+
return []
|
|
39
|
+
try:
|
|
40
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
41
|
+
except Exception as e:
|
|
42
|
+
_log.warning("find_assignments failed: %s", e)
|
|
43
|
+
return []
|
|
44
|
+
results: list[dict] = []
|
|
45
|
+
_walk_assignments(tree.root_node, results, lang)
|
|
46
|
+
return results
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Python ────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_assignments_python(content: str) -> list[dict]:
|
|
53
|
+
"""Walk the AST for variable assignments."""
|
|
54
|
+
import ast
|
|
55
|
+
|
|
56
|
+
from .helpers import _py_assign_target_name
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
tree = ast.parse(content)
|
|
60
|
+
except SyntaxError:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
src_lines = content.splitlines()
|
|
64
|
+
results: list[dict] = []
|
|
65
|
+
|
|
66
|
+
def _record(target_node: ast.AST, value_node: ast.AST | None, line_no: int) -> None:
|
|
67
|
+
target = _py_assign_target_name(target_node)
|
|
68
|
+
if not target:
|
|
69
|
+
return
|
|
70
|
+
full_text = src_lines[line_no - 1] if 0 < line_no <= len(src_lines) else ""
|
|
71
|
+
try:
|
|
72
|
+
value = ast.unparse(value_node) if value_node is not None else ""
|
|
73
|
+
except Exception:
|
|
74
|
+
value = ""
|
|
75
|
+
results.append(
|
|
76
|
+
{
|
|
77
|
+
"target": target,
|
|
78
|
+
"value": value,
|
|
79
|
+
"line": line_no,
|
|
80
|
+
"full_text": full_text,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for node in ast.walk(tree):
|
|
85
|
+
if isinstance(node, ast.Assign):
|
|
86
|
+
for target in node.targets:
|
|
87
|
+
_record(target, node.value, node.lineno)
|
|
88
|
+
elif isinstance(node, ast.AnnAssign):
|
|
89
|
+
# Annotated assignment: config.debug: bool = True (value may be None)
|
|
90
|
+
_record(node.target, node.value, node.lineno)
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── Tree-sitter languages ──────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
# Per-language assignment node types
|
|
97
|
+
_ASSIGN_NODE_TYPES: dict[Lang, set[str]] = {
|
|
98
|
+
Lang.JAVASCRIPT: {"variable_declarator", "assignment_expression"},
|
|
99
|
+
Lang.TYPESCRIPT: {"variable_declarator", "assignment_expression"},
|
|
100
|
+
Lang.JAVA: {"local_variable_declaration", "assignment_expression"},
|
|
101
|
+
Lang.GO: {"short_var_declaration", "var_declaration", "assignment_statement"},
|
|
102
|
+
Lang.RUST: {"let_declaration", "assignment_expression"},
|
|
103
|
+
Lang.C: {"declaration", "assignment_expression"},
|
|
104
|
+
Lang.CPP: {"declaration", "assignment_expression"},
|
|
105
|
+
Lang.SWIFT: {"variable_declaration"},
|
|
106
|
+
Lang.RUBY: {"assignment"},
|
|
107
|
+
Lang.PHP: {"assignment_expression"},
|
|
108
|
+
Lang.C_SHARP: {"local_declaration_statement", "assignment_expression"},
|
|
109
|
+
Lang.KOTLIN: {"property_declaration", "assignment"},
|
|
110
|
+
Lang.DART: {"variable_declaration"},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _walk_assignments(node: object, results: list[dict], lang: Lang) -> None:
|
|
115
|
+
try:
|
|
116
|
+
ntype = node.type if hasattr(node, "type") else ""
|
|
117
|
+
except Exception as e:
|
|
118
|
+
_log.warning("_walk_assignments failed: %s", e)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
assign_types = _ASSIGN_NODE_TYPES.get(lang, set())
|
|
122
|
+
if ntype in assign_types:
|
|
123
|
+
target, value = _extract_assignment(node, lang)
|
|
124
|
+
if target:
|
|
125
|
+
try:
|
|
126
|
+
line = node.start_point[0] + 1 if node.start_point else 0
|
|
127
|
+
except Exception as e:
|
|
128
|
+
_log.warning("_walk_assignments failed: %s", e)
|
|
129
|
+
line = 0
|
|
130
|
+
results.append(
|
|
131
|
+
{
|
|
132
|
+
"target": target,
|
|
133
|
+
"value": value,
|
|
134
|
+
"line": line,
|
|
135
|
+
"full_text": node_text(node),
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
for child in children(node):
|
|
140
|
+
_walk_assignments(child, results, lang)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _extract_assignment(node: object, lang: Lang) -> tuple[str, str]:
|
|
144
|
+
"""Return (target_name, value_text) for an assignment node."""
|
|
145
|
+
if lang in (Lang.JAVASCRIPT, Lang.TYPESCRIPT):
|
|
146
|
+
# variable_declarator: name + value ; assignment_expression: left + right
|
|
147
|
+
if ntype := getattr(node, "type", ""):
|
|
148
|
+
if ntype == "variable_declarator":
|
|
149
|
+
name_node = child_by_field(node, "name")
|
|
150
|
+
val_node = child_by_field(node, "value")
|
|
151
|
+
return node_text(name_node), node_text(val_node)
|
|
152
|
+
if ntype == "assignment_expression":
|
|
153
|
+
left = child_by_field(node, "left")
|
|
154
|
+
right = child_by_field(node, "right")
|
|
155
|
+
return node_text(left), node_text(right)
|
|
156
|
+
if lang == Lang.RUST and getattr(node, "type", "") == "let_declaration":
|
|
157
|
+
# let x = ... ; pattern = identifier, value = value
|
|
158
|
+
pat = child_by_field(node, "pattern") or child_by_field(node, "name")
|
|
159
|
+
val = child_by_field(node, "value")
|
|
160
|
+
return node_text(pat), node_text(val)
|
|
161
|
+
if lang in (Lang.JAVA, Lang.C_SHARP) and getattr(node, "type", "") in (
|
|
162
|
+
"local_variable_declaration",
|
|
163
|
+
"local_declaration_statement",
|
|
164
|
+
):
|
|
165
|
+
decl = children(node)
|
|
166
|
+
# Find the declarator child holding name/value
|
|
167
|
+
for child in decl:
|
|
168
|
+
ct = child.type
|
|
169
|
+
if ct in ("variable_declarator", "declarator"):
|
|
170
|
+
name_node = child_by_field(child, "name")
|
|
171
|
+
val_node = child_by_field(child, "value")
|
|
172
|
+
return node_text(name_node), node_text(val_node)
|
|
173
|
+
return "", ""
|
|
174
|
+
if lang == Lang.GO:
|
|
175
|
+
if getattr(node, "type", "") == "short_var_declaration":
|
|
176
|
+
left = child_by_field(node, "left")
|
|
177
|
+
right = child_by_field(node, "right")
|
|
178
|
+
return node_text(left), node_text(right)
|
|
179
|
+
if getattr(node, "type", "") in ("var_declaration", "assignment_statement"):
|
|
180
|
+
left = child_by_field(node, "name") or child_by_field(node, "left")
|
|
181
|
+
right = child_by_field(node, "value") or child_by_field(node, "right")
|
|
182
|
+
return node_text(left), node_text(right)
|
|
183
|
+
if lang in (Lang.C, Lang.CPP) and getattr(node, "type", "") == "declaration":
|
|
184
|
+
decl = children(node)
|
|
185
|
+
for child in decl:
|
|
186
|
+
if child.type == "init_declarator":
|
|
187
|
+
name_node = child_by_field(child, "declarator")
|
|
188
|
+
val_node = child_by_field(child, "value")
|
|
189
|
+
return node_text(name_node), node_text(val_node)
|
|
190
|
+
return "", ""
|
|
191
|
+
if lang == Lang.SWIFT and getattr(node, "type", "") == "variable_declaration":
|
|
192
|
+
pat = child_by_field(node, "pattern")
|
|
193
|
+
val = child_by_field(node, "value")
|
|
194
|
+
return node_text(pat), node_text(val)
|
|
195
|
+
if lang == Lang.RUBY and getattr(node, "type", "") == "assignment":
|
|
196
|
+
left = child_by_field(node, "left")
|
|
197
|
+
right = child_by_field(node, "right")
|
|
198
|
+
return node_text(left), node_text(right)
|
|
199
|
+
if lang == Lang.PHP and getattr(node, "type", "") == "assignment_expression":
|
|
200
|
+
left = child_by_field(node, "left")
|
|
201
|
+
right = child_by_field(node, "right")
|
|
202
|
+
return node_text(left), node_text(right)
|
|
203
|
+
if lang == Lang.KOTLIN and getattr(node, "type", "") in ("property_declaration", "assignment"):
|
|
204
|
+
if getattr(node, "type", "") == "property_declaration":
|
|
205
|
+
name_node = child_by_field(node, "name")
|
|
206
|
+
val = child_by_field(node, "initializer")
|
|
207
|
+
return node_text(name_node), node_text(val)
|
|
208
|
+
left = child_by_field(node, "left")
|
|
209
|
+
right = child_by_field(node, "right")
|
|
210
|
+
return node_text(left), node_text(right)
|
|
211
|
+
if lang == Lang.DART and getattr(node, "type", "") == "variable_declaration":
|
|
212
|
+
# Dart uses declarator children
|
|
213
|
+
for child in children(node):
|
|
214
|
+
if child.type == "declared_identifier":
|
|
215
|
+
name_node = child_by_field(child, "name")
|
|
216
|
+
val = child_by_field(node, "value")
|
|
217
|
+
return node_text(name_node), node_text(val)
|
|
218
|
+
return "", ""
|
|
219
|
+
return "", ""
|