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,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser Tester — Playwright-based security testing for live web apps.
|
|
3
|
+
|
|
4
|
+
Drives a real browser through the app map to test:
|
|
5
|
+
- Authentication bypass (bad/empty credentials)
|
|
6
|
+
- XSS injection in form fields
|
|
7
|
+
- Authorization checks (accessing pages without session)
|
|
8
|
+
- Form submission with malicious payloads
|
|
9
|
+
- Coverage: clicking every button/link
|
|
10
|
+
|
|
11
|
+
Requires: pip install playwright && playwright install
|
|
12
|
+
Falls back gracefully when Playwright is not installed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from patchi.core.agents.base import (
|
|
20
|
+
AgentGroup,
|
|
21
|
+
AgentInput,
|
|
22
|
+
AgentResult,
|
|
23
|
+
BaseAgent,
|
|
24
|
+
Finding,
|
|
25
|
+
Severity,
|
|
26
|
+
register,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# ── Test payloads ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
_XSS_PAYLOADS = [
|
|
32
|
+
'<script>alert("xss")</script>',
|
|
33
|
+
'"><img src=x onerror=alert(1)>',
|
|
34
|
+
"javascript:alert(1)",
|
|
35
|
+
"<svg/onload=alert(1)>",
|
|
36
|
+
"{{7*7}}",
|
|
37
|
+
"${7*7}",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_AUTH_BYPASS_PAYLOADS = [
|
|
41
|
+
{"username": "admin", "password": ""},
|
|
42
|
+
{"username": "admin", "password": "admin"},
|
|
43
|
+
{"username": "' OR '1'='1", "password": "' OR '1'='1"},
|
|
44
|
+
{"username": "admin", "password": "password"},
|
|
45
|
+
{"username": "administrator", "password": ""},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
_SQLI_PAYLOADS = [
|
|
49
|
+
"' OR '1'='1",
|
|
50
|
+
"1; DROP TABLE users--",
|
|
51
|
+
"' UNION SELECT NULL--",
|
|
52
|
+
"admin'--",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_log = logging.getLogger("patchi.security.browser_tester")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_playwright_available() -> bool:
|
|
60
|
+
try:
|
|
61
|
+
import importlib.util
|
|
62
|
+
|
|
63
|
+
return importlib.util.find_spec("playwright") is not None
|
|
64
|
+
except (ImportError, ValueError):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def patchi_browser_test(url: str, app_map: dict | None = None) -> list[dict]:
|
|
69
|
+
"""Run browser-based security tests against a live app."""
|
|
70
|
+
if not _check_playwright_available():
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
"type": "playwright_unavailable",
|
|
74
|
+
"message": "Playwright not installed — pip install playwright",
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
findings = []
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
from playwright.sync_api import sync_playwright
|
|
82
|
+
|
|
83
|
+
with sync_playwright() as p:
|
|
84
|
+
browser = p.chromium.launch(headless=True)
|
|
85
|
+
context = browser.new_context(
|
|
86
|
+
user_agent="Patchi-SecurityTester/1.0",
|
|
87
|
+
ignore_https_errors=True,
|
|
88
|
+
)
|
|
89
|
+
page = context.new_page()
|
|
90
|
+
|
|
91
|
+
# 1. Auth bypass test
|
|
92
|
+
findings.extend(_test_auth_bypass(page, url))
|
|
93
|
+
|
|
94
|
+
# 2. XSS injection in forms
|
|
95
|
+
findings.extend(_test_xss_in_forms(page, url, app_map))
|
|
96
|
+
|
|
97
|
+
# 3. SQLi in form fields
|
|
98
|
+
findings.extend(_test_sqli_in_forms(page, url, app_map))
|
|
99
|
+
|
|
100
|
+
browser.close()
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
findings.append({"type": "browser_test_error", "message": f"Browser test failed: {e}"})
|
|
104
|
+
|
|
105
|
+
return findings
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _test_auth_bypass(page, url: str) -> list[dict]:
|
|
109
|
+
"""Try logging in with bad credentials."""
|
|
110
|
+
findings = []
|
|
111
|
+
try:
|
|
112
|
+
page.goto(url, timeout=10000)
|
|
113
|
+
|
|
114
|
+
# Look for login forms
|
|
115
|
+
login_forms = page.query_selector_all(
|
|
116
|
+
'form[action*="login"], form[action*="auth"], form:has(input[type="password"])'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for form in login_forms:
|
|
120
|
+
for payload in _AUTH_BYPASS_PAYLOADS:
|
|
121
|
+
try:
|
|
122
|
+
username_input = form.query_selector(
|
|
123
|
+
'input[name="username"], input[name="email"], input[type="email"]'
|
|
124
|
+
)
|
|
125
|
+
password_input = form.query_selector('input[type="password"]')
|
|
126
|
+
|
|
127
|
+
if username_input and password_input:
|
|
128
|
+
username_input.fill(payload["username"])
|
|
129
|
+
password_input.fill(payload["password"])
|
|
130
|
+
form.evaluate("el => el.submit()")
|
|
131
|
+
|
|
132
|
+
page.wait_for_timeout(2000)
|
|
133
|
+
|
|
134
|
+
# Check if we got past login (no login form visible)
|
|
135
|
+
still_on_login = page.query_selector('input[type="password"]')
|
|
136
|
+
if not still_on_login:
|
|
137
|
+
findings.append(
|
|
138
|
+
{
|
|
139
|
+
"type": "auth_bypass",
|
|
140
|
+
"severity": "critical",
|
|
141
|
+
"message": f"Possible auth bypass with credentials: {payload['username']}/{payload['password']}",
|
|
142
|
+
"cwe": "CWE-287",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
page.goto(url, timeout=10000)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
_log.warning("_test_auth_bypass failed: %s", e)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
_log.warning("_test_auth_bypass failed: %s", e)
|
|
154
|
+
|
|
155
|
+
return findings
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _test_xss_in_forms(page, url: str, app_map: dict | None) -> list[dict]:
|
|
159
|
+
"""Inject XSS payloads into form fields."""
|
|
160
|
+
findings = []
|
|
161
|
+
|
|
162
|
+
pages_to_test = []
|
|
163
|
+
if app_map and "pages" in app_map:
|
|
164
|
+
pages_to_test = [(p["url"], p["forms"]) for p in app_map["pages"] if p["forms"]]
|
|
165
|
+
else:
|
|
166
|
+
pages_to_test = [(url, [])]
|
|
167
|
+
|
|
168
|
+
for page_url, _forms in pages_to_test:
|
|
169
|
+
try:
|
|
170
|
+
page.goto(page_url, timeout=10000)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
_log.warning("_test_xss_in_forms failed: %s", e)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
page_forms = page.query_selector_all("form")
|
|
176
|
+
for form in page_forms:
|
|
177
|
+
inputs = form.query_selector_all('input[type="text"], input[type="search"], textarea')
|
|
178
|
+
for inp_field in inputs:
|
|
179
|
+
for payload in _XSS_PAYLOADS[:2]:
|
|
180
|
+
try:
|
|
181
|
+
name = inp_field.get_attribute("name") or ""
|
|
182
|
+
inp_field.fill(payload)
|
|
183
|
+
|
|
184
|
+
# Check if payload reflected in page source
|
|
185
|
+
page_content = page.content()
|
|
186
|
+
if payload in page_content and payload not in ("{{7*7}}", "${7*7}"):
|
|
187
|
+
findings.append(
|
|
188
|
+
{
|
|
189
|
+
"type": "reflected_xss",
|
|
190
|
+
"severity": "high",
|
|
191
|
+
"message": f"XSS payload reflected in form field '{name}' at {page_url}",
|
|
192
|
+
"cwe": "CWE-79",
|
|
193
|
+
"evidence": payload,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
break
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_log.warning("_test_xss_in_forms failed: %s", e)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
return findings
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _test_sqli_in_forms(page, url: str, app_map: dict | None) -> list[dict]:
|
|
205
|
+
"""Test for SQL error-based injection in form fields."""
|
|
206
|
+
findings = []
|
|
207
|
+
|
|
208
|
+
pages_to_test = []
|
|
209
|
+
if app_map and "pages" in app_map:
|
|
210
|
+
pages_to_test = [(p["url"], p["forms"]) for p in app_map["pages"] if p["forms"]]
|
|
211
|
+
else:
|
|
212
|
+
pages_to_test = [(url, [])]
|
|
213
|
+
|
|
214
|
+
SQL_ERROR_PATTERNS = [
|
|
215
|
+
"sql syntax",
|
|
216
|
+
"mysql_fetch",
|
|
217
|
+
"sqlite3.",
|
|
218
|
+
"postgresql",
|
|
219
|
+
"ora-",
|
|
220
|
+
"syntax error",
|
|
221
|
+
"unterminated",
|
|
222
|
+
"pg_query",
|
|
223
|
+
"warning:",
|
|
224
|
+
"fatal error",
|
|
225
|
+
"microsoft odbc",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
for page_url, _forms in pages_to_test:
|
|
229
|
+
try:
|
|
230
|
+
page.goto(page_url, timeout=10000)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
_log.warning("_test_sqli_in_forms failed: %s", e)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
page_forms = page.query_selector_all("form")
|
|
236
|
+
for form in page_forms:
|
|
237
|
+
inputs = form.query_selector_all('input[type="text"], input[type="search"], textarea')
|
|
238
|
+
for inp_field in inputs:
|
|
239
|
+
for payload in _SQLI_PAYLOADS[:2]:
|
|
240
|
+
try:
|
|
241
|
+
name = inp_field.get_attribute("name") or ""
|
|
242
|
+
inp_field.fill(payload)
|
|
243
|
+
form.evaluate("el => el.submit()")
|
|
244
|
+
|
|
245
|
+
page.wait_for_timeout(2000)
|
|
246
|
+
page_content = page.content().lower()
|
|
247
|
+
|
|
248
|
+
for pattern in SQL_ERROR_PATTERNS:
|
|
249
|
+
if pattern in page_content:
|
|
250
|
+
findings.append(
|
|
251
|
+
{
|
|
252
|
+
"type": "sql_injection_error",
|
|
253
|
+
"severity": "critical",
|
|
254
|
+
"message": f"SQL error pattern '{pattern}' after injecting '{payload}' in field '{name}' at {page_url}",
|
|
255
|
+
"cwe": "CWE-89",
|
|
256
|
+
"evidence": pattern,
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
page.goto(page_url, timeout=10000)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
_log.warning("_test_sqli_in_forms failed: %s", e)
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
return findings
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ── Browser Tester Agent ──────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@register
|
|
273
|
+
class BrowserTesterAgent(BaseAgent):
|
|
274
|
+
"""Playwright-based security testing for live web apps."""
|
|
275
|
+
|
|
276
|
+
name = "BrowserTesterAgent"
|
|
277
|
+
group = AgentGroup.SECURITY
|
|
278
|
+
timeout = 120
|
|
279
|
+
|
|
280
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
281
|
+
app_url = (
|
|
282
|
+
inp.brain.get("app_url", "")
|
|
283
|
+
or inp.config.get("app_url", "")
|
|
284
|
+
or inp.extra.get("app_url", "")
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if not app_url:
|
|
288
|
+
result.add_finding(
|
|
289
|
+
Finding(
|
|
290
|
+
agent=self.name,
|
|
291
|
+
type="no_target_url",
|
|
292
|
+
severity=Severity.INFO,
|
|
293
|
+
file="",
|
|
294
|
+
message="No app URL configured — set 'app_url' in config to enable browser testing",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
app_map = inp.extra.get("app_map", None)
|
|
300
|
+
test_results = patchi_browser_test(app_url, app_map)
|
|
301
|
+
|
|
302
|
+
for tr in test_results:
|
|
303
|
+
if tr.get("type") == "playwright_unavailable":
|
|
304
|
+
result.add_finding(
|
|
305
|
+
Finding(
|
|
306
|
+
agent=self.name,
|
|
307
|
+
type="playwright_unavailable",
|
|
308
|
+
severity=Severity.INFO,
|
|
309
|
+
file="",
|
|
310
|
+
message=tr["message"],
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
if tr.get("type") == "browser_test_error":
|
|
315
|
+
result.add_error(tr["message"])
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
sev = tr.get("severity", "medium")
|
|
319
|
+
result.add_finding(
|
|
320
|
+
Finding(
|
|
321
|
+
agent=self.name,
|
|
322
|
+
type=tr.get("type", "browser_finding"),
|
|
323
|
+
severity=Severity(sev)
|
|
324
|
+
if sev in ("critical", "high", "medium", "low", "info")
|
|
325
|
+
else Severity.MEDIUM,
|
|
326
|
+
file="",
|
|
327
|
+
message=tr.get("message", ""),
|
|
328
|
+
cwe=tr.get("cwe", ""),
|
|
329
|
+
evidence=tr.get("evidence", ""),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
result.files_scanned = 1
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BusinessLogicAgent — Business logic abuse detection.
|
|
3
|
+
|
|
4
|
+
Detects:
|
|
5
|
+
- Mass assignment / broken object property level mapping
|
|
6
|
+
- Excessive data exposure (returning full models)
|
|
7
|
+
- Missing pagination on list endpoints
|
|
8
|
+
- Missing ownership checks (IDOR)
|
|
9
|
+
- Rate limiting missing on mutation endpoints
|
|
10
|
+
- Unvalidated business flow state transitions
|
|
11
|
+
|
|
12
|
+
Structural detections (mass assignment, pagination, state transitions) use
|
|
13
|
+
tree-sitter AST (find_calls / find_assignments). Route markers, ownership
|
|
14
|
+
checks, and serialization calls use high-quality literal-pattern regex.
|
|
15
|
+
Does NOT call AI. Does NOT write to disk. Does NOT touch the queue.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from ..agents.base import (
|
|
23
|
+
AgentDomain,
|
|
24
|
+
AgentGroup,
|
|
25
|
+
AgentInput,
|
|
26
|
+
AgentResult,
|
|
27
|
+
BaseAgent,
|
|
28
|
+
Finding,
|
|
29
|
+
Severity,
|
|
30
|
+
register,
|
|
31
|
+
safe_rglob,
|
|
32
|
+
)
|
|
33
|
+
from ..brain.ast_utils import find_assignments, find_calls
|
|
34
|
+
from ..brain.languages import EXTENSION_MAP, Lang
|
|
35
|
+
|
|
36
|
+
# Source extensions to scan (language parity).
|
|
37
|
+
_SOURCE_EXTENSIONS = [
|
|
38
|
+
"*.py",
|
|
39
|
+
"*.js",
|
|
40
|
+
"*.jsx",
|
|
41
|
+
"*.ts",
|
|
42
|
+
"*.tsx",
|
|
43
|
+
"*.java",
|
|
44
|
+
"*.rb",
|
|
45
|
+
"*.php",
|
|
46
|
+
"*.go",
|
|
47
|
+
"*.rs",
|
|
48
|
+
"*.cs",
|
|
49
|
+
"*.kt",
|
|
50
|
+
"*.kts",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Path keywords that indicate an API/route file (early-exit gate).
|
|
54
|
+
_API_KEYWORDS = [
|
|
55
|
+
"route",
|
|
56
|
+
"view",
|
|
57
|
+
"api",
|
|
58
|
+
"controller",
|
|
59
|
+
"serializer",
|
|
60
|
+
"resource",
|
|
61
|
+
"handler",
|
|
62
|
+
"endpoint",
|
|
63
|
+
"action",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Request-body sources passed directly to model updates (mass assignment).
|
|
67
|
+
_REQUEST_BODY_RE = re.compile(r"(?:request\.(?:json|data|form|args)|body)", re.I)
|
|
68
|
+
# Model mutation methods that, applied to a request body, are mass assignment.
|
|
69
|
+
_MUTATION_CALLS = {"update", "put", "patch", "save"}
|
|
70
|
+
# Pagination call names.
|
|
71
|
+
_PAGINATION_CALLS = {"limit", "paginate", "offset", "page"}
|
|
72
|
+
# Objects whose status/state changes are business-critical.
|
|
73
|
+
_STATE_BASES = {"order", "payment", "subscription", "ticket", "workflow"}
|
|
74
|
+
_STATE_VALIDATION_RE = re.compile(
|
|
75
|
+
r"(?i)(?:\ballowed\b|\bvalid\b|\btransition\b|state_machine|STATUS_FLOW|\benum\b)"
|
|
76
|
+
)
|
|
77
|
+
# Ownership-identifying symbols for IDOR checks, including Patchi's own
|
|
78
|
+
# tenant-isolation idiom (with tenant_context(...) + per-project ownership
|
|
79
|
+
# comparison) so fixed endpoints stop flagging.
|
|
80
|
+
_OWNERSHIP_RE = re.compile(
|
|
81
|
+
r"(?i)(?:current_user|user\.id|\bowner\b|request\.user|\.user_id\s*=|user_id\s*=="
|
|
82
|
+
r"|tenant_context|patch_project|\btenant\b)"
|
|
83
|
+
)
|
|
84
|
+
_RATE_LIMIT_RE = re.compile(r"(?i)@(?:ratelimit|throttle|limit)")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@register
|
|
88
|
+
class BusinessLogicAgent(BaseAgent):
|
|
89
|
+
"""Detects business logic abuse, mass assignment, IDOR, excessive data exposure."""
|
|
90
|
+
|
|
91
|
+
group = AgentGroup.SECURITY
|
|
92
|
+
domain = AgentDomain.SECURITY
|
|
93
|
+
name = "BusinessLogicAgent"
|
|
94
|
+
description = (
|
|
95
|
+
"Business logic abuse: mass assignment, IDOR, excessive data exposure, missing pagination"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
99
|
+
route_guard = re.compile(
|
|
100
|
+
r"(?i)(?:@(?:app\.)?route|def\s+\w+|class\s+\w+[^(]*View|/api/)", re.I
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for pattern in _SOURCE_EXTENSIONS:
|
|
104
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
105
|
+
if not fpath.is_file():
|
|
106
|
+
continue
|
|
107
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
108
|
+
ext = fpath.suffix.lower()
|
|
109
|
+
lang = EXTENSION_MAP.get(ext)
|
|
110
|
+
|
|
111
|
+
# Skip non-route files early.
|
|
112
|
+
if not any(kw in rel.lower() for kw in _API_KEYWORDS):
|
|
113
|
+
try:
|
|
114
|
+
first_kb = fpath.read_text(encoding="utf-8", errors="ignore")[:1024]
|
|
115
|
+
except OSError:
|
|
116
|
+
continue
|
|
117
|
+
if not route_guard.search(first_kb):
|
|
118
|
+
continue
|
|
119
|
+
content = first_kb
|
|
120
|
+
else:
|
|
121
|
+
try:
|
|
122
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
123
|
+
except OSError:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
result.files_scanned += 1
|
|
127
|
+
self._scan_business_logic(content, rel, result, lang)
|
|
128
|
+
|
|
129
|
+
def _scan_business_logic(
|
|
130
|
+
self, content: str, rel: str, result: AgentResult, lang: Lang | None
|
|
131
|
+
) -> None:
|
|
132
|
+
# ── Mass assignment detection (AST) ───────────────────────────────────
|
|
133
|
+
if lang is not None:
|
|
134
|
+
for call in find_calls(content, lang, _MUTATION_CALLS):
|
|
135
|
+
if _REQUEST_BODY_RE.search(call.get("full_text", "")):
|
|
136
|
+
result.add_finding(
|
|
137
|
+
Finding(
|
|
138
|
+
agent=self.name,
|
|
139
|
+
type="mass_assignment",
|
|
140
|
+
severity=Severity.HIGH,
|
|
141
|
+
file=rel,
|
|
142
|
+
line=call.get("line", 0),
|
|
143
|
+
message="Mass assignment — request body applied directly to model without field whitelist",
|
|
144
|
+
suggestion="Define an explicit field allowlist and only update whitelisted fields",
|
|
145
|
+
cwe="CWE-915",
|
|
146
|
+
extra={"skill": "api-security.skill"},
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# ── Excessive data exposure (__dict__, vars(), .to_dict()) ──────────
|
|
151
|
+
if any(kw in rel.lower() for kw in _API_KEYWORDS):
|
|
152
|
+
for m in re.finditer(
|
|
153
|
+
r"(?i)(?:__dict__|vars\s*\(|\.to_dict\s*\(|\.serialize\s*\()", content
|
|
154
|
+
):
|
|
155
|
+
result.add_finding(
|
|
156
|
+
Finding(
|
|
157
|
+
agent=self.name,
|
|
158
|
+
type="excessive_data_exposure",
|
|
159
|
+
severity=Severity.MEDIUM,
|
|
160
|
+
file=rel,
|
|
161
|
+
line=content[: m.start()].count("\n") + 1,
|
|
162
|
+
message="Returning full model serialization — may leak sensitive fields",
|
|
163
|
+
suggestion="Define explicit response fields; never return __dict__ or vars()",
|
|
164
|
+
cwe="CWE-200",
|
|
165
|
+
extra={"skill": "api-security.skill"},
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ── Missing pagination on collection endpoints (AST) ──────────────────
|
|
170
|
+
if lang is not None:
|
|
171
|
+
all_calls = find_calls(content, lang, {"all"})
|
|
172
|
+
paginate_calls = find_calls(content, lang, _PAGINATION_CALLS)
|
|
173
|
+
if all_calls and not paginate_calls:
|
|
174
|
+
result.add_finding(
|
|
175
|
+
Finding(
|
|
176
|
+
agent=self.name,
|
|
177
|
+
type="missing_pagination",
|
|
178
|
+
severity=Severity.LOW,
|
|
179
|
+
file=rel,
|
|
180
|
+
line=0,
|
|
181
|
+
message="Collection endpoint uses .all() without pagination — unbounded resource consumption",
|
|
182
|
+
suggestion="Add pagination with .paginate() or .limit()/.offset()",
|
|
183
|
+
cwe="CWE-770",
|
|
184
|
+
extra={"skill": "api-security.skill"},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# ── IDOR — missing ownership check (route marker regex) ───────────────
|
|
189
|
+
for m in re.finditer(
|
|
190
|
+
r"(?i)(?:@(?:app\.)?route|(?:app|router)\.(?:get|post|put|delete|patch))\s*\([^)]*\{(\w+_)?id\}",
|
|
191
|
+
content,
|
|
192
|
+
):
|
|
193
|
+
line_start = content[: m.start()].count("\n") + 1
|
|
194
|
+
lines = content.splitlines()
|
|
195
|
+
surrounding = "\n".join(lines[line_start : min(line_start + 15, len(lines))])
|
|
196
|
+
if not _OWNERSHIP_RE.search(surrounding):
|
|
197
|
+
result.add_finding(
|
|
198
|
+
Finding(
|
|
199
|
+
agent=self.name,
|
|
200
|
+
type="idor_missing_ownership_check",
|
|
201
|
+
severity=Severity.HIGH,
|
|
202
|
+
file=rel,
|
|
203
|
+
line=line_start,
|
|
204
|
+
message="Endpoint with ID parameter lacks ownership verification — potential IDOR",
|
|
205
|
+
suggestion="Add ownership check: filter query by current_user.id or owner_id",
|
|
206
|
+
cwe="CWE-862",
|
|
207
|
+
extra={"skill": "api-security.skill"},
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ── Missing rate limiting on mutation endpoints (route marker regex) ──
|
|
212
|
+
for m in re.finditer(
|
|
213
|
+
r"(?i)(?:@(?:app\.)?route|(?:app|router)\.(?:get|post|put|delete|patch))\s*\(", content
|
|
214
|
+
):
|
|
215
|
+
line_start = content[: m.start()].count("\n") + 1
|
|
216
|
+
lines = content.splitlines()
|
|
217
|
+
surrounding = "\n".join(
|
|
218
|
+
lines[max(0, line_start - 3) : min(line_start + 10, len(lines))]
|
|
219
|
+
)
|
|
220
|
+
decorator_line = lines[line_start - 1] if line_start <= len(lines) else ""
|
|
221
|
+
method_line = lines[line_start] if line_start < len(lines) else ""
|
|
222
|
+
if re.search(r"(?i)(?:post|put|delete|patch)", decorator_line[:80]) or re.search(
|
|
223
|
+
r"(?i)(?:post|put|delete|patch)", method_line[:80]
|
|
224
|
+
):
|
|
225
|
+
if not _RATE_LIMIT_RE.search(surrounding[:200]):
|
|
226
|
+
result.add_finding(
|
|
227
|
+
Finding(
|
|
228
|
+
agent=self.name,
|
|
229
|
+
type="missing_rate_limit",
|
|
230
|
+
severity=Severity.LOW,
|
|
231
|
+
file=rel,
|
|
232
|
+
line=line_start,
|
|
233
|
+
message="Mutation endpoint missing rate limiting — potential for abuse",
|
|
234
|
+
suggestion="Add @ratelimit decorator with appropriate limits",
|
|
235
|
+
cwe="CWE-799",
|
|
236
|
+
extra={"skill": "api-security.skill"},
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# ── Unvalidated state transitions (AST assignments) ───────────────────
|
|
241
|
+
if lang is not None:
|
|
242
|
+
for asn in find_assignments(content, lang):
|
|
243
|
+
target = asn.get("target", "")
|
|
244
|
+
if "." not in target:
|
|
245
|
+
continue
|
|
246
|
+
base, _, leaf = target.rpartition(".")
|
|
247
|
+
if leaf in ("status", "state") and base.lower() in _STATE_BASES:
|
|
248
|
+
line_start = asn.get("line", 0)
|
|
249
|
+
ctx = content[max(0, line_start - 15) : line_start + 5]
|
|
250
|
+
if not _STATE_VALIDATION_RE.search(ctx):
|
|
251
|
+
result.add_finding(
|
|
252
|
+
Finding(
|
|
253
|
+
agent=self.name,
|
|
254
|
+
type="unvalidated_state_transition",
|
|
255
|
+
severity=Severity.MEDIUM,
|
|
256
|
+
file=rel,
|
|
257
|
+
line=line_start,
|
|
258
|
+
message="Status/state change without transition validation — business logic bypass",
|
|
259
|
+
suggestion="Validate state transitions against allowed flow matrix",
|
|
260
|
+
cwe="CWE-284",
|
|
261
|
+
extra={"skill": "api-security.skill"},
|
|
262
|
+
)
|
|
263
|
+
)
|