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,137 @@
|
|
|
1
|
+
"""FuzzCorpus — persistent corpus manager for accumulating interesting inputs.
|
|
2
|
+
|
|
3
|
+
Stores fuzz inputs that triggered interesting behavior (crashes, errors,
|
|
4
|
+
new code paths) so subsequent runs can build on prior discoveries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("patchi.fuzz.corpus")
|
|
17
|
+
|
|
18
|
+
CORPUS_FILE = ".patchi/fuzz_corpus.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CorpusEntry:
|
|
23
|
+
"""One entry in the fuzz corpus."""
|
|
24
|
+
|
|
25
|
+
label: str
|
|
26
|
+
value: Any
|
|
27
|
+
strategy: str
|
|
28
|
+
score: float # interestingness score (higher = more interesting)
|
|
29
|
+
hits: int = 1 # how many times this triggered something
|
|
30
|
+
added_at: float = 0.0
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
if self.added_at == 0.0:
|
|
34
|
+
self.added_at = time.time()
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"label": self.label,
|
|
39
|
+
"value": str(self.value)[:500],
|
|
40
|
+
"strategy": self.strategy,
|
|
41
|
+
"score": self.score,
|
|
42
|
+
"hits": self.hits,
|
|
43
|
+
"added_at": self.added_at,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, d: dict) -> CorpusEntry:
|
|
48
|
+
return cls(
|
|
49
|
+
label=d.get("label", "?"),
|
|
50
|
+
value=d.get("value", ""),
|
|
51
|
+
strategy=d.get("strategy", "unknown"),
|
|
52
|
+
score=float(d.get("score", 0)),
|
|
53
|
+
hits=int(d.get("hits", 1)),
|
|
54
|
+
added_at=float(d.get("added_at", 0)),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FuzzCorpus:
|
|
59
|
+
"""Persistent corpus for accumulating interesting fuzz inputs."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, root: Path):
|
|
62
|
+
self._root = root
|
|
63
|
+
self._path = root / CORPUS_FILE
|
|
64
|
+
self._entries: dict[str, CorpusEntry] = {}
|
|
65
|
+
self._load()
|
|
66
|
+
|
|
67
|
+
def _load(self) -> None:
|
|
68
|
+
"""Load corpus from disk."""
|
|
69
|
+
if not self._path.is_file():
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
73
|
+
for ed in data.get("entries", []):
|
|
74
|
+
entry = CorpusEntry.from_dict(ed)
|
|
75
|
+
self._entries[entry.label] = entry
|
|
76
|
+
except Exception as e:
|
|
77
|
+
_log.warning("Failed to load fuzz corpus: %s", e)
|
|
78
|
+
|
|
79
|
+
def save(self) -> None:
|
|
80
|
+
"""Persist corpus to disk."""
|
|
81
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
payload = {
|
|
83
|
+
"version": 1,
|
|
84
|
+
"entries": [e.to_dict() for e in self._entries.values()],
|
|
85
|
+
}
|
|
86
|
+
tmp = self._path.with_suffix(".json.tmp")
|
|
87
|
+
tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
88
|
+
tmp.replace(self._path)
|
|
89
|
+
|
|
90
|
+
def add(self, entry: CorpusEntry) -> None:
|
|
91
|
+
"""Add or update a corpus entry."""
|
|
92
|
+
if entry.label in self._entries:
|
|
93
|
+
existing = self._entries[entry.label]
|
|
94
|
+
existing.hits += 1
|
|
95
|
+
existing.score = max(existing.score, entry.score)
|
|
96
|
+
else:
|
|
97
|
+
self._entries[entry.label] = entry
|
|
98
|
+
|
|
99
|
+
def get_interesting(self, min_score: float = 0.5, limit: int = 50) -> list[CorpusEntry]:
|
|
100
|
+
"""Get the most interesting entries, sorted by score."""
|
|
101
|
+
candidates = [e for e in self._entries.values() if e.score >= min_score]
|
|
102
|
+
candidates.sort(key=lambda e: (-e.score, -e.hits))
|
|
103
|
+
return candidates[:limit]
|
|
104
|
+
|
|
105
|
+
def get_all(self, limit: int = 200) -> list[CorpusEntry]:
|
|
106
|
+
"""Get all entries, sorted by score."""
|
|
107
|
+
entries = list(self._entries.values())
|
|
108
|
+
entries.sort(key=lambda e: (-e.score, -e.hits))
|
|
109
|
+
return entries[:limit]
|
|
110
|
+
|
|
111
|
+
def update_score(self, label: str, score: float) -> bool:
|
|
112
|
+
"""Update the score of an existing entry. Returns True if found."""
|
|
113
|
+
if label in self._entries:
|
|
114
|
+
self._entries[label].score = max(self._entries[label].score, score)
|
|
115
|
+
self._entries[label].hits += 1
|
|
116
|
+
return True
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def __len__(self) -> int:
|
|
120
|
+
return len(self._entries)
|
|
121
|
+
|
|
122
|
+
def stats(self) -> dict:
|
|
123
|
+
"""Return corpus statistics."""
|
|
124
|
+
entries = list(self._entries.values())
|
|
125
|
+
if not entries:
|
|
126
|
+
return {"total": 0, "avg_score": 0, "strategies": {}}
|
|
127
|
+
|
|
128
|
+
strategies: dict[str, int] = {}
|
|
129
|
+
for e in entries:
|
|
130
|
+
strategies[e.strategy] = strategies.get(e.strategy, 0) + 1
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"total": len(entries),
|
|
134
|
+
"avg_score": sum(e.score for e in entries) / len(entries),
|
|
135
|
+
"max_score": max(e.score for e in entries),
|
|
136
|
+
"strategies": strategies,
|
|
137
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""InputFuzzer — mutate inputs for security boundary testing.
|
|
2
|
+
|
|
3
|
+
Generates mutated variants of input values to test injection, overflow,
|
|
4
|
+
encoding bypass, and format-string vulnerabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FuzzInput:
|
|
16
|
+
"""A single fuzz input variant."""
|
|
17
|
+
|
|
18
|
+
label: str
|
|
19
|
+
value: Any
|
|
20
|
+
strategy: str # "boundary", "injection", "encoding", "overflow", "format"
|
|
21
|
+
expected_impact: str = "" # "crash", "injection", "bypass", "error"
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"label": self.label,
|
|
26
|
+
"value": str(self.value)[:200],
|
|
27
|
+
"strategy": self.strategy,
|
|
28
|
+
"expected_impact": self.expected_impact,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Boundary values per type ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
_BOUNDARY_STRINGS = [
|
|
35
|
+
"", # empty
|
|
36
|
+
" ", # whitespace
|
|
37
|
+
"a" * 10_000, # long
|
|
38
|
+
"\x00", # null byte
|
|
39
|
+
"🔥" * 100, # emoji
|
|
40
|
+
"\n\r\t", # control chars
|
|
41
|
+
"true",
|
|
42
|
+
"false",
|
|
43
|
+
"null", # type confusion
|
|
44
|
+
"-1",
|
|
45
|
+
"0",
|
|
46
|
+
"2147483647", # numeric boundaries
|
|
47
|
+
"99999999999999999999", # overflow
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
_INJECTION_STRINGS = [
|
|
51
|
+
"' OR 1=1 --",
|
|
52
|
+
"'; DROP TABLE users; --",
|
|
53
|
+
"<script>alert(1)</script>",
|
|
54
|
+
"{{7*7}}",
|
|
55
|
+
"${7*7}",
|
|
56
|
+
"{{constructor.constructor('return this')()}}",
|
|
57
|
+
"../../etc/passwd",
|
|
58
|
+
"'; exec('ls'); --",
|
|
59
|
+
"| cat /etc/passwd",
|
|
60
|
+
"$(whoami)",
|
|
61
|
+
"`id`",
|
|
62
|
+
"1; rm -rf /",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
_ENCODING_STRINGS = [
|
|
66
|
+
"%27%20OR%201%3D1", # URL-encoded
|
|
67
|
+
"<script>", # HTML entities
|
|
68
|
+
"JCBhbGVydCgxKQ==", # base64
|
|
69
|
+
"%00", # null byte URL
|
|
70
|
+
"\\u0027", # unicode escape
|
|
71
|
+
"%EF%BC%87", # fullwidth apostrophe
|
|
72
|
+
".LogInformation", # log injection
|
|
73
|
+
"\r\nSet-Cookie: admin=1", # CRLF injection
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
_FORMAT_STRINGS = [
|
|
77
|
+
"%s%s%s%s%s%s%s%s",
|
|
78
|
+
"%x%x%x%x%x%x%x%x",
|
|
79
|
+
"%n%n%n%n%n%n%n%n",
|
|
80
|
+
"{0.__class__.__bases__}",
|
|
81
|
+
"${{7*7}}",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class InputFuzzer:
|
|
86
|
+
"""Generate mutated input variants for endpoint/parameter testing."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, seed: int | None = None):
|
|
89
|
+
self._rng = random.Random(seed)
|
|
90
|
+
|
|
91
|
+
def fuzz_string(self, original: str, count: int = 20) -> list[FuzzInput]:
|
|
92
|
+
"""Generate mutated string variants."""
|
|
93
|
+
results: list[FuzzInput] = []
|
|
94
|
+
|
|
95
|
+
# Boundary values
|
|
96
|
+
for val in _BOUNDARY_STRINGS:
|
|
97
|
+
results.append(
|
|
98
|
+
FuzzInput(label=f"boundary_{val[:10]!r}", value=val, strategy="boundary")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Injection payloads
|
|
102
|
+
for val in _INJECTION_STRINGS:
|
|
103
|
+
results.append(
|
|
104
|
+
FuzzInput(
|
|
105
|
+
label=f"injection_{val[:10]!r}",
|
|
106
|
+
value=val,
|
|
107
|
+
strategy="injection",
|
|
108
|
+
expected_impact="injection",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Encoding bypasses
|
|
113
|
+
for val in _ENCODING_STRINGS:
|
|
114
|
+
results.append(
|
|
115
|
+
FuzzInput(
|
|
116
|
+
label=f"encoding_{val[:10]!r}",
|
|
117
|
+
value=val,
|
|
118
|
+
strategy="encoding",
|
|
119
|
+
expected_impact="bypass",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Format strings
|
|
124
|
+
for val in _FORMAT_STRINGS:
|
|
125
|
+
results.append(
|
|
126
|
+
FuzzInput(
|
|
127
|
+
label=f"format_{val[:10]!r}",
|
|
128
|
+
value=val,
|
|
129
|
+
strategy="format",
|
|
130
|
+
expected_impact="error",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Random mutations of the original
|
|
135
|
+
for i in range(max(0, count - len(results))):
|
|
136
|
+
mutated = self._mutate_string(original)
|
|
137
|
+
results.append(
|
|
138
|
+
FuzzInput(
|
|
139
|
+
label=f"random_{i}",
|
|
140
|
+
value=mutated,
|
|
141
|
+
strategy="random",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return results[:count]
|
|
146
|
+
|
|
147
|
+
def fuzz_numeric(self, original: int, count: int = 10) -> list[FuzzInput]:
|
|
148
|
+
"""Generate mutated numeric variants."""
|
|
149
|
+
boundaries = [0, -1, 1, 2**31 - 1, 2**31, 2**63 - 1, -(2**31), -(2**63)]
|
|
150
|
+
results = [
|
|
151
|
+
FuzzInput(label=f"boundary_{v}", value=v, strategy="boundary") for v in boundaries
|
|
152
|
+
]
|
|
153
|
+
for i in range(max(0, count - len(results))):
|
|
154
|
+
results.append(
|
|
155
|
+
FuzzInput(
|
|
156
|
+
label=f"random_{i}",
|
|
157
|
+
value=self._rng.randint(-10_000, 10_000),
|
|
158
|
+
strategy="random",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return results[:count]
|
|
162
|
+
|
|
163
|
+
def fuzz_dict(self, original: dict, count: int = 15) -> list[FuzzInput]:
|
|
164
|
+
"""Generate mutated dict variants (extra keys, type confusion, nested depth)."""
|
|
165
|
+
results: list[FuzzInput] = []
|
|
166
|
+
|
|
167
|
+
# Extra keys
|
|
168
|
+
results.append(
|
|
169
|
+
FuzzInput(
|
|
170
|
+
label="extra_admin_key",
|
|
171
|
+
value={**original, "admin": True, "is_admin": True, "role": "admin"},
|
|
172
|
+
strategy="boundary",
|
|
173
|
+
expected_impact="privilege_escalation",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Null values
|
|
178
|
+
results.append(
|
|
179
|
+
FuzzInput(
|
|
180
|
+
label="null_values",
|
|
181
|
+
value=dict.fromkeys(original),
|
|
182
|
+
strategy="boundary",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Nested depth bomb
|
|
187
|
+
nested: Any = "leaf"
|
|
188
|
+
for _ in range(100):
|
|
189
|
+
nested = {"a": nested}
|
|
190
|
+
results.append(
|
|
191
|
+
FuzzInput(
|
|
192
|
+
label="nested_depth_100",
|
|
193
|
+
value={"__proto__": nested},
|
|
194
|
+
strategy="overflow",
|
|
195
|
+
expected_impact="crash",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Prototype pollution
|
|
200
|
+
results.append(
|
|
201
|
+
FuzzInput(
|
|
202
|
+
label="proto_pollution",
|
|
203
|
+
value={**original, "__proto__": {"admin": True}},
|
|
204
|
+
strategy="injection",
|
|
205
|
+
expected_impact="privilege_escalation",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Type confusion
|
|
210
|
+
for key in list(original.keys())[:3]:
|
|
211
|
+
confused = {**original}
|
|
212
|
+
confused[key] = [original[key]]
|
|
213
|
+
confused[f"{key}_array"] = original[key]
|
|
214
|
+
results.append(
|
|
215
|
+
FuzzInput(
|
|
216
|
+
label=f"type_confusion_{key}",
|
|
217
|
+
value=confused,
|
|
218
|
+
strategy="injection",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return results[:count]
|
|
223
|
+
|
|
224
|
+
def _mutate_string(self, s: str) -> str:
|
|
225
|
+
"""Apply a random mutation to a string."""
|
|
226
|
+
ops = [
|
|
227
|
+
self._flip_case,
|
|
228
|
+
self._insert_special,
|
|
229
|
+
self._truncate,
|
|
230
|
+
self._repeat,
|
|
231
|
+
self._shuffle,
|
|
232
|
+
self._append_null,
|
|
233
|
+
]
|
|
234
|
+
return self._rng.choice(ops)(s)
|
|
235
|
+
|
|
236
|
+
def _flip_case(self, s: str) -> str:
|
|
237
|
+
return "".join(c.swapcase() if c.isalpha() else c for c in s)
|
|
238
|
+
|
|
239
|
+
def _insert_special(self, s: str) -> str:
|
|
240
|
+
specials = ["\x00", "\n", "\r", "'", '"', "\\", "%00"]
|
|
241
|
+
pos = self._rng.randint(0, max(len(s), 1))
|
|
242
|
+
return s[:pos] + self._rng.choice(specials) + s[pos:]
|
|
243
|
+
|
|
244
|
+
def _truncate(self, s: str) -> str:
|
|
245
|
+
return s[: self._rng.randint(0, max(len(s) - 1, 0))]
|
|
246
|
+
|
|
247
|
+
def _repeat(self, s: str) -> str:
|
|
248
|
+
return s * self._rng.randint(2, 5)
|
|
249
|
+
|
|
250
|
+
def _shuffle(self, s: str) -> str:
|
|
251
|
+
lst = list(s)
|
|
252
|
+
self._rng.shuffle(lst)
|
|
253
|
+
return "".join(lst)
|
|
254
|
+
|
|
255
|
+
def _append_null(self, s: str) -> str:
|
|
256
|
+
return s + "\x00" * self._rng.randint(1, 10)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""SequenceFuzzer — reorder and combine API operations to find race conditions.
|
|
2
|
+
|
|
3
|
+
Given a set of API operations, generates permutations that test TOCTOU,
|
|
4
|
+
replay, and ordering vulnerabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import itertools
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FuzzSequence:
|
|
16
|
+
"""A sequence of operations to execute against the target."""
|
|
17
|
+
|
|
18
|
+
label: str
|
|
19
|
+
operations: list[dict[str, Any]]
|
|
20
|
+
strategy: str # "permutation", "replay", "concurrent", "interleaved"
|
|
21
|
+
description: str = ""
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"label": self.label,
|
|
26
|
+
"operations": self.operations[:10], # cap for display
|
|
27
|
+
"strategy": self.strategy,
|
|
28
|
+
"description": self.description,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SequenceFuzzer:
|
|
33
|
+
"""Generate operation sequences that test ordering vulnerabilities."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, max_permutations: int = 20):
|
|
36
|
+
self._max_perm = max_permutations
|
|
37
|
+
|
|
38
|
+
def generate(
|
|
39
|
+
self,
|
|
40
|
+
operations: list[dict[str, Any]],
|
|
41
|
+
max_depth: int = 4,
|
|
42
|
+
) -> list[FuzzSequence]:
|
|
43
|
+
"""Generate fuzz sequences from a list of API operations."""
|
|
44
|
+
results: list[FuzzSequence] = []
|
|
45
|
+
|
|
46
|
+
if len(operations) < 1:
|
|
47
|
+
return results
|
|
48
|
+
|
|
49
|
+
# 1. Pairwise permutations (all 2-element orderings)
|
|
50
|
+
results.extend(self._pairwise_permutations(operations))
|
|
51
|
+
|
|
52
|
+
# 2. Replay sequences (same op repeated)
|
|
53
|
+
results.extend(self._replay_sequences(operations))
|
|
54
|
+
|
|
55
|
+
# 3. Concurrent pairs (two ops that should be atomic)
|
|
56
|
+
results.extend(self._concurrent_pairs(operations))
|
|
57
|
+
|
|
58
|
+
# 4. Full permutations (if small enough)
|
|
59
|
+
if len(operations) <= 6:
|
|
60
|
+
results.extend(self._full_permutations(operations))
|
|
61
|
+
|
|
62
|
+
return results[: self._max_perm]
|
|
63
|
+
|
|
64
|
+
def _pairwise_permutations(self, operations: list[dict[str, Any]]) -> list[FuzzSequence]:
|
|
65
|
+
"""Test every pair of operations in both orderings."""
|
|
66
|
+
results: list[FuzzSequence] = []
|
|
67
|
+
for i, op_a in enumerate(operations):
|
|
68
|
+
for j, op_b in enumerate(operations):
|
|
69
|
+
if i >= j:
|
|
70
|
+
continue
|
|
71
|
+
# Normal order
|
|
72
|
+
results.append(
|
|
73
|
+
FuzzSequence(
|
|
74
|
+
label=f"pair_{i}_{j}_normal",
|
|
75
|
+
operations=[op_a, op_b],
|
|
76
|
+
strategy="permutation",
|
|
77
|
+
description=f"Execute {op_a.get('method', '?')} then {op_b.get('method', '?')}",
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
# Reversed order
|
|
81
|
+
results.append(
|
|
82
|
+
FuzzSequence(
|
|
83
|
+
label=f"pair_{j}_{i}_reversed",
|
|
84
|
+
operations=[op_b, op_a],
|
|
85
|
+
strategy="permutation",
|
|
86
|
+
description=f"Execute {op_b.get('method', '?')} then {op_a.get('method', '?')} (reversed)",
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
def _replay_sequences(self, operations: list[dict[str, Any]]) -> list[FuzzSequence]:
|
|
92
|
+
"""Test replaying the same operation multiple times."""
|
|
93
|
+
results: list[FuzzSequence] = []
|
|
94
|
+
for i, op in enumerate(operations):
|
|
95
|
+
# Double replay
|
|
96
|
+
results.append(
|
|
97
|
+
FuzzSequence(
|
|
98
|
+
label=f"replay_{i}_double",
|
|
99
|
+
operations=[op, op],
|
|
100
|
+
strategy="replay",
|
|
101
|
+
description=f"Replay {op.get('method', '?')} twice (idempotency test)",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
# Triple replay
|
|
105
|
+
results.append(
|
|
106
|
+
FuzzSequence(
|
|
107
|
+
label=f"replay_{i}_triple",
|
|
108
|
+
operations=[op, op, op],
|
|
109
|
+
strategy="replay",
|
|
110
|
+
description=f"Replay {op.get('method', '?')} three times (idempotency test)",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return results
|
|
114
|
+
|
|
115
|
+
def _concurrent_pairs(self, operations: list[dict[str, Any]]) -> list[FuzzSequence]:
|
|
116
|
+
"""Mark operation pairs as concurrent (for testing race conditions)."""
|
|
117
|
+
results: list[FuzzSequence] = []
|
|
118
|
+
for i, op_a in enumerate(operations):
|
|
119
|
+
for j, op_b in enumerate(operations):
|
|
120
|
+
if i >= j:
|
|
121
|
+
continue
|
|
122
|
+
if self._are_concurrent(op_a, op_b):
|
|
123
|
+
results.append(
|
|
124
|
+
FuzzSequence(
|
|
125
|
+
label=f"concurrent_{i}_{j}",
|
|
126
|
+
operations=[op_a, op_b],
|
|
127
|
+
strategy="concurrent",
|
|
128
|
+
description=f"Execute {op_a.get('method', '?')} and {op_b.get('method', '?')} concurrently (race condition test)",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
def _full_permutations(self, operations: list[dict[str, Any]]) -> list[FuzzSequence]:
|
|
134
|
+
"""Generate all permutations of small operation sets."""
|
|
135
|
+
results: list[FuzzSequence] = []
|
|
136
|
+
for perm in itertools.permutations(range(len(operations))):
|
|
137
|
+
ops = [operations[i] for i in perm]
|
|
138
|
+
results.append(
|
|
139
|
+
FuzzSequence(
|
|
140
|
+
label=f"perm_{'_'.join(map(str, perm))}",
|
|
141
|
+
operations=ops,
|
|
142
|
+
strategy="permutation",
|
|
143
|
+
description=f"Permutation: {' → '.join(o.get('method', '?') for o in ops)}",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
def _are_concurrent(self, op_a: dict, op_b: dict) -> bool:
|
|
149
|
+
"""Determine if two operations could race (same resource, different actions)."""
|
|
150
|
+
path_a = op_a.get("path", "")
|
|
151
|
+
path_b = op_b.get("path", "")
|
|
152
|
+
method_a = op_a.get("method", "")
|
|
153
|
+
method_b = op_b.get("method", "")
|
|
154
|
+
|
|
155
|
+
# Same path but different HTTP methods
|
|
156
|
+
if path_a == path_b and method_a != method_b:
|
|
157
|
+
return True
|
|
158
|
+
# Both mutating the same resource
|
|
159
|
+
mutating = {"POST", "PUT", "PATCH", "DELETE"}
|
|
160
|
+
if path_a == path_b and method_a in mutating and method_b in mutating:
|
|
161
|
+
return True
|
|
162
|
+
return False
|