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,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reasoning Engine — queries the Layered Brain for structured impact analysis.
|
|
3
|
+
|
|
4
|
+
Answers questions like:
|
|
5
|
+
- "What changed?" → reads git diff, maps to layers, lists affected subsystems
|
|
6
|
+
- "What does auth do?" → reads layer summaries, shows public API + dependencies
|
|
7
|
+
- "What imports X?" → traces dependency graph through layers
|
|
8
|
+
- "What are the security hotspots?" → cross-references charter rules with layers
|
|
9
|
+
|
|
10
|
+
No AI tokens required — pure heuristic reasoning over the layered brain.
|
|
11
|
+
No regex — uses keyword matching for classification.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from patchi.core.brain.layered_brain import (
|
|
21
|
+
Layer,
|
|
22
|
+
find_layers_for_file,
|
|
23
|
+
get_layer_context,
|
|
24
|
+
layers_from_dict,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ReasoningResult:
|
|
30
|
+
"""Structured answer from the reasoning engine."""
|
|
31
|
+
|
|
32
|
+
question: str
|
|
33
|
+
answer: str # human-readable summary
|
|
34
|
+
layers: list[str] = field(default_factory=list) # affected layer names
|
|
35
|
+
files: list[str] = field(default_factory=list) # affected files
|
|
36
|
+
details: dict[str, Any] = field(default_factory=dict) # structured data
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"question": self.question,
|
|
41
|
+
"answer": self.answer,
|
|
42
|
+
"layers": self.layers,
|
|
43
|
+
"files": self.files,
|
|
44
|
+
"details": self.details,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Layered Brain loader ─────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_layers(root: Path) -> dict[str, Layer]:
|
|
52
|
+
"""Load cached layers from .patchi/memory/layers.json."""
|
|
53
|
+
path = root / ".patchi" / "memory" / "layers.json"
|
|
54
|
+
if not path.exists():
|
|
55
|
+
return {}
|
|
56
|
+
try:
|
|
57
|
+
data = {}
|
|
58
|
+
with open(path, encoding="utf-8") as f:
|
|
59
|
+
import json
|
|
60
|
+
data = json.load(f)
|
|
61
|
+
return layers_from_dict(data)
|
|
62
|
+
except Exception:
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Question classifiers (keyword-based, no regex) ───────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def classify_question(question: str) -> str:
|
|
70
|
+
"""Classify a question into a category using keyword matching."""
|
|
71
|
+
q = question.lower().strip()
|
|
72
|
+
|
|
73
|
+
# What changed?
|
|
74
|
+
change_words = ["changed", "new", "modified", "different", "diff"]
|
|
75
|
+
if any(w in q for w in change_words):
|
|
76
|
+
return "what_changed"
|
|
77
|
+
|
|
78
|
+
# What does X do?
|
|
79
|
+
does_words = ["does", "is", "are"]
|
|
80
|
+
do_words = ["do", "about"]
|
|
81
|
+
if any(w in q for w in does_words) and any(w in q for w in do_words):
|
|
82
|
+
return "what_does"
|
|
83
|
+
|
|
84
|
+
# What imports/uses/depends on X?
|
|
85
|
+
import_words = ["import", "use", "depend"]
|
|
86
|
+
if any(w in q for w in import_words):
|
|
87
|
+
return "what_imports"
|
|
88
|
+
|
|
89
|
+
# Security hotspots
|
|
90
|
+
security_words = ["security", "hotspot", "vuln", "vulnerability", "risk", "dangerous"]
|
|
91
|
+
if any(w in q for w in security_words):
|
|
92
|
+
return "hotspots"
|
|
93
|
+
|
|
94
|
+
# Routes/endpoints
|
|
95
|
+
route_words = ["route", "endpoint", "api", "handler"]
|
|
96
|
+
if any(w in q for w in route_words):
|
|
97
|
+
return "routes"
|
|
98
|
+
|
|
99
|
+
# Layers/modules
|
|
100
|
+
layer_words = ["layer", "subsystem", "module", "component"]
|
|
101
|
+
if any(w in q for w in layer_words):
|
|
102
|
+
return "layers"
|
|
103
|
+
|
|
104
|
+
# Dependencies
|
|
105
|
+
dep_words = ["depend", "import", "use"]
|
|
106
|
+
if any(w in q for w in dep_words):
|
|
107
|
+
return "imports"
|
|
108
|
+
|
|
109
|
+
return "general"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Answer generators ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _answer_what_changed(root: Path, layers: dict[str, Layer]) -> ReasoningResult:
|
|
116
|
+
"""Answer 'what changed?' by checking git diff and mapping to layers."""
|
|
117
|
+
import subprocess
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
["git", "diff", "--name-only", "HEAD~1"],
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
cwd=str(root),
|
|
125
|
+
timeout=10,
|
|
126
|
+
)
|
|
127
|
+
changed_files = [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
|
|
128
|
+
except Exception:
|
|
129
|
+
changed_files = []
|
|
130
|
+
|
|
131
|
+
if not changed_files:
|
|
132
|
+
return ReasoningResult(
|
|
133
|
+
question="what changed",
|
|
134
|
+
answer="No recent changes detected (or not a git repo).",
|
|
135
|
+
layers=[],
|
|
136
|
+
files=[],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Map files to layers
|
|
140
|
+
affected_layers: set[str] = set()
|
|
141
|
+
affected_files: list[str] = []
|
|
142
|
+
layer_details: dict[str, list[str]] = {}
|
|
143
|
+
|
|
144
|
+
for fp in changed_files:
|
|
145
|
+
layer_names = find_layers_for_file(layers, fp)
|
|
146
|
+
for ln in layer_names:
|
|
147
|
+
affected_layers.add(ln)
|
|
148
|
+
layer_details.setdefault(ln, []).append(fp)
|
|
149
|
+
affected_files.append(fp)
|
|
150
|
+
|
|
151
|
+
# Build answer
|
|
152
|
+
parts = [f"Changed {len(changed_files)} file(s) across {len(affected_layers)} layer(s):"]
|
|
153
|
+
for ln in sorted(affected_layers):
|
|
154
|
+
layer = layers.get(ln)
|
|
155
|
+
summary = layer.summary if layer else "unknown"
|
|
156
|
+
files_in = layer_details.get(ln, [])
|
|
157
|
+
parts.append(f" {ln}: {summary}")
|
|
158
|
+
for f in files_in[:5]:
|
|
159
|
+
parts.append(f" - {f}")
|
|
160
|
+
if len(files_in) > 5:
|
|
161
|
+
parts.append(f" ... and {len(files_in) - 5} more")
|
|
162
|
+
|
|
163
|
+
# Check for boundary violations
|
|
164
|
+
if affected_layers:
|
|
165
|
+
parts.append("")
|
|
166
|
+
parts.append("Dependency analysis:")
|
|
167
|
+
for ln in sorted(affected_layers):
|
|
168
|
+
layer = layers.get(ln)
|
|
169
|
+
if layer and layer.dependents:
|
|
170
|
+
cross = [d for d in layer.dependents if d not in affected_layers]
|
|
171
|
+
if cross:
|
|
172
|
+
parts.append(f" {ln} is imported by: {', '.join(cross[:5])}")
|
|
173
|
+
|
|
174
|
+
return ReasoningResult(
|
|
175
|
+
question="what changed",
|
|
176
|
+
answer="\n".join(parts),
|
|
177
|
+
layers=sorted(affected_layers),
|
|
178
|
+
files=affected_files,
|
|
179
|
+
details={"layer_files": layer_details},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _answer_what_does(root: Path, layers: dict[str, Layer], module_name: str) -> ReasoningResult:
|
|
184
|
+
"""Answer 'what does X do?' by reading layer summaries."""
|
|
185
|
+
# Try exact match first
|
|
186
|
+
layer = layers.get(module_name)
|
|
187
|
+
|
|
188
|
+
# Try partial match
|
|
189
|
+
if not layer:
|
|
190
|
+
for name, lay in layers.items():
|
|
191
|
+
if module_name.lower() in name.lower():
|
|
192
|
+
layer = lay
|
|
193
|
+
module_name = name
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if not layer:
|
|
197
|
+
return ReasoningResult(
|
|
198
|
+
question=f"what does {module_name} do",
|
|
199
|
+
answer=f"Layer '{module_name}' not found in the layered brain.",
|
|
200
|
+
layers=[],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
parts = [f"=== {layer.name} (Level {layer.level}) ==="]
|
|
204
|
+
parts.append(f"Summary: {layer.summary}")
|
|
205
|
+
parts.append(f"Purpose: {layer.purpose}")
|
|
206
|
+
if layer.files:
|
|
207
|
+
parts.append(f"Files: {len(layer.files)}")
|
|
208
|
+
for f in layer.files[:10]:
|
|
209
|
+
parts.append(f" - {f}")
|
|
210
|
+
if layer.public_api:
|
|
211
|
+
parts.append(f"Public API: {len(layer.public_api)} symbols")
|
|
212
|
+
for api in layer.public_api[:15]:
|
|
213
|
+
parts.append(f" - {api}")
|
|
214
|
+
if layer.depends_on:
|
|
215
|
+
parts.append(f"Depends on: {', '.join(layer.depends_on[:10])}")
|
|
216
|
+
if layer.dependents:
|
|
217
|
+
parts.append(f"Used by: {', '.join(layer.dependents[:10])}")
|
|
218
|
+
|
|
219
|
+
# Get context from dependencies
|
|
220
|
+
context = get_layer_context(layers, module_name, max_depth=1)
|
|
221
|
+
if len(context) > 1:
|
|
222
|
+
parts.append(f"\nDirect dependencies ({len(context) - 1}):")
|
|
223
|
+
for name, summary in context.items():
|
|
224
|
+
if name != module_name:
|
|
225
|
+
parts.append(f" {name}: {summary[:100]}")
|
|
226
|
+
|
|
227
|
+
return ReasoningResult(
|
|
228
|
+
question=f"what does {module_name} do",
|
|
229
|
+
answer="\n".join(parts),
|
|
230
|
+
layers=[module_name] + layer.depends_on,
|
|
231
|
+
files=layer.files,
|
|
232
|
+
details={
|
|
233
|
+
"api": layer.public_api,
|
|
234
|
+
"depends": layer.depends_on,
|
|
235
|
+
"dependents": layer.dependents,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _answer_what_imports(root: Path, layers: dict[str, Layer], module_name: str) -> ReasoningResult:
|
|
241
|
+
"""Answer 'what imports X?' by tracing dependency graph."""
|
|
242
|
+
# Find all layers that depend on the target
|
|
243
|
+
importers: list[str] = []
|
|
244
|
+
for name, layer in layers.items():
|
|
245
|
+
if module_name in layer.depends_on:
|
|
246
|
+
importers.append(name)
|
|
247
|
+
|
|
248
|
+
if not importers:
|
|
249
|
+
return ReasoningResult(
|
|
250
|
+
question=f"what imports {module_name}",
|
|
251
|
+
answer=f"No layers import '{module_name}'.",
|
|
252
|
+
layers=[module_name] if module_name in layers else [],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
parts = [f"Layers that import '{module_name}':"]
|
|
256
|
+
for imp in importers:
|
|
257
|
+
layer = layers.get(imp)
|
|
258
|
+
summary = layer.summary[:80] if layer else "unknown"
|
|
259
|
+
parts.append(f" {imp}: {summary}")
|
|
260
|
+
|
|
261
|
+
# Check for circular dependencies
|
|
262
|
+
if module_name in layers and module_name in layers[module_name].dependents:
|
|
263
|
+
parts.append(f"\nCircular dependency detected: {module_name} imports itself!")
|
|
264
|
+
|
|
265
|
+
return ReasoningResult(
|
|
266
|
+
question=f"what imports {module_name}",
|
|
267
|
+
answer="\n".join(parts),
|
|
268
|
+
layers=importers + [module_name],
|
|
269
|
+
details={"importers": importers},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _answer_hotspots(root: Path, layers: dict[str, Layer]) -> ReasoningResult:
|
|
274
|
+
"""Answer 'what are the security hotspots?' by analyzing layer properties."""
|
|
275
|
+
hotspots: list[dict] = []
|
|
276
|
+
|
|
277
|
+
for name, layer in layers.items():
|
|
278
|
+
if name.startswith("__"):
|
|
279
|
+
continue
|
|
280
|
+
risk_score = 0
|
|
281
|
+
reasons: list[str] = []
|
|
282
|
+
|
|
283
|
+
# High dependency count = more attack surface
|
|
284
|
+
if len(layer.dependents) > 5:
|
|
285
|
+
risk_score += 2
|
|
286
|
+
reasons.append(f"imported by {len(layer.dependents)} modules (high attack surface)")
|
|
287
|
+
|
|
288
|
+
# Auth/security layers are critical
|
|
289
|
+
security_keywords = ["auth", "security", "session", "crypto"]
|
|
290
|
+
if any(kw in name.lower() for kw in security_keywords):
|
|
291
|
+
risk_score += 3
|
|
292
|
+
reasons.append("security-critical layer")
|
|
293
|
+
|
|
294
|
+
# Large layers with many files are harder to audit
|
|
295
|
+
if len(layer.files) > 20:
|
|
296
|
+
risk_score += 1
|
|
297
|
+
reasons.append(f"{len(layer.files)} files (large layer)")
|
|
298
|
+
|
|
299
|
+
# API layers accept external input
|
|
300
|
+
api_keywords = ["api", "route", "endpoint", "handler", "view"]
|
|
301
|
+
if any(kw in name.lower() for kw in api_keywords):
|
|
302
|
+
risk_score += 2
|
|
303
|
+
reasons.append("accepts external input")
|
|
304
|
+
|
|
305
|
+
if risk_score > 0:
|
|
306
|
+
hotspots.append({
|
|
307
|
+
"name": name,
|
|
308
|
+
"risk_score": risk_score,
|
|
309
|
+
"reasons": reasons,
|
|
310
|
+
"summary": layer.summary[:100],
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
hotspots.sort(key=lambda h: -h["risk_score"])
|
|
314
|
+
|
|
315
|
+
parts = ["Security hotspots (sorted by risk):"]
|
|
316
|
+
for h in hotspots[:10]:
|
|
317
|
+
parts.append(f" [{h['risk_score']}] {h['name']}: {', '.join(h['reasons'])}")
|
|
318
|
+
|
|
319
|
+
return ReasoningResult(
|
|
320
|
+
question="security hotspots",
|
|
321
|
+
answer="\n".join(parts),
|
|
322
|
+
layers=[h["name"] for h in hotspots],
|
|
323
|
+
details={"hotspots": hotspots},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _answer_layers(root: Path, layers: dict[str, Layer]) -> ReasoningResult:
|
|
328
|
+
"""Answer 'what layers exist?' by listing all layers."""
|
|
329
|
+
parts = [f"Layered Brain contains {len(layers)} layers:"]
|
|
330
|
+
for name, layer in sorted(layers.items()):
|
|
331
|
+
if name.startswith("__"):
|
|
332
|
+
continue
|
|
333
|
+
parts.append(f" [{layer.level}] {name}: {layer.summary[:80]}")
|
|
334
|
+
|
|
335
|
+
return ReasoningResult(
|
|
336
|
+
question="layers",
|
|
337
|
+
answer="\n".join(parts),
|
|
338
|
+
layers=list(layers.keys()),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _answer_general(root: Path, layers: dict[str, Layer], question: str) -> ReasoningResult:
|
|
343
|
+
"""Generic answer — list all layers with summaries."""
|
|
344
|
+
project = layers.get("__project__")
|
|
345
|
+
parts = ["Project overview:"]
|
|
346
|
+
if project:
|
|
347
|
+
parts.append(f" {project.summary}")
|
|
348
|
+
|
|
349
|
+
parts.append(f"\nLayers ({len(layers)}):")
|
|
350
|
+
for name, layer in sorted(layers.items()):
|
|
351
|
+
if name.startswith("__"):
|
|
352
|
+
continue
|
|
353
|
+
parts.append(f" [{layer.level}] {name}: {layer.summary[:80]}")
|
|
354
|
+
|
|
355
|
+
return ReasoningResult(
|
|
356
|
+
question=question,
|
|
357
|
+
answer="\n".join(parts),
|
|
358
|
+
layers=list(layers.keys()),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ── Main entry point ─────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def answer_question(question: str, root: Path | None = None) -> ReasoningResult:
|
|
366
|
+
"""Answer a question about the codebase using the layered brain."""
|
|
367
|
+
r = root or Path.cwd()
|
|
368
|
+
layers = _load_layers(r)
|
|
369
|
+
|
|
370
|
+
if not layers:
|
|
371
|
+
return ReasoningResult(
|
|
372
|
+
question=question,
|
|
373
|
+
answer="No layered brain found. Run 'p scan' first to build the layer cache.",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
category = classify_question(question)
|
|
377
|
+
|
|
378
|
+
if category == "what_changed":
|
|
379
|
+
return _answer_what_changed(r, layers)
|
|
380
|
+
|
|
381
|
+
if category == "what_does":
|
|
382
|
+
# Extract module name from question
|
|
383
|
+
q = question.lower()
|
|
384
|
+
module_name = ""
|
|
385
|
+
# Try to find a quoted word
|
|
386
|
+
if "'" in question:
|
|
387
|
+
parts = question.split("'")
|
|
388
|
+
if len(parts) >= 2:
|
|
389
|
+
module_name = parts[1]
|
|
390
|
+
elif '"' in question:
|
|
391
|
+
parts = question.split('"')
|
|
392
|
+
if len(parts) >= 2:
|
|
393
|
+
module_name = parts[1]
|
|
394
|
+
else:
|
|
395
|
+
# Try to find word after "does" or "is"
|
|
396
|
+
words = q.split()
|
|
397
|
+
for i, word in enumerate(words):
|
|
398
|
+
if word in ("does", "is", "are") and i + 1 < len(words):
|
|
399
|
+
next_word = words[i + 1]
|
|
400
|
+
if next_word not in ("the", "a", "an"):
|
|
401
|
+
module_name = next_word
|
|
402
|
+
break
|
|
403
|
+
return _answer_what_does(r, layers, module_name)
|
|
404
|
+
|
|
405
|
+
if category in ("what_imports", "imports"):
|
|
406
|
+
# Extract module name from question
|
|
407
|
+
q = question.lower()
|
|
408
|
+
module_name = ""
|
|
409
|
+
words = q.split()
|
|
410
|
+
for i, word in enumerate(words):
|
|
411
|
+
if word in ("imports", "import", "uses", "use", "depends", "depend", "on") and i + 1 < len(words):
|
|
412
|
+
next_word = words[i + 1]
|
|
413
|
+
if next_word not in ("the", "a", "an"):
|
|
414
|
+
module_name = next_word
|
|
415
|
+
break
|
|
416
|
+
return _answer_what_imports(r, layers, module_name)
|
|
417
|
+
|
|
418
|
+
if category == "hotspots":
|
|
419
|
+
return _answer_hotspots(r, layers)
|
|
420
|
+
|
|
421
|
+
if category == "routes":
|
|
422
|
+
# List API layers
|
|
423
|
+
api_layers = {
|
|
424
|
+
name: layer
|
|
425
|
+
for name, layer in layers.items()
|
|
426
|
+
if any(kw in name.lower() for kw in ["api", "route", "endpoint", "handler", "view"])
|
|
427
|
+
}
|
|
428
|
+
if api_layers:
|
|
429
|
+
parts = ["API/Route layers:"]
|
|
430
|
+
for name, layer in api_layers.items():
|
|
431
|
+
parts.append(f" {name}: {layer.summary[:80]}")
|
|
432
|
+
return ReasoningResult(
|
|
433
|
+
question="routes",
|
|
434
|
+
answer="\n".join(parts),
|
|
435
|
+
layers=list(api_layers.keys()),
|
|
436
|
+
)
|
|
437
|
+
return _answer_general(r, layers, question)
|
|
438
|
+
|
|
439
|
+
if category == "layers":
|
|
440
|
+
return _answer_layers(r, layers)
|
|
441
|
+
|
|
442
|
+
return _answer_general(r, layers, question)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Red Team Agent — adversarial testing and attack simulation.
|
|
3
|
+
|
|
4
|
+
Generates adversarial test cases and simulates attacker workflows.
|
|
5
|
+
Reports findings with exploit scenarios and severity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from patchi.core.agents.base import (
|
|
13
|
+
AgentDomain,
|
|
14
|
+
AgentGroup,
|
|
15
|
+
AgentInput,
|
|
16
|
+
AgentResult,
|
|
17
|
+
BaseAgent,
|
|
18
|
+
Finding,
|
|
19
|
+
Severity,
|
|
20
|
+
register,
|
|
21
|
+
safe_rglob,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@register
|
|
26
|
+
class RedTeamAgent(BaseAgent):
|
|
27
|
+
"""Adversarial testing: attack trees, exploit scenarios, fuzzing targets."""
|
|
28
|
+
|
|
29
|
+
name = "RedTeamAgent"
|
|
30
|
+
group = AgentGroup.SECURITY
|
|
31
|
+
domain = AgentDomain.SECURITY
|
|
32
|
+
timeout = 120
|
|
33
|
+
|
|
34
|
+
# Static-analysis hits mapped to the attack-scenario library
|
|
35
|
+
# (patchi/core/security/attack_scenarios/) and fix playbooks, so findings
|
|
36
|
+
# from this agent feed the Red Team Engine / AutoFixer pipeline instead of
|
|
37
|
+
# being dead-end pattern reports.
|
|
38
|
+
_PATTERN_LINKS: dict[str, tuple[str, str]] = {
|
|
39
|
+
# finding-message substring -> (scenario_family, remediation_playbook)
|
|
40
|
+
"eval(": ("injection-command", "fix-command-injection-no-eval"),
|
|
41
|
+
"exec(": ("injection-command", "fix-command-injection-no-exec"),
|
|
42
|
+
"os.system": ("injection-command", "fix-command-injection-shell-false"),
|
|
43
|
+
"subprocess.call": ("injection-command", "fix-command-injection-shell-false"),
|
|
44
|
+
"pickle deserialization": ("deserialization", "fix-deserialization-safe-load"),
|
|
45
|
+
"marshal deserialization": ("deserialization", "fix-deserialization-safe-load"),
|
|
46
|
+
"yaml.load": ("deserialization", "fix-deserialization-safe-load"),
|
|
47
|
+
"template rendering": ("xss-template", "fix-xss-template-autoescape"),
|
|
48
|
+
"debug mode enabled": ("security-headers", "fix-misconfig-debug-off"),
|
|
49
|
+
"allowed_hosts": ("security-headers", "fix-misconfig-host-allowlist"),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def _link_for(self, message: str) -> tuple[str, str]:
|
|
53
|
+
for needle, link in self._PATTERN_LINKS.items():
|
|
54
|
+
if needle.lower() in message.lower():
|
|
55
|
+
return link
|
|
56
|
+
return ("", "")
|
|
57
|
+
|
|
58
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
59
|
+
routes = inp.brain.get("routes", [])
|
|
60
|
+
files_scanned = 0
|
|
61
|
+
|
|
62
|
+
# Analyze routes for attack surface
|
|
63
|
+
for route_info in routes:
|
|
64
|
+
route = (
|
|
65
|
+
route_info.get("path", "")
|
|
66
|
+
if isinstance(route_info, dict)
|
|
67
|
+
else getattr(route_info, "path", "")
|
|
68
|
+
)
|
|
69
|
+
method = (
|
|
70
|
+
route_info.get("method", "get")
|
|
71
|
+
if isinstance(route_info, dict)
|
|
72
|
+
else getattr(route_info, "method", "get")
|
|
73
|
+
).lower()
|
|
74
|
+
|
|
75
|
+
# Check for parameterized routes (injection surface)
|
|
76
|
+
if re.search(r"\{[^}]+\}|:<\w+>|\d+", route):
|
|
77
|
+
result.add_finding(
|
|
78
|
+
Finding(
|
|
79
|
+
agent=self.name,
|
|
80
|
+
type="attack_surface",
|
|
81
|
+
severity=Severity.MEDIUM,
|
|
82
|
+
file="",
|
|
83
|
+
line=0,
|
|
84
|
+
message=f"Parameterized route: {method.upper()} {route}",
|
|
85
|
+
detail="Route accepts dynamic parameters — verify input validation",
|
|
86
|
+
suggestion="Validate/parameterize inputs; see sqli-basic-union scenario",
|
|
87
|
+
cwe="CWE-20",
|
|
88
|
+
extra={
|
|
89
|
+
"attack_surface": "static",
|
|
90
|
+
"scenario_family": "injection",
|
|
91
|
+
"remediation_playbook": "fix-sqli-parameterized-queries",
|
|
92
|
+
"route": f"{method.upper()} {route}",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# State-changing without apparent auth
|
|
98
|
+
if method in ("post", "put", "patch", "delete"):
|
|
99
|
+
result.add_finding(
|
|
100
|
+
Finding(
|
|
101
|
+
agent=self.name,
|
|
102
|
+
type="attack_surface",
|
|
103
|
+
severity=Severity.HIGH,
|
|
104
|
+
file="",
|
|
105
|
+
line=0,
|
|
106
|
+
message=f"State-changing endpoint: {method.upper()} {route}",
|
|
107
|
+
detail="Verify authentication and authorization are enforced",
|
|
108
|
+
suggestion="Enforce auth decorators + object-level authz checks",
|
|
109
|
+
cwe="CWE-306",
|
|
110
|
+
extra={
|
|
111
|
+
"attack_surface": "static",
|
|
112
|
+
"scenario_family": "authorization",
|
|
113
|
+
"remediation_playbook": "fix-authz-role-checks",
|
|
114
|
+
"route": f"{method.upper()} {route}",
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Scan source for attack patterns
|
|
120
|
+
source_patterns = ["*.py", "*.js", "*.ts", "*.jsx", "*.tsx", "*.go", "*.java"]
|
|
121
|
+
attack_patterns = [
|
|
122
|
+
(r"eval\s*\(", Severity.CRITICAL, "eval() usage — potential code injection"),
|
|
123
|
+
(r"exec\s*\(", Severity.CRITICAL, "exec() usage — potential code injection"),
|
|
124
|
+
(r"os\.system\s*\(", Severity.CRITICAL, "os.system() — command injection risk"),
|
|
125
|
+
(
|
|
126
|
+
r"subprocess\.call\s*\(",
|
|
127
|
+
Severity.HIGH,
|
|
128
|
+
"subprocess.call — verify input sanitization",
|
|
129
|
+
),
|
|
130
|
+
(r"pickle\.loads?\s*\(", Severity.CRITICAL, "pickle deserialization — RCE risk"),
|
|
131
|
+
(r"marshal\.loads?\s*\(", Severity.CRITICAL, "marshal deserialization — RCE risk"),
|
|
132
|
+
(
|
|
133
|
+
r"yaml\.load\s*\([^)]*\)",
|
|
134
|
+
Severity.HIGH,
|
|
135
|
+
"yaml.load without SafeLoader — code execution risk",
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
r"template\.render\s*\(",
|
|
139
|
+
Severity.MEDIUM,
|
|
140
|
+
"Template rendering — verify XSS protection",
|
|
141
|
+
),
|
|
142
|
+
(r"DEBUG\s*=\s*True", Severity.HIGH, "Debug mode enabled — information disclosure"),
|
|
143
|
+
(r'ALLOWED_HOSTS\s*=\s*\["?\*"?\]', Severity.HIGH, "ALLOWED_HOSTS set to wildcard"),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
for pattern in source_patterns:
|
|
147
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
148
|
+
if not fpath.is_file():
|
|
149
|
+
continue
|
|
150
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
151
|
+
try:
|
|
152
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
153
|
+
except OSError:
|
|
154
|
+
continue
|
|
155
|
+
files_scanned += 1
|
|
156
|
+
|
|
157
|
+
lines = content.splitlines()
|
|
158
|
+
for i, line in enumerate(lines, 1):
|
|
159
|
+
for rx, sev, msg in attack_patterns:
|
|
160
|
+
if re.search(rx, line):
|
|
161
|
+
family, playbook = self._link_for(msg)
|
|
162
|
+
result.add_finding(
|
|
163
|
+
Finding(
|
|
164
|
+
agent=self.name,
|
|
165
|
+
type="vulnerable_pattern",
|
|
166
|
+
severity=sev,
|
|
167
|
+
file=rel,
|
|
168
|
+
line=i,
|
|
169
|
+
message=msg,
|
|
170
|
+
code_snippet=line.strip()[:120],
|
|
171
|
+
suggestion=(
|
|
172
|
+
f"Remediate via playbook: {playbook}"
|
|
173
|
+
if playbook
|
|
174
|
+
else "Review for safe usage or remove"
|
|
175
|
+
),
|
|
176
|
+
extra={
|
|
177
|
+
"attack_surface": "static",
|
|
178
|
+
"scenario_family": family,
|
|
179
|
+
"remediation_playbook": playbook,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
result.files_scanned = files_scanned
|