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,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security Test Agent — generates security-focused tests per route/endpoint.
|
|
3
|
+
|
|
4
|
+
For each route found by RouteMapScanner, generates:
|
|
5
|
+
- Authentication bypass tests
|
|
6
|
+
- SQL injection payload tests
|
|
7
|
+
- XSS payload injection tests
|
|
8
|
+
- CSRF token validation tests
|
|
9
|
+
- Rate limiting tests
|
|
10
|
+
- Header validation tests
|
|
11
|
+
- CORS origin tests
|
|
12
|
+
|
|
13
|
+
Outputs pytest files in tests/security/.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from patchi.core.agents.base import (
|
|
22
|
+
AgentDomain,
|
|
23
|
+
AgentGroup,
|
|
24
|
+
AgentInput,
|
|
25
|
+
AgentResult,
|
|
26
|
+
BaseAgent,
|
|
27
|
+
Finding,
|
|
28
|
+
Severity,
|
|
29
|
+
register,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ── Test templates ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
_AUTH_BYPASS_TEMPLATE = '''\
|
|
35
|
+
def test_{func}_requires_auth(client):
|
|
36
|
+
"""{route} should reject unauthenticated requests."""
|
|
37
|
+
resp = client.{method}("{route}")
|
|
38
|
+
assert resp.status_code in (401, 403), \
|
|
39
|
+
f"Expected 401/403, got {{resp.status_code}}"
|
|
40
|
+
'''
|
|
41
|
+
|
|
42
|
+
_SQLI_TEMPLATE = '''\
|
|
43
|
+
def test_{func}_sqli_protection(client):
|
|
44
|
+
"""{route} should reject SQL injection payloads."""
|
|
45
|
+
payloads = ["' OR '1'='1", "1; DROP TABLE--", "' UNION SELECT NULL--"]
|
|
46
|
+
for payload in payloads:
|
|
47
|
+
resp = client.{method}("{route}", json={{"input": payload}})
|
|
48
|
+
assert resp.status_code != 500, \
|
|
49
|
+
f"Server error on SQLi payload: {{payload}}"
|
|
50
|
+
'''
|
|
51
|
+
|
|
52
|
+
_XSS_TEMPLATE = '''\
|
|
53
|
+
def test_{func}_xss_protection(client):
|
|
54
|
+
"""{route} should sanitize or escape XSS payloads."""
|
|
55
|
+
payloads = ["<script>alert(1)</script>", "<img onerror=alert(1)>"]
|
|
56
|
+
for payload in payloads:
|
|
57
|
+
resp = client.{method}("{route}", json={{"input": payload}})
|
|
58
|
+
if resp.status_code == 200:
|
|
59
|
+
assert payload not in resp.text, \
|
|
60
|
+
f"XSS payload reflected in response: {{payload}}"
|
|
61
|
+
'''
|
|
62
|
+
|
|
63
|
+
_CSRF_TEMPLATE = '''\
|
|
64
|
+
def test_{func}_csrf_protection(client):
|
|
65
|
+
"""{route} should validate CSRF tokens for state-changing operations."""
|
|
66
|
+
resp = client.{method}("{route}", headers={{"X-Requested-With": ""}})
|
|
67
|
+
assert resp.status_code in (400, 403), \
|
|
68
|
+
f"Expected CSRF rejection, got {{resp.status_code}}"
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
_HEADER_TEMPLATE = '''\
|
|
72
|
+
def test_{func}_security_headers(client):
|
|
73
|
+
"""{route} should include security headers."""
|
|
74
|
+
resp = client.get("{route}")
|
|
75
|
+
headers = {h.lower() for h in resp.headers}
|
|
76
|
+
# At minimum, these should be present on sensitive endpoints
|
|
77
|
+
recommended = ["x-content-type-options", "x-frame-options"]
|
|
78
|
+
for h in recommended:
|
|
79
|
+
# Only warn, don't fail — not all routes need all headers
|
|
80
|
+
pass
|
|
81
|
+
'''
|
|
82
|
+
|
|
83
|
+
_RATE_LIMIT_TEMPLATE = '''\
|
|
84
|
+
def test_{func}_rate_limiting(client):
|
|
85
|
+
"""{route} should enforce rate limiting on repeated requests."""
|
|
86
|
+
responses = [client.{method}("{route}") for _ in range(50)]
|
|
87
|
+
status_codes = [r.status_code for r in responses]
|
|
88
|
+
# At least some requests should be throttled (429) or the endpoint
|
|
89
|
+
# should handle rapid requests gracefully (no 500s)
|
|
90
|
+
assert 500 not in status_codes, "Server crashed under rapid requests"
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
_CORS_TEMPLATE = '''\
|
|
94
|
+
def test_{func}_cors_restrictions(client):
|
|
95
|
+
"""{route} should not allow arbitrary origins."""
|
|
96
|
+
resp = client.options("{route}", headers={{{{ \
|
|
97
|
+
"Origin": "https://evil.com", \
|
|
98
|
+
"Access-Control-Request-Method": "{method_upper}", \
|
|
99
|
+
}}}})
|
|
100
|
+
acao = resp.headers.get("access-control-allow-origin", "")
|
|
101
|
+
assert acao != "*", \
|
|
102
|
+
f"CORS allows all origins on {route}"
|
|
103
|
+
'''
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Agent ─────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@register
|
|
110
|
+
class SecurityTestAgent(BaseAgent):
|
|
111
|
+
"""Generates security-focused pytest tests for detected routes."""
|
|
112
|
+
|
|
113
|
+
name = "SecurityTestAgent"
|
|
114
|
+
group = AgentGroup.TEST
|
|
115
|
+
domain = AgentDomain.TESTING
|
|
116
|
+
timeout = 120
|
|
117
|
+
|
|
118
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
119
|
+
routes = inp.brain.get("routes", [])
|
|
120
|
+
if not routes:
|
|
121
|
+
result.add_finding(
|
|
122
|
+
Finding(
|
|
123
|
+
agent=self.name,
|
|
124
|
+
type="info",
|
|
125
|
+
severity=Severity.INFO,
|
|
126
|
+
file="",
|
|
127
|
+
message="No routes detected — security tests require a web app with routes",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
test_dir = inp.root / "tests" / "security"
|
|
133
|
+
test_files: list[Path] = []
|
|
134
|
+
|
|
135
|
+
framework = self._detect_framework(inp.brain)
|
|
136
|
+
for route_info in routes:
|
|
137
|
+
route = (
|
|
138
|
+
route_info.get("path", "")
|
|
139
|
+
if isinstance(route_info, dict)
|
|
140
|
+
else getattr(route_info, "path", "")
|
|
141
|
+
)
|
|
142
|
+
method = (
|
|
143
|
+
route_info.get("method", "get")
|
|
144
|
+
if isinstance(route_info, dict)
|
|
145
|
+
else getattr(route_info, "method", "get")
|
|
146
|
+
).lower()
|
|
147
|
+
func_name = self._route_to_func(route, method)
|
|
148
|
+
|
|
149
|
+
if not func_name:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
tests = self._generate_tests(route, method, func_name)
|
|
153
|
+
if tests:
|
|
154
|
+
test_file = test_dir / f"test_security_{func_name}.py"
|
|
155
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
header = self._build_header(framework, route, method)
|
|
157
|
+
test_file.write_text(header + tests, encoding="utf-8")
|
|
158
|
+
test_files.append(test_file)
|
|
159
|
+
result.files_scanned += 1
|
|
160
|
+
|
|
161
|
+
if test_files:
|
|
162
|
+
result.data["generated_files"] = [str(f.relative_to(inp.root)) for f in test_files]
|
|
163
|
+
result.data["test_count"] = len(test_files)
|
|
164
|
+
|
|
165
|
+
def _route_to_func(self, route: str, method: str) -> str:
|
|
166
|
+
"""Convert /api/users/{id} -> api_users_id."""
|
|
167
|
+
clean = re.sub(r"[^a-zA-Z0-9/]", "", route)
|
|
168
|
+
parts = [p for p in clean.split("/") if p]
|
|
169
|
+
name = "_".join(parts[:4]) # limit depth
|
|
170
|
+
return f"{method}_{name}" if name else ""
|
|
171
|
+
|
|
172
|
+
def _detect_framework(self, brain: dict) -> str:
|
|
173
|
+
fw = brain.get("framework", "")
|
|
174
|
+
fw_raw = brain.get("frameworks", fw)
|
|
175
|
+
if isinstance(fw_raw, list):
|
|
176
|
+
fw = fw_raw[0] if fw_raw else ""
|
|
177
|
+
else:
|
|
178
|
+
fw = fw_raw or ""
|
|
179
|
+
fw_lower = fw.lower()
|
|
180
|
+
if "fastapi" in fw_lower or "starlette" in fw_lower:
|
|
181
|
+
return "fastapi"
|
|
182
|
+
if "flask" in fw_lower:
|
|
183
|
+
return "flask"
|
|
184
|
+
if "django" in fw_lower:
|
|
185
|
+
return "django"
|
|
186
|
+
if "express" in fw_lower or "node" in fw_lower:
|
|
187
|
+
return "express"
|
|
188
|
+
return "generic"
|
|
189
|
+
|
|
190
|
+
def _build_header(self, framework: str, route: str, method: str) -> str:
|
|
191
|
+
fixtures = {
|
|
192
|
+
"fastapi": (
|
|
193
|
+
"@pytest.fixture\ndef client():\n"
|
|
194
|
+
" from fastapi.testclient import TestClient\n"
|
|
195
|
+
" from app import app\n"
|
|
196
|
+
" return TestClient(app)\n"
|
|
197
|
+
),
|
|
198
|
+
"flask": (
|
|
199
|
+
"@pytest.fixture\ndef client():\n"
|
|
200
|
+
" from flask import Flask\n"
|
|
201
|
+
" app = Flask(__name__)\n"
|
|
202
|
+
" return app.test_client()\n"
|
|
203
|
+
),
|
|
204
|
+
"django": (
|
|
205
|
+
"@pytest.fixture\ndef client():\n"
|
|
206
|
+
" from django.test import Client\n"
|
|
207
|
+
" return Client()\n"
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
fixture = fixtures.get(
|
|
211
|
+
framework,
|
|
212
|
+
(
|
|
213
|
+
"@pytest.fixture\ndef client():\n"
|
|
214
|
+
' """Override with your app\'s test client."""\n'
|
|
215
|
+
" import pytest\n"
|
|
216
|
+
' raise NotImplementedError("Define a client fixture for your framework")\n'
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
return (
|
|
220
|
+
'"""Auto-generated security tests by Patchi SecurityTestAgent."""\n'
|
|
221
|
+
"import pytest\n\n\n"
|
|
222
|
+
f"{fixture}\n\n"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _generate_tests(self, route: str, method: str, func: str) -> str:
|
|
226
|
+
"""Generate test code for a single route."""
|
|
227
|
+
tests = []
|
|
228
|
+
templates = [
|
|
229
|
+
("auth", _AUTH_BYPASS_TEMPLATE),
|
|
230
|
+
("sqli", _SQLI_TEMPLATE),
|
|
231
|
+
("xss", _XSS_TEMPLATE),
|
|
232
|
+
]
|
|
233
|
+
# Only add CSRF for state-changing methods
|
|
234
|
+
if method in ("post", "put", "patch", "delete"):
|
|
235
|
+
templates.append(("csrf", _CSRF_TEMPLATE))
|
|
236
|
+
|
|
237
|
+
# CORS for all
|
|
238
|
+
templates.append(("cors", _CORS_TEMPLATE))
|
|
239
|
+
|
|
240
|
+
# Rate limit for all
|
|
241
|
+
templates.append(("rate_limit", _RATE_LIMIT_TEMPLATE))
|
|
242
|
+
|
|
243
|
+
for _name, tpl in templates:
|
|
244
|
+
tests.append(
|
|
245
|
+
tpl.format(
|
|
246
|
+
func=func,
|
|
247
|
+
route=route,
|
|
248
|
+
method=method,
|
|
249
|
+
method_upper=method.upper(),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return "\n".join(tests)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StressTestAgent — Load testing via StressOrchestrator.
|
|
3
|
+
|
|
4
|
+
Thin wrapper around StressOrchestrator that runs as a registered BaseAgent.
|
|
5
|
+
Supports load, spike, soak, and breakpoint testing scenarios.
|
|
6
|
+
|
|
7
|
+
Uses aiohttp for async HTTP requests (no external binary needed).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..agents.base import (
|
|
17
|
+
AgentDomain,
|
|
18
|
+
AgentGroup,
|
|
19
|
+
AgentInput,
|
|
20
|
+
AgentResult,
|
|
21
|
+
AgentStatus,
|
|
22
|
+
BaseAgent,
|
|
23
|
+
Severity,
|
|
24
|
+
make_finding,
|
|
25
|
+
register,
|
|
26
|
+
)
|
|
27
|
+
from ..testing.live_v2.stress_orchestrator import (
|
|
28
|
+
StressConfig,
|
|
29
|
+
StressOrchestrator,
|
|
30
|
+
StressTestReport,
|
|
31
|
+
run_stress_test,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger("patchi.testing.stress_test_agent")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@register
|
|
38
|
+
class StressTestAgent(BaseAgent):
|
|
39
|
+
"""Load/stress testing agent using StressOrchestrator."""
|
|
40
|
+
|
|
41
|
+
group = AgentGroup.TEST
|
|
42
|
+
domain = AgentDomain.TESTING
|
|
43
|
+
name = "StressTestAgent"
|
|
44
|
+
description = "Stress testing: load, spike, soak, breakpoint scenarios via async HTTP"
|
|
45
|
+
timeout = 600 # 10 minutes max for stress tests
|
|
46
|
+
|
|
47
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
48
|
+
target_url = self._detect_target(inp)
|
|
49
|
+
if not target_url:
|
|
50
|
+
self.skip(result, "No running web application detected. Start the web server first.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
_log.info("StressTestAgent: Testing %s with StressOrchestrator", target_url)
|
|
54
|
+
|
|
55
|
+
# Get scenario from config (default: load)
|
|
56
|
+
scenario = inp.config.get("stress_scenario", "load")
|
|
57
|
+
users = inp.config.get("stress_users", 10)
|
|
58
|
+
duration = inp.config.get("stress_duration", 60)
|
|
59
|
+
|
|
60
|
+
# Build endpoints from route map
|
|
61
|
+
route_map = inp.brain.get("route_map", {})
|
|
62
|
+
endpoints = []
|
|
63
|
+
for k, v in route_map.items():
|
|
64
|
+
if isinstance(v, dict):
|
|
65
|
+
path = v.get("path", k.split(" ", 1)[-1])
|
|
66
|
+
endpoints.append({"method": "GET", "path": path})
|
|
67
|
+
if not endpoints:
|
|
68
|
+
endpoints = [{"method": "GET", "path": "/"}]
|
|
69
|
+
|
|
70
|
+
# Run stress test
|
|
71
|
+
try:
|
|
72
|
+
report = asyncio.run(self._run_stress(target_url, scenario, users, duration, endpoints))
|
|
73
|
+
self._process_report(report, result)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
_log.error("StressTestAgent failed: %s", e)
|
|
76
|
+
result.add_error(f"Stress test failed: {e}")
|
|
77
|
+
result.status = AgentStatus.FAILED
|
|
78
|
+
|
|
79
|
+
async def _run_stress(
|
|
80
|
+
self,
|
|
81
|
+
target_url: str,
|
|
82
|
+
scenario: str,
|
|
83
|
+
users: int,
|
|
84
|
+
duration: int,
|
|
85
|
+
endpoints: list[dict],
|
|
86
|
+
) -> StressTestReport:
|
|
87
|
+
"""Run the stress test via StressOrchestrator."""
|
|
88
|
+
config = StressConfig(
|
|
89
|
+
base_url=target_url,
|
|
90
|
+
scenario=scenario,
|
|
91
|
+
users=users,
|
|
92
|
+
duration_seconds=duration,
|
|
93
|
+
endpoints=endpoints,
|
|
94
|
+
)
|
|
95
|
+
orchestrator = StressOrchestrator(config)
|
|
96
|
+
return await orchestrator.run()
|
|
97
|
+
|
|
98
|
+
def _process_report(self, report: StressTestReport, result: AgentResult) -> None:
|
|
99
|
+
"""Process StressTestReport into AgentResult findings."""
|
|
100
|
+
result.data["report"] = report.to_dict()
|
|
101
|
+
result.data["scenario"] = report.config.scenario
|
|
102
|
+
result.data["users"] = report.config.users
|
|
103
|
+
result.data["duration_seconds"] = report.config.duration_seconds
|
|
104
|
+
|
|
105
|
+
# Add latency findings
|
|
106
|
+
latency = report.latency
|
|
107
|
+
if latency:
|
|
108
|
+
p95 = latency.get("p95", 0)
|
|
109
|
+
p99 = latency.get("p99", 0)
|
|
110
|
+
|
|
111
|
+
if p95 > 2000:
|
|
112
|
+
result.add_finding(
|
|
113
|
+
make_finding(
|
|
114
|
+
agent=self.name,
|
|
115
|
+
finding_type="slow_p95",
|
|
116
|
+
severity=Severity.MEDIUM,
|
|
117
|
+
file="stress_test",
|
|
118
|
+
message=f"Slow p95 latency under load: {p95:.0f}ms",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
if p99 > 5000:
|
|
122
|
+
result.add_finding(
|
|
123
|
+
make_finding(
|
|
124
|
+
agent=self.name,
|
|
125
|
+
finding_type="slow_p99",
|
|
126
|
+
severity=Severity.HIGH,
|
|
127
|
+
file="stress_test",
|
|
128
|
+
message=f"Slow p99 latency under load: {p99:.0f}ms",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Add error rate findings
|
|
133
|
+
if report.total_requests > 0:
|
|
134
|
+
error_rate = report.failed_requests / report.total_requests
|
|
135
|
+
if error_rate > 0.05: # 5%
|
|
136
|
+
result.add_finding(
|
|
137
|
+
make_finding(
|
|
138
|
+
agent=self.name,
|
|
139
|
+
finding_type="high_error_rate",
|
|
140
|
+
severity=Severity.HIGH,
|
|
141
|
+
file="stress_test",
|
|
142
|
+
message=f"High load-test error rate: {error_rate:.1%}",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Add breakpoint findings
|
|
147
|
+
if report.breakpoint_found:
|
|
148
|
+
result.add_finding(
|
|
149
|
+
make_finding(
|
|
150
|
+
agent=self.name,
|
|
151
|
+
finding_type="breakpoint_found",
|
|
152
|
+
severity=Severity.HIGH,
|
|
153
|
+
file="stress_test",
|
|
154
|
+
message=f"Breakpoint found at {report.breakpoint_users} users",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Add soak stability findings
|
|
159
|
+
if not report.soak_stability:
|
|
160
|
+
result.add_finding(
|
|
161
|
+
make_finding(
|
|
162
|
+
agent=self.name,
|
|
163
|
+
finding_type="soak_instability",
|
|
164
|
+
severity=Severity.MEDIUM,
|
|
165
|
+
file="stress_test",
|
|
166
|
+
message="System instability detected during soak test",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
result.status = AgentStatus.DONE
|
|
171
|
+
result.files_scanned = 1
|
|
172
|
+
|
|
173
|
+
def _detect_target(self, inp: AgentInput) -> str | None:
|
|
174
|
+
"""Detect the target URL for testing."""
|
|
175
|
+
import socket
|
|
176
|
+
|
|
177
|
+
# Check for explicit base_url in config
|
|
178
|
+
if inp.config.get("base_url"):
|
|
179
|
+
return inp.config["base_url"]
|
|
180
|
+
|
|
181
|
+
# Try common ports
|
|
182
|
+
ports = [1612, 8000, 3000, 8080, 80]
|
|
183
|
+
|
|
184
|
+
for port in ports:
|
|
185
|
+
try:
|
|
186
|
+
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
187
|
+
return f"http://127.0.0.1:{port}"
|
|
188
|
+
except (ConnectionRefusedError, OSError):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Check config for web port
|
|
192
|
+
try:
|
|
193
|
+
from patchi.core import config as cfg
|
|
194
|
+
|
|
195
|
+
config = cfg.load(inp.root)
|
|
196
|
+
web_config = config.get("web", {})
|
|
197
|
+
host = web_config.get("host", "127.0.0.1")
|
|
198
|
+
port = web_config.get("port", 1612)
|
|
199
|
+
return f"http://{host}:{port}"
|
|
200
|
+
except Exception as _exc:
|
|
201
|
+
_log.warning("_detect_target failed: %s", _exc)
|
|
202
|
+
|
|
203
|
+
return None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test agent aggregation module for Patchi.
|
|
3
|
+
|
|
4
|
+
Exports all test agent classes and shared test helper types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..agents.base import register
|
|
10
|
+
from .app_discovery_agent import AppDiscoveryAgent # noqa: F401
|
|
11
|
+
from .api_contract_agent import APIContractAgent
|
|
12
|
+
from .browser_test_agent import BrowserTestAgent
|
|
13
|
+
from .e2e_flow_agent import E2EFlowAgent
|
|
14
|
+
from .flake_detector_agent import FlakeDetectorAgent # noqa: F401
|
|
15
|
+
from .regression_agent import RegressionAgent
|
|
16
|
+
from .security_test_agent import SecurityTestAgent
|
|
17
|
+
from .stress_test_agent import StressTestAgent
|
|
18
|
+
from .ui_accessibility_agent import UIAccessibilityAgent
|
|
19
|
+
from .ui_button_agent import UIButtonAgent
|
|
20
|
+
from .ui_layout_agent import UILayoutAgent
|
|
21
|
+
from .unit_test_agent import TestCase, TestSuite, UnitTestAgent, _run
|
|
22
|
+
from .visual_regression_agent import VisualRegressionAgent
|
|
23
|
+
|
|
24
|
+
# Live v2 runner registers its agent on import (additive — never breaks v1)
|
|
25
|
+
try:
|
|
26
|
+
from .live_v2.runner import LiveTestRunnerV2Agent # noqa: F401
|
|
27
|
+
except Exception as _e: # pragma: no cover — playwright-less envs still work
|
|
28
|
+
import logging as _logging
|
|
29
|
+
|
|
30
|
+
_logging.getLogger("patchi.testing").warning("LiveTestRunnerV2Agent unavailable: %s", _e)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AppDiscoveryAgent",
|
|
34
|
+
"APIContractAgent",
|
|
35
|
+
"BrowserTestAgent",
|
|
36
|
+
"E2EFlowAgent",
|
|
37
|
+
"FlakeDetectorAgent",
|
|
38
|
+
"RegressionAgent",
|
|
39
|
+
"SecurityTestAgent",
|
|
40
|
+
"StressTestAgent",
|
|
41
|
+
"TestCase",
|
|
42
|
+
"TestSuite",
|
|
43
|
+
"UIAccessibilityAgent",
|
|
44
|
+
"UIButtonAgent",
|
|
45
|
+
"UILayoutAgent",
|
|
46
|
+
"UnitTestAgent",
|
|
47
|
+
"VisualRegressionAgent",
|
|
48
|
+
"_run",
|
|
49
|
+
"register",
|
|
50
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test configuration for Patchi.
|
|
3
|
+
|
|
4
|
+
Reads/writes .patchi/tests.json — project-level test settings.
|
|
5
|
+
|
|
6
|
+
Config shape:
|
|
7
|
+
{
|
|
8
|
+
"base_url": "http://localhost:3000",
|
|
9
|
+
"viewport_widths": [375, 768, 1024, 1440],
|
|
10
|
+
"timeout_per_agent": 120,
|
|
11
|
+
"parallel": false,
|
|
12
|
+
"skip_agents": [],
|
|
13
|
+
"only_agents": [],
|
|
14
|
+
"ai_generate": true,
|
|
15
|
+
"screenshots_dir": ".patchi/screenshots",
|
|
16
|
+
"baselines_dir": ".patchi/baselines",
|
|
17
|
+
"custom_flows": [
|
|
18
|
+
{"name": "login", "url": "/login", "steps": ["fill email", "fill password", "click submit"]}
|
|
19
|
+
],
|
|
20
|
+
"exclude_paths": ["node_modules", ".venv", "dist"],
|
|
21
|
+
"report_format": "rich"
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
|
|
32
|
+
|
|
33
|
+
from ..constants import PATCHI_DIR
|
|
34
|
+
|
|
35
|
+
TESTS_CONFIG = f"{PATCHI_DIR}/tests.json"
|
|
36
|
+
|
|
37
|
+
DEFAULT_TEST_CONFIG = {
|
|
38
|
+
"base_url": None,
|
|
39
|
+
"viewport_widths": [375, 768, 1024, 1440],
|
|
40
|
+
"timeout_per_agent": 120,
|
|
41
|
+
"parallel": False,
|
|
42
|
+
"skip_agents": [],
|
|
43
|
+
"only_agents": [],
|
|
44
|
+
"ai_generate": True,
|
|
45
|
+
"screenshots_dir": f"{PATCHI_DIR}/screenshots",
|
|
46
|
+
"baselines_dir": f"{PATCHI_DIR}/baselines",
|
|
47
|
+
"custom_flows": [],
|
|
48
|
+
"exclude_paths": sorted(DEFAULT_IGNORE_DIRS),
|
|
49
|
+
"report_format": "rich",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_log = logging.getLogger("patchi.testing.test_config")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _config_path(root: Path) -> Path:
|
|
57
|
+
return root / TESTS_CONFIG
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load(root: Path | None = None) -> dict:
|
|
61
|
+
"""Load test config, merging with defaults."""
|
|
62
|
+
from ..config import require_project_root
|
|
63
|
+
|
|
64
|
+
root = root or require_project_root()
|
|
65
|
+
path = _config_path(root)
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return dict(DEFAULT_TEST_CONFIG)
|
|
68
|
+
try:
|
|
69
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
70
|
+
return {**DEFAULT_TEST_CONFIG, **raw}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
_log.warning("load failed: %s", e)
|
|
73
|
+
return dict(DEFAULT_TEST_CONFIG)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def save(config: dict, root: Path | None = None) -> None:
|
|
77
|
+
"""Write test config to disk."""
|
|
78
|
+
from ..config import require_project_root
|
|
79
|
+
|
|
80
|
+
root = root or require_project_root()
|
|
81
|
+
path = _config_path(root)
|
|
82
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get(key: str, root: Path | None = None) -> Any:
|
|
87
|
+
"""Get a single test config value."""
|
|
88
|
+
return load(root).get(key)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def set_value(key: str, value: Any, root: Path | None = None) -> None:
|
|
92
|
+
"""Update a single test config key."""
|
|
93
|
+
config = load(root)
|
|
94
|
+
config[key] = value
|
|
95
|
+
save(config, root)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def add_custom_flow(name: str, url: str, steps: list[str], root: Path | None = None) -> None:
|
|
99
|
+
"""Add a custom test flow."""
|
|
100
|
+
config = load(root)
|
|
101
|
+
flows = config.get("custom_flows", [])
|
|
102
|
+
flows.append({"name": name, "url": url, "steps": steps})
|
|
103
|
+
config["custom_flows"] = flows
|
|
104
|
+
save(config, root)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def remove_custom_flow(name: str, root: Path | None = None) -> bool:
|
|
108
|
+
"""Remove a custom test flow by name."""
|
|
109
|
+
config = load(root)
|
|
110
|
+
flows = config.get("custom_flows", [])
|
|
111
|
+
before = len(flows)
|
|
112
|
+
config["custom_flows"] = [f for f in flows if f.get("name") != name]
|
|
113
|
+
save(config, root)
|
|
114
|
+
return len(config["custom_flows"]) < before
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_test_history(root: Path | None = None) -> list[dict]:
|
|
118
|
+
"""Load test run history from memory."""
|
|
119
|
+
from .. import memory as mem
|
|
120
|
+
from ..config import require_project_root
|
|
121
|
+
|
|
122
|
+
root = root or require_project_root()
|
|
123
|
+
brain = mem.get_brain(root)
|
|
124
|
+
return brain.get("test_history", [])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def append_test_history(entry: dict, root: Path | None = None) -> None:
|
|
128
|
+
"""Append a test run entry to history."""
|
|
129
|
+
from .. import memory as mem
|
|
130
|
+
from ..config import require_project_root
|
|
131
|
+
|
|
132
|
+
root = root or require_project_root()
|
|
133
|
+
brain = mem.get_brain(root)
|
|
134
|
+
history = brain.get("test_history", [])
|
|
135
|
+
history.append(entry)
|
|
136
|
+
# Keep last 50 runs
|
|
137
|
+
if len(history) > 50:
|
|
138
|
+
history = history[-50:]
|
|
139
|
+
brain["test_history"] = history
|
|
140
|
+
mem.save_brain(brain, root)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_coverage(root: Path | None = None) -> dict:
|
|
144
|
+
"""Get cumulative test coverage data."""
|
|
145
|
+
from .. import memory as mem
|
|
146
|
+
from ..config import require_project_root
|
|
147
|
+
|
|
148
|
+
root = root or require_project_root()
|
|
149
|
+
brain = mem.get_brain(root)
|
|
150
|
+
return brain.get(
|
|
151
|
+
"test_coverage",
|
|
152
|
+
{
|
|
153
|
+
"files_tested": [],
|
|
154
|
+
"agents_run": {},
|
|
155
|
+
"total_runs": 0,
|
|
156
|
+
"last_run": None,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def update_coverage(run_result: dict, root: Path | None = None) -> None:
|
|
162
|
+
"""Update coverage data after a test run."""
|
|
163
|
+
from .. import memory as mem
|
|
164
|
+
from ..config import require_project_root
|
|
165
|
+
|
|
166
|
+
root = root or require_project_root()
|
|
167
|
+
brain = mem.get_brain(root)
|
|
168
|
+
coverage = brain.get(
|
|
169
|
+
"test_coverage",
|
|
170
|
+
{
|
|
171
|
+
"files_tested": [],
|
|
172
|
+
"agents_run": {},
|
|
173
|
+
"total_runs": 0,
|
|
174
|
+
"last_run": None,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
coverage["total_runs"] = coverage.get("total_runs", 0) + 1
|
|
179
|
+
coverage["last_run"] = run_result.get("timestamp")
|
|
180
|
+
|
|
181
|
+
# Track which agents have been run
|
|
182
|
+
for agent_name in run_result.get("agents", []):
|
|
183
|
+
counts = coverage.get("agents_run", {})
|
|
184
|
+
counts[agent_name] = counts.get(agent_name, 0) + 1
|
|
185
|
+
coverage["agents_run"] = counts
|
|
186
|
+
|
|
187
|
+
# Track files tested
|
|
188
|
+
tested_files = set(coverage.get("files_tested", []))
|
|
189
|
+
for f in run_result.get("files_tested", []):
|
|
190
|
+
tested_files.add(f)
|
|
191
|
+
coverage["files_tested"] = sorted(tested_files)
|
|
192
|
+
|
|
193
|
+
brain["test_coverage"] = coverage
|
|
194
|
+
mem.save_brain(brain, root)
|