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,476 @@
|
|
|
1
|
+
"""Secrets runtime management agent — validates secrets handling, rotation, and hardcoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from ..agents.base import (
|
|
11
|
+
AgentDomain,
|
|
12
|
+
AgentGroup,
|
|
13
|
+
AgentInput,
|
|
14
|
+
AgentResult,
|
|
15
|
+
AgentStatus,
|
|
16
|
+
BaseAgent,
|
|
17
|
+
Finding,
|
|
18
|
+
Severity,
|
|
19
|
+
make_finding,
|
|
20
|
+
register,
|
|
21
|
+
safe_rglob,
|
|
22
|
+
)
|
|
23
|
+
from ..brain.trace_log import trace_agent
|
|
24
|
+
|
|
25
|
+
# Secrets provider detection patterns
|
|
26
|
+
SECRETS_PROVIDERS = {
|
|
27
|
+
"hashicorp_vault": {
|
|
28
|
+
"import": r"import\s+hvac|from\s+hvac\b|import\s+vault|from\s+vault\b|vault\.client|vault\.Vault",
|
|
29
|
+
"config": r"VAULT_ADDR|VAULT_TOKEN|vault\.addr|vault\.token|VaultClient",
|
|
30
|
+
"lease": r"lease|renew|revoke|ttl|max_ttl",
|
|
31
|
+
},
|
|
32
|
+
"aws_secrets_manager": {
|
|
33
|
+
"import": r"import\s+boto3|from\s+boto3\b|aws.*secrets|secretsmanager",
|
|
34
|
+
"client": r"boto3\.client.*secretsmanager|SecretsManagerClient|get_secret_value",
|
|
35
|
+
"rotation": r"rotation|rotate.*secret|SecretRotation",
|
|
36
|
+
},
|
|
37
|
+
"aws_ssm": {
|
|
38
|
+
"import": r"import\s+boto3|from\s+boto3\b",
|
|
39
|
+
"client": r"ssm\.|SSM|get_parameter|ParameterStore",
|
|
40
|
+
},
|
|
41
|
+
"azure_key_vault": {
|
|
42
|
+
"import": r"from\s+azure\.keyvault|import\s+azure\.keyvault|KeyVaultClient",
|
|
43
|
+
"config": r"AZURE_KEYVAULT|KeyVaultSecret|vault_url",
|
|
44
|
+
},
|
|
45
|
+
"gcp_secret_manager": {
|
|
46
|
+
"import": r"from\s+google\.cloud.*secretmanager|import\s+google\.cloud.*secretmanager",
|
|
47
|
+
"client": r"SecretManagerServiceClient|access_secret_version",
|
|
48
|
+
},
|
|
49
|
+
"dotenv": {
|
|
50
|
+
"import": r"from\s+dotenv|import\s+dotenv|load_dotenv|dotenv_values",
|
|
51
|
+
"usage": r"os\.environ|os\.getenv|ENV\[|environ\[",
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Hardcoded secret patterns (merged from SecretsGuard)
|
|
56
|
+
HARDCODED_SECRET_PATTERNS = [
|
|
57
|
+
(r"(?:password|passwd|pwd)\s*=\s*[\"'][^\"']+[\"']", "Hardcoded password", Severity.CRITICAL),
|
|
58
|
+
(
|
|
59
|
+
r"(?:api[_-]?key|apikey)\s*=\s*[\"'][A-Za-z0-9_\-]{16,}[\"']",
|
|
60
|
+
"Hardcoded API key",
|
|
61
|
+
Severity.CRITICAL,
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
r"(?:secret|token)\s*=\s*[\"'][A-Za-z0-9_\-]{16,}[\"']",
|
|
65
|
+
"Hardcoded secret/token",
|
|
66
|
+
Severity.CRITICAL,
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
r"(?:aws[_-]?access[_-]?key[_-]?id)\s*=\s*[\"']AKIA[0-9A-Z]{16}[\"']",
|
|
70
|
+
"Hardcoded AWS access key",
|
|
71
|
+
Severity.CRITICAL,
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
r"(?:aws[_-]?secret[_-]?access[_-]?key)\s*=\s*[\"'][0-9a-zA-Z/+=]{40}[\"']",
|
|
75
|
+
"Hardcoded AWS secret key",
|
|
76
|
+
Severity.CRITICAL,
|
|
77
|
+
),
|
|
78
|
+
(r"(?:private[_-]?key)\s*=\s*[\"']-----BEGIN", "Hardcoded private key", Severity.CRITICAL),
|
|
79
|
+
(
|
|
80
|
+
r"(?:connection[_-]?string|dsn)\s*=\s*[\"'][^\"']*(?:password|pwd)=[^\"']+[\"']",
|
|
81
|
+
"Hardcoded connection string with password",
|
|
82
|
+
Severity.HIGH,
|
|
83
|
+
),
|
|
84
|
+
# Extended patterns from SecretsGuard
|
|
85
|
+
(
|
|
86
|
+
re.compile(
|
|
87
|
+
r'(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?key)\s*[:=]\s*["\'][A-Za-z0-9+/=_-]{16,}["\']',
|
|
88
|
+
re.I,
|
|
89
|
+
),
|
|
90
|
+
Severity.CRITICAL,
|
|
91
|
+
"Hardcoded API key (extended)",
|
|
92
|
+
),
|
|
93
|
+
(re.compile(r'(?:password|passwd|pwd)\s*[:=]\s*["\'][^"\']{4,}["\']', re.I), Severity.CRITICAL, "Hardcoded password (extended)"),
|
|
94
|
+
(re.compile(r'(?:secret|client[_-]?secret)\s*[:=]\s*["\'][A-Za-z0-9+/=_-]{8,}["\']', re.I), Severity.CRITICAL, "Hardcoded secret (extended)"),
|
|
95
|
+
(re.compile(r"AKIA[0-9A-Z]{16}"), Severity.CRITICAL, "AWS Access Key ID"),
|
|
96
|
+
(re.compile(r'["\']AIza[0-9A-Za-z_-]{35}["\']'), Severity.CRITICAL, "GCP API key"),
|
|
97
|
+
(re.compile(r"-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----", re.I), Severity.CRITICAL, "Private key"),
|
|
98
|
+
(re.compile(r"(?:mongodb|mysql|postgres|redis|amqp|smtp)://[^:]+:[^@]+@", re.I), Severity.HIGH, "Connection string with credentials"),
|
|
99
|
+
(re.compile(r'DATABASE_URL\s*[:=]\s*["\'][^"\']+:.*@', re.I), Severity.HIGH, "DATABASE_URL with password"),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
# Core dump and memory protection patterns
|
|
103
|
+
COREDUMP_PATTERNS = [
|
|
104
|
+
(r"core.*dump|coredump|CORE_PATTERN", "Core dump configuration"),
|
|
105
|
+
(r"ulimit.*-c|RLIMIT_CORE", "Core dump size limit"),
|
|
106
|
+
(r"/proc/sys/kernel/core_pattern", "Core dump pattern"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
CONTROL_IDS = {
|
|
110
|
+
"SECRETS-01": ("Hardcoded credentials in source", Severity.CRITICAL),
|
|
111
|
+
"SECRETS-02": ("Secrets in environment variables", Severity.MEDIUM),
|
|
112
|
+
"SECRETS-03": ("Missing lease renewal", Severity.HIGH),
|
|
113
|
+
"SECRETS-04": ("Secrets provider detected", Severity.INFO),
|
|
114
|
+
"SECRETS-05": ("Core dump exposure risk", Severity.HIGH),
|
|
115
|
+
"SECRETS-06": ("Secrets caching TTL misconfigured", Severity.MEDIUM),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Config/CI file patterns from SecretsGuard
|
|
119
|
+
CONFIG_CI_PATTERNS = [
|
|
120
|
+
".env*",
|
|
121
|
+
"*.yml",
|
|
122
|
+
"*.yaml",
|
|
123
|
+
"*.toml",
|
|
124
|
+
"*.ini",
|
|
125
|
+
"*.cfg",
|
|
126
|
+
"Dockerfile*",
|
|
127
|
+
"docker-compose*.yml",
|
|
128
|
+
"*.conf",
|
|
129
|
+
"**/.github/**",
|
|
130
|
+
"**/.gitlab-ci*",
|
|
131
|
+
"**/Jenkinsfile*",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def scan_code_for_secrets(code: str, file_path: str) -> list[dict]:
|
|
136
|
+
"""Scan a code string for secrets. Returns list of dicts with line, severity, message."""
|
|
137
|
+
findings = []
|
|
138
|
+
lines = code.splitlines()
|
|
139
|
+
for i, line in enumerate(lines, 1):
|
|
140
|
+
for pattern in HARDCODED_SECRET_PATTERNS:
|
|
141
|
+
if isinstance(pattern, tuple):
|
|
142
|
+
# Handle two formats: (regex, desc, severity) and (regex, severity, desc)
|
|
143
|
+
if len(pattern) == 3:
|
|
144
|
+
p0, p1, p2 = pattern
|
|
145
|
+
# If p1 is Severity enum, format is (regex, severity, desc)
|
|
146
|
+
if isinstance(p1, Severity):
|
|
147
|
+
regex, severity, desc = p0, p1, p2
|
|
148
|
+
else:
|
|
149
|
+
# Format is (regex, desc, severity)
|
|
150
|
+
regex, desc, severity = p0, p1, p2
|
|
151
|
+
else:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if isinstance(regex, str):
|
|
155
|
+
regex = re.compile(regex, re.I)
|
|
156
|
+
if regex.search(line):
|
|
157
|
+
findings.append(
|
|
158
|
+
{
|
|
159
|
+
"line": i,
|
|
160
|
+
"severity": severity.value,
|
|
161
|
+
"message": desc,
|
|
162
|
+
"evidence": line.strip()[:100],
|
|
163
|
+
"file": file_path,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
return findings
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def gate_check_proposed_code(proposed_code: str, file_path: str) -> tuple[bool, list[dict]]:
|
|
170
|
+
"""Check if proposed code introduces new secrets. Returns (safe, findings)."""
|
|
171
|
+
findings = scan_code_for_secrets(proposed_code, file_path)
|
|
172
|
+
return len(findings) == 0, findings
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
_log = logging.getLogger("patchi.security.secrets_runtime_agent")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@register
|
|
179
|
+
class SecretsRuntimeAgent(BaseAgent):
|
|
180
|
+
name = "SecretsRuntimeAgent"
|
|
181
|
+
group = AgentGroup.SECURITY
|
|
182
|
+
domain = AgentDomain.SECURITY
|
|
183
|
+
description = "Secrets runtime management: lease renewal, file-mount vs env var, dynamic secrets, caching TTL"
|
|
184
|
+
|
|
185
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
186
|
+
with trace_agent(self.name, inp.root) as trace:
|
|
187
|
+
findings: list[Finding] = []
|
|
188
|
+
files_scanned = 0
|
|
189
|
+
|
|
190
|
+
# Phase 1: Static analysis of source code
|
|
191
|
+
for fpath in safe_rglob(inp.root, "*.py"):
|
|
192
|
+
files_scanned += 1
|
|
193
|
+
try:
|
|
194
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
_log.warning("SecretsRuntimeAgent._run failed: %s", e)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
rel = str(fpath.relative_to(inp.root))
|
|
200
|
+
|
|
201
|
+
# Detect hardcoded secrets
|
|
202
|
+
for pattern, desc, severity in HARDCODED_SECRET_PATTERNS:
|
|
203
|
+
for m in re.finditer(pattern, content, re.IGNORECASE):
|
|
204
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
205
|
+
# Skip if it's in a test file or comment
|
|
206
|
+
if re.search(
|
|
207
|
+
r"#.*" + re.escape(m.group()),
|
|
208
|
+
content.split("\n")[line_no - 1 : line_no][0],
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
findings.append(
|
|
212
|
+
make_finding(
|
|
213
|
+
severity,
|
|
214
|
+
rel,
|
|
215
|
+
line_no,
|
|
216
|
+
f"SECRETS-01: {desc}",
|
|
217
|
+
f"Found {desc.lower()} in source code. Secrets should never be hardcoded.",
|
|
218
|
+
code_snippet=m.group()[:80]
|
|
219
|
+
+ ("..." if len(m.group()) > 80 else ""),
|
|
220
|
+
suggestion="Use a secrets manager or environment variables with proper secret rotation",
|
|
221
|
+
cwe="CWE-798",
|
|
222
|
+
control_id="SECRETS-01",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Detect secrets provider usage
|
|
227
|
+
provider_found = None
|
|
228
|
+
for provider_name, provider_patterns in SECRETS_PROVIDERS.items():
|
|
229
|
+
for _pattern_key, pattern in provider_patterns.items():
|
|
230
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
231
|
+
provider_found = provider_name
|
|
232
|
+
break
|
|
233
|
+
if provider_found:
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if provider_found:
|
|
237
|
+
findings.append(
|
|
238
|
+
make_finding(
|
|
239
|
+
Severity.INFO,
|
|
240
|
+
rel,
|
|
241
|
+
0,
|
|
242
|
+
f"SECRETS-04: Secrets provider detected: {provider_found}",
|
|
243
|
+
f"Code uses {provider_found} for secrets management. Verify proper configuration.",
|
|
244
|
+
suggestion=f"Review {provider_name} configuration for security best practices",
|
|
245
|
+
control_id="SECRETS-04",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Check for secrets in environment variables (os.environ usage)
|
|
250
|
+
env_usage = re.findall(r"os\.environ(?:\.get|\.pop|\[)|os\.getenv\(", content)
|
|
251
|
+
if env_usage:
|
|
252
|
+
# Check if secrets-related env vars are accessed
|
|
253
|
+
for m in re.finditer(r"os\.environ(?:\.get|\.pop|\[)|os\.getenv\(", content):
|
|
254
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
255
|
+
line_text = content.split("\n")[line_no - 1 : line_no][0]
|
|
256
|
+
# Check if the env var name contains secret-related keywords
|
|
257
|
+
secret_keywords = [
|
|
258
|
+
"password",
|
|
259
|
+
"secret",
|
|
260
|
+
"token",
|
|
261
|
+
"key",
|
|
262
|
+
"credential",
|
|
263
|
+
"auth",
|
|
264
|
+
]
|
|
265
|
+
if any(kw in line_text.lower() for kw in secret_keywords):
|
|
266
|
+
findings.append(
|
|
267
|
+
make_finding(
|
|
268
|
+
Severity.MEDIUM,
|
|
269
|
+
rel,
|
|
270
|
+
line_no,
|
|
271
|
+
"SECRETS-02: Secret accessed via environment variable",
|
|
272
|
+
"Secrets in environment variables may be exposed via /proc, logs, or crash dumps. Consider file mounts.",
|
|
273
|
+
code_snippet=line_text.strip()[:100],
|
|
274
|
+
suggestion="Use file-mounted secrets (e.g., Kubernetes secrets, Vault file backend) instead of env vars for sensitive values",
|
|
275
|
+
control_id="SECRETS-02",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Check for lease renewal / TTL handling
|
|
280
|
+
has_vault = re.search(
|
|
281
|
+
r"hvac|vault\.client|VAULT_ADDR|VaultClient", content, re.IGNORECASE
|
|
282
|
+
)
|
|
283
|
+
if has_vault:
|
|
284
|
+
has_lease = re.search(r"lease|renew|revoke|ttl|max_ttl", content, re.IGNORECASE)
|
|
285
|
+
if not has_lease:
|
|
286
|
+
findings.append(
|
|
287
|
+
make_finding(
|
|
288
|
+
Severity.HIGH,
|
|
289
|
+
rel,
|
|
290
|
+
0,
|
|
291
|
+
"SECRETS-03: Missing Vault lease renewal",
|
|
292
|
+
"Vault client detected without lease renewal logic. Secrets may expire and cause outages.",
|
|
293
|
+
suggestion="Implement lease renewal and automatic re-authentication",
|
|
294
|
+
control_id="SECRETS-03",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Check for caching TTL
|
|
299
|
+
has_cache = re.search(
|
|
300
|
+
r"cache|lru_cache|functools\.cache|TTLCache|cachetools", content, re.IGNORECASE
|
|
301
|
+
)
|
|
302
|
+
if has_cache and has_vault:
|
|
303
|
+
has_ttl = re.search(
|
|
304
|
+
r"ttl|expire|timeout|max_age|maxsize", content, re.IGNORECASE
|
|
305
|
+
)
|
|
306
|
+
if not has_ttl:
|
|
307
|
+
findings.append(
|
|
308
|
+
make_finding(
|
|
309
|
+
Severity.MEDIUM,
|
|
310
|
+
rel,
|
|
311
|
+
0,
|
|
312
|
+
"SECRETS-06: Secret caching without TTL",
|
|
313
|
+
"Secrets may be cached without expiration. Stale secrets could be used after rotation.",
|
|
314
|
+
suggestion="Set appropriate TTL on cached secrets and invalidate on rotation events",
|
|
315
|
+
control_id="SECRETS-06",
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Check for core dump exposure
|
|
320
|
+
for pattern, desc in COREDUMP_PATTERNS:
|
|
321
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
322
|
+
# Check if secrets exist in the same file
|
|
323
|
+
has_secrets = re.search(
|
|
324
|
+
r"password|secret|token|key|credential",
|
|
325
|
+
content,
|
|
326
|
+
re.IGNORECASE,
|
|
327
|
+
)
|
|
328
|
+
if has_secrets:
|
|
329
|
+
findings.append(
|
|
330
|
+
make_finding(
|
|
331
|
+
Severity.HIGH,
|
|
332
|
+
rel,
|
|
333
|
+
0,
|
|
334
|
+
"SECRETS-05: Core dump may expose secrets",
|
|
335
|
+
f"Core dump configuration found ({desc}) in code that handles secrets. Secrets may leak in core dumps.",
|
|
336
|
+
suggestion="Disable core dumps or ensure secrets are not in memory during crash-prone operations",
|
|
337
|
+
cwe="CWE-244",
|
|
338
|
+
control_id="SECRETS-05",
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Phase 2: Check configuration files (extended from SecretsGuard)
|
|
343
|
+
scanned = 0
|
|
344
|
+
for pattern in CONFIG_CI_PATTERNS:
|
|
345
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
346
|
+
if not fpath.is_file():
|
|
347
|
+
continue
|
|
348
|
+
files_scanned += 1
|
|
349
|
+
try:
|
|
350
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
351
|
+
except OSError:
|
|
352
|
+
continue
|
|
353
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
354
|
+
scanned += 1
|
|
355
|
+
secrets = scan_code_for_secrets(content, rel)
|
|
356
|
+
for s in secrets:
|
|
357
|
+
findings.append(
|
|
358
|
+
make_finding(
|
|
359
|
+
Severity(s["severity"]),
|
|
360
|
+
s["file"],
|
|
361
|
+
s["line"],
|
|
362
|
+
s["message"],
|
|
363
|
+
code_snippet=s["evidence"],
|
|
364
|
+
cwe="CWE-798",
|
|
365
|
+
control_id="SECRETS-01",
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Phase 3: Try gitleaks for hardcoded secrets detection
|
|
370
|
+
gitleaks_path = shutil.which("gitleaks")
|
|
371
|
+
if gitleaks_path:
|
|
372
|
+
try:
|
|
373
|
+
proc = subprocess.run(
|
|
374
|
+
[
|
|
375
|
+
gitleaks_path,
|
|
376
|
+
"detect",
|
|
377
|
+
"--source",
|
|
378
|
+
str(inp.root),
|
|
379
|
+
"--report-format",
|
|
380
|
+
"json",
|
|
381
|
+
"--no-banner",
|
|
382
|
+
],
|
|
383
|
+
capture_output=True,
|
|
384
|
+
text=True,
|
|
385
|
+
timeout=120,
|
|
386
|
+
)
|
|
387
|
+
if proc.stdout:
|
|
388
|
+
import json
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
data = json.loads(proc.stdout)
|
|
392
|
+
for item in data:
|
|
393
|
+
findings.append(
|
|
394
|
+
make_finding(
|
|
395
|
+
Severity.CRITICAL,
|
|
396
|
+
item.get("File", ""),
|
|
397
|
+
item.get("StartLine", 0),
|
|
398
|
+
"SECRETS-01: Hardcoded secret (gitleaks)",
|
|
399
|
+
f"Gitleaks detected: {item.get('Description', 'Unknown secret')}",
|
|
400
|
+
code_snippet=item.get("Match", "")[:100],
|
|
401
|
+
suggestion="Remove hardcoded secret and use secrets manager",
|
|
402
|
+
cwe="CWE-798",
|
|
403
|
+
control_id="SECRETS-01",
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
except json.JSONDecodeError:
|
|
407
|
+
pass
|
|
408
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
# Phase 4: Try semgrep for secrets patterns
|
|
412
|
+
semgrep_path = shutil.which("semgrep")
|
|
413
|
+
if semgrep_path:
|
|
414
|
+
from .security_config import _semgrep_config_value
|
|
415
|
+
semgrep_config = _semgrep_config_value(inp.root)
|
|
416
|
+
if not semgrep_config:
|
|
417
|
+
semgrep_path = None # offline + no local pack -> skip
|
|
418
|
+
if semgrep_path:
|
|
419
|
+
try:
|
|
420
|
+
proc = subprocess.run(
|
|
421
|
+
[
|
|
422
|
+
semgrep_path,
|
|
423
|
+
"--config",
|
|
424
|
+
semgrep_config,
|
|
425
|
+
"--include",
|
|
426
|
+
"*.py",
|
|
427
|
+
"--json",
|
|
428
|
+
"--quiet",
|
|
429
|
+
"--max-target-bytes",
|
|
430
|
+
"100000",
|
|
431
|
+
str(inp.root),
|
|
432
|
+
],
|
|
433
|
+
capture_output=True,
|
|
434
|
+
text=True,
|
|
435
|
+
timeout=120,
|
|
436
|
+
)
|
|
437
|
+
if proc.returncode == 0 and proc.stdout:
|
|
438
|
+
import json
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
data = json.loads(proc.stdout)
|
|
442
|
+
for r in data.get("results", []):
|
|
443
|
+
rule_id = r.get("check_id", "")
|
|
444
|
+
if any(
|
|
445
|
+
kw in rule_id.lower()
|
|
446
|
+
for kw in [
|
|
447
|
+
"secret",
|
|
448
|
+
"password",
|
|
449
|
+
"credential",
|
|
450
|
+
"hardcoded",
|
|
451
|
+
"key",
|
|
452
|
+
]
|
|
453
|
+
):
|
|
454
|
+
findings.append(
|
|
455
|
+
make_finding(
|
|
456
|
+
Severity.HIGH,
|
|
457
|
+
r.get("path", ""),
|
|
458
|
+
r.get("start", {}).get("line", 0),
|
|
459
|
+
f"Semgrep: {rule_id}",
|
|
460
|
+
r.get("extra", {}).get("message", ""),
|
|
461
|
+
code_snippet=r.get("extra", {}).get("lines", ""),
|
|
462
|
+
suggestion="Review semgrep finding for secrets security impact",
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
except json.JSONDecodeError:
|
|
466
|
+
pass
|
|
467
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
result.findings = findings
|
|
471
|
+
result.files_scanned = files_scanned
|
|
472
|
+
result.status = AgentStatus.DONE
|
|
473
|
+
trace.findings = len(findings)
|
|
474
|
+
trace.files_scanned = files_scanned
|
|
475
|
+
|
|
476
|
+
return
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security agents for Patchi.
|
|
3
|
+
|
|
4
|
+
All 54 agents are loaded lazily via _LazyAgent to ensure one failed import
|
|
5
|
+
does not crash the entire security module.
|
|
6
|
+
|
|
7
|
+
Tool wrappers:
|
|
8
|
+
- Bandit (Apache-2.0) — pip install bandit
|
|
9
|
+
- Semgrep CE (LGPL-2.1) — pip install semgrep
|
|
10
|
+
- Gitleaks (MIT) — binary from github.com/gitleaks/gitleaks/releases
|
|
11
|
+
- OSV-Scanner (Apache 2.0) — binary from github.com/google/osv-scanner/releases
|
|
12
|
+
- httpx (BSD) — pip install httpx (for dynamic CORS + header probes)
|
|
13
|
+
- Pysa (MIT) — pip install pyre-check fb-sapp
|
|
14
|
+
- CodeQL (Restricted) — binary from github.com/github/codeql-cli-binaries/releases
|
|
15
|
+
|
|
16
|
+
DO NOT use TruffleHog in hosted SaaS mode (AGPL-3.0).
|
|
17
|
+
Gitleaks is the correct secret scanner here (MIT).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
_log = logging.getLogger("patchi.security.agents")
|
|
26
|
+
|
|
27
|
+
_AGENT_MODULES: dict[str, str] = {
|
|
28
|
+
"AppMapperAgent": ".app_mapper",
|
|
29
|
+
"AuthenticationAuditAgent": ".auth_audit_agent",
|
|
30
|
+
"AuthZAgent": ".authz_agent",
|
|
31
|
+
"BanditAgent": ".bandit_agent",
|
|
32
|
+
"BlastRadiusAgent": ".blast_radius",
|
|
33
|
+
"BrowserTesterAgent": ".browser_tester",
|
|
34
|
+
"BusinessLogicAgent": ".business_logic_agent",
|
|
35
|
+
"CDNCacheSecurityAgent": ".cdn_cache_agent",
|
|
36
|
+
"CloudWAFDetector": ".cloud_waf_detector",
|
|
37
|
+
"ComplianceAgent": ".compliance_agent",
|
|
38
|
+
"CatchBlockAuditor": ".catch_block_auditor",
|
|
39
|
+
"ConfigAuditAgent": ".security_config",
|
|
40
|
+
"ContainerScannerAgent": ".container_scanner",
|
|
41
|
+
"CORSAuditor": ".security_probe",
|
|
42
|
+
"CodeqlAgent": ".codeql_agent",
|
|
43
|
+
"CryptoAgent": ".crypto_agent",
|
|
44
|
+
"CVEMonitorAgent": ".cve_monitor",
|
|
45
|
+
"DependencyCVEChecker": ".security_probe",
|
|
46
|
+
"DependencyVulnerabilityAgent": ".dependency_vulnerability_agent", "DASTAgent": ".dast_agent",
|
|
47
|
+
"DNSSecurityAgent": ".dns_security_agent",
|
|
48
|
+
"EmailAuthenticationAgent": ".email_authentication_agent",
|
|
49
|
+
"EvidenceAgent": ".evidence",
|
|
50
|
+
"FalcoRuntimeAgent": ".falco_runtime_agent",
|
|
51
|
+
"EnvVarValidator": ".env_var_validator",
|
|
52
|
+
"GovernanceAgent": ".governance",
|
|
53
|
+
"HeaderAuditAgent": ".security_config",
|
|
54
|
+
"HistoryAgent": ".history",
|
|
55
|
+
"IaCScannerAgent": ".iac_scanner",
|
|
56
|
+
"InsecureRandomnessAgent": ".insecure_randomness_agent",
|
|
57
|
+
"InjectionAgent": ".injection_agent",
|
|
58
|
+
"SemgrepAgent": ".sast_agent",
|
|
59
|
+
"JWTSecurityAgent": ".jwt_agent",
|
|
60
|
+
"KubernetesAgent": ".kubernetes_agent",
|
|
61
|
+
"LLMSecurityAgent": ".llm_security_agent",
|
|
62
|
+
"MisconfigAgent": ".misconfig_agent",
|
|
63
|
+
"MobileSecurityAgent": ".mobile_security_agent",
|
|
64
|
+
"NetworkAgent": ".network_agent",
|
|
65
|
+
"PlanAuditorAgent": ".plan_auditor",
|
|
66
|
+
"PolicyEngineAgent": ".policy_engine",
|
|
67
|
+
"PreCheckAgent": ".prechecks",
|
|
68
|
+
"PrivacyAgent": ".privacy_agent",
|
|
69
|
+
"PushNotificationAgent": ".push_notification_agent",
|
|
70
|
+
"PysaAgent": ".pysa_agent",
|
|
71
|
+
"RateLimitAuditor": ".security_config",
|
|
72
|
+
"RedTeamAgent": ".red_team_agent",
|
|
73
|
+
"RedTeamEngineAgent": ".red_team_engine",
|
|
74
|
+
"RuntimeValidatorAgent": ".runtime_validator",
|
|
75
|
+
"SamlSSOAgent": ".saml_sso_agent",
|
|
76
|
+
"SecretsRuntimeAgent": ".secrets_runtime_agent",
|
|
77
|
+
"SecretScanner": ".security_taint",
|
|
78
|
+
"SecurityProber": ".security_probe",
|
|
79
|
+
"SensitiveDataAgent": ".sensitive_data_agent",
|
|
80
|
+
"ServiceMeshAgent": ".service_mesh_agent",
|
|
81
|
+
"SessionManagementAgent": ".session_management_agent",
|
|
82
|
+
"SSRFProtectionAgent": ".ssrf_agent",
|
|
83
|
+
"SupplyChainAgent": ".supply_chain",
|
|
84
|
+
"TaintAnalyzer": ".security_taint",
|
|
85
|
+
"WebSocketSecurityAgent": ".websocket_security_agent",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _LazyAgent:
|
|
90
|
+
"""Lazily imports and caches a single agent class."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, name: str, module_path: str):
|
|
93
|
+
self._name = name
|
|
94
|
+
self._module_path = module_path
|
|
95
|
+
self._cls = None
|
|
96
|
+
self._error = None
|
|
97
|
+
|
|
98
|
+
def get(self):
|
|
99
|
+
if self._cls is not None:
|
|
100
|
+
return self._cls
|
|
101
|
+
if self._error is not None:
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
mod = importlib.import_module(self._module_path, __package__)
|
|
105
|
+
self._cls = getattr(mod, self._name, None)
|
|
106
|
+
if self._cls is None:
|
|
107
|
+
self._error = f"Class {self._name} not found in {self._module_path}"
|
|
108
|
+
_log.warning(self._error)
|
|
109
|
+
return self._cls
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self._error = f"Failed to import {self._name} from {self._module_path}: {e}"
|
|
112
|
+
_log.warning(self._error)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _make_proxy(name: str) -> _LazyAgent:
|
|
117
|
+
"""Create a lazy proxy and register it in this module's globals."""
|
|
118
|
+
return _LazyAgent(name, _AGENT_MODULES[name])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _LazyModuleProxy:
|
|
122
|
+
"""Module-level __getattr__ that lazily resolves agent imports."""
|
|
123
|
+
|
|
124
|
+
def __init__(self):
|
|
125
|
+
self._agents: dict[str, _LazyAgent] = {name: _make_proxy(name) for name in _AGENT_MODULES}
|
|
126
|
+
|
|
127
|
+
def __getattr__(self, name: str):
|
|
128
|
+
if name in self._agents:
|
|
129
|
+
cls = self._agents[name].get()
|
|
130
|
+
if cls is None:
|
|
131
|
+
msg = f"Security agent {name} failed to import (see logs)"
|
|
132
|
+
raise ImportError(msg)
|
|
133
|
+
return cls
|
|
134
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
135
|
+
raise AttributeError(msg)
|
|
136
|
+
|
|
137
|
+
def __dir__(self):
|
|
138
|
+
return list(self._agents.keys())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_proxy = _LazyModuleProxy()
|
|
142
|
+
|
|
143
|
+
__all__ = sorted(_AGENT_MODULES.keys())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def __getattr__(name: str):
|
|
147
|
+
return getattr(_proxy, name)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ── Eager registration pass ────────────────────────────────────────────────────
|
|
151
|
+
# The @register decorators only fire when each agent's module is actually
|
|
152
|
+
# imported. Without this pass, `import security_agents` would register ZERO
|
|
153
|
+
# agents (the lazy proxy defers everything), so Coordinator.run_group(SECURITY),
|
|
154
|
+
# `p security`, and `p agents list security` would silently run nothing.
|
|
155
|
+
# Each proxy.get() is individually exception-safe — one broken module logs a
|
|
156
|
+
# warning and is skipped, exactly the resilience the lazy design promised.
|
|
157
|
+
_registered = 0
|
|
158
|
+
_failures = 0
|
|
159
|
+
for _agent_name in sorted(_AGENT_MODULES):
|
|
160
|
+
try:
|
|
161
|
+
_cls = _proxy._agents[_agent_name].get()
|
|
162
|
+
if _cls is None:
|
|
163
|
+
_failures += 1
|
|
164
|
+
else:
|
|
165
|
+
_registered += 1
|
|
166
|
+
except Exception as e: # defensive: never let one agent block the rest
|
|
167
|
+
_failures += 1
|
|
168
|
+
_log.warning("security_agents eager registration failed for %s: %s", _agent_name, e)
|
|
169
|
+
if _failures:
|
|
170
|
+
_log.warning(
|
|
171
|
+
"security_agents: registered %d/%d agents (%d failed)",
|
|
172
|
+
_registered,
|
|
173
|
+
len(_AGENT_MODULES),
|
|
174
|
+
_failures,
|
|
175
|
+
)
|