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,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layer 2 AI Orchestrator — confirms medium-confidence findings via LLM.
|
|
3
|
+
|
|
4
|
+
Design:
|
|
5
|
+
- Batches similar findings (same file + CWE) into single AI calls
|
|
6
|
+
- Rate-limits: max N calls/minute (configurable, default 10)
|
|
7
|
+
- Caches results by (CWE + file + type) hash with 24h TTL
|
|
8
|
+
- Returns structured AIAnalysisResult per input finding
|
|
9
|
+
|
|
10
|
+
Token optimization:
|
|
11
|
+
- One AI call per batch instead of per-finding
|
|
12
|
+
- Cache avoids re-analyzing identical findings
|
|
13
|
+
- Rate limiting prevents token bursts
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
_log = logging.getLogger("patchi.security.layer2_orchestrator")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AIAnalysisResult:
|
|
30
|
+
"""Result from Layer 2 AI analysis."""
|
|
31
|
+
|
|
32
|
+
confirmed: bool = False
|
|
33
|
+
summary: str = ""
|
|
34
|
+
fix_code: str = ""
|
|
35
|
+
test_code: str = ""
|
|
36
|
+
confidence_adjustment: float = 0.0
|
|
37
|
+
tokens_used: int = 0
|
|
38
|
+
model: str = ""
|
|
39
|
+
need_human_review: bool = False
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"confirmed": self.confirmed,
|
|
44
|
+
"summary": self.summary,
|
|
45
|
+
"fix_code": self.fix_code,
|
|
46
|
+
"test_code": self.test_code,
|
|
47
|
+
"confidence_adjustment": self.confidence_adjustment,
|
|
48
|
+
"tokens_used": self.tokens_used,
|
|
49
|
+
"model": self.model,
|
|
50
|
+
"need_human_review": self.need_human_review,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_SYSTEM_PROMPT = """You are Patchi's Layer 2 AI security arbiter — the final
|
|
55
|
+
evidence check before a finding is auto-defended or discarded. Your verdicts
|
|
56
|
+
directly drive automation, so they must be evidence-based, never vibes.
|
|
57
|
+
|
|
58
|
+
For EACH finding you must decide: real exploitable issue, or false positive?
|
|
59
|
+
|
|
60
|
+
CONFIRM only if ALL of these hold:
|
|
61
|
+
1. You can quote the exact vulnerable code from the provided snippet as
|
|
62
|
+
`evidence_quote` (verbatim substring). No quote -> no confirmation.
|
|
63
|
+
2. You can name the taint SOURCE (where untrusted data enters) and the SINK
|
|
64
|
+
(dangerous operation) and connect them. Pattern-shaped code without an
|
|
65
|
+
attacker-reachable source is NOT confirmed (e.g. test fixtures, sample
|
|
66
|
+
configs, migrations, admin-only CLI scripts).
|
|
67
|
+
3. The code path is reachable in this project (not obviously dead code).
|
|
68
|
+
|
|
69
|
+
DISMISS (confirmed=false) when:
|
|
70
|
+
- The match is a test file, fixture, mock, or documentation example.
|
|
71
|
+
- A sanitizer/validation neutralizes the flow between source and sink.
|
|
72
|
+
- The "vulnerability" requires assumptions the snippet contradicts.
|
|
73
|
+
- It matches a known-safe wrapper the project defines.
|
|
74
|
+
|
|
75
|
+
Tool context: `tool_confidence` values are PRIORS from deterministic tools,
|
|
76
|
+
not proof. Multiple agreeing tools raise your prior; a lone regex-tier tool
|
|
77
|
+
lowers it. You are the tie-breaker, not an echo.
|
|
78
|
+
|
|
79
|
+
Output STRICT JSON only (no prose, no markdown fences):
|
|
80
|
+
{"findings": [
|
|
81
|
+
{"index": 0,
|
|
82
|
+
"confirmed": false,
|
|
83
|
+
"confidence_adjustment": -0.6,
|
|
84
|
+
"summary": "one sentence verdict with the reason",
|
|
85
|
+
"evidence_quote": "exact line from Code, or empty if dismissed",
|
|
86
|
+
"source_sink_path": "request.args.get -> execute (or 'none found')",
|
|
87
|
+
"fix_code": "",
|
|
88
|
+
"test_code": "",
|
|
89
|
+
"need_human_review": false}
|
|
90
|
+
]}
|
|
91
|
+
Rules:
|
|
92
|
+
- confidence_adjustment in [-1.0, +1.0]; dismissals usually negative,
|
|
93
|
+
confirmations positive; magnitude = how sure.
|
|
94
|
+
- Fixes minimal & secure (parameterized queries, arg lists, no eval).
|
|
95
|
+
- Tests must pass without mocking security controls.
|
|
96
|
+
- One entry per input finding, same order, same count."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_CACHE_TTL = 86400 # 24 hours
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Layer2Orchestrator:
|
|
103
|
+
"""AI analysis engine for medium-confidence findings."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, root: Path, config: dict | None = None):
|
|
106
|
+
self.root = root
|
|
107
|
+
self.config = config or {}
|
|
108
|
+
self._batch_size = self.config.get("pipeline", {}).get("ai_batch_size", 3)
|
|
109
|
+
self._rate_per_min = self.config.get("pipeline", {}).get("ai_rate_per_min", 10)
|
|
110
|
+
self._cache: dict[str, tuple[float, AIAnalysisResult]] = {}
|
|
111
|
+
self._call_timestamps: list[float] = []
|
|
112
|
+
|
|
113
|
+
def analyze(self, findings: list) -> list[AIAnalysisResult]:
|
|
114
|
+
"""Analyze findings, batching similar ones. Returns one result per input."""
|
|
115
|
+
if not findings:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
results: list[AIAnalysisResult | None] = [None] * len(findings)
|
|
119
|
+
uncached_indices: list[int] = []
|
|
120
|
+
|
|
121
|
+
# Step 1: Check cache
|
|
122
|
+
for i, gf in enumerate(findings):
|
|
123
|
+
key = self._cache_key(gf)
|
|
124
|
+
if key in self._cache:
|
|
125
|
+
ts, cached = self._cache[key]
|
|
126
|
+
if time.time() - ts < _CACHE_TTL:
|
|
127
|
+
results[i] = cached
|
|
128
|
+
continue
|
|
129
|
+
uncached_indices.append(i)
|
|
130
|
+
|
|
131
|
+
if not uncached_indices:
|
|
132
|
+
return results # type: ignore
|
|
133
|
+
|
|
134
|
+
# Step 2: Build batches from uncached findings
|
|
135
|
+
uncached = [findings[i] for i in uncached_indices]
|
|
136
|
+
batches = self._build_batches(uncached)
|
|
137
|
+
batch_map: dict[int, int] = {} # finding_index -> batch_index
|
|
138
|
+
for bi, batch in enumerate(batches):
|
|
139
|
+
for gf in batch:
|
|
140
|
+
idx = uncached_indices[uncached.index(gf)]
|
|
141
|
+
batch_map[idx] = bi
|
|
142
|
+
|
|
143
|
+
# Step 3: Analyze each batch (one AI call, per-finding verdicts)
|
|
144
|
+
batch_results: list[list[AIAnalysisResult]] = []
|
|
145
|
+
for batch in batches:
|
|
146
|
+
self._throttle_if_needed()
|
|
147
|
+
ai_results = self._analyze_batch(batch)
|
|
148
|
+
batch_results.append(ai_results)
|
|
149
|
+
|
|
150
|
+
# Cache per-finding results
|
|
151
|
+
for j, gf in enumerate(batch):
|
|
152
|
+
key = self._cache_key(gf)
|
|
153
|
+
cached_res = ai_results[j] if j < len(ai_results) else ai_results[-1]
|
|
154
|
+
self._cache[key] = (time.time(), cached_res)
|
|
155
|
+
|
|
156
|
+
# Step 4: Map batch results back to finding order
|
|
157
|
+
for idx, bi in batch_map.items():
|
|
158
|
+
per_finding = batch_results[bi]
|
|
159
|
+
results[idx] = per_finding[0] if len(per_finding) == 1 else None
|
|
160
|
+
|
|
161
|
+
# Positional pass for multi-finding batches: _analyze_batch already
|
|
162
|
+
# returned results aligned to the batch order; recover via batches.
|
|
163
|
+
if any(r is None for r in results):
|
|
164
|
+
for bi, batch in enumerate(batches):
|
|
165
|
+
per_finding = batch_results[bi]
|
|
166
|
+
for j, gf in enumerate(batch):
|
|
167
|
+
idx = uncached_indices[uncached.index(gf)]
|
|
168
|
+
if j < len(per_finding):
|
|
169
|
+
results[idx] = per_finding[j]
|
|
170
|
+
|
|
171
|
+
return results # type: ignore
|
|
172
|
+
|
|
173
|
+
def _build_batches(self, findings: list) -> list[list]:
|
|
174
|
+
"""Group findings by (file, CWE) to batch into single AI calls."""
|
|
175
|
+
groups: dict[str, list] = {}
|
|
176
|
+
for gf in findings:
|
|
177
|
+
f = gf.finding
|
|
178
|
+
group_key = f"{f.file}||{f.cwe}"
|
|
179
|
+
if group_key not in groups:
|
|
180
|
+
groups[group_key] = []
|
|
181
|
+
groups[group_key].append(gf)
|
|
182
|
+
|
|
183
|
+
batches = list(groups.values())
|
|
184
|
+
# Further split if a single group exceeds batch_size
|
|
185
|
+
result: list[list] = []
|
|
186
|
+
for batch in batches:
|
|
187
|
+
for i in range(0, len(batch), self._batch_size):
|
|
188
|
+
result.append(batch[i : i + self._batch_size])
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
def _analyze_batch(self, batch: list) -> list[AIAnalysisResult]:
|
|
192
|
+
"""One AI call per batch; returns one verdict per finding, in order."""
|
|
193
|
+
try:
|
|
194
|
+
from patchi.core.ai.client import call_ai_structured
|
|
195
|
+
except ImportError:
|
|
196
|
+
return [self._offline_result(batch)] * len(batch)
|
|
197
|
+
|
|
198
|
+
prompt = self._build_prompt(batch)
|
|
199
|
+
try:
|
|
200
|
+
response = call_ai_structured(
|
|
201
|
+
prompt=prompt,
|
|
202
|
+
system=_SYSTEM_PROMPT,
|
|
203
|
+
output_model=dict,
|
|
204
|
+
)
|
|
205
|
+
parsed = (
|
|
206
|
+
self._extract_json(response)
|
|
207
|
+
if isinstance(response, str)
|
|
208
|
+
else (response if isinstance(response, dict) else None)
|
|
209
|
+
)
|
|
210
|
+
if not parsed and isinstance(response, str):
|
|
211
|
+
return [self._offline_result(batch)] * len(batch)
|
|
212
|
+
if parsed is None and isinstance(response, dict):
|
|
213
|
+
parsed = response
|
|
214
|
+
|
|
215
|
+
if parsed and isinstance(parsed.get("findings"), list):
|
|
216
|
+
entries = parsed["findings"]
|
|
217
|
+
out: list[AIAnalysisResult] = []
|
|
218
|
+
for _j, entry in enumerate(entries[: len(batch)]):
|
|
219
|
+
if not isinstance(entry, dict):
|
|
220
|
+
continue
|
|
221
|
+
out.append(
|
|
222
|
+
AIAnalysisResult(
|
|
223
|
+
confirmed=bool(entry.get("confirmed", False)),
|
|
224
|
+
summary=str(entry.get("summary", ""))[:300],
|
|
225
|
+
fix_code=str(entry.get("fix_code", "") or ""),
|
|
226
|
+
test_code=str(entry.get("test_code", "") or ""),
|
|
227
|
+
confidence_adjustment=float(
|
|
228
|
+
entry.get("confidence_adjustment", 0.0) or 0.0
|
|
229
|
+
),
|
|
230
|
+
need_human_review=bool(entry.get("need_human_review", False)),
|
|
231
|
+
tokens_used=int(parsed.get("tokens_used", 0) or 0),
|
|
232
|
+
model=str(parsed.get("model", "unknown")),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
# fill any missing tail positions honestly
|
|
236
|
+
while len(out) < len(batch):
|
|
237
|
+
out.append(self._offline_result([batch[len(out)]]))
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
# Legacy single-verdict shape: applies to whole batch
|
|
241
|
+
if parsed and "confirmed" in parsed:
|
|
242
|
+
single = AIAnalysisResult(
|
|
243
|
+
confirmed=bool(parsed.get("confirmed", False)),
|
|
244
|
+
summary=str(parsed.get("summary", ""))[:300],
|
|
245
|
+
fix_code=str(parsed.get("fix_code", "") or ""),
|
|
246
|
+
test_code=str(parsed.get("test_code", "") or ""),
|
|
247
|
+
confidence_adjustment=float(parsed.get("confidence_adjustment", 0.0) or 0.0),
|
|
248
|
+
need_human_review=bool(parsed.get("need_human_review", False)),
|
|
249
|
+
tokens_used=int(parsed.get("tokens_used", 0) or 0),
|
|
250
|
+
model=str(parsed.get("model", "unknown")),
|
|
251
|
+
)
|
|
252
|
+
return [single] * len(batch)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
_log.warning("Layer2Orchestrator._analyze_batch failed: %s", e)
|
|
255
|
+
|
|
256
|
+
return [self._offline_result(batch)] * len(batch)
|
|
257
|
+
|
|
258
|
+
def _offline_result(self, batch: list) -> AIAnalysisResult:
|
|
259
|
+
"""Default result when AI is unavailable — queue for human review."""
|
|
260
|
+
return AIAnalysisResult(
|
|
261
|
+
confirmed=False,
|
|
262
|
+
summary="AI unavailable — queued for human review",
|
|
263
|
+
need_human_review=True,
|
|
264
|
+
confidence_adjustment=0.0,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _build_prompt(self, batch: list) -> str:
|
|
268
|
+
parts = [f"Analyze {len(batch)} security finding(s) from the same context:"]
|
|
269
|
+
for i, gf in enumerate(batch):
|
|
270
|
+
f = gf.finding
|
|
271
|
+
ext = Path(f.file).suffix if f.file else ""
|
|
272
|
+
sev = f.severity.value if hasattr(f.severity, "value") else str(f.severity)
|
|
273
|
+
# Tool-native confidence priors (bandit/semgrep tiers etc.)
|
|
274
|
+
extra = getattr(f, "extra", {}) or {}
|
|
275
|
+
tool_conf = extra.get("tool_confidence")
|
|
276
|
+
conf_note = (
|
|
277
|
+
f"ToolConfidence: {tool_conf:.2f} (deterministic-tool prior)"
|
|
278
|
+
if isinstance(tool_conf, (int, float))
|
|
279
|
+
else "ToolConfidence: none"
|
|
280
|
+
)
|
|
281
|
+
noise = getattr(f, "noise_category", None)
|
|
282
|
+
noise_note = f"\nNoiseCategory: {noise} (from a low-signal file class)" if noise else ""
|
|
283
|
+
parts.append(
|
|
284
|
+
f"\n--- Finding {i + 1} ---\n"
|
|
285
|
+
f"File: {f.file}:{f.line}\n"
|
|
286
|
+
f"CWE: {f.cwe}\n"
|
|
287
|
+
f"Severity: {sev}\n"
|
|
288
|
+
f"Type: {f.type}\n"
|
|
289
|
+
f"Message: {f.message}\n"
|
|
290
|
+
f"Detail: {getattr(f, 'detail', '')}\n"
|
|
291
|
+
f"Code:\n```{ext}\n{f.code_snippet}\n```\n"
|
|
292
|
+
f"Confirmed by: {', '.join(gf.confirmed_by) or 'single agent'}\n"
|
|
293
|
+
f"{conf_note}{noise_note}\n"
|
|
294
|
+
f"OWASP: {gf.owasp_category}\n"
|
|
295
|
+
)
|
|
296
|
+
parts.append(
|
|
297
|
+
"\nFor EACH finding return a verdict entry with evidence_quote "
|
|
298
|
+
"quoted verbatim from its Code block. JSON only."
|
|
299
|
+
)
|
|
300
|
+
return "\n".join(parts)
|
|
301
|
+
|
|
302
|
+
def _extract_json(self, text: str) -> dict | None:
|
|
303
|
+
"""Tolerant structured-output parser: bare JSON, fenced, or embedded."""
|
|
304
|
+
if not text:
|
|
305
|
+
return None
|
|
306
|
+
candidate = text.strip()
|
|
307
|
+
# strip markdown fences if present
|
|
308
|
+
if candidate.startswith("```"):
|
|
309
|
+
candidate = candidate.strip("`")
|
|
310
|
+
if candidate.lower().startswith("json"):
|
|
311
|
+
candidate = candidate[4:]
|
|
312
|
+
try:
|
|
313
|
+
parsed = json.loads(candidate)
|
|
314
|
+
return parsed if isinstance(parsed, dict) else None
|
|
315
|
+
except (json.JSONDecodeError, ValueError):
|
|
316
|
+
pass
|
|
317
|
+
# embedded object fallback: first { to matching last }
|
|
318
|
+
start, end = candidate.find("{"), candidate.rfind("}")
|
|
319
|
+
if 0 <= start < end:
|
|
320
|
+
try:
|
|
321
|
+
parsed = json.loads(candidate[start : end + 1])
|
|
322
|
+
return parsed if isinstance(parsed, dict) else None
|
|
323
|
+
except (json.JSONDecodeError, ValueError):
|
|
324
|
+
return None
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def _throttle_if_needed(self) -> None:
|
|
328
|
+
now = time.time()
|
|
329
|
+
window = 60
|
|
330
|
+
# Remove timestamps older than 60s
|
|
331
|
+
self._call_timestamps = [t for t in self._call_timestamps if now - t < window]
|
|
332
|
+
if len(self._call_timestamps) >= self._rate_per_min:
|
|
333
|
+
sleep_sec = window - (now - self._call_timestamps[0])
|
|
334
|
+
if sleep_sec > 0:
|
|
335
|
+
time.sleep(sleep_sec)
|
|
336
|
+
self._call_timestamps.append(time.time())
|
|
337
|
+
|
|
338
|
+
def _cache_key(self, gf) -> str:
|
|
339
|
+
f = gf.finding
|
|
340
|
+
raw = f"{f.file}|{f.line}|{f.type}|{f.cwe}"
|
|
341
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLMSecurityAgent — LLM/Agent prompt injection, insecure output handling, tool abuse.
|
|
3
|
+
|
|
4
|
+
Detects LLM security issues:
|
|
5
|
+
- Direct/indirect prompt injection vectors
|
|
6
|
+
- LLM output executed as code, SQL, or rendered as HTML
|
|
7
|
+
- Unbounded input lengths / recursive agent loops
|
|
8
|
+
- Sensitive data in prompt context
|
|
9
|
+
- Untrusted model sources
|
|
10
|
+
- Verbose error messages to LLM
|
|
11
|
+
- Tool permissions too broad
|
|
12
|
+
- Missing human-in-the-loop for destructive actions
|
|
13
|
+
|
|
14
|
+
Uses static pattern matching, NOT AI.
|
|
15
|
+
Does NOT write to disk. Does NOT touch the queue.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ast as py_ast
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
from ..agents.base import (
|
|
24
|
+
AgentDomain,
|
|
25
|
+
AgentGroup,
|
|
26
|
+
AgentInput,
|
|
27
|
+
AgentResult,
|
|
28
|
+
BaseAgent,
|
|
29
|
+
Finding,
|
|
30
|
+
Severity,
|
|
31
|
+
register,
|
|
32
|
+
safe_rglob,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register
|
|
37
|
+
class LLMSecurityAgent(BaseAgent):
|
|
38
|
+
"""Detects LLM/agent security vulnerabilities."""
|
|
39
|
+
|
|
40
|
+
group = AgentGroup.SECURITY
|
|
41
|
+
domain = AgentDomain.SECURITY
|
|
42
|
+
name = "LLMSecurityAgent"
|
|
43
|
+
description = "LLM prompt injection, insecure output handling, tool abuse, excessive agency"
|
|
44
|
+
|
|
45
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
46
|
+
inp.extra.get("skill_context", {})
|
|
47
|
+
source_patterns = ["*.py", "*.js", "*.ts", "*.jsx", "*.tsx"]
|
|
48
|
+
|
|
49
|
+
for pattern in source_patterns:
|
|
50
|
+
for fpath in safe_rglob(inp.root, pattern):
|
|
51
|
+
if not fpath.is_file():
|
|
52
|
+
continue
|
|
53
|
+
rel = fpath.relative_to(inp.root).as_posix()
|
|
54
|
+
try:
|
|
55
|
+
content = fpath.read_text(encoding="utf-8", errors="ignore")
|
|
56
|
+
except OSError:
|
|
57
|
+
continue
|
|
58
|
+
result.files_scanned += 1
|
|
59
|
+
lang = "python" if fpath.suffix == ".py" else "javascript"
|
|
60
|
+
|
|
61
|
+
if lang == "python":
|
|
62
|
+
self._scan_python(content, rel, result)
|
|
63
|
+
else:
|
|
64
|
+
self._scan_js_ts(content, rel, result)
|
|
65
|
+
|
|
66
|
+
def _scan_python(self, content: str, rel: str, result: AgentResult) -> None:
|
|
67
|
+
# ── Prompt injection — user input in system prompt ─────────────────────
|
|
68
|
+
for m in re.finditer(
|
|
69
|
+
r'(?i)(?:system_prompt|system_message|prompt)\s*=\s*f["\'].*\{', content
|
|
70
|
+
):
|
|
71
|
+
result.add_finding(
|
|
72
|
+
Finding(
|
|
73
|
+
agent=self.name,
|
|
74
|
+
type="prompt_injection",
|
|
75
|
+
severity=Severity.HIGH,
|
|
76
|
+
file=rel,
|
|
77
|
+
line=content[: m.start()].count("\n") + 1,
|
|
78
|
+
message="User-controlled string in system prompt — potential prompt injection",
|
|
79
|
+
suggestion="Use chat template separation with instruction/data delimiters",
|
|
80
|
+
cwe="CWE-94",
|
|
81
|
+
extra={"skill": "agent-security-audit.skill"},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# ── Insecure output — LLM output exec/eval'd ─────────────────────────
|
|
86
|
+
for m in re.finditer(
|
|
87
|
+
r"(?i)(?:exec|eval)\s*\(\s*(?:llm\.|model\.|response|result)\s*\[", content
|
|
88
|
+
):
|
|
89
|
+
result.add_finding(
|
|
90
|
+
Finding(
|
|
91
|
+
agent=self.name,
|
|
92
|
+
type="insecure_llm_output",
|
|
93
|
+
severity=Severity.CRITICAL,
|
|
94
|
+
file=rel,
|
|
95
|
+
line=content[: m.start()].count("\n") + 1,
|
|
96
|
+
message="LLM output passed to exec/eval — remote code execution risk",
|
|
97
|
+
suggestion="Use structured output (Pydantic) with schema validation instead",
|
|
98
|
+
cwe="CWE-94",
|
|
99
|
+
extra={"skill": "llm-risk-assess.skill"},
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# ── Recursive agent loop without depth limit ──────────────────────────
|
|
104
|
+
# (The function-self-call AST check below is authoritative; the old
|
|
105
|
+
# regex heuristic referenced a nonexistent \1 capture group and raised
|
|
106
|
+
# "invalid group reference" — removed.)
|
|
107
|
+
try:
|
|
108
|
+
tree = py_ast.parse(content)
|
|
109
|
+
for node in py_ast.walk(tree):
|
|
110
|
+
if isinstance(node, py_ast.FunctionDef):
|
|
111
|
+
self_name = node.name
|
|
112
|
+
for child in py_ast.walk(node):
|
|
113
|
+
if isinstance(child, py_ast.Call) and isinstance(child.func, py_ast.Name):
|
|
114
|
+
if child.func.id == self_name:
|
|
115
|
+
# Self-call — check no iteration guard
|
|
116
|
+
body_str = (
|
|
117
|
+
content[node.lineno : node.end_lineno]
|
|
118
|
+
if hasattr(node, "end_lineno")
|
|
119
|
+
else ""
|
|
120
|
+
)
|
|
121
|
+
if (
|
|
122
|
+
"max_iter" not in body_str
|
|
123
|
+
and "depth" not in body_str
|
|
124
|
+
and "count" not in body_str
|
|
125
|
+
):
|
|
126
|
+
result.add_finding(
|
|
127
|
+
Finding(
|
|
128
|
+
agent=self.name,
|
|
129
|
+
type="recursive_agent_loop",
|
|
130
|
+
severity=Severity.MEDIUM,
|
|
131
|
+
file=rel,
|
|
132
|
+
line=node.lineno,
|
|
133
|
+
message=f"Recursive self-call in {self_name} without iteration guard",
|
|
134
|
+
suggestion="Add max iteration counter (recommended: 25)",
|
|
135
|
+
cwe="CWE-835",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
break
|
|
139
|
+
except SyntaxError:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# ── Untrusted model source ─────────────────────────────────────────
|
|
143
|
+
bad_loads = [
|
|
144
|
+
r'(?i)AutoModel\.from_pretrained\(\s*["\'][^"\']*["\']\s*\)',
|
|
145
|
+
r'(?i)torch\.hub\.load\(\s*["\'][^"\']*["\']\s*\)',
|
|
146
|
+
r'(?i)torch\.load\(\s*["\'][^"\']*\.pt',
|
|
147
|
+
r"(?i)pickle\.load",
|
|
148
|
+
]
|
|
149
|
+
for pat in bad_loads:
|
|
150
|
+
for m in re.finditer(pat, content):
|
|
151
|
+
result.add_finding(
|
|
152
|
+
Finding(
|
|
153
|
+
agent=self.name,
|
|
154
|
+
type="untrusted_model_source",
|
|
155
|
+
severity=Severity.HIGH,
|
|
156
|
+
file=rel,
|
|
157
|
+
line=content[: m.start()].count("\n") + 1,
|
|
158
|
+
message="Model loaded from potentially untrusted source — verify origin or use safetensors",
|
|
159
|
+
suggestion="Pin model to SHA256 digest or verify source against allowlist",
|
|
160
|
+
cwe="CWE-1104",
|
|
161
|
+
extra={"skill": "llm-risk-assess.skill"},
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# ── Sensitive data in prompt context ────────────────────────────────
|
|
166
|
+
for m in re.finditer(
|
|
167
|
+
r'(?i)(?:db_url|api_key|password|\bsecret\b|\btoken\b).*llm\.|context.*=.*f["\']',
|
|
168
|
+
content,
|
|
169
|
+
):
|
|
170
|
+
result.add_finding(
|
|
171
|
+
Finding(
|
|
172
|
+
agent=self.name,
|
|
173
|
+
type="sensitive_data_in_prompt",
|
|
174
|
+
severity=Severity.HIGH,
|
|
175
|
+
file=rel,
|
|
176
|
+
line=content[: m.start()].count("\n") + 1,
|
|
177
|
+
message="Sensitive data potentially passed to LLM context",
|
|
178
|
+
suggestion="Filter or redact secrets before LLM call",
|
|
179
|
+
cwe="CWE-200",
|
|
180
|
+
extra={"skill": "llm-risk-assess.skill"},
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ── Tool permissions too broad ──────────────────────────────────────
|
|
185
|
+
for m in re.finditer(
|
|
186
|
+
r'(?i)permissions?\s*=\s*\["read"[,\s]*"write"[,\s]*"execute"\]', content
|
|
187
|
+
):
|
|
188
|
+
result.add_finding(
|
|
189
|
+
Finding(
|
|
190
|
+
agent=self.name,
|
|
191
|
+
type="excessive_tool_permissions",
|
|
192
|
+
severity=Severity.MEDIUM,
|
|
193
|
+
file=rel,
|
|
194
|
+
line=content[: m.start()].count("\n") + 1,
|
|
195
|
+
message="Tool has read+write+execute — apply least privilege",
|
|
196
|
+
suggestion="Restrict permissions per tool; use granular scopes",
|
|
197
|
+
cwe="CWE-269",
|
|
198
|
+
extra={"skill": "agent-security-audit.skill"},
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# ── Auto-approve destructive actions ───────────────────────────────
|
|
203
|
+
for m in re.finditer(r'(?i)agent\.run\(\s*["\'](?:delete|drop|remove|destroy)', content):
|
|
204
|
+
result.add_finding(
|
|
205
|
+
Finding(
|
|
206
|
+
agent=self.name,
|
|
207
|
+
type="excessive_agency",
|
|
208
|
+
severity=Severity.CRITICAL,
|
|
209
|
+
file=rel,
|
|
210
|
+
line=content[: m.start()].count("\n") + 1,
|
|
211
|
+
message="Destructive action auto-approved — no human in the loop",
|
|
212
|
+
suggestion="Implement confirmation gate for DELETE/DROP/REMOVE operations",
|
|
213
|
+
cwe="CWE-862",
|
|
214
|
+
extra={"skill": "agent-security-audit.skill"},
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _scan_js_ts(self, content: str, rel: str, result: AgentResult) -> None:
|
|
219
|
+
# ── LLM output rendered as HTML ─────────────────────────────────────
|
|
220
|
+
for m in re.finditer(
|
|
221
|
+
r"(?i)(?:innerHTML|outerHTML|\.html\(|dangerouslySetInnerHTML)\s*[=:].*llm|response|result",
|
|
222
|
+
content,
|
|
223
|
+
):
|
|
224
|
+
result.add_finding(
|
|
225
|
+
Finding(
|
|
226
|
+
agent=self.name,
|
|
227
|
+
type="insecure_llm_output_rendering",
|
|
228
|
+
severity=Severity.HIGH,
|
|
229
|
+
file=rel,
|
|
230
|
+
line=content[: m.start()].count("\n") + 1,
|
|
231
|
+
message="LLM output rendered as HTML without sanitization",
|
|
232
|
+
suggestion="Use textContent or DOMPurify sanitization",
|
|
233
|
+
cwe="CWE-79",
|
|
234
|
+
extra={"skill": "agent-security-audit.skill"},
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ── Tool shell injection (no validation) ───────────────────────────
|
|
239
|
+
for m in re.finditer(r"(?i)shell\s*:\s*true|shell\s*=\s*true", content):
|
|
240
|
+
result.add_finding(
|
|
241
|
+
Finding(
|
|
242
|
+
agent=self.name,
|
|
243
|
+
type="tool_shell_injection",
|
|
244
|
+
severity=Severity.HIGH,
|
|
245
|
+
file=rel,
|
|
246
|
+
line=content[: m.start()].count("\n") + 1,
|
|
247
|
+
message="Tool uses shell=true without input validation",
|
|
248
|
+
suggestion="Use subprocess without shell=True, validate input against allowlist",
|
|
249
|
+
cwe="CWE-78",
|
|
250
|
+
extra={"skill": "agent-security-audit.skill"},
|
|
251
|
+
)
|
|
252
|
+
)
|