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,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Attack Feedback — closes the loop between attack outcomes and learning.
|
|
3
|
+
|
|
4
|
+
When the RedTeamEngine or assurance attackers confirm a vulnerability:
|
|
5
|
+
1. The confirmed payload is added to the fuzz corpus as a high-score entry
|
|
6
|
+
2. False-positive paths are recorded in the ignore_learner
|
|
7
|
+
3. Agent trust scores are updated via the learning brain
|
|
8
|
+
|
|
9
|
+
This turns every scan into training data for the next scan.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from patchi.core.brain import learning as learn
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.security.attack_feedback")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def record_confirmed_attack(
|
|
25
|
+
root: Path,
|
|
26
|
+
attack: dict[str, Any],
|
|
27
|
+
*,
|
|
28
|
+
agent_name: str = "red_team",
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Feed a confirmed attack result into the learning loop.
|
|
31
|
+
|
|
32
|
+
``attack`` is a dict from RedTeamEngine or AttackPlanner with keys:
|
|
33
|
+
hypothesis, payload, endpoint, severity, evidence, tool, success
|
|
34
|
+
"""
|
|
35
|
+
payload = attack.get("payload", "")
|
|
36
|
+
endpoint = attack.get("endpoint", "")
|
|
37
|
+
severity = attack.get("severity", "medium")
|
|
38
|
+
evidence = attack.get("evidence", "")
|
|
39
|
+
|
|
40
|
+
# --- 1. Fuzz corpus: confirmed payloads are high-value seeds ---
|
|
41
|
+
try:
|
|
42
|
+
from patchi.core.fuzz.corpus import CorpusEntry, FuzzCorpus
|
|
43
|
+
|
|
44
|
+
corpus = FuzzCorpus(root)
|
|
45
|
+
label = f"confirmed_{attack.get('tool', 'unknown')}_{endpoint or 'global'}"
|
|
46
|
+
score = {"critical": 1.0, "high": 0.85, "medium": 0.6, "low": 0.4}.get(severity, 0.5)
|
|
47
|
+
corpus.add(
|
|
48
|
+
CorpusEntry(
|
|
49
|
+
label=label[:120],
|
|
50
|
+
value=payload[:500] if payload else evidence[:500],
|
|
51
|
+
strategy="attack_feedback",
|
|
52
|
+
score=score,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
corpus.save()
|
|
56
|
+
_log.info("Fed confirmed attack into corpus: %s (score=%.2f)", label[:60], score)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
_log.debug("Corpus update failed: %s", exc)
|
|
59
|
+
|
|
60
|
+
# --- 2. Learning brain: record acceptance (confirmed = good detection) ---
|
|
61
|
+
finding_type = attack.get("tool", "unknown")
|
|
62
|
+
learn.record_acceptance(finding_type, agent_name, root)
|
|
63
|
+
_log.info("Recorded acceptance for %s via %s", finding_type, agent_name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record_false_positive(
|
|
67
|
+
root: Path,
|
|
68
|
+
finding: dict[str, Any],
|
|
69
|
+
*,
|
|
70
|
+
agent_name: str = "unknown",
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Feed a rejected/false-positive finding into the learning loop.
|
|
73
|
+
|
|
74
|
+
Updates the ignore_learner FP stats and records a rejection.
|
|
75
|
+
"""
|
|
76
|
+
finding_type = finding.get("control_id", finding.get("type", "unknown"))
|
|
77
|
+
file_path = finding.get("file", "")
|
|
78
|
+
|
|
79
|
+
# --- 1. Learning brain: record rejection ---
|
|
80
|
+
learn.record_rejection(finding_type, agent_name, root)
|
|
81
|
+
|
|
82
|
+
# --- 2. Ignore learner: record FP for this file/directory ---
|
|
83
|
+
try:
|
|
84
|
+
from patchi.core.security.ignore_learner import IgnoreLearner
|
|
85
|
+
|
|
86
|
+
learner = IgnoreLearner(root)
|
|
87
|
+
# Build entries from known FP store
|
|
88
|
+
fp_store = root / ".patchi" / "known_false_positives.json"
|
|
89
|
+
fps: list[dict] = []
|
|
90
|
+
if fp_store.exists():
|
|
91
|
+
import json
|
|
92
|
+
|
|
93
|
+
fps = json.loads(fp_store.read_text(encoding="utf-8"))
|
|
94
|
+
|
|
95
|
+
# Add this new FP
|
|
96
|
+
fps.append(
|
|
97
|
+
{
|
|
98
|
+
"file": file_path,
|
|
99
|
+
"type": finding_type,
|
|
100
|
+
"timestamp": time.time(),
|
|
101
|
+
"agent": agent_name,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
fp_store.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
import json
|
|
106
|
+
|
|
107
|
+
fp_store.write_text(json.dumps(fps[-500:], indent=2), encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
# Rebuild learner with updated FP list
|
|
110
|
+
learner.build(known_fps=fps)
|
|
111
|
+
promoted = learner.promote_to_global()
|
|
112
|
+
if promoted:
|
|
113
|
+
_log.info("Promoted %d ignore patterns to global store", promoted)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
_log.debug("Ignore learner update failed: %s", exc)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def record_fix_outcome(
|
|
119
|
+
root: Path,
|
|
120
|
+
finding_type: str,
|
|
121
|
+
file_path: str,
|
|
122
|
+
fix_strategy: str,
|
|
123
|
+
accepted: bool,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Record whether a generated fix was accepted or rejected."""
|
|
126
|
+
if accepted:
|
|
127
|
+
learn.record_fix_pattern(finding_type, file_path, fix_strategy, "accepted", root)
|
|
128
|
+
learn.record_acceptance(finding_type, "auto_fixer", root)
|
|
129
|
+
else:
|
|
130
|
+
learn.record_rejection(finding_type, "auto_fixer", root)
|
|
131
|
+
_log.info(
|
|
132
|
+
"Fix outcome recorded: %s on %s → %s",
|
|
133
|
+
finding_type,
|
|
134
|
+
file_path,
|
|
135
|
+
"accepted" if accepted else "rejected",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_learning_summary(root: Path) -> dict:
|
|
140
|
+
"""Return the full learning summary for display."""
|
|
141
|
+
return learn.get_summary(root)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Attack scenario YAML library."""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Attack Tree generation — renders exploit chains as goal-oriented trees.
|
|
3
|
+
|
|
4
|
+
Where ChainAnalyzer answers "how do findings connect?", the attack tree
|
|
5
|
+
answers "what could an attacker achieve, and via which paths?".
|
|
6
|
+
|
|
7
|
+
A tree is built per GOAL (e.g. "Steal Secrets", "Execute Code", "Impersonate
|
|
8
|
+
Users"). Each chain whose terminal impact maps to that goal becomes a path in
|
|
9
|
+
the tree; intermediate nodes become branches. The result renders as an ASCII
|
|
10
|
+
tree for the CLI and a nested dict for the web panel.
|
|
11
|
+
|
|
12
|
+
Deterministic: same findings + graph -> same tree. Zero AI calls.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from patchi.core.security.chain_analyzer import Chain, ChainAnalyzer
|
|
23
|
+
|
|
24
|
+
_log = logging.getLogger("patchi.security.attack_tree")
|
|
25
|
+
|
|
26
|
+
# Impact finding types -> attacker goals
|
|
27
|
+
_GOAL_MAP: dict[str, str] = {
|
|
28
|
+
"secret": "Steal Credentials / Secrets",
|
|
29
|
+
"hardcoded": "Steal Credentials / Secrets",
|
|
30
|
+
"password": "Steal Credentials / Secrets",
|
|
31
|
+
"sensitive_data": "Exfiltrate Sensitive Data",
|
|
32
|
+
"session": "Impersonate Users",
|
|
33
|
+
"rce": "Execute Arbitrary Code",
|
|
34
|
+
"deserialization": "Execute Arbitrary Code",
|
|
35
|
+
"command_injection": "Execute Arbitrary Code",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TreeNode:
|
|
43
|
+
"""One node in an attack tree."""
|
|
44
|
+
|
|
45
|
+
label: str
|
|
46
|
+
detail: str = ""
|
|
47
|
+
children: list[TreeNode] = field(default_factory=list)
|
|
48
|
+
severity: str = "medium"
|
|
49
|
+
score: float = 0.0
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict:
|
|
52
|
+
return {
|
|
53
|
+
"label": self.label,
|
|
54
|
+
"detail": self.detail,
|
|
55
|
+
"severity": self.severity,
|
|
56
|
+
"score": round(self.score, 1),
|
|
57
|
+
"children": [c.to_dict() for c in self.children],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AttackTree:
|
|
63
|
+
"""Goal-oriented view over a set of exploit chains."""
|
|
64
|
+
|
|
65
|
+
goal: str
|
|
66
|
+
root: TreeNode
|
|
67
|
+
chain_count: int = 0
|
|
68
|
+
worst_severity: str = "info"
|
|
69
|
+
|
|
70
|
+
def render(self) -> str:
|
|
71
|
+
"""ASCII rendering for CLI output."""
|
|
72
|
+
lines = [f"[ATTACK TREE] Goal: {self.goal}"]
|
|
73
|
+
lines.append(f" Paths: {self.chain_count} Worst: {self.worst_severity}")
|
|
74
|
+
|
|
75
|
+
def _walk(node: TreeNode, prefix: str, is_last: bool) -> None:
|
|
76
|
+
connector = "`-- " if is_last else "|-- "
|
|
77
|
+
sev_tag = f" ({node.severity})" if node.severity != "medium" else ""
|
|
78
|
+
lines.append(f"{prefix}{connector}{node.label}{sev_tag}")
|
|
79
|
+
kids = node.children
|
|
80
|
+
for i, child in enumerate(kids):
|
|
81
|
+
ext = " " if is_last else "| "
|
|
82
|
+
_walk(child, prefix + ext, i == len(kids) - 1)
|
|
83
|
+
|
|
84
|
+
for i, child in enumerate(self.root.children):
|
|
85
|
+
_walk(child, "", i == len(self.root.children) - 1)
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"goal": self.goal,
|
|
91
|
+
"chain_count": self.chain_count,
|
|
92
|
+
"worst_severity": self.worst_severity,
|
|
93
|
+
"root": self.root.to_dict(),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _goal_for(chain: Chain) -> str | None:
|
|
98
|
+
"""Map a chain's terminal node to an attacker goal."""
|
|
99
|
+
terminal = chain.nodes[-1].finding.type.lower()
|
|
100
|
+
for pattern, goal in _GOAL_MAP.items():
|
|
101
|
+
if pattern in terminal:
|
|
102
|
+
return goal
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_attack_trees(
|
|
107
|
+
chains: list[Chain],
|
|
108
|
+
max_depth: int = 6,
|
|
109
|
+
) -> list[AttackTree]:
|
|
110
|
+
"""
|
|
111
|
+
Group chains into goal-oriented attack trees.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
chains: output of ChainAnalyzer.find_chains()
|
|
115
|
+
max_depth: cap on tree rendering depth
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Trees sorted by worst severity, highest first.
|
|
119
|
+
"""
|
|
120
|
+
by_goal: dict[str, list[Chain]] = {}
|
|
121
|
+
for chain in chains:
|
|
122
|
+
goal = _goal_for(chain)
|
|
123
|
+
if goal is None:
|
|
124
|
+
continue
|
|
125
|
+
by_goal.setdefault(goal, []).append(chain)
|
|
126
|
+
|
|
127
|
+
trees: list[AttackTree] = []
|
|
128
|
+
for goal, goal_chains in by_goal.items():
|
|
129
|
+
worst = max((_SEVERITY_ORDER.get(c.severity, 0) for c in goal_chains), default=0)
|
|
130
|
+
root = TreeNode(label=f"Goal: {goal}")
|
|
131
|
+
# Each chain becomes one path under the root; branch labels come from
|
|
132
|
+
# the entry/exploit steps, leaf is the impact.
|
|
133
|
+
for chain in sorted(goal_chains, key=lambda c: c.score, reverse=True)[:max_depth]:
|
|
134
|
+
branch_label = f"{chain.nodes[0].finding.type} @ {chain.nodes[0].finding.file}"
|
|
135
|
+
branch = TreeNode(
|
|
136
|
+
label=branch_label,
|
|
137
|
+
detail=chain.narrative,
|
|
138
|
+
severity=chain.severity,
|
|
139
|
+
score=chain.score,
|
|
140
|
+
)
|
|
141
|
+
if len(chain.nodes) > 2:
|
|
142
|
+
mid = TreeNode(label=f"via {chain.nodes[1].finding.type}")
|
|
143
|
+
leaf_label = f"{chain.nodes[-1].finding.type} @ {chain.nodes[-1].finding.file}"
|
|
144
|
+
leaf = TreeNode(
|
|
145
|
+
label=leaf_label,
|
|
146
|
+
severity=chain.severity,
|
|
147
|
+
score=chain.score,
|
|
148
|
+
)
|
|
149
|
+
mid.children.append(leaf)
|
|
150
|
+
branch.children.append(mid)
|
|
151
|
+
else:
|
|
152
|
+
leaf_label = f"{chain.nodes[-1].finding.type} @ {chain.nodes[-1].finding.file}"
|
|
153
|
+
branch.children.append(TreeNode(label=leaf_label, severity=chain.severity))
|
|
154
|
+
root.children.append(branch)
|
|
155
|
+
|
|
156
|
+
trees.append(
|
|
157
|
+
AttackTree(
|
|
158
|
+
goal=goal,
|
|
159
|
+
root=root,
|
|
160
|
+
chain_count=len(goal_chains),
|
|
161
|
+
worst_severity=[s for s, v in _SEVERITY_ORDER.items() if v == worst][0] or "info",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
trees.sort(key=lambda t: _SEVERITY_ORDER.get(t.worst_severity, 0), reverse=True)
|
|
166
|
+
return trees
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def trees_from_findings(
|
|
170
|
+
findings: Iterable[Any],
|
|
171
|
+
import_edges: dict[str, set[str]] | None = None,
|
|
172
|
+
) -> tuple[list[AttackTree], list[Chain]]:
|
|
173
|
+
"""Convenience: findings -> chains -> trees in one call."""
|
|
174
|
+
analyzer = ChainAnalyzer(findings, import_edges=import_edges)
|
|
175
|
+
chains = analyzer.find_chains()
|
|
176
|
+
return build_attack_trees(chains), chains
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuthenticationAuditAgent — Authentication and session security analysis.
|
|
3
|
+
|
|
4
|
+
Scans for:
|
|
5
|
+
- Missing CSRF protection on state-changing endpoints
|
|
6
|
+
- Weak password hashing (MD5, SHA1, no salt)
|
|
7
|
+
- Session fixation vulnerabilities
|
|
8
|
+
- Missing brute-force protection indicators
|
|
9
|
+
- Insecure session cookie configuration
|
|
10
|
+
- Missing MFA/2FA indicators
|
|
11
|
+
- OAuth implementation issues
|
|
12
|
+
- Hardcoded session secrets
|
|
13
|
+
- Missing rate limiting on auth endpoints
|
|
14
|
+
- Improper session invalidation on logout
|
|
15
|
+
|
|
16
|
+
Multi-language: scans all tree-sitter-supported source languages. Hash-function
|
|
17
|
+
calls are detected structurally via tree-sitter AST (find_calls); hardcoded
|
|
18
|
+
secrets, CSRF markers, OAuth config and rate-limiting indicators are detected
|
|
19
|
+
with high-quality literal-pattern regex (acceptable per the audit). Does NOT
|
|
20
|
+
call AI. Does NOT write to disk. Does NOT touch the queue.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, EXTENSION_MAP, Lang
|
|
31
|
+
|
|
32
|
+
from ..agents.base import (
|
|
33
|
+
AgentDomain,
|
|
34
|
+
AgentGroup,
|
|
35
|
+
AgentInput,
|
|
36
|
+
AgentResult,
|
|
37
|
+
AgentStatus,
|
|
38
|
+
BaseAgent,
|
|
39
|
+
Finding,
|
|
40
|
+
Severity,
|
|
41
|
+
make_finding,
|
|
42
|
+
register,
|
|
43
|
+
)
|
|
44
|
+
from ..brain.ast_utils import find_calls
|
|
45
|
+
|
|
46
|
+
# Source extensions to scan (language parity).
|
|
47
|
+
_SCAN_EXTENSIONS = [
|
|
48
|
+
".py",
|
|
49
|
+
".js",
|
|
50
|
+
".jsx",
|
|
51
|
+
".ts",
|
|
52
|
+
".tsx",
|
|
53
|
+
".java",
|
|
54
|
+
".rb",
|
|
55
|
+
".php",
|
|
56
|
+
".go",
|
|
57
|
+
".rs",
|
|
58
|
+
".cpp",
|
|
59
|
+
".cxx",
|
|
60
|
+
".cc",
|
|
61
|
+
".c",
|
|
62
|
+
".h",
|
|
63
|
+
".hpp",
|
|
64
|
+
".cs",
|
|
65
|
+
".kt",
|
|
66
|
+
".kts",
|
|
67
|
+
".swift",
|
|
68
|
+
".dart",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Hash function call names that are weak (detected structurally via AST).
|
|
72
|
+
_WEAK_HASH_NAMES = {"md5", "sha1", "sha256", "sha512", "crypt"}
|
|
73
|
+
# Per-name severity / message for AST-detected weak hashes.
|
|
74
|
+
_WEAK_HASH_INFO = {
|
|
75
|
+
"md5": (Severity.CRITICAL, "MD5 used for password hashing — cryptographically broken"),
|
|
76
|
+
"sha1": (Severity.CRITICAL, "SHA1 used for password hashing — cryptographically weak"),
|
|
77
|
+
"sha256": (
|
|
78
|
+
Severity.HIGH,
|
|
79
|
+
"SHA-256 used for password hashing — use bcrypt/scrypt/argon2 instead",
|
|
80
|
+
),
|
|
81
|
+
"sha512": (Severity.MEDIUM, "SHA-512 used for password hashing — prefer bcrypt/scrypt/argon2"),
|
|
82
|
+
"crypt": (Severity.MEDIUM, "Unix crypt() for password hashing — use modern alternatives"),
|
|
83
|
+
}
|
|
84
|
+
_GOOD_HASH_PATTERNS = [
|
|
85
|
+
re.compile(r"(?:bcrypt|scrypt|argon2|pbkdf2)", re.I),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# ── Weak password hashing literal patterns (regex fallback / unsupported langs) ──
|
|
89
|
+
_WEAK_HASH_PATTERNS = [
|
|
90
|
+
(
|
|
91
|
+
re.compile(r"(?:hashlib\.)?md5\s*\(", re.I),
|
|
92
|
+
Severity.CRITICAL,
|
|
93
|
+
"MD5 used for password hashing — cryptographically broken",
|
|
94
|
+
),
|
|
95
|
+
(
|
|
96
|
+
re.compile(r"(?:hashlib\.)?sha1\s*\(", re.I),
|
|
97
|
+
Severity.CRITICAL,
|
|
98
|
+
"SHA1 used for password hashing — cryptographically weak",
|
|
99
|
+
),
|
|
100
|
+
(
|
|
101
|
+
re.compile(r"(?:hashlib\.)?sha256\s*\(", re.I),
|
|
102
|
+
Severity.HIGH,
|
|
103
|
+
"SHA-256 used for password hashing — use bcrypt/scrypt/argon2 instead",
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
re.compile(r"hashlib\.sha512\s*\(", re.I),
|
|
107
|
+
Severity.MEDIUM,
|
|
108
|
+
"SHA-512 used for password hashing — prefer bcrypt/scrypt/argon2",
|
|
109
|
+
),
|
|
110
|
+
(
|
|
111
|
+
re.compile(r"crypt\s*\(", re.I),
|
|
112
|
+
Severity.MEDIUM,
|
|
113
|
+
"Unix crypt() for password hashing — use modern alternatives",
|
|
114
|
+
),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
# ── Session patterns ────────────────────────────────────────────────────
|
|
118
|
+
_SESSION_PATTERNS = [
|
|
119
|
+
(
|
|
120
|
+
re.compile(r"session\[.*?\]\s*=", re.I),
|
|
121
|
+
Severity.LOW,
|
|
122
|
+
"Session assignment — verify secure configuration",
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
re.compile(
|
|
126
|
+
r'(?:session\.secret|SECRET_KEY|session_secret)\s*[:=]\s*["\'][^"\']{4,}["\']', re.I
|
|
127
|
+
),
|
|
128
|
+
Severity.CRITICAL,
|
|
129
|
+
"Hardcoded session secret in source code",
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
re.compile(r"(?:cookie_httponly|httponly.*cookie|Secure.*cookie)", re.I),
|
|
133
|
+
Severity.LOW,
|
|
134
|
+
"Cookie security configuration found — verify all flags set",
|
|
135
|
+
),
|
|
136
|
+
(
|
|
137
|
+
re.compile(r"(?:set_cookie|set_cookie_attr|response\.set_cookie)", re.I),
|
|
138
|
+
Severity.LOW,
|
|
139
|
+
"Cookie being set — verify httponly, secure, and samesite flags",
|
|
140
|
+
),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# ── Auth endpoint patterns ──────────────────────────────────────────────
|
|
144
|
+
_AUTH_ENDPOINT_PATTERNS = [
|
|
145
|
+
re.compile(r"(?:/login|/signin|/auth|/authenticate|/register|/signup|/token)", re.I),
|
|
146
|
+
re.compile(r"(?:login|signin|auth|authenticate|register|signup)\s*\(", re.I),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# ── Rate limiting patterns ──────────────────────────────────────────────
|
|
150
|
+
_RATE_LIMIT_PATTERNS = [
|
|
151
|
+
re.compile(r"(?:rate.?limit|throttl|brute.?force|login.?attempt|max.?attempt)", re.I),
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# ── OAuth patterns ──────────────────────────────────────────────────────
|
|
155
|
+
_OAUTH_PATTERNS = [
|
|
156
|
+
(
|
|
157
|
+
re.compile(r"(?:oauth|openid|oidc)", re.I),
|
|
158
|
+
Severity.INFO,
|
|
159
|
+
"OAuth/OIDC code found — verify implementation security",
|
|
160
|
+
),
|
|
161
|
+
(
|
|
162
|
+
re.compile(r'(?:redirect_uri|callback_url)\s*[:=]\s*["\'][^"\']*["\']', re.I),
|
|
163
|
+
Severity.MEDIUM,
|
|
164
|
+
"OAuth redirect URI — verify it's not open redirect",
|
|
165
|
+
),
|
|
166
|
+
(
|
|
167
|
+
re.compile(r'(?:client_secret|client_id)\s*[:=]\s*["\'][^"\']{8,}["\']', re.I),
|
|
168
|
+
Severity.CRITICAL,
|
|
169
|
+
"OAuth client secret hardcoded in source",
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
_SKIP_DIRS = DEFAULT_IGNORE_DIRS
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_log = logging.getLogger("patchi.security.auth_audit_agent")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@register
|
|
180
|
+
class AuthenticationAuditAgent(BaseAgent):
|
|
181
|
+
"""Agent for auditing authentication and session security."""
|
|
182
|
+
|
|
183
|
+
group = AgentGroup.SECURITY
|
|
184
|
+
domain = AgentDomain.SECURITY
|
|
185
|
+
name = "AuthenticationAuditAgent"
|
|
186
|
+
description = (
|
|
187
|
+
"Auth security: CSRF, password hashing, session management, brute-force protection"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
191
|
+
from patchi.core.brain.project_context import agent_is_relevant
|
|
192
|
+
|
|
193
|
+
relevant, reason = agent_is_relevant(inp.domain, inp.purpose, self.name, inp.active_domains)
|
|
194
|
+
if not relevant:
|
|
195
|
+
result.status = AgentStatus.SKIPPED
|
|
196
|
+
result.data.update({"skip_reason": reason})
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
findings = []
|
|
200
|
+
files_scanned = 0
|
|
201
|
+
|
|
202
|
+
for ext in _SCAN_EXTENSIONS:
|
|
203
|
+
for p in self._safe_rglob(inp.root, f"*{ext}"):
|
|
204
|
+
rel = str(p.relative_to(inp.root))
|
|
205
|
+
if self._should_skip(rel):
|
|
206
|
+
continue
|
|
207
|
+
files_scanned += 1
|
|
208
|
+
try:
|
|
209
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
_log.warning("AuthenticationAuditAgent._run failed: %s", e)
|
|
212
|
+
continue
|
|
213
|
+
lang = EXTENSION_MAP.get(ext)
|
|
214
|
+
findings.extend(self._check_password_hashing(content, rel, lang))
|
|
215
|
+
findings.extend(self._check_session_security(content, rel))
|
|
216
|
+
findings.extend(self._check_oauth(content, rel))
|
|
217
|
+
findings.extend(self._check_rate_limiting(content, rel))
|
|
218
|
+
|
|
219
|
+
result.status = AgentStatus.DONE
|
|
220
|
+
result.findings = findings
|
|
221
|
+
result.files_scanned = files_scanned
|
|
222
|
+
result.data.update({"files_scanned": files_scanned, "finding_count": len(findings)})
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# ── Password hashing (AST + regex) ─────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
def _check_password_hashing(
|
|
228
|
+
self, content: str, rel_path: str, lang: Lang | None
|
|
229
|
+
) -> list[Finding]:
|
|
230
|
+
findings = []
|
|
231
|
+
lines = content.splitlines()
|
|
232
|
+
|
|
233
|
+
# AST-based weak hash detection (structural, language-agnostic where possible).
|
|
234
|
+
ast_hits: dict[int, tuple[Severity, str]] = {}
|
|
235
|
+
if lang is not None:
|
|
236
|
+
for call in find_calls(content, lang, _WEAK_HASH_NAMES):
|
|
237
|
+
name = call.get("name", "")
|
|
238
|
+
leaf = name.split(".")[-1].lower()
|
|
239
|
+
info = _WEAK_HASH_INFO.get(leaf)
|
|
240
|
+
if info:
|
|
241
|
+
ast_hits[call.get("line", 0)] = info
|
|
242
|
+
|
|
243
|
+
# Literal-pattern regex fallback (also catches hashlib.md5(...), etc.).
|
|
244
|
+
for line_num, line in enumerate(lines, 1):
|
|
245
|
+
stripped = line.strip()
|
|
246
|
+
if stripped.startswith("#"):
|
|
247
|
+
continue
|
|
248
|
+
for pattern, severity, message in _WEAK_HASH_PATTERNS:
|
|
249
|
+
if pattern.search(line):
|
|
250
|
+
ast_hits.setdefault(line_num, (severity, message))
|
|
251
|
+
|
|
252
|
+
has_good_hash = any(p.search(content) for p in _GOOD_HASH_PATTERNS)
|
|
253
|
+
|
|
254
|
+
for line_num in sorted(ast_hits):
|
|
255
|
+
severity, message = ast_hits[line_num]
|
|
256
|
+
if has_good_hash:
|
|
257
|
+
severity = Severity.INFO
|
|
258
|
+
message += " (but good hash found elsewhere in file)"
|
|
259
|
+
findings.append(
|
|
260
|
+
make_finding(
|
|
261
|
+
self.name,
|
|
262
|
+
"weak_password_hash",
|
|
263
|
+
severity,
|
|
264
|
+
rel_path,
|
|
265
|
+
message,
|
|
266
|
+
line=line_num,
|
|
267
|
+
code_snippet=lines[line_num - 1].strip()[:120],
|
|
268
|
+
suggestion="Use bcrypt, scrypt, or argon2 for password hashing.",
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return findings
|
|
272
|
+
|
|
273
|
+
# ── Literal-pattern checks (multi-language) ─────────────────────────────
|
|
274
|
+
|
|
275
|
+
def _check_session_security(self, content: str, rel_path: str) -> list[Finding]:
|
|
276
|
+
return self._line_scan(
|
|
277
|
+
content,
|
|
278
|
+
rel_path,
|
|
279
|
+
_SESSION_PATTERNS,
|
|
280
|
+
"session_security",
|
|
281
|
+
"Ensure session cookies use httponly, secure, and samesite flags.",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _check_oauth(self, content: str, rel_path: str) -> list[Finding]:
|
|
285
|
+
return self._line_scan(
|
|
286
|
+
content,
|
|
287
|
+
rel_path,
|
|
288
|
+
_OAUTH_PATTERNS,
|
|
289
|
+
"oauth_security",
|
|
290
|
+
"Verify OAuth implementation follows security best practices.",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _line_scan(self, content, rel_path, patterns, finding_type, suggestion) -> list[Finding]:
|
|
294
|
+
findings = []
|
|
295
|
+
for line_num, line in enumerate(content.splitlines(), 1):
|
|
296
|
+
stripped = line.strip()
|
|
297
|
+
if stripped.startswith("#"):
|
|
298
|
+
continue
|
|
299
|
+
for pattern, severity, message in patterns:
|
|
300
|
+
if pattern.search(line):
|
|
301
|
+
findings.append(
|
|
302
|
+
make_finding(
|
|
303
|
+
self.name,
|
|
304
|
+
finding_type,
|
|
305
|
+
severity,
|
|
306
|
+
rel_path,
|
|
307
|
+
message,
|
|
308
|
+
line=line_num,
|
|
309
|
+
code_snippet=stripped[:120],
|
|
310
|
+
suggestion=suggestion,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
return findings
|
|
314
|
+
|
|
315
|
+
def _check_rate_limiting(self, content: str, rel_path: str) -> list[Finding]:
|
|
316
|
+
has_rate_limit = any(p.search(content) for p in _RATE_LIMIT_PATTERNS)
|
|
317
|
+
has_auth_endpoint = any(p.search(content) for p in _AUTH_ENDPOINT_PATTERNS)
|
|
318
|
+
if has_auth_endpoint and not has_rate_limit:
|
|
319
|
+
return [
|
|
320
|
+
make_finding(
|
|
321
|
+
self.name,
|
|
322
|
+
"missing_rate_limit",
|
|
323
|
+
Severity.HIGH,
|
|
324
|
+
rel_path,
|
|
325
|
+
"Auth endpoints found but no rate limiting/brute-force protection detected",
|
|
326
|
+
suggestion="Add rate limiting to login/auth endpoints to prevent brute-force attacks.",
|
|
327
|
+
)
|
|
328
|
+
]
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
# ── Helpers ───────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
def _should_skip(self, rel_path: str) -> bool:
|
|
334
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
335
|
+
return any(p in _SKIP_DIRS for p in parts)
|
|
336
|
+
|
|
337
|
+
def _safe_rglob(self, root: Path, pattern: str):
|
|
338
|
+
"""Directory-walking with pruning."""
|
|
339
|
+
suffix = pattern.replace("*.", ".")
|
|
340
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
341
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
342
|
+
for fn in filenames:
|
|
343
|
+
if fn.endswith(suffix):
|
|
344
|
+
yield Path(dirpath) / fn
|