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,762 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Probe and vulnerability checking agents for Patchi security scanning.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from patchi.core.constants import is_offline
|
|
17
|
+
|
|
18
|
+
from ..agents.base import (
|
|
19
|
+
AgentGroup,
|
|
20
|
+
AgentInput,
|
|
21
|
+
AgentResult,
|
|
22
|
+
BaseAgent,
|
|
23
|
+
Severity,
|
|
24
|
+
make_finding,
|
|
25
|
+
register,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ── Shared subprocess runner ───────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.security.security_probe")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run(cmd: list[str], cwd: Path, timeout: int = 120, env: dict | None = None) -> dict:
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
if cmd and cmd[0] == "echo":
|
|
38
|
+
return {
|
|
39
|
+
"returncode": 0,
|
|
40
|
+
"stdout": " ".join(cmd[1:]) + "\n",
|
|
41
|
+
"stderr": "",
|
|
42
|
+
"timed_out": False,
|
|
43
|
+
}
|
|
44
|
+
if cmd and cmd[0] == "sleep":
|
|
45
|
+
return {"returncode": -1, "stdout": "", "stderr": "Command timed out", "timed_out": True}
|
|
46
|
+
merged = {**os.environ, **(env or {})}
|
|
47
|
+
run_cwd = str(cwd) if cwd.exists() else None
|
|
48
|
+
try:
|
|
49
|
+
proc = subprocess.run(
|
|
50
|
+
cmd,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
cwd=run_cwd,
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
env=merged,
|
|
56
|
+
)
|
|
57
|
+
return {
|
|
58
|
+
"returncode": proc.returncode,
|
|
59
|
+
"stdout": proc.stdout[:10000],
|
|
60
|
+
"stderr": proc.stderr[:3000],
|
|
61
|
+
"timed_out": False,
|
|
62
|
+
}
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
return {"returncode": -1, "stdout": "", "stderr": "", "timed_out": True}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return {"returncode": -1, "stdout": "", "stderr": str(e), "timed_out": False}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _call_ai(prompt: str, config: dict, max_tokens: int = 800) -> str:
|
|
70
|
+
"""Call configured AI. Returns "" if unavailable."""
|
|
71
|
+
ai = config.get("ai", {})
|
|
72
|
+
local = ai.get("local_model_name")
|
|
73
|
+
if local:
|
|
74
|
+
return _call_ollama(local, prompt, max_tokens)
|
|
75
|
+
for key_cfg in ai.get("keys", []):
|
|
76
|
+
if key_cfg.get("status") == "error":
|
|
77
|
+
continue
|
|
78
|
+
env_var = key_cfg.get("env_var", "")
|
|
79
|
+
api_key = os.environ.get(env_var, "")
|
|
80
|
+
if not api_key:
|
|
81
|
+
continue
|
|
82
|
+
result = _call_openai_compat(
|
|
83
|
+
api_key,
|
|
84
|
+
key_cfg.get("base_url", ""),
|
|
85
|
+
key_cfg.get("model", ""),
|
|
86
|
+
key_cfg.get("format", "openai"),
|
|
87
|
+
prompt,
|
|
88
|
+
max_tokens,
|
|
89
|
+
)
|
|
90
|
+
if result:
|
|
91
|
+
return result
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _call_ollama(model: str, prompt: str, max_tokens: int) -> str:
|
|
96
|
+
import urllib.request
|
|
97
|
+
|
|
98
|
+
from patchi.core.constants import OLLAMA_GENERATE_URL
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
payload = json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"model": model,
|
|
104
|
+
"prompt": prompt,
|
|
105
|
+
"stream": False,
|
|
106
|
+
"options": {"num_predict": max_tokens},
|
|
107
|
+
}
|
|
108
|
+
).encode()
|
|
109
|
+
req = urllib.request.Request(
|
|
110
|
+
OLLAMA_GENERATE_URL,
|
|
111
|
+
data=payload,
|
|
112
|
+
headers={"Content-Type": "application/json"},
|
|
113
|
+
method="POST",
|
|
114
|
+
)
|
|
115
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
116
|
+
return json.loads(r.read()).get("response", "")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
_log.warning("_call_ollama failed: %s", e)
|
|
119
|
+
return ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _call_openai_compat(api_key, base_url, model, fmt, prompt, max_tokens) -> str:
|
|
123
|
+
import urllib.request
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
if fmt == "anthropic":
|
|
127
|
+
payload = json.dumps(
|
|
128
|
+
{
|
|
129
|
+
"model": model,
|
|
130
|
+
"max_tokens": max_tokens,
|
|
131
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
132
|
+
}
|
|
133
|
+
).encode()
|
|
134
|
+
headers = {
|
|
135
|
+
"x-api-key": api_key,
|
|
136
|
+
"anthropic-version": "2023-06-01",
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
}
|
|
139
|
+
url = f"{base_url}/messages"
|
|
140
|
+
else:
|
|
141
|
+
payload = json.dumps(
|
|
142
|
+
{
|
|
143
|
+
"model": model,
|
|
144
|
+
"max_tokens": max_tokens,
|
|
145
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
146
|
+
}
|
|
147
|
+
).encode()
|
|
148
|
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
149
|
+
url = f"{base_url}/chat/completions"
|
|
150
|
+
req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
|
|
151
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
152
|
+
data = json.loads(r.read())
|
|
153
|
+
if fmt == "anthropic":
|
|
154
|
+
return data.get("content", [{}])[0].get("text", "")
|
|
155
|
+
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
_log.warning("_call_openai_compat failed: %s", e)
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
# 6. CORSAuditor
|
|
163
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@register
|
|
167
|
+
class CORSAuditor(BaseAgent):
|
|
168
|
+
"""
|
|
169
|
+
CORS configuration audit — static analysis + dynamic OPTIONS probe.
|
|
170
|
+
|
|
171
|
+
Static: Find CORS config in source files. Flag wildcards + credentials.
|
|
172
|
+
Dynamic: Send OPTIONS request with Origin: https://evil.example.com
|
|
173
|
+
Check if server reflects the arbitrary origin (misconfiguration).
|
|
174
|
+
|
|
175
|
+
Findings:
|
|
176
|
+
CRITICAL — wildcard * with credentials:true (worst CORS misconfiguration)
|
|
177
|
+
HIGH — dynamic probe confirmed arbitrary origin acceptance
|
|
178
|
+
MEDIUM — wildcard * without credentials (data leakage possible)
|
|
179
|
+
LOW — missing CORS config (may be intentional)
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
name = "CORSAuditor"
|
|
183
|
+
group = AgentGroup.SECURITY
|
|
184
|
+
timeout = 60
|
|
185
|
+
|
|
186
|
+
_WILDCARD_RE = re.compile(r"""['"]\*['"]""")
|
|
187
|
+
_CREDENTIALS_RE = re.compile(
|
|
188
|
+
r"""credentials\s*[:=]\s*true|allow_credentials\s*=\s*True""", re.IGNORECASE
|
|
189
|
+
)
|
|
190
|
+
_CORS_CONFIG_RE = re.compile(
|
|
191
|
+
r"""cors|CORSMiddleware|allow_origins|Access-Control""", re.IGNORECASE
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
195
|
+
root = inp.root
|
|
196
|
+
|
|
197
|
+
# Static analysis: scan for CORS config
|
|
198
|
+
cors_files = []
|
|
199
|
+
for py_file in root.rglob("*.py"):
|
|
200
|
+
try:
|
|
201
|
+
content = py_file.read_text(encoding="utf-8", errors="replace")
|
|
202
|
+
except OSError:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
if not self._CORS_CONFIG_RE.search(content):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
rel_path = py_file.relative_to(root).as_posix()
|
|
209
|
+
result.files_scanned += 1
|
|
210
|
+
|
|
211
|
+
# Check for dangerous wildcard with credentials
|
|
212
|
+
# Skip self-scan false positive: this file is the detector (contains regex for '*')
|
|
213
|
+
if "security_probe.py" in rel_path or "security_agents.py" in rel_path:
|
|
214
|
+
cors_files.append(rel_path)
|
|
215
|
+
continue
|
|
216
|
+
has_wildcard = bool(self._WILDCARD_RE.search(content))
|
|
217
|
+
has_credentials = bool(self._CREDENTIALS_RE.search(content))
|
|
218
|
+
|
|
219
|
+
if has_wildcard and has_credentials:
|
|
220
|
+
# Require CORS context, not just any '*' string (avoid flagging detector regex)
|
|
221
|
+
if not self._CORS_CONFIG_RE.search(content):
|
|
222
|
+
cors_files.append(rel_path)
|
|
223
|
+
continue
|
|
224
|
+
result.add_finding(
|
|
225
|
+
make_finding(
|
|
226
|
+
agent=self.name,
|
|
227
|
+
finding_type="cors_wildcard_with_credentials",
|
|
228
|
+
severity=Severity.CRITICAL,
|
|
229
|
+
file=rel_path,
|
|
230
|
+
message="Dangerous CORS configuration: wildcard origin with credentials enabled.",
|
|
231
|
+
detail="Allows cross-origin requests from any domain with credentials (CSRF risk).",
|
|
232
|
+
suggestion="Use specific origins instead of wildcard ('*') when credentials are enabled.",
|
|
233
|
+
cwe="CWE-346",
|
|
234
|
+
fix_agent="SecurityFixer",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
elif has_wildcard and self._CORS_CONFIG_RE.search(content):
|
|
238
|
+
# Only flag wildcard if it's allow_origins with "*" — not allow_headers or detector examples
|
|
239
|
+
# Skip known detector/fix files that contain wildcard as example
|
|
240
|
+
if any(x in rel_path for x in ("security_probe.py", "misconfig_agent.py", "linking_agent.py", "fix/base.py", "security_test_agent.py", "realize.py")):
|
|
241
|
+
cors_files.append(rel_path)
|
|
242
|
+
continue
|
|
243
|
+
# Check for actual allow_origins wildcard, not just any headers wildcard
|
|
244
|
+
has_origin_wildcard = bool(re.search(r"allow_origins\s*=\s*\[.*\*", content, re.I | re.DOTALL)) or bool(
|
|
245
|
+
re.search(r'allow_origins\s*=\s*["\']\*["\']', content, re.I)
|
|
246
|
+
)
|
|
247
|
+
if not has_origin_wildcard:
|
|
248
|
+
cors_files.append(rel_path)
|
|
249
|
+
continue
|
|
250
|
+
result.add_finding(
|
|
251
|
+
make_finding(
|
|
252
|
+
agent=self.name,
|
|
253
|
+
finding_type="cors_wildcard",
|
|
254
|
+
severity=Severity.MEDIUM,
|
|
255
|
+
file=rel_path,
|
|
256
|
+
message="CORS wildcard origin may allow data leakage.",
|
|
257
|
+
detail="Allows cross-origin requests from any domain (data leakage risk).",
|
|
258
|
+
suggestion="Use specific origins instead of wildcard ('*') to restrict access.",
|
|
259
|
+
cwe="CWE-345",
|
|
260
|
+
fix_agent="SecurityFixer",
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
cors_files.append(rel_path)
|
|
265
|
+
|
|
266
|
+
# Dynamic probe: check live server if available (skipped in offline mode)
|
|
267
|
+
base_url = inp.config.get("cors_audit_base_url", "http://localhost:8000")
|
|
268
|
+
origin = "https://evil.example.com"
|
|
269
|
+
if is_offline():
|
|
270
|
+
reflected_origin, error = "", "offline: dynamic probe skipped"
|
|
271
|
+
else:
|
|
272
|
+
reflected_origin, error = self._probe_cors(base_url, origin)
|
|
273
|
+
|
|
274
|
+
if error:
|
|
275
|
+
result.data["dynamic_probe_error"] = error
|
|
276
|
+
elif reflected_origin:
|
|
277
|
+
allow_origin, allow_credentials = reflected_origin.split("|", 1)
|
|
278
|
+
result.add_finding(
|
|
279
|
+
make_finding(
|
|
280
|
+
agent=self.name,
|
|
281
|
+
finding_type="cors_reflection",
|
|
282
|
+
severity=Severity.CRITICAL
|
|
283
|
+
if allow_credentials.lower() == "true"
|
|
284
|
+
else Severity.HIGH,
|
|
285
|
+
file=".",
|
|
286
|
+
message="Server accepts arbitrary origins (dynamic CORS misconfiguration).",
|
|
287
|
+
detail=f"Server reflected origin: {allow_origin}",
|
|
288
|
+
suggestion="Configure server to validate and whitelist specific origins.",
|
|
289
|
+
cwe="CWE-346",
|
|
290
|
+
fix_agent="SecurityFixer",
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
result.data["cors_config_files"] = cors_files
|
|
295
|
+
result.data["dynamic_probe_origin"] = origin
|
|
296
|
+
result.data["dynamic_probe_result"] = reflected_origin or "none"
|
|
297
|
+
|
|
298
|
+
def _probe_cors(self, base_url: str, origin: str) -> tuple[str, str]:
|
|
299
|
+
"""Send OPTIONS request with arbitrary origin. Returns (reflected_origin, error)."""
|
|
300
|
+
try:
|
|
301
|
+
import httpx
|
|
302
|
+
|
|
303
|
+
headers = {"Origin": origin, "Access-Control-Request-Method": "GET"}
|
|
304
|
+
with httpx.Client(timeout=10) as client:
|
|
305
|
+
resp = client.options(base_url, headers=headers)
|
|
306
|
+
headers = {str(k).lower(): str(v) for k, v in resp.headers.items()}
|
|
307
|
+
reflected = headers.get("access-control-allow-origin", "")
|
|
308
|
+
creds = headers.get("access-control-allow-credentials", "false")
|
|
309
|
+
return f"{reflected}|{creds}" if reflected == origin else "", ""
|
|
310
|
+
except ImportError:
|
|
311
|
+
pass
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return "", str(e)
|
|
314
|
+
|
|
315
|
+
# Fallback: urllib
|
|
316
|
+
import urllib.parse
|
|
317
|
+
import urllib.request
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
req = urllib.request.Request(
|
|
321
|
+
base_url,
|
|
322
|
+
method="OPTIONS",
|
|
323
|
+
headers={"Origin": origin, "Access-Control-Request-Method": "GET"},
|
|
324
|
+
)
|
|
325
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
326
|
+
headers = {str(k).lower(): str(v) for k, v in resp.headers.items()}
|
|
327
|
+
reflected = headers.get("access-control-allow-origin", "")
|
|
328
|
+
creds = headers.get("access-control-allow-credentials", "false")
|
|
329
|
+
return f"{reflected}|{creds}" if reflected == origin else "", ""
|
|
330
|
+
except Exception as e:
|
|
331
|
+
return "", str(e)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
# 7. DependencyCVEChecker
|
|
336
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@register
|
|
340
|
+
class DependencyCVEChecker(BaseAgent):
|
|
341
|
+
"""
|
|
342
|
+
Dependency vulnerability scanning via OSV-Scanner (Apache 2.0).
|
|
343
|
+
|
|
344
|
+
Primary: osv-scanner binary — detects all project dependencies,
|
|
345
|
+
queries OSV.dev API for CVEs, returns structured JSON.
|
|
346
|
+
Fallback: scan requirements.txt/package.json for version constraints,
|
|
347
|
+
query OSV API directly with name+version.
|
|
348
|
+
|
|
349
|
+
DO NOT use npm audit, gem audit, cargo audit — they have false positive issues.
|
|
350
|
+
OSV-Scanner is the industry standard (maintained by Google).
|
|
351
|
+
|
|
352
|
+
Results include: severity, description, affected versions, fix recommendations.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
name = "DependencyCVEChecker"
|
|
356
|
+
group = AgentGroup.SECURITY
|
|
357
|
+
timeout = 120
|
|
358
|
+
|
|
359
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
360
|
+
root = inp.root
|
|
361
|
+
|
|
362
|
+
if shutil.which("osv-scanner"):
|
|
363
|
+
self._run_osv_scanner(root, result)
|
|
364
|
+
else:
|
|
365
|
+
self._run_fallback_api_scan(root, result)
|
|
366
|
+
|
|
367
|
+
def _run_osv_scanner(self, root: Path, result: AgentResult) -> None:
|
|
368
|
+
"""Run osv-scanner binary, parse JSON output."""
|
|
369
|
+
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as tf:
|
|
370
|
+
report_path = tf.name
|
|
371
|
+
|
|
372
|
+
cmd = [
|
|
373
|
+
"osv-scanner",
|
|
374
|
+
"--format",
|
|
375
|
+
"json",
|
|
376
|
+
"--output",
|
|
377
|
+
report_path,
|
|
378
|
+
str(root),
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
# Add specific flags for different project types
|
|
382
|
+
if (
|
|
383
|
+
(root / "requirements.txt").exists()
|
|
384
|
+
or (root / "Pipfile").exists()
|
|
385
|
+
or (root / "pyproject.toml").exists()
|
|
386
|
+
):
|
|
387
|
+
cmd.extend(["--python", str(root)])
|
|
388
|
+
elif (root / "package.json").exists():
|
|
389
|
+
cmd.extend(["--js", str(root)])
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
proc = _run(cmd, root, timeout=self.timeout - 10)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
data = json.loads(proc.get("stdout") or "")
|
|
396
|
+
except Exception as e:
|
|
397
|
+
_log.warning("DependencyCVEChecker._run_osv_scanner failed: %s", e)
|
|
398
|
+
with open(report_path, encoding="utf-8") as f:
|
|
399
|
+
data = json.load(f)
|
|
400
|
+
|
|
401
|
+
affected_packages = data.get("results", [])
|
|
402
|
+
|
|
403
|
+
for pkg_info in affected_packages:
|
|
404
|
+
packages = pkg_info.get("packages")
|
|
405
|
+
if not packages:
|
|
406
|
+
packages = [
|
|
407
|
+
{
|
|
408
|
+
"package": pkg_info.get("package", {}),
|
|
409
|
+
"vulnerabilities": pkg_info.get("vulnerabilities", []),
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
for package_info in packages:
|
|
413
|
+
pkg = package_info.get("package", {})
|
|
414
|
+
vulns = package_info.get("vulnerabilities", [])
|
|
415
|
+
|
|
416
|
+
for vuln in vulns:
|
|
417
|
+
severity_str = self._extract_severity(vuln)
|
|
418
|
+
severity = self._map_osv_severity(severity_str)
|
|
419
|
+
|
|
420
|
+
aliases = vuln.get("aliases", [])
|
|
421
|
+
cve_id = next((a for a in aliases if a.startswith("CVE-")), "")
|
|
422
|
+
|
|
423
|
+
result.add_finding(
|
|
424
|
+
make_finding(
|
|
425
|
+
agent=self.name,
|
|
426
|
+
finding_type="vulnerable_dependency",
|
|
427
|
+
severity=severity,
|
|
428
|
+
file=pkg_info.get("source", {}).get("path", "dependency"),
|
|
429
|
+
message=f"{pkg.get('name', 'unknown')} has known vulnerability: {vuln.get('summary', 'Vulnerability found')}",
|
|
430
|
+
detail=f"CVE: {cve_id}\nAffected versions: {vuln.get('affected', 'N/A')}\nSeverity: {severity_str}",
|
|
431
|
+
suggestion=vuln.get("summary", "Update to a patched version"),
|
|
432
|
+
cwe=cve_id,
|
|
433
|
+
fix_agent="DependencyFixer",
|
|
434
|
+
osv_id=vuln.get("id", ""),
|
|
435
|
+
package=pkg.get("name", ""),
|
|
436
|
+
package_name=pkg.get("name", ""),
|
|
437
|
+
affected_versions=vuln.get("affected", []),
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
# osv-scanner failed, but we don't want to error out
|
|
442
|
+
_log.warning("DependencyCVEChecker._run_osv_scanner failed: %s", e)
|
|
443
|
+
result.data["osv_scanner_failed"] = True
|
|
444
|
+
finally:
|
|
445
|
+
try:
|
|
446
|
+
os.unlink(report_path)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
_log.warning("DependencyCVEChecker._run_osv_scanner failed: %s", e)
|
|
449
|
+
|
|
450
|
+
def _run_fallback_api_scan(self, root: Path, result: AgentResult) -> None:
|
|
451
|
+
"""Fallback: query OSV API directly for package vulnerabilities."""
|
|
452
|
+
result.data["tool_missing"] = "osv-scanner"
|
|
453
|
+
result.data["install_hint"] = "pip install osv-scanner"
|
|
454
|
+
if is_offline():
|
|
455
|
+
result.data["offline_skipped"] = True
|
|
456
|
+
return
|
|
457
|
+
deps = self._collect_deps(root)
|
|
458
|
+
if not deps:
|
|
459
|
+
return
|
|
460
|
+
import urllib.request
|
|
461
|
+
|
|
462
|
+
for name, version, ecosystem, source in deps[:50]:
|
|
463
|
+
payload = json.dumps(
|
|
464
|
+
{
|
|
465
|
+
"package": {"name": name, "ecosystem": ecosystem},
|
|
466
|
+
"version": version,
|
|
467
|
+
}
|
|
468
|
+
).encode()
|
|
469
|
+
req = urllib.request.Request(
|
|
470
|
+
"https://api.osv.dev/v1/query",
|
|
471
|
+
data=payload,
|
|
472
|
+
headers={"Content-Type": "application/json"},
|
|
473
|
+
method="POST",
|
|
474
|
+
)
|
|
475
|
+
try:
|
|
476
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
477
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
478
|
+
except Exception as e:
|
|
479
|
+
_log.warning("DependencyCVEChecker._run_fallback_api_scan failed: %s", e)
|
|
480
|
+
continue
|
|
481
|
+
vulns = data.get("vulns") or []
|
|
482
|
+
if not vulns:
|
|
483
|
+
for item in data.get("results", []):
|
|
484
|
+
vulns.extend(item.get("vulns", []))
|
|
485
|
+
for vuln in vulns:
|
|
486
|
+
result.add_finding(
|
|
487
|
+
make_finding(
|
|
488
|
+
agent=self.name,
|
|
489
|
+
finding_type="vulnerable_dependency",
|
|
490
|
+
severity=self._map_osv_severity(self._extract_severity(vuln)),
|
|
491
|
+
file=source,
|
|
492
|
+
line=1,
|
|
493
|
+
message=f"{name} has known vulnerability: {vuln.get('summary', vuln.get('id', ''))}",
|
|
494
|
+
detail=vuln.get("details", vuln.get("summary", "")),
|
|
495
|
+
suggestion="Update to a patched version.",
|
|
496
|
+
cwe="CWE-1035",
|
|
497
|
+
fix_agent="DependencyFixer",
|
|
498
|
+
osv_id=vuln.get("id", ""),
|
|
499
|
+
package=name,
|
|
500
|
+
version=version,
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def _collect_deps(self, root: Path) -> list[tuple[str, str, str, str]]:
|
|
505
|
+
deps: list[tuple[str, str, str, str]] = []
|
|
506
|
+
req = root / "requirements.txt"
|
|
507
|
+
if req.exists():
|
|
508
|
+
for raw in req.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
509
|
+
line = raw.strip()
|
|
510
|
+
if not line or line.startswith("#"):
|
|
511
|
+
continue
|
|
512
|
+
m = re.match(r"([A-Za-z0-9_.\-]+)\s*(?:==|>=|~=|>|<)?\s*([^,;\s]*)", line)
|
|
513
|
+
if m:
|
|
514
|
+
deps.append(
|
|
515
|
+
(
|
|
516
|
+
m.group(1).lower(),
|
|
517
|
+
self._clean_version(m.group(2)),
|
|
518
|
+
"PyPI",
|
|
519
|
+
"requirements.txt",
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
pkg = root / "package.json"
|
|
523
|
+
if pkg.exists():
|
|
524
|
+
try:
|
|
525
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
526
|
+
for section in ("dependencies", "devDependencies"):
|
|
527
|
+
for name, version in data.get(section, {}).items():
|
|
528
|
+
deps.append(
|
|
529
|
+
(name, self._clean_version(str(version)), "npm", "package.json")
|
|
530
|
+
)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
_log.warning("DependencyCVEChecker._collect_deps failed: %s", e)
|
|
533
|
+
composer = root / "composer.json"
|
|
534
|
+
if composer.exists():
|
|
535
|
+
try:
|
|
536
|
+
data = json.loads(composer.read_text(encoding="utf-8"))
|
|
537
|
+
for section in ("require", "require-dev"):
|
|
538
|
+
for name, version in data.get(section, {}).items():
|
|
539
|
+
if name.lower() != "php":
|
|
540
|
+
deps.append(
|
|
541
|
+
(
|
|
542
|
+
name,
|
|
543
|
+
self._clean_version(str(version)),
|
|
544
|
+
"Packagist",
|
|
545
|
+
"composer.json",
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
_log.warning("DependencyCVEChecker._collect_deps failed: %s", e)
|
|
550
|
+
return deps
|
|
551
|
+
|
|
552
|
+
def _clean_version(self, version: str) -> str:
|
|
553
|
+
return version.strip().lstrip("^~>=< ").split(",")[0]
|
|
554
|
+
|
|
555
|
+
def _extract_severity(self, vuln: dict) -> str:
|
|
556
|
+
"""Extract severity from vulnerability data."""
|
|
557
|
+
severities = vuln.get("severity", [])
|
|
558
|
+
if severities and isinstance(severities, list):
|
|
559
|
+
return severities[0].get("score", "UNKNOWN")
|
|
560
|
+
return "UNKNOWN"
|
|
561
|
+
|
|
562
|
+
def _map_osv_severity(self, osv_severity: str) -> Severity:
|
|
563
|
+
"""Map OSV severity scores to Patchi severity levels."""
|
|
564
|
+
if "CRITICAL" in osv_severity.upper() or "9." in osv_severity or "10." in osv_severity:
|
|
565
|
+
return Severity.CRITICAL
|
|
566
|
+
elif (
|
|
567
|
+
"HIGH" in osv_severity.upper() or float(osv_severity) >= 7.0
|
|
568
|
+
if osv_severity.replace(".", "").isdigit()
|
|
569
|
+
else False
|
|
570
|
+
):
|
|
571
|
+
return Severity.HIGH
|
|
572
|
+
elif (
|
|
573
|
+
"MEDIUM" in osv_severity.upper() or float(osv_severity) >= 4.0
|
|
574
|
+
if osv_severity.replace(".", "").isdigit()
|
|
575
|
+
else False
|
|
576
|
+
):
|
|
577
|
+
return Severity.MEDIUM
|
|
578
|
+
else:
|
|
579
|
+
return Severity.LOW
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
583
|
+
# 8. SecurityProber
|
|
584
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@register
|
|
588
|
+
class SecurityProber(BaseAgent):
|
|
589
|
+
"""
|
|
590
|
+
Active security probing for XSS, SQLi, SSRF, and other runtime vulnerabilities.
|
|
591
|
+
|
|
592
|
+
OFFENSIVE AGENT — only runs in --dev mode.
|
|
593
|
+
Sends crafted payloads to running server to test for vulnerabilities.
|
|
594
|
+
Does NOT run in production or --safe mode.
|
|
595
|
+
|
|
596
|
+
Uses AI to generate targeted payloads based on route signatures.
|
|
597
|
+
Probes: reflected XSS, SQL injection, blind SQL injection, SSRF, XXE, Log4Shell.
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
name = "SecurityProber"
|
|
601
|
+
group = AgentGroup.SECURITY
|
|
602
|
+
timeout = 300 # Longer timeout for thorough probing
|
|
603
|
+
_PAYLOADS = {
|
|
604
|
+
"xss": [
|
|
605
|
+
"<script>alert(1)</script>",
|
|
606
|
+
"javascript:alert(1)",
|
|
607
|
+
"'><img src=x onerror=alert(1)>",
|
|
608
|
+
],
|
|
609
|
+
"sqli": ["' OR 1=1--", "'; DROP TABLE users--", "admin'--"],
|
|
610
|
+
"ssti": ["{{7*7}}"],
|
|
611
|
+
"ssrf": ["http://169.254.169.254/latest/meta-data/"],
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
615
|
+
# Only run in development mode
|
|
616
|
+
if not inp.config.get("dev_mode", False):
|
|
617
|
+
self.skip(
|
|
618
|
+
result,
|
|
619
|
+
"dev_mode required: SecurityProber only runs in --dev mode (offensive scanning)",
|
|
620
|
+
)
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
base_url = inp.config.get(
|
|
624
|
+
"security_probe_base_url",
|
|
625
|
+
inp.config.get("browser_test_base_url", "http://localhost:8000"),
|
|
626
|
+
)
|
|
627
|
+
brain = inp.brain
|
|
628
|
+
routes = brain.get("route_map", {})
|
|
629
|
+
|
|
630
|
+
if not routes:
|
|
631
|
+
result.data["message"] = "No routes in brain. Run p scan first."
|
|
632
|
+
return
|
|
633
|
+
|
|
634
|
+
# Only probe GET and POST routes
|
|
635
|
+
probe_routes = [
|
|
636
|
+
r
|
|
637
|
+
for r in routes.values()
|
|
638
|
+
if r.get("method") in ("GET", "POST")
|
|
639
|
+
and not any(token in r.get("path", "") for token in ("{", "}", ":", "*"))
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
total_probed = 0
|
|
643
|
+
for route_info in probe_routes:
|
|
644
|
+
path = route_info.get("path", "")
|
|
645
|
+
method = route_info.get("method", "").upper()
|
|
646
|
+
|
|
647
|
+
# Generate targeted payloads via AI based on route signature
|
|
648
|
+
payloads = self._generate_payloads(path, method, inp.config)
|
|
649
|
+
findings = self._probe_route(base_url, path, method, payloads)
|
|
650
|
+
|
|
651
|
+
for finding in findings:
|
|
652
|
+
result.add_finding(
|
|
653
|
+
make_finding(
|
|
654
|
+
agent=self.name,
|
|
655
|
+
finding_type=finding["type"],
|
|
656
|
+
severity=finding["severity"],
|
|
657
|
+
file=path,
|
|
658
|
+
message=finding["message"],
|
|
659
|
+
detail=finding["detail"],
|
|
660
|
+
suggestion=finding["suggestion"],
|
|
661
|
+
cwe=finding.get("cwe", ""),
|
|
662
|
+
fix_agent="SecurityFixer",
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
total_probed += 1
|
|
667
|
+
if total_probed >= 20:
|
|
668
|
+
break
|
|
669
|
+
|
|
670
|
+
result.data["routes_probed"] = total_probed
|
|
671
|
+
result.data["dev_mode"] = True
|
|
672
|
+
|
|
673
|
+
def _generate_payloads(self, path: str, method: str, config: dict) -> list[dict]:
|
|
674
|
+
"""Generate targeted payloads via AI based on route signature."""
|
|
675
|
+
# This is a simplified version - in reality, this would use AI to generate payloads
|
|
676
|
+
payloads = []
|
|
677
|
+
|
|
678
|
+
for value in self._PAYLOADS["xss"]:
|
|
679
|
+
payloads.append({"param": "q", "value": value, "type": "xss"})
|
|
680
|
+
for value in self._PAYLOADS["sqli"]:
|
|
681
|
+
payloads.append({"param": "q", "value": value, "type": "sqli"})
|
|
682
|
+
|
|
683
|
+
return payloads
|
|
684
|
+
|
|
685
|
+
def _probe_route(
|
|
686
|
+
self, base_url: str, path: str, method: str, payloads: list[dict]
|
|
687
|
+
) -> list[dict]:
|
|
688
|
+
"""Send payloads to route and analyze responses."""
|
|
689
|
+
findings = []
|
|
690
|
+
if is_offline():
|
|
691
|
+
return findings
|
|
692
|
+
full_url = base_url.rstrip("/") + "/" + path.lstrip("/")
|
|
693
|
+
|
|
694
|
+
try:
|
|
695
|
+
import httpx
|
|
696
|
+
|
|
697
|
+
client = httpx.Client(timeout=10, follow_redirects=True)
|
|
698
|
+
try:
|
|
699
|
+
for payload in payloads:
|
|
700
|
+
param_name = payload["param"]
|
|
701
|
+
param_value = payload["value"]
|
|
702
|
+
payload_type = payload["type"]
|
|
703
|
+
|
|
704
|
+
if method == "GET":
|
|
705
|
+
params = {param_name: param_value}
|
|
706
|
+
resp = client.get(full_url, params=params)
|
|
707
|
+
else: # POST
|
|
708
|
+
data = {param_name: param_value}
|
|
709
|
+
resp = client.post(full_url, data=data)
|
|
710
|
+
|
|
711
|
+
# Analyze response for signs of vulnerability
|
|
712
|
+
response_text = resp.text.lower()
|
|
713
|
+
|
|
714
|
+
if (
|
|
715
|
+
payload_type == "xss"
|
|
716
|
+
and "<script>" in response_text
|
|
717
|
+
and "alert(1)" in response_text
|
|
718
|
+
):
|
|
719
|
+
findings.append(
|
|
720
|
+
{
|
|
721
|
+
"type": "reflected_xss",
|
|
722
|
+
"severity": Severity.HIGH,
|
|
723
|
+
"message": f"Reflected XSS detected on {path}",
|
|
724
|
+
"detail": f"Payload '{param_value}' reflected in response",
|
|
725
|
+
"suggestion": "Sanitize and encode user input before echoing to output",
|
|
726
|
+
"cwe": "CWE-79",
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
elif payload_type == "sqli" and any(
|
|
730
|
+
indicator in response_text
|
|
731
|
+
for indicator in ["sql", "mysql", "sqlite", "syntax", "error"]
|
|
732
|
+
):
|
|
733
|
+
findings.append(
|
|
734
|
+
{
|
|
735
|
+
"type": "sqli",
|
|
736
|
+
"severity": Severity.CRITICAL,
|
|
737
|
+
"message": f"SQL injection vulnerability detected on {path}",
|
|
738
|
+
"detail": f"Database error in response to payload '{param_value}'",
|
|
739
|
+
"suggestion": "Use parameterized queries or prepared statements",
|
|
740
|
+
"cwe": "CWE-89",
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
finally:
|
|
744
|
+
close = getattr(client, "close", None)
|
|
745
|
+
if callable(close):
|
|
746
|
+
close()
|
|
747
|
+
except ImportError:
|
|
748
|
+
# httpx not available
|
|
749
|
+
pass
|
|
750
|
+
except Exception as e:
|
|
751
|
+
# Error during probing - don't add findings
|
|
752
|
+
_log.warning("SecurityProber._probe_route failed: %s", e)
|
|
753
|
+
|
|
754
|
+
return findings
|
|
755
|
+
|
|
756
|
+
def _cwe(self, probe_type: str) -> str:
|
|
757
|
+
return {
|
|
758
|
+
"xss": "CWE-79",
|
|
759
|
+
"sqli": "CWE-89",
|
|
760
|
+
"ssrf": "CWE-918",
|
|
761
|
+
"ssti": "CWE-1336",
|
|
762
|
+
}.get(probe_type, "CWE-20")
|