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,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invariant engine — what should be impossible, checked statically.
|
|
3
|
+
|
|
4
|
+
Each Invariant pairs a property statement with a static verifier that runs
|
|
5
|
+
over project data (routes, findings, config). Verification produces Evidence;
|
|
6
|
+
the AssuranceGraph decides the verdict.
|
|
7
|
+
|
|
8
|
+
InvariantType mirrors UNIFIED_UPGRADE_PLAN.md's semantics:
|
|
9
|
+
MUST — property must hold unconditionally
|
|
10
|
+
MUST_NOT — named condition must never occur
|
|
11
|
+
ONLY_IF — action permitted only under stated precondition
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
_log = logging.getLogger("patchi.core.assurance.invariants")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvariantType(StrEnum):
|
|
26
|
+
MUST = "must"
|
|
27
|
+
MUST_NOT = "must_not"
|
|
28
|
+
ONLY_IF = "only_if"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Invariant:
|
|
33
|
+
"""One provable property about the project."""
|
|
34
|
+
|
|
35
|
+
id: str # stable slug
|
|
36
|
+
invariant_type: InvariantType
|
|
37
|
+
statement: str # human-readable property
|
|
38
|
+
domain: str # owning domain tag
|
|
39
|
+
severity_if_disproved: str = "high"
|
|
40
|
+
|
|
41
|
+
# verifier(project_data) -> (supports: bool, detail: str, artifact: dict)
|
|
42
|
+
# project_data keys available: routes(IntentReport|None), findings(list[dict]),
|
|
43
|
+
# config(dict), files(set[str])
|
|
44
|
+
verifier: Callable[[dict], tuple[bool, str, dict]] | None = None
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"id": self.id,
|
|
49
|
+
"type": self.invariant_type.value,
|
|
50
|
+
"statement": self.statement,
|
|
51
|
+
"domain": self.domain,
|
|
52
|
+
"severity_if_disproved": self.severity_if_disproved,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def verify_invariants(
|
|
57
|
+
invariants: list[Invariant],
|
|
58
|
+
project_data: dict[str, Any],
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Run every invariant's verifier against project_data.
|
|
62
|
+
|
|
63
|
+
Returns list of (invariant, supports, detail, artifact) tuples.
|
|
64
|
+
Invariants without a verifier yield NOT_PROVED evidence (never silently
|
|
65
|
+
proved — honesty rule).
|
|
66
|
+
"""
|
|
67
|
+
results = []
|
|
68
|
+
for inv in invariants:
|
|
69
|
+
if inv.verifier is None:
|
|
70
|
+
results.append((inv, False, "no verifier registered for this invariant", {}))
|
|
71
|
+
continue
|
|
72
|
+
try:
|
|
73
|
+
supports, detail, artifact = inv.verifier(project_data)
|
|
74
|
+
results.append((inv, bool(supports), detail, artifact))
|
|
75
|
+
except Exception as e: # noqa: BLE001 — a broken check is NOT_PROVED
|
|
76
|
+
_log.warning("invariant %s verifier failed: %s", inv.id, e)
|
|
77
|
+
results.append((inv, False, f"verifier error: {e}", {}))
|
|
78
|
+
return results
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Built-in invariant library ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check_all_writes_authenticated(data: dict) -> tuple[bool, str, dict]:
|
|
85
|
+
"""Every state-changing route must carry an auth guard."""
|
|
86
|
+
report = data.get("routes")
|
|
87
|
+
if report is None or not getattr(report, "routes", None):
|
|
88
|
+
return False, "no route data available", {}
|
|
89
|
+
bad = [
|
|
90
|
+
r
|
|
91
|
+
for r in report.routes
|
|
92
|
+
if r.method.lower() in ("post", "put", "delete", "patch") and not r.has_auth_guard
|
|
93
|
+
]
|
|
94
|
+
total_writes = sum(
|
|
95
|
+
1 for r in report.routes if r.method.lower() in ("post", "put", "delete", "patch")
|
|
96
|
+
)
|
|
97
|
+
if not total_writes:
|
|
98
|
+
return False, "no state-changing routes discovered", {}
|
|
99
|
+
artifact = {"unguarded": [r.file + ":" + str(r.line) for r in bad]}
|
|
100
|
+
if bad:
|
|
101
|
+
names = ", ".join(f"{r.path or '?'}" for r in bad[:3])
|
|
102
|
+
return (
|
|
103
|
+
False,
|
|
104
|
+
f"{len(bad)}/{total_writes} write endpoints lack auth ({names}...)",
|
|
105
|
+
artifact,
|
|
106
|
+
)
|
|
107
|
+
return True, f"all {total_writes} write endpoints carry auth guards", {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_no_hardcoded_secrets(data: dict) -> tuple[bool, str, dict]:
|
|
111
|
+
"""No finding of type hardcoded_secret may exist."""
|
|
112
|
+
findings = data.get("findings", [])
|
|
113
|
+
hits = [f for f in findings if "hardcoded_secret" in str(f.get("type", ""))]
|
|
114
|
+
artifact = {"count": len(hits)}
|
|
115
|
+
if hits:
|
|
116
|
+
first = hits[0]
|
|
117
|
+
return (
|
|
118
|
+
False,
|
|
119
|
+
f"{len(hits)} hardcoded secret(s), e.g. {first.get('file', '?')}:{first.get('line', 0)}",
|
|
120
|
+
artifact,
|
|
121
|
+
)
|
|
122
|
+
return True, "no hardcoded secrets detected by scanners", {}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_no_debug_mode(data: dict) -> tuple[bool, str, dict]:
|
|
126
|
+
"""Application must not run with debug enabled."""
|
|
127
|
+
findings = data.get("findings", [])
|
|
128
|
+
hits = [
|
|
129
|
+
f
|
|
130
|
+
for f in findings
|
|
131
|
+
if "debug" in str(f.get("type", "")).lower()
|
|
132
|
+
and f.get("severity", "").lower() in ("high", "critical")
|
|
133
|
+
]
|
|
134
|
+
if hits:
|
|
135
|
+
return (
|
|
136
|
+
False,
|
|
137
|
+
f"{len(hits)} debug-enabled finding(s) at high+ severity",
|
|
138
|
+
{"count": len(hits)},
|
|
139
|
+
)
|
|
140
|
+
return True, "no high+ debug-mode findings", {}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
BUILTIN_INVARIANTS: list[Invariant] = [
|
|
144
|
+
Invariant(
|
|
145
|
+
id="auth-all-writes",
|
|
146
|
+
invariant_type=InvariantType.MUST,
|
|
147
|
+
statement="All state-changing endpoints require authentication",
|
|
148
|
+
domain="access-control-authz",
|
|
149
|
+
severity_if_disproved="critical",
|
|
150
|
+
verifier=_check_all_writes_authenticated,
|
|
151
|
+
),
|
|
152
|
+
Invariant(
|
|
153
|
+
id="secrets-none-hardcoded",
|
|
154
|
+
invariant_type=InvariantType.MUST_NOT,
|
|
155
|
+
statement="No credentials committed to source",
|
|
156
|
+
domain="secrets-runtime-management",
|
|
157
|
+
severity_if_disproved="critical",
|
|
158
|
+
verifier=_check_no_hardcoded_secrets,
|
|
159
|
+
),
|
|
160
|
+
Invariant(
|
|
161
|
+
id="debug-not-in-prod",
|
|
162
|
+
invariant_type=InvariantType.MUST_NOT,
|
|
163
|
+
statement="Debug mode is never enabled in production configuration",
|
|
164
|
+
domain="configuration-hardening",
|
|
165
|
+
severity_if_disproved="high",
|
|
166
|
+
verifier=_check_no_debug_mode,
|
|
167
|
+
),
|
|
168
|
+
]
|
patchi/core/atomic.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atomic file operations for Patchi.
|
|
3
|
+
|
|
4
|
+
All persistent writes go through these helpers to prevent data loss on crash.
|
|
5
|
+
The pattern is: write to a temp file, then atomically rename to the target.
|
|
6
|
+
|
|
7
|
+
On Windows, ``Path.replace()`` fails if the target exists, so we unlink first.
|
|
8
|
+
Retries handle transient PermissionError from antivirus or indexer locks.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def atomic_replace(src: Path, dst: Path, retries: int = 3) -> None:
|
|
19
|
+
"""Replace *dst* with *src* atomically, with retries for Windows file-lock races.
|
|
20
|
+
|
|
21
|
+
This is the single source of truth for atomic file replacement across Patchi.
|
|
22
|
+
Every module that writes persistent JSON or source files should use this.
|
|
23
|
+
"""
|
|
24
|
+
for attempt in range(retries):
|
|
25
|
+
try:
|
|
26
|
+
if sys.platform == "win32":
|
|
27
|
+
if dst.exists():
|
|
28
|
+
dst.unlink()
|
|
29
|
+
src.replace(dst)
|
|
30
|
+
return
|
|
31
|
+
except PermissionError:
|
|
32
|
+
if attempt < retries - 1:
|
|
33
|
+
time.sleep(0.05 * (attempt + 1))
|
|
34
|
+
continue
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def atomic_write_json(path: Path, data, indent: int = 2, **dump_kwargs) -> None:
|
|
39
|
+
"""Write *data* as JSON to *path* atomically (tmp + rename).
|
|
40
|
+
|
|
41
|
+
Convenience wrapper: serializes to JSON, writes to a sibling ``.tmp`` file,
|
|
42
|
+
then calls :func:`atomic_replace`.
|
|
43
|
+
"""
|
|
44
|
+
import json
|
|
45
|
+
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
tmp = path.with_suffix(".tmp")
|
|
48
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
49
|
+
json.dump(data, f, indent=indent, **dump_kwargs)
|
|
50
|
+
atomic_replace(tmp, path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def atomic_write_text(path: Path, text: str) -> None:
|
|
54
|
+
"""Write *text* to *path* atomically (tmp + rename).
|
|
55
|
+
|
|
56
|
+
Used by the patch applier to write source files without risk of partial writes.
|
|
57
|
+
"""
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
60
|
+
tmp.write_text(text, encoding="utf-8")
|
|
61
|
+
atomic_replace(tmp, path)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Attackers module — adversarial personalities for security testing."""
|
|
2
|
+
|
|
3
|
+
from .api_abuse import ApiAbuseAttacker
|
|
4
|
+
from .auth_bypass import AuthBypassAttacker
|
|
5
|
+
from .business_logic import BusinessLogicAttacker
|
|
6
|
+
from .planner import AttackPlanner
|
|
7
|
+
from .priv_escalation import PrivEscAttacker
|
|
8
|
+
from .recon import ReconAttacker
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AttackPlanner",
|
|
12
|
+
"ReconAttacker",
|
|
13
|
+
"AuthBypassAttacker",
|
|
14
|
+
"PrivEscAttacker",
|
|
15
|
+
"BusinessLogicAttacker",
|
|
16
|
+
"ApiAbuseAttacker",
|
|
17
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""ApiAbuseAttacker — test API abuse (injection, rate limiting, info disclosure)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiAbuseAttacker(BaseAttacker):
|
|
9
|
+
"""Tests for injection risks, missing rate limits, information disclosure."""
|
|
10
|
+
|
|
11
|
+
name = "api_abuse"
|
|
12
|
+
objective = "api_abuse"
|
|
13
|
+
|
|
14
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
15
|
+
hypotheses: list[Hypothesis] = []
|
|
16
|
+
|
|
17
|
+
for claim in self._graph.claims.values():
|
|
18
|
+
# Injection risks
|
|
19
|
+
if any(kw in claim.statement.lower() for kw in ["inject", "sanitiz", "validat"]):
|
|
20
|
+
if claim.verdict.value in ("not_proved", "unproven"):
|
|
21
|
+
hypotheses.append(
|
|
22
|
+
Hypothesis(
|
|
23
|
+
attacker=self.name,
|
|
24
|
+
objective=self.objective,
|
|
25
|
+
target=claim.id,
|
|
26
|
+
risk="high",
|
|
27
|
+
confidence=0.7,
|
|
28
|
+
description=f"Injection risk unverified: {claim.statement}",
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Rate limiting
|
|
33
|
+
if "rate" in claim.statement.lower() and "limit" in claim.statement.lower():
|
|
34
|
+
hypotheses.append(
|
|
35
|
+
Hypothesis(
|
|
36
|
+
attacker=self.name,
|
|
37
|
+
objective=self.objective,
|
|
38
|
+
target=claim.id,
|
|
39
|
+
risk="medium",
|
|
40
|
+
confidence=0.5,
|
|
41
|
+
description=f"Rate limiting unverified: {claim.statement}",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Information disclosure
|
|
46
|
+
if "error" in claim.statement.lower() and (
|
|
47
|
+
"detail" in claim.statement.lower() or "disclose" in claim.statement.lower()
|
|
48
|
+
):
|
|
49
|
+
hypotheses.append(
|
|
50
|
+
Hypothesis(
|
|
51
|
+
attacker=self.name,
|
|
52
|
+
objective=self.objective,
|
|
53
|
+
target=claim.id,
|
|
54
|
+
risk="medium",
|
|
55
|
+
confidence=0.4,
|
|
56
|
+
description=f"Info disclosure unverified: {claim.statement}",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return hypotheses
|
|
61
|
+
|
|
62
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
63
|
+
claim = self._graph.claims.get(hypothesis.target)
|
|
64
|
+
if claim is None:
|
|
65
|
+
return AttackResult(hypothesis=hypothesis, confirmed=False, evidence="Claim not found")
|
|
66
|
+
|
|
67
|
+
refuting = [e for e in claim.evidence if not e.supports]
|
|
68
|
+
if refuting:
|
|
69
|
+
return AttackResult(
|
|
70
|
+
hypothesis=hypothesis,
|
|
71
|
+
confirmed=False,
|
|
72
|
+
evidence=f"API protection verified: {refuting[0].detail}",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
supporting = [e for e in claim.evidence if e.supports]
|
|
76
|
+
if supporting:
|
|
77
|
+
return AttackResult(
|
|
78
|
+
hypothesis=hypothesis,
|
|
79
|
+
confirmed=True,
|
|
80
|
+
evidence=f"API abuse confirmed: {supporting[0].detail}",
|
|
81
|
+
severity=hypothesis.risk,
|
|
82
|
+
remediation="Add input validation, rate limiting, or error sanitization",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return AttackResult(
|
|
86
|
+
hypothesis=hypothesis,
|
|
87
|
+
confirmed=False,
|
|
88
|
+
evidence="Insufficient evidence",
|
|
89
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""AuthBypassAttacker — test authentication bypass vulnerabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthBypassAttacker(BaseAttacker):
|
|
9
|
+
"""Tests for unauthenticated access, unguarded state transitions, weak auth mechanisms."""
|
|
10
|
+
|
|
11
|
+
name = "auth_bypass"
|
|
12
|
+
objective = "authentication_bypass"
|
|
13
|
+
|
|
14
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
15
|
+
hypotheses: list[Hypothesis] = []
|
|
16
|
+
|
|
17
|
+
for claim in self._graph.claims.values():
|
|
18
|
+
if "auth" in claim.domain.lower() or "session" in claim.domain.lower():
|
|
19
|
+
if claim.verdict.value in ("not_proved", "unproven"):
|
|
20
|
+
hypotheses.append(
|
|
21
|
+
Hypothesis(
|
|
22
|
+
attacker=self.name,
|
|
23
|
+
objective=self.objective,
|
|
24
|
+
target=claim.id,
|
|
25
|
+
risk="high",
|
|
26
|
+
confidence=0.7,
|
|
27
|
+
description=f"Auth property unverified: {claim.statement}",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# State transitions without auth guards
|
|
32
|
+
if "transition" in claim.statement.lower() and "guard" not in claim.statement.lower():
|
|
33
|
+
hypotheses.append(
|
|
34
|
+
Hypothesis(
|
|
35
|
+
attacker=self.name,
|
|
36
|
+
objective=self.objective,
|
|
37
|
+
target=claim.id,
|
|
38
|
+
risk="high",
|
|
39
|
+
confidence=0.5,
|
|
40
|
+
description=f"State transition may lack auth guard: {claim.statement}",
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return hypotheses
|
|
45
|
+
|
|
46
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
47
|
+
claim = self._graph.claims.get(hypothesis.target)
|
|
48
|
+
if claim is None:
|
|
49
|
+
return AttackResult(hypothesis=hypothesis, confirmed=False, evidence="Claim not found")
|
|
50
|
+
|
|
51
|
+
refuting = [e for e in claim.evidence if not e.supports]
|
|
52
|
+
if refuting:
|
|
53
|
+
return AttackResult(
|
|
54
|
+
hypothesis=hypothesis,
|
|
55
|
+
confirmed=False,
|
|
56
|
+
evidence=f"Auth verified: {refuting[0].detail}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
supporting = [e for e in claim.evidence if e.supports]
|
|
60
|
+
if supporting:
|
|
61
|
+
return AttackResult(
|
|
62
|
+
hypothesis=hypothesis,
|
|
63
|
+
confirmed=True,
|
|
64
|
+
evidence=f"Auth bypass confirmed: {supporting[0].detail}",
|
|
65
|
+
severity="critical",
|
|
66
|
+
remediation="Add authentication check before this operation",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return AttackResult(
|
|
70
|
+
hypothesis=hypothesis,
|
|
71
|
+
confirmed=False,
|
|
72
|
+
evidence="Insufficient evidence",
|
|
73
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Base attacker — shared types and ABC for adversarial personalities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.core.attackers")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Hypothesis:
|
|
16
|
+
"""An attacker's hypothesis about where a vulnerability exists."""
|
|
17
|
+
|
|
18
|
+
attacker: str
|
|
19
|
+
objective: str
|
|
20
|
+
target: str # file, endpoint, or state
|
|
21
|
+
risk: str # "critical", "high", "medium", "low"
|
|
22
|
+
confidence: float # 0.0–1.0
|
|
23
|
+
description: str = ""
|
|
24
|
+
evidence_required: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict:
|
|
27
|
+
return {
|
|
28
|
+
"attacker": self.attacker,
|
|
29
|
+
"objective": self.objective,
|
|
30
|
+
"target": self.target,
|
|
31
|
+
"risk": self.risk,
|
|
32
|
+
"confidence": self.confidence,
|
|
33
|
+
"description": self.description,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AttackResult:
|
|
39
|
+
"""Result of testing a hypothesis."""
|
|
40
|
+
|
|
41
|
+
hypothesis: Hypothesis
|
|
42
|
+
confirmed: bool
|
|
43
|
+
evidence: str = ""
|
|
44
|
+
severity: str = ""
|
|
45
|
+
remediation: str = ""
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"hypothesis": self.hypothesis.to_dict(),
|
|
50
|
+
"confirmed": self.confirmed,
|
|
51
|
+
"evidence": self.evidence,
|
|
52
|
+
"severity": self.severity,
|
|
53
|
+
"remediation": self.remediation,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseAttacker(abc.ABC):
|
|
58
|
+
"""Base class for adversarial personalities."""
|
|
59
|
+
|
|
60
|
+
name: str = "base"
|
|
61
|
+
objective: str = "unknown"
|
|
62
|
+
|
|
63
|
+
def __init__(self, graph: AssuranceGraph):
|
|
64
|
+
self._graph = graph
|
|
65
|
+
|
|
66
|
+
@abc.abstractmethod
|
|
67
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
68
|
+
"""Generate hypotheses about where vulnerabilities might exist."""
|
|
69
|
+
|
|
70
|
+
@abc.abstractmethod
|
|
71
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
72
|
+
"""Test a specific hypothesis."""
|
|
73
|
+
|
|
74
|
+
def run(self) -> list[AttackResult]:
|
|
75
|
+
"""Generate and test all hypotheses."""
|
|
76
|
+
hypotheses = self.hypothesize()
|
|
77
|
+
results: list[AttackResult] = []
|
|
78
|
+
for h in hypotheses:
|
|
79
|
+
try:
|
|
80
|
+
result = self.test(h)
|
|
81
|
+
results.append(result)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
_log.warning("Attacker %s failed testing %s: %s", self.name, h.target, e)
|
|
84
|
+
results.append(
|
|
85
|
+
AttackResult(
|
|
86
|
+
hypothesis=h,
|
|
87
|
+
confirmed=False,
|
|
88
|
+
evidence=f"Test error: {e}",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
return results
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""BusinessLogicAttacker — test business logic abuse (replay, race conditions)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import AttackResult, BaseAttacker, Hypothesis
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BusinessLogicAttacker(BaseAttacker):
|
|
9
|
+
"""Tests for replay attacks, concurrent races, unprotected business flows."""
|
|
10
|
+
|
|
11
|
+
name = "business_logic"
|
|
12
|
+
objective = "business_logic_abuse"
|
|
13
|
+
|
|
14
|
+
def hypothesize(self) -> list[Hypothesis]:
|
|
15
|
+
hypotheses: list[Hypothesis] = []
|
|
16
|
+
|
|
17
|
+
for claim in self._graph.claims.values():
|
|
18
|
+
# Idempotency concerns
|
|
19
|
+
if "idempoten" in claim.statement.lower():
|
|
20
|
+
if claim.verdict.value in ("not_proved", "unproven"):
|
|
21
|
+
hypotheses.append(
|
|
22
|
+
Hypothesis(
|
|
23
|
+
attacker=self.name,
|
|
24
|
+
objective=self.objective,
|
|
25
|
+
target=claim.id,
|
|
26
|
+
risk="medium",
|
|
27
|
+
confidence=0.6,
|
|
28
|
+
description=f"Idempotency unverified: {claim.statement}",
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Race conditions
|
|
33
|
+
if "concurrent" in claim.statement.lower() or "race" in claim.statement.lower():
|
|
34
|
+
hypotheses.append(
|
|
35
|
+
Hypothesis(
|
|
36
|
+
attacker=self.name,
|
|
37
|
+
objective=self.objective,
|
|
38
|
+
target=claim.id,
|
|
39
|
+
risk="high",
|
|
40
|
+
confidence=0.5,
|
|
41
|
+
description=f"Race condition unverified: {claim.statement}",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# State machine integrity
|
|
46
|
+
if "state" in claim.domain.lower() and "transition" in claim.statement.lower():
|
|
47
|
+
if claim.verdict.value == "unproven":
|
|
48
|
+
hypotheses.append(
|
|
49
|
+
Hypothesis(
|
|
50
|
+
attacker=self.name,
|
|
51
|
+
objective=self.objective,
|
|
52
|
+
target=claim.id,
|
|
53
|
+
risk="medium",
|
|
54
|
+
confidence=0.4,
|
|
55
|
+
description=f"State machine integrity unverified: {claim.statement}",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return hypotheses
|
|
60
|
+
|
|
61
|
+
def test(self, hypothesis: Hypothesis) -> AttackResult:
|
|
62
|
+
claim = self._graph.claims.get(hypothesis.target)
|
|
63
|
+
if claim is None:
|
|
64
|
+
return AttackResult(hypothesis=hypothesis, confirmed=False, evidence="Claim not found")
|
|
65
|
+
|
|
66
|
+
refuting = [e for e in claim.evidence if not e.supports]
|
|
67
|
+
if refuting:
|
|
68
|
+
return AttackResult(
|
|
69
|
+
hypothesis=hypothesis,
|
|
70
|
+
confirmed=False,
|
|
71
|
+
evidence=f"Business logic verified: {refuting[0].detail}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
supporting = [e for e in claim.evidence if e.supports]
|
|
75
|
+
if supporting:
|
|
76
|
+
return AttackResult(
|
|
77
|
+
hypothesis=hypothesis,
|
|
78
|
+
confirmed=True,
|
|
79
|
+
evidence=f"Business logic abuse confirmed: {supporting[0].detail}",
|
|
80
|
+
severity=hypothesis.risk,
|
|
81
|
+
remediation="Add idempotency keys, rate limiting, or state guards",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return AttackResult(
|
|
85
|
+
hypothesis=hypothesis,
|
|
86
|
+
confirmed=False,
|
|
87
|
+
evidence="Insufficient evidence",
|
|
88
|
+
)
|