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,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security Orchestrator — deduplication, correlation, and composite risk scoring.
|
|
3
|
+
|
|
4
|
+
Receives raw AgentResult objects from all security agents and produces
|
|
5
|
+
a unified SecurityReport with deduplicated, correlated findings.
|
|
6
|
+
|
|
7
|
+
Deterministic first, AI second.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from patchi.core.agents.base import AgentResult, Finding
|
|
18
|
+
from patchi.core.security.chain_analyzer import Chain, ChainAnalyzer
|
|
19
|
+
from patchi.core.security.intent_analyzer import IntentAnalyzer, IntentReport
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.core.security.orchestrator")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Security Report ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CorrelatedFinding:
|
|
29
|
+
"""A deduplicated finding, possibly confirmed by multiple agents."""
|
|
30
|
+
|
|
31
|
+
finding: Finding
|
|
32
|
+
confirmed_by: list[str] = field(default_factory=list)
|
|
33
|
+
composite_score: float = 0.0
|
|
34
|
+
owasp_category: str = ""
|
|
35
|
+
cwe_ids: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
d = self.finding.to_dict()
|
|
39
|
+
d["confirmed_by"] = self.confirmed_by
|
|
40
|
+
d["composite_score"] = self.composite_score
|
|
41
|
+
d["owasp_category"] = self.owasp_category
|
|
42
|
+
d["cwe_ids"] = self.cwe_ids
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class SecurityReport:
|
|
48
|
+
"""Unified output from all security agents after orchestration."""
|
|
49
|
+
|
|
50
|
+
findings: list[CorrelatedFinding] = field(default_factory=list)
|
|
51
|
+
total_findings: int = 0
|
|
52
|
+
by_severity: dict[str, int] = field(default_factory=dict)
|
|
53
|
+
by_owasp: dict[str, int] = field(default_factory=dict)
|
|
54
|
+
agents_run: list[str] = field(default_factory=list)
|
|
55
|
+
correlation_count: int = 0
|
|
56
|
+
chains: list[Chain] = field(default_factory=list)
|
|
57
|
+
intent_report: IntentReport | None = None
|
|
58
|
+
charter_violations: list[dict] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
d = {
|
|
62
|
+
"total_findings": self.total_findings,
|
|
63
|
+
"by_severity": self.by_severity,
|
|
64
|
+
"by_owasp": self.by_owasp,
|
|
65
|
+
"agents_run": self.agents_run,
|
|
66
|
+
"correlation_count": self.correlation_count,
|
|
67
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
68
|
+
"chains": [
|
|
69
|
+
{
|
|
70
|
+
"steps": [
|
|
71
|
+
{
|
|
72
|
+
"file": n.finding.file,
|
|
73
|
+
"line": n.finding.line,
|
|
74
|
+
"type": n.finding.type,
|
|
75
|
+
"label": lbl,
|
|
76
|
+
}
|
|
77
|
+
for n, lbl in zip(c.nodes, c.labels, strict=False)
|
|
78
|
+
],
|
|
79
|
+
"total_severity": c.total_severity,
|
|
80
|
+
}
|
|
81
|
+
for c in self.chains
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
if self.intent_report:
|
|
85
|
+
d["intent_report"] = self.intent_report.to_dict()
|
|
86
|
+
if self.charter_violations:
|
|
87
|
+
d["charter_violations"] = self.charter_violations
|
|
88
|
+
return d
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── OWASP Top 10 2021 mapping (CWE ranges) ──────────────────────────────────
|
|
92
|
+
|
|
93
|
+
_OWASP_MAPPING: dict | None = None
|
|
94
|
+
_OWASP_BY_CWE: dict[int, str] | None = None
|
|
95
|
+
_OWASP_KEYWORD_FALLBACK: dict[str, list[str]] | None = None
|
|
96
|
+
|
|
97
|
+
_MAPPING_FILE = Path(__file__).parent / "owasp_mapping.json"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_owasp_mapping() -> dict:
|
|
101
|
+
"""Load OWASP mapping from JSON file (cached)."""
|
|
102
|
+
global _OWASP_MAPPING, _OWASP_BY_CWE, _OWASP_KEYWORD_FALLBACK
|
|
103
|
+
if _OWASP_MAPPING is not None:
|
|
104
|
+
return _OWASP_MAPPING
|
|
105
|
+
|
|
106
|
+
with open(_MAPPING_FILE, encoding="utf-8") as f:
|
|
107
|
+
data = json.load(f)
|
|
108
|
+
|
|
109
|
+
# Build lookup: CWE number → full category name (e.g. "A01:2021 Broken Access Control")
|
|
110
|
+
by_cwe: dict[int, str] = {}
|
|
111
|
+
for cat in data["categories"]:
|
|
112
|
+
name = f"{cat['code']}:2021 {cat['name']}"
|
|
113
|
+
for cwe in cat["cwes"]:
|
|
114
|
+
by_cwe[cwe] = name
|
|
115
|
+
|
|
116
|
+
_OWASP_MAPPING = data
|
|
117
|
+
_OWASP_BY_CWE = by_cwe
|
|
118
|
+
_OWASP_KEYWORD_FALLBACK = data.get("keyword_fallback", {})
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _classify_owasp(finding: Finding) -> str:
|
|
123
|
+
"""Map a finding to an OWASP Top 10 category via CWE ID or keyword fallback."""
|
|
124
|
+
_load_owasp_mapping()
|
|
125
|
+
cwe_str = finding.cwe.upper().replace("CWE-", "").strip()
|
|
126
|
+
if cwe_str.isdigit() and _OWASP_BY_CWE is not None:
|
|
127
|
+
cwe_num = int(cwe_str)
|
|
128
|
+
if cwe_num in _OWASP_BY_CWE:
|
|
129
|
+
return _OWASP_BY_CWE[cwe_num]
|
|
130
|
+
# Fallback by finding type keywords
|
|
131
|
+
ftype = finding.type.lower()
|
|
132
|
+
msg = finding.message.lower()
|
|
133
|
+
combined = ftype + msg
|
|
134
|
+
if _OWASP_KEYWORD_FALLBACK is not None:
|
|
135
|
+
for code, keywords in _OWASP_KEYWORD_FALLBACK.items():
|
|
136
|
+
if any(w in combined for w in keywords):
|
|
137
|
+
# Map code back to full name via the mapping data
|
|
138
|
+
for cat in _OWASP_MAPPING["categories"]:
|
|
139
|
+
if cat["code"] == code:
|
|
140
|
+
return f"{code}:2021 {cat['name']}"
|
|
141
|
+
return "A05:2021 Security Misconfiguration"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── Composite risk score ─────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
_SEV_WEIGHTS = {"critical": 10.0, "high": 7.0, "medium": 4.0, "low": 1.5, "info": 0.0}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _composite_score(finding: Finding, confirm_count: int) -> float:
|
|
150
|
+
"""0-100 score: severity + confirmation bonus + confidence."""
|
|
151
|
+
base = _SEV_WEIGHTS.get(finding.severity.value, 0.0)
|
|
152
|
+
confirm_bonus = min(confirm_count * 3.0, 15.0) # up to +15 for multi-agent confirmation
|
|
153
|
+
return min(100.0, base * 10.0 + confirm_bonus)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Orchestrator ─────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SecurityOrchestrator:
|
|
160
|
+
"""
|
|
161
|
+
Takes raw AgentResult list, deduplicates, correlates, scores.
|
|
162
|
+
|
|
163
|
+
Usage:
|
|
164
|
+
orch = SecurityOrchestrator()
|
|
165
|
+
report = orch.correlate(agent_results)
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def correlate(self, results: list[AgentResult]) -> SecurityReport:
|
|
169
|
+
all_findings: list[Finding] = []
|
|
170
|
+
agents_run = []
|
|
171
|
+
|
|
172
|
+
for r in results:
|
|
173
|
+
agents_run.append(r.agent_name)
|
|
174
|
+
all_findings.extend(r.findings)
|
|
175
|
+
|
|
176
|
+
# Step 1: Group by (file, type, cwe) with line-tolerance window (LIMIT-08)
|
|
177
|
+
LINE_TOLERANCE = 5
|
|
178
|
+
groups: dict[tuple, list[Finding]] = {}
|
|
179
|
+
used: set[tuple] = set()
|
|
180
|
+
# CWE secondary index: different tools name the same weakness
|
|
181
|
+
# differently ("B608" vs "patchi-sql-injection-..."), but the CWE is
|
|
182
|
+
# the semantic join. Same file + same CWE within tolerance => merge.
|
|
183
|
+
cwe_index: dict[tuple[str, str], tuple] = {}
|
|
184
|
+
for f in sorted(all_findings, key=lambda x: (x.file, x.line)):
|
|
185
|
+
cwe_key = f.cwe.upper().replace("CWE-", "").strip() if f.cwe else ""
|
|
186
|
+
# Try exact match first
|
|
187
|
+
exact_key = (f.file, f.line, f.type, cwe_key)
|
|
188
|
+
if exact_key not in used:
|
|
189
|
+
# Try line-tolerance match within same file + type + cwe
|
|
190
|
+
matched = False
|
|
191
|
+
for tolerance_line in range(
|
|
192
|
+
max(1, f.line - LINE_TOLERANCE), f.line + LINE_TOLERANCE + 1
|
|
193
|
+
):
|
|
194
|
+
tol_key = (f.file, tolerance_line, f.type, cwe_key)
|
|
195
|
+
if tol_key in used:
|
|
196
|
+
matched = True
|
|
197
|
+
break
|
|
198
|
+
if not matched and cwe_key and f.line > 0:
|
|
199
|
+
# Cross-tool join on (file, cwe) proximity
|
|
200
|
+
gkey = cwe_index.get((f.file, cwe_key))
|
|
201
|
+
if gkey is not None and abs(gkey[1] - f.line) <= LINE_TOLERANCE:
|
|
202
|
+
groups[gkey].append(f)
|
|
203
|
+
matched = True
|
|
204
|
+
if not matched:
|
|
205
|
+
key = (f.file, f.line, f.type, cwe_key)
|
|
206
|
+
used.add(key)
|
|
207
|
+
groups[key] = [f]
|
|
208
|
+
if cwe_key and f.line > 0:
|
|
209
|
+
existing = cwe_index.get((f.file, cwe_key))
|
|
210
|
+
if existing is None or abs(existing[1] - f.line) <= LINE_TOLERANCE:
|
|
211
|
+
cwe_index[(f.file, cwe_key)] = key
|
|
212
|
+
else:
|
|
213
|
+
# Add to existing tolerance group
|
|
214
|
+
for gkey in groups:
|
|
215
|
+
if (
|
|
216
|
+
gkey[0] == f.file
|
|
217
|
+
and gkey[2] == f.type
|
|
218
|
+
and gkey[3] == cwe_key
|
|
219
|
+
and abs(gkey[1] - f.line) <= LINE_TOLERANCE
|
|
220
|
+
):
|
|
221
|
+
groups[gkey].append(f)
|
|
222
|
+
break
|
|
223
|
+
else:
|
|
224
|
+
groups[exact_key].append(f)
|
|
225
|
+
|
|
226
|
+
correlated: list[CorrelatedFinding] = []
|
|
227
|
+
|
|
228
|
+
for _key, group in groups.items():
|
|
229
|
+
primary = group[0]
|
|
230
|
+
confirmed_by = list({f.agent for f in group})
|
|
231
|
+
owasp = _classify_owasp(primary)
|
|
232
|
+
cwe_ids = list({f.cwe for f in group if f.cwe})
|
|
233
|
+
|
|
234
|
+
score = _composite_score(primary, len(confirmed_by))
|
|
235
|
+
|
|
236
|
+
# Take highest severity from the group
|
|
237
|
+
best = max(group, key=lambda f: f.severity.sort_key())
|
|
238
|
+
|
|
239
|
+
correlated.append(
|
|
240
|
+
CorrelatedFinding(
|
|
241
|
+
finding=best,
|
|
242
|
+
confirmed_by=confirmed_by,
|
|
243
|
+
composite_score=score,
|
|
244
|
+
owasp_category=owasp,
|
|
245
|
+
cwe_ids=cwe_ids,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Sort by composite score descending (most dangerous first)
|
|
250
|
+
correlated.sort(key=lambda c: (-c.composite_score, c.finding.file, c.finding.line))
|
|
251
|
+
|
|
252
|
+
# Build summary
|
|
253
|
+
by_sev: dict[str, int] = {}
|
|
254
|
+
by_owasp: dict[str, int] = {}
|
|
255
|
+
for c in correlated:
|
|
256
|
+
sev = c.finding.severity.value
|
|
257
|
+
by_sev[sev] = by_sev.get(sev, 0) + 1
|
|
258
|
+
by_owasp[c.owasp_category] = by_owasp.get(c.owasp_category, 0) + 1
|
|
259
|
+
|
|
260
|
+
# ── Chain analysis (cross-file exploit chains) ──────────────────────
|
|
261
|
+
chains: list[Chain] = []
|
|
262
|
+
import_edges: dict[str, set[str]] = {}
|
|
263
|
+
try:
|
|
264
|
+
from patchi.core.brain.import_graph import build_import_graph
|
|
265
|
+
|
|
266
|
+
ig = build_import_graph(Path("."))
|
|
267
|
+
import_edges = dict(ig.edges)
|
|
268
|
+
except Exception as _exc:
|
|
269
|
+
_log.warning('correlate failed: %s', _exc)
|
|
270
|
+
try:
|
|
271
|
+
chain_analyzer = ChainAnalyzer(all_findings, import_edges=import_edges)
|
|
272
|
+
chains = chain_analyzer.find_chains(max_chains=20)
|
|
273
|
+
except Exception as _exc:
|
|
274
|
+
_log.warning('correlate failed: %s', _exc)
|
|
275
|
+
|
|
276
|
+
# ── Intent analysis (route ↔ code gap detection) ──────────────────────
|
|
277
|
+
intent_report: IntentReport | None = None
|
|
278
|
+
try:
|
|
279
|
+
intent_analyzer = IntentAnalyzer()
|
|
280
|
+
intent_report = intent_analyzer.analyze_root(Path("."))
|
|
281
|
+
except Exception as _exc:
|
|
282
|
+
_log.warning('correlate failed: %s', _exc)
|
|
283
|
+
|
|
284
|
+
# ── Charter drift detection ───────────────────────────────────────────
|
|
285
|
+
charter_violations: list[dict] = []
|
|
286
|
+
try:
|
|
287
|
+
from patchi.core.security.charter import (
|
|
288
|
+
check_all_violations,
|
|
289
|
+
load_charter,
|
|
290
|
+
)
|
|
291
|
+
charter = load_charter(Path("."))
|
|
292
|
+
if charter.rules:
|
|
293
|
+
flat_edges: list[tuple[str, str]] = []
|
|
294
|
+
for src, dsts in import_edges.items():
|
|
295
|
+
for dst in dsts:
|
|
296
|
+
flat_edges.append((src, dst))
|
|
297
|
+
charter_violations = [
|
|
298
|
+
v.to_dict() for v in check_all_violations(charter, flat_edges)
|
|
299
|
+
]
|
|
300
|
+
except Exception as _exc:
|
|
301
|
+
_log.warning('correlate failed: %s', _exc)
|
|
302
|
+
|
|
303
|
+
return SecurityReport(
|
|
304
|
+
findings=correlated,
|
|
305
|
+
total_findings=len(correlated),
|
|
306
|
+
by_severity=by_sev,
|
|
307
|
+
by_owasp=by_owasp,
|
|
308
|
+
agents_run=agents_run,
|
|
309
|
+
correlation_count=sum(1 for c in correlated if len(c.confirmed_by) > 1),
|
|
310
|
+
chains=chains,
|
|
311
|
+
intent_report=intent_report,
|
|
312
|
+
charter_violations=charter_violations,
|
|
313
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern-context suppression for security agents.
|
|
3
|
+
|
|
4
|
+
Detects when a flagged line lives inside:
|
|
5
|
+
- A regex definition (re.compile, _PATTERNS list)
|
|
6
|
+
- A docstring or multi-line comment block
|
|
7
|
+
- A string literal in a variable named *pattern*, *regex*, *payload*, etc.
|
|
8
|
+
- A test fixture file (tests/ or test_*.py)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _try_read(file_path: str) -> str | None:
|
|
18
|
+
"""Try reading a file, resolving relative paths against CWD."""
|
|
19
|
+
p = Path(file_path)
|
|
20
|
+
if not p.is_absolute():
|
|
21
|
+
p = Path.cwd() / file_path
|
|
22
|
+
if not p.is_file():
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return p.read_text(encoding="utf-8", errors="ignore")
|
|
26
|
+
except OSError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_pattern_context(file_path: str, line_num: int) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Return True if line_num in file_path is inside a pattern definition,
|
|
33
|
+
docstring, or test fixture — meaning a security finding there is a
|
|
34
|
+
self-referential false positive.
|
|
35
|
+
"""
|
|
36
|
+
if line_num < 1:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
content = _try_read(file_path)
|
|
40
|
+
if content is None:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
lines = content.splitlines()
|
|
44
|
+
if line_num > len(lines):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
target_line = lines[line_num - 1].strip()
|
|
48
|
+
|
|
49
|
+
# ── Test file check ───────────────────────────────────────────────────
|
|
50
|
+
fp = file_path.replace("\\", "/")
|
|
51
|
+
if "/tests/" in fp or fp.split("/")[-1].startswith("test_"):
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
# ── Variable name check ───────────────────────────────────────────────
|
|
55
|
+
assign_match = re.match(r"(\w+)\s*[:=]", target_line)
|
|
56
|
+
if assign_match:
|
|
57
|
+
var_name = assign_match.group(1).lower()
|
|
58
|
+
if any(
|
|
59
|
+
kw in var_name
|
|
60
|
+
for kw in (
|
|
61
|
+
"pattern",
|
|
62
|
+
"regex",
|
|
63
|
+
"payload",
|
|
64
|
+
"explanation",
|
|
65
|
+
"template",
|
|
66
|
+
"descriptions",
|
|
67
|
+
"protocol",
|
|
68
|
+
"metadata",
|
|
69
|
+
"indicator",
|
|
70
|
+
)
|
|
71
|
+
):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# ── re.compile() check ────────────────────────────────────────────────
|
|
75
|
+
if "re.compile(" in target_line or "re.match(" in target_line:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
# ── Triple-quote / docstring check ────────────────────────────────────
|
|
79
|
+
in_triple = False
|
|
80
|
+
tq_char = None
|
|
81
|
+
bracket_depth = 0
|
|
82
|
+
|
|
83
|
+
for i in range(line_num - 1):
|
|
84
|
+
line = lines[i].strip()
|
|
85
|
+
for tq in ('"""', "'''"):
|
|
86
|
+
count = line.count(tq)
|
|
87
|
+
if count % 2 == 1:
|
|
88
|
+
if in_triple and tq_char == tq:
|
|
89
|
+
in_triple = False
|
|
90
|
+
elif not in_triple:
|
|
91
|
+
in_triple = True
|
|
92
|
+
tq_char = tq
|
|
93
|
+
if in_triple:
|
|
94
|
+
return True
|
|
95
|
+
for ch in line:
|
|
96
|
+
if ch in "({[":
|
|
97
|
+
bracket_depth += 1
|
|
98
|
+
elif ch in ")}]":
|
|
99
|
+
bracket_depth -= 1
|
|
100
|
+
|
|
101
|
+
# ── Pattern block in list/dict literal ─────────────────────────────────
|
|
102
|
+
if bracket_depth > 0:
|
|
103
|
+
start = max(0, line_num - 8)
|
|
104
|
+
end = min(len(lines), line_num + 4)
|
|
105
|
+
block = "\n".join(lines[start:end])
|
|
106
|
+
if re.search(r're\.compile\(|r["\']|\\[dDwWsSbB]|\[\^', block):
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def suppress_findings(result) -> int:
|
|
113
|
+
"""
|
|
114
|
+
Remove pattern-context false positives from an AgentResult in place.
|
|
115
|
+
Returns the count of findings removed.
|
|
116
|
+
"""
|
|
117
|
+
from patchi.core.agents.base import AgentResult
|
|
118
|
+
|
|
119
|
+
if not isinstance(result, AgentResult):
|
|
120
|
+
return 0
|
|
121
|
+
original = len(result.findings)
|
|
122
|
+
result.findings = [
|
|
123
|
+
f for f in result.findings if not is_pattern_context(f.file, f.line if f.line else 0)
|
|
124
|
+
]
|
|
125
|
+
return original - len(result.findings)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern loader for security agents.
|
|
3
|
+
|
|
4
|
+
Loads sink/source patterns from YAML config and merges them with
|
|
5
|
+
agent-specific defaults. Agents call `load_patterns()` once at
|
|
6
|
+
startup and use the returned dict.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
_PATTERNS_CACHE: dict[str, Any] | None = None
|
|
18
|
+
_PATTERNS_PATH = Path(__file__).parent / "security_patterns.yaml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_patterns(path: Path | None = None) -> dict[str, Any]:
|
|
22
|
+
"""Load and cache security patterns from YAML config."""
|
|
23
|
+
global _PATTERNS_CACHE
|
|
24
|
+
if _PATTERNS_CACHE is not None and path is None:
|
|
25
|
+
return _PATTERNS_CACHE
|
|
26
|
+
|
|
27
|
+
config_path = path or _PATTERNS_PATH
|
|
28
|
+
if not config_path.exists():
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
with open(config_path, encoding="utf-8") as f:
|
|
32
|
+
data = yaml.safe_load(f) or {}
|
|
33
|
+
|
|
34
|
+
_PATTERNS_CACHE = data
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_injection_sinks(patterns: dict | None = None) -> dict[str, dict[str, list[str]]]:
|
|
39
|
+
"""Get injection sinks grouped by type and language."""
|
|
40
|
+
p = patterns or load_patterns()
|
|
41
|
+
return p.get("injection", {})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_ssrf_patterns(patterns: dict | None = None) -> dict[str, Any]:
|
|
45
|
+
"""Get SSRF detection patterns."""
|
|
46
|
+
p = patterns or load_patterns()
|
|
47
|
+
return p.get("ssrf", {})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_ssrf_protocol_patterns(patterns: dict | None = None) -> list[tuple[re.Pattern, str, str]]:
|
|
51
|
+
"""Get compiled SSRF protocol regex patterns with severity and message."""
|
|
52
|
+
ssrf = get_ssrf_patterns(patterns)
|
|
53
|
+
result = []
|
|
54
|
+
for entry in ssrf.get("protocol_patterns", []):
|
|
55
|
+
try:
|
|
56
|
+
compiled = re.compile(entry["pattern"], re.IGNORECASE)
|
|
57
|
+
result.append((compiled, entry["severity"], entry["message"]))
|
|
58
|
+
except re.error:
|
|
59
|
+
continue
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_ssrf_metadata_patterns(patterns: dict | None = None) -> list[tuple[re.Pattern, str, str]]:
|
|
64
|
+
"""Get compiled SSRF metadata endpoint regex patterns."""
|
|
65
|
+
ssrf = get_ssrf_patterns(patterns)
|
|
66
|
+
result = []
|
|
67
|
+
for entry in ssrf.get("metadata_patterns", []):
|
|
68
|
+
try:
|
|
69
|
+
compiled = re.compile(entry["pattern"], re.IGNORECASE)
|
|
70
|
+
result.append((compiled, entry["severity"], entry["message"]))
|
|
71
|
+
except re.error:
|
|
72
|
+
continue
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_ssrf_internal_network_patterns(
|
|
77
|
+
patterns: dict | None = None,
|
|
78
|
+
) -> list[tuple[re.Pattern, str, str]]:
|
|
79
|
+
"""Get compiled SSRF internal network regex patterns."""
|
|
80
|
+
ssrf = get_ssrf_patterns(patterns)
|
|
81
|
+
result = []
|
|
82
|
+
for entry in ssrf.get("internal_network", []):
|
|
83
|
+
try:
|
|
84
|
+
compiled = re.compile(entry["pattern"])
|
|
85
|
+
result.append((compiled, entry["severity"], entry["message"]))
|
|
86
|
+
except re.error:
|
|
87
|
+
continue
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_catch_noop_patterns(patterns: dict | None = None) -> list[re.Pattern]:
|
|
92
|
+
"""Get compiled catch block no-op regex patterns."""
|
|
93
|
+
p = patterns or load_patterns()
|
|
94
|
+
cb = p.get("catch_block", {})
|
|
95
|
+
result = []
|
|
96
|
+
for pat_str in cb.get("noop_patterns", []):
|
|
97
|
+
try:
|
|
98
|
+
result.append(re.compile(pat_str))
|
|
99
|
+
except re.error:
|
|
100
|
+
continue
|
|
101
|
+
return result
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""PentestTool base — interface every real DAST adapter implements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_log = logging.getLogger("patchi.pentest.base")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PentestResult:
|
|
17
|
+
tool: str
|
|
18
|
+
success: bool
|
|
19
|
+
evidence: str = ""
|
|
20
|
+
findings: list[dict] = field(default_factory=list)
|
|
21
|
+
raw_output: str = ""
|
|
22
|
+
duration_ms: int = 0
|
|
23
|
+
error: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PentestTool(ABC):
|
|
27
|
+
"""Abstract base for real exploit tools."""
|
|
28
|
+
|
|
29
|
+
name: str = "base"
|
|
30
|
+
binary: str = "" # e.g. "nuclei" or "npx"
|
|
31
|
+
docker_image: str = "" # fallback
|
|
32
|
+
requires_target: bool = True
|
|
33
|
+
|
|
34
|
+
def is_available(self) -> bool:
|
|
35
|
+
if self.binary and shutil.which(self.binary):
|
|
36
|
+
return True
|
|
37
|
+
if self.docker_image and shutil.which("docker"):
|
|
38
|
+
try:
|
|
39
|
+
out = subprocess.run(
|
|
40
|
+
["docker", "image", "inspect", self.docker_image],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
timeout=5,
|
|
43
|
+
)
|
|
44
|
+
if out.returncode == 0:
|
|
45
|
+
return True
|
|
46
|
+
except Exception as exc: # noqa: BLE001
|
|
47
|
+
_log.debug("%s docker check failed: %s", self.name, exc)
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def run(
|
|
52
|
+
self,
|
|
53
|
+
target: str,
|
|
54
|
+
safe_mode: bool = True,
|
|
55
|
+
workspace: Path | None = None,
|
|
56
|
+
extra: dict | None = None,
|
|
57
|
+
) -> PentestResult:
|
|
58
|
+
"""Execute tool against target. Never raises — returns PentestResult."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Dalfox adapter — XSS (reflected/stored)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from patchi.core.security.pentest.base import PentestResult, PentestTool
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.pentest.dalfox")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DalfoxTool(PentestTool):
|
|
17
|
+
name = "dalfox"
|
|
18
|
+
binary = "dalfox"
|
|
19
|
+
docker_image = "hahwul/dalfox:latest"
|
|
20
|
+
|
|
21
|
+
def run(self, target: str, safe_mode: bool = True, workspace: Path | None = None, extra: dict | None = None) -> PentestResult:
|
|
22
|
+
t0 = time.monotonic()
|
|
23
|
+
base = ["dalfox", "url", target, "--silence", "--format", "json"]
|
|
24
|
+
if safe_mode:
|
|
25
|
+
base += ["--skip-bav"]
|
|
26
|
+
cmd = base if shutil.which("dalfox") else (["docker","run","--rm","hahwul/dalfox:latest","url",target,"--silence","--format","json"] if shutil.which("docker") else None)
|
|
27
|
+
if not cmd:
|
|
28
|
+
return PentestResult(tool=self.name, success=False, error="dalfox not available")
|
|
29
|
+
try:
|
|
30
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
31
|
+
out = proc.stdout + proc.stderr
|
|
32
|
+
findings=[]
|
|
33
|
+
for line in out.splitlines()[:100]:
|
|
34
|
+
if "poc" in line.lower() or "vuln" in line.lower():
|
|
35
|
+
findings.append({"type":"xss","detail": line[:300]})
|
|
36
|
+
return PentestResult(tool=self.name, success=True, evidence=f"dalfox {len(findings)} poc", findings=findings, raw_output=out[:8000], duration_ms=int((time.monotonic()-t0)*1000))
|
|
37
|
+
except subprocess.TimeoutExpired:
|
|
38
|
+
return PentestResult(tool=self.name, success=False, error="dalfox timeout 5m", duration_ms=int((time.monotonic()-t0)*1000))
|
|
39
|
+
except Exception as exc: # noqa: BLE001
|
|
40
|
+
return PentestResult(tool=self.name, success=False, error=str(exc), duration_ms=int((time.monotonic()-t0)*1000))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""FFuF adapter — discovery/fuzz."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from patchi.core.security.pentest.base import PentestResult, PentestTool
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.pentest.ffuf")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FfufTool(PentestTool):
|
|
17
|
+
name = "ffuf"
|
|
18
|
+
binary = "ffuf"
|
|
19
|
+
docker_image = "ffuf/ffuf:latest"
|
|
20
|
+
|
|
21
|
+
def run(self, target: str, safe_mode: bool = True, workspace: Path | None = None, extra: dict | None = None) -> PentestResult:
|
|
22
|
+
extra = extra or {}
|
|
23
|
+
wordlist = extra.get("wordlist", "/usr/share/wordlists/common.txt")
|
|
24
|
+
t0 = time.monotonic()
|
|
25
|
+
# ffuf target expected as https://host/FUZZ
|
|
26
|
+
if "FUZZ" not in target:
|
|
27
|
+
target = target.rstrip("/") + "/FUZZ"
|
|
28
|
+
base = ["ffuf", "-u", target, "-w", wordlist, "-json", "-s"]
|
|
29
|
+
if safe_mode:
|
|
30
|
+
base += ["-rate", "10"]
|
|
31
|
+
cmd = base if shutil.which("ffuf") else (["docker","run","--rm","ffuf/ffuf:latest","-u",target,"-w",wordlist,"-json","-s"] if shutil.which("docker") else None)
|
|
32
|
+
if not cmd:
|
|
33
|
+
return PentestResult(tool=self.name, success=False, error="ffuf not available")
|
|
34
|
+
try:
|
|
35
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
|
36
|
+
dur = int((time.monotonic()-t0)*1000)
|
|
37
|
+
findings=[]
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(proc.stdout)
|
|
40
|
+
for r in data.get("results", [])[:30]:
|
|
41
|
+
findings.append({"url": r.get("url",""), "status": r.get("status"), "length": r.get("length")})
|
|
42
|
+
except Exception as _exc:
|
|
43
|
+
_log.debug("ffuf json parse skipped: %s", _exc)
|
|
44
|
+
return PentestResult(tool=self.name, success=True, evidence=f"ffuf {len(findings)} hits", findings=findings, raw_output=proc.stdout[:6000], duration_ms=dur)
|
|
45
|
+
except subprocess.TimeoutExpired:
|
|
46
|
+
return PentestResult(tool=self.name, success=False, error="ffuf timeout 3m", duration_ms=int((time.monotonic()-t0)*1000))
|
|
47
|
+
except Exception as exc: # noqa: BLE001
|
|
48
|
+
return PentestResult(tool=self.name, success=False, error=str(exc), duration_ms=int((time.monotonic()-t0)*1000))
|