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,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaintAnalyzer — performs static application security testing using Semgrep.
|
|
3
|
+
|
|
4
|
+
PRIMARY ENGINE: Semgrep (github.com/returntocorp/semgrep, LGPL-2.1)
|
|
5
|
+
pip install semgrep OR brew install semgrep OR apt-get install semgrep
|
|
6
|
+
|
|
7
|
+
Semgrep is an industry-standard static analysis tool that uses pattern-based
|
|
8
|
+
scanning to detect security vulnerabilities, code quality issues, and best
|
|
9
|
+
practice violations.
|
|
10
|
+
|
|
11
|
+
FALLBACK: When semgrep is not installed, uses basic regex patterns with
|
|
12
|
+
false positive filtering similar to the secret scanner.
|
|
13
|
+
|
|
14
|
+
Design decisions:
|
|
15
|
+
- Only reports findings that trace to actual lines in actual files
|
|
16
|
+
- Handles missing tools gracefully without crashing
|
|
17
|
+
- Provides detailed vulnerability information when available
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from patchi.core.constants import is_offline
|
|
27
|
+
|
|
28
|
+
from ..agents.base import (
|
|
29
|
+
AgentDomain,
|
|
30
|
+
AgentGroup,
|
|
31
|
+
AgentInput,
|
|
32
|
+
AgentResult,
|
|
33
|
+
AgentStatus,
|
|
34
|
+
BaseAgent,
|
|
35
|
+
Finding,
|
|
36
|
+
Severity,
|
|
37
|
+
make_finding,
|
|
38
|
+
register,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_log = logging.getLogger("patchi.security.agents.sast_agent")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@register
|
|
45
|
+
class SemgrepAgent(BaseAgent):
|
|
46
|
+
"""Pattern-tier SAST via a local semgrep rule pack (no network needed).
|
|
47
|
+
|
|
48
|
+
Distinct from security_taint.TaintAnalyzer: that agent is AST taint
|
|
49
|
+
tracking + AI confirmation; this one is deterministic pattern matching.
|
|
50
|
+
Both feed the ConfidenceGate as separate confirming witnesses.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
group = AgentGroup.SECURITY
|
|
54
|
+
domain = AgentDomain.CODE_QUALITY
|
|
55
|
+
name = "SemgrepAgent"
|
|
56
|
+
description = "Deterministic pattern SAST using the bundled semgrep rule pack"
|
|
57
|
+
|
|
58
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
59
|
+
"""Run Semgrep if installed, otherwise use fallback regex."""
|
|
60
|
+
if not inp.scope:
|
|
61
|
+
# Scan entire project
|
|
62
|
+
scan_root = inp.root
|
|
63
|
+
else:
|
|
64
|
+
# Scan only specified files/directories
|
|
65
|
+
scan_root = inp.root
|
|
66
|
+
|
|
67
|
+
# Primary: run semgrep if installed
|
|
68
|
+
if self._is_semgrep_available():
|
|
69
|
+
semgrep_result = self._run_semgrep(scan_root, inp.scope)
|
|
70
|
+
if semgrep_result is not None:
|
|
71
|
+
result.findings.extend(semgrep_result)
|
|
72
|
+
result.status = AgentStatus.DONE
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Fallback: use existing patterns but with false-positive filtering
|
|
76
|
+
result.data["tool_missing"] = "semgrep"
|
|
77
|
+
result.data["install_hint"] = "pip install semgrep"
|
|
78
|
+
fallback_findings = self._run_fallback_regex(scan_root, inp.scope)
|
|
79
|
+
result.findings.extend(fallback_findings)
|
|
80
|
+
result.status = AgentStatus.DONE
|
|
81
|
+
|
|
82
|
+
def _is_semgrep_available(self) -> bool:
|
|
83
|
+
"""Check if semgrep is installed."""
|
|
84
|
+
try:
|
|
85
|
+
subprocess.run(["semgrep", "--version"], capture_output=True, check=True)
|
|
86
|
+
return True
|
|
87
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def _rules_pack(self, root: Path) -> Path | None:
|
|
91
|
+
"""Locate the in-repo deterministic rule pack (no network needed)."""
|
|
92
|
+
candidates = [
|
|
93
|
+
root / "patchi" / "core" / "security" / "semgrep_rules" / "security.yaml",
|
|
94
|
+
# when patchi itself is installed as a package
|
|
95
|
+
Path(__file__).parent / "semgrep_rules" / "security.yaml",
|
|
96
|
+
]
|
|
97
|
+
for c in candidates:
|
|
98
|
+
if c.is_file():
|
|
99
|
+
return c
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def _run_semgrep(self, root: Path, scope: list[str] | None) -> list[Finding] | None:
|
|
103
|
+
"""Run semgrep with the local rules pack; bounded network fallback.
|
|
104
|
+
|
|
105
|
+
Never falls back to --config=auto (the full registry pull can take many
|
|
106
|
+
minutes). When no local pack exists we only try the single, bounded
|
|
107
|
+
p/owasp-top-ten registry pack while online.
|
|
108
|
+
"""
|
|
109
|
+
pack = self._rules_pack(root)
|
|
110
|
+
configs = []
|
|
111
|
+
if pack is not None:
|
|
112
|
+
configs.append(str(pack))
|
|
113
|
+
elif not is_offline():
|
|
114
|
+
configs.append("p/owasp-top-ten")
|
|
115
|
+
if not configs:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
target = str(root)
|
|
119
|
+
env = {**os.environ, "SEMGREP_SEND_METRICS": "off"}
|
|
120
|
+
for config_value in configs:
|
|
121
|
+
cmd = ["semgrep", "--json", "--config", config_value, target]
|
|
122
|
+
try:
|
|
123
|
+
process_result = subprocess.run(
|
|
124
|
+
cmd,
|
|
125
|
+
capture_output=True,
|
|
126
|
+
text=True,
|
|
127
|
+
encoding="utf-8",
|
|
128
|
+
errors="replace",
|
|
129
|
+
timeout=300,
|
|
130
|
+
cwd=str(root),
|
|
131
|
+
env=env,
|
|
132
|
+
)
|
|
133
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
134
|
+
continue
|
|
135
|
+
except Exception: # noqa: BLE001
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# semgrep: 0 = clean, 1 = findings, 2 = config/runtime error
|
|
139
|
+
if process_result.returncode not in (0, 1):
|
|
140
|
+
_log.debug("semgrep rc=%s for config %s", process_result.returncode, config_value)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
parsed = json.loads(process_result.stdout)
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
continue
|
|
147
|
+
return self._parse_semgrep_results(parsed, root)
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _parse_semgrep_results(self, parsed: dict, root: Path) -> list[Finding]:
|
|
152
|
+
"""Parse semgrep JSON with full metadata fidelity."""
|
|
153
|
+
findings = []
|
|
154
|
+
for item in parsed.get("results", []):
|
|
155
|
+
path = item.get("path", "")
|
|
156
|
+
start_line = item.get("start", {}).get("line", 1)
|
|
157
|
+
check_id = item.get("check_id", "unknown")
|
|
158
|
+
# strip registry path prefixes ("python.lang.security.audit" etc.)
|
|
159
|
+
short_id = check_id.split(".")[-1] if "." in check_id else check_id
|
|
160
|
+
extra = item.get("extra", {})
|
|
161
|
+
message = extra.get("message", "Security vulnerability detected")
|
|
162
|
+
severity_raw = extra.get("severity", "WARNING")
|
|
163
|
+
|
|
164
|
+
severity = self._map_severity_from_semgrep(severity_raw)
|
|
165
|
+
meta = extra.get("metadata", {}) or {}
|
|
166
|
+
cwe_raw = meta.get("cwe") or ""
|
|
167
|
+
if isinstance(cwe_raw, list):
|
|
168
|
+
cwe_raw = cwe_raw[0] if cwe_raw else ""
|
|
169
|
+
cwe = str(cwe_raw).strip()
|
|
170
|
+
# normalize "CWE-89 (…)" / "89" spellings to CWE-<int>
|
|
171
|
+
if cwe and not cwe.upper().startswith("CWE"):
|
|
172
|
+
digits = "".join(ch for ch in cwe if ch.isdigit())
|
|
173
|
+
cwe = f"CWE-{int(digits)}" if digits else ""
|
|
174
|
+
|
|
175
|
+
abs_file_path = Path(root) / path
|
|
176
|
+
if not abs_file_path.exists():
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
finding = make_finding(
|
|
180
|
+
agent=self.name,
|
|
181
|
+
finding_type=short_id,
|
|
182
|
+
severity=severity,
|
|
183
|
+
file=path,
|
|
184
|
+
line=start_line,
|
|
185
|
+
message=f"SAST: {message}"[:400],
|
|
186
|
+
detail=message,
|
|
187
|
+
code_snippet=(extra.get("lines", "") or "")[:500],
|
|
188
|
+
suggestion="Review and fix the identified security vulnerability",
|
|
189
|
+
fix_agent="SecurityFixer",
|
|
190
|
+
cwe=cwe,
|
|
191
|
+
)
|
|
192
|
+
# tool's own confidence tier preserved for the ConfidenceGate
|
|
193
|
+
tier = str(meta.get("confidence", "")).upper()
|
|
194
|
+
if tier in ("HIGH", "MEDIUM", "LOW"):
|
|
195
|
+
finding.extra["tool_confidence"] = {"HIGH": 0.9, "MEDIUM": 0.6, "LOW": 0.3}[tier]
|
|
196
|
+
findings.append(finding)
|
|
197
|
+
|
|
198
|
+
return findings
|
|
199
|
+
|
|
200
|
+
def _map_severity_from_semgrep(self, semgrep_severity: str) -> Severity:
|
|
201
|
+
"""Map Semgrep severity levels to Patchi severity levels."""
|
|
202
|
+
mapping = {
|
|
203
|
+
"ERROR": Severity.HIGH,
|
|
204
|
+
"WARNING": Severity.MEDIUM,
|
|
205
|
+
"INFO": Severity.LOW,
|
|
206
|
+
}
|
|
207
|
+
return mapping.get(semgrep_severity.upper(), Severity.MEDIUM)
|
|
208
|
+
|
|
209
|
+
def _run_fallback_regex(self, root: Path, scope: list[str] | None) -> list[Finding]:
|
|
210
|
+
"""Fallback regex-based SAST detection with false positive filtering."""
|
|
211
|
+
# This would contain the original regex-based approach as fallback
|
|
212
|
+
# For now, empty: the local rule pack covers the primary path.
|
|
213
|
+
return []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sast_gate — fast synchronous SAST gate for the pre-commit hook.
|
|
3
|
+
|
|
4
|
+
Runs Bandit + Semgrep against a list of staged files and exits non-zero when a
|
|
5
|
+
HIGH/CRITICAL finding is present. Intended to be invoked from the generated
|
|
6
|
+
pre-commit hook as:
|
|
7
|
+
|
|
8
|
+
python -m patchi.core.security.sast_gate file1.py file2.py
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 clean (or tools unavailable — fail-open so the hook never hard-blocks
|
|
12
|
+
the developer when a binary is missing)
|
|
13
|
+
1 at least one HIGH/CRITICAL finding (blocking in --strict mode)
|
|
14
|
+
2 usage / unexpected error
|
|
15
|
+
|
|
16
|
+
This keeps the commit path fast: it scans only the staged files, never the
|
|
17
|
+
whole tree, and reuses the same agents/normalization as the full scanner.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from patchi.core.agents.base import AgentInput, Severity
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.core.security.sast_gate")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _gather(tool_name: str, files: list[Path]) -> list:
|
|
33
|
+
"""Run one tool over the given files; return normalized findings."""
|
|
34
|
+
import patchi.core.security.security_agents # noqa: F401 (registers agents)
|
|
35
|
+
from patchi.core.agents.base import list_agents
|
|
36
|
+
|
|
37
|
+
agents = {a.name: a for a in list_agents("security")}
|
|
38
|
+
cls = agents.get(tool_name)
|
|
39
|
+
if not cls:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
findings: list = []
|
|
43
|
+
# Scan each staged file in isolation so a single bad file can't poison the
|
|
44
|
+
# whole run and so output maps directly to the staged path.
|
|
45
|
+
for f in files:
|
|
46
|
+
if not f.exists():
|
|
47
|
+
continue
|
|
48
|
+
res = cls().run(AgentInput(root=f.parent, scope=[], brain={}, config={}, extra={}))
|
|
49
|
+
for finding in res.findings or []:
|
|
50
|
+
# Re-anchor the finding to the real (staged) path for reporting.
|
|
51
|
+
try:
|
|
52
|
+
finding.file = str(f)
|
|
53
|
+
except Exception as _exc: # pragma: no cover - defensive
|
|
54
|
+
_log.warning('_gather failed: %s', _exc)
|
|
55
|
+
findings.append(finding)
|
|
56
|
+
return findings
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str]) -> int:
|
|
60
|
+
files = [Path(a) for a in argv[1:] if a.endswith(".py")]
|
|
61
|
+
if not files:
|
|
62
|
+
sys.stdout.write("sast_gate: no Python files to scan\n")
|
|
63
|
+
sys.stdout.flush()
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
high_or_critical = 0
|
|
67
|
+
total = 0
|
|
68
|
+
for tool in ("BanditAgent", "SemgrepAgent"):
|
|
69
|
+
try:
|
|
70
|
+
found = _gather(tool, files)
|
|
71
|
+
except Exception as e: # fail-open: never hard-block on tool errors
|
|
72
|
+
sys.stderr.write(f"sast_gate: {tool} skipped ({e})\n")
|
|
73
|
+
sys.stderr.flush()
|
|
74
|
+
continue
|
|
75
|
+
for f in found:
|
|
76
|
+
total += 1
|
|
77
|
+
sev = getattr(f, "severity", None)
|
|
78
|
+
if sev in (Severity.HIGH, Severity.CRITICAL):
|
|
79
|
+
high_or_critical += 1
|
|
80
|
+
sys.stdout.write(
|
|
81
|
+
f" [{tool}] {getattr(f, 'severity', '?')} {getattr(f, 'type', '?')} "
|
|
82
|
+
f"{getattr(f, 'file', '?')}:{getattr(f, 'line', 0)} "
|
|
83
|
+
f"{getattr(f, 'cwe', '')}\n"
|
|
84
|
+
)
|
|
85
|
+
sys.stdout.flush()
|
|
86
|
+
|
|
87
|
+
sys.stdout.write(f"sast_gate: {total} finding(s), {high_or_critical} high/critical\n")
|
|
88
|
+
sys.stdout.flush()
|
|
89
|
+
if high_or_critical:
|
|
90
|
+
return 1
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScanScheduler — configurable periodic execution of Layer 1 detectors.
|
|
3
|
+
|
|
4
|
+
Config:
|
|
5
|
+
{
|
|
6
|
+
"pipeline": {
|
|
7
|
+
"scheduler": {
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"intervals": {
|
|
10
|
+
"default": "1h",
|
|
11
|
+
"overrides": {
|
|
12
|
+
"SecretScanner": "5m",
|
|
13
|
+
"CVEMonitorAgent": "30m",
|
|
14
|
+
"DependencyScanner": "24h",
|
|
15
|
+
"EnvScanner": "15m"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
The scheduler runs a background thread that checks due detectors every 60s.
|
|
23
|
+
Results persist to the existing SQLite history database.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
import threading
|
|
31
|
+
import time
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from datetime import UTC, datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from loguru import logger
|
|
38
|
+
|
|
39
|
+
from patchi.core import config as cfg
|
|
40
|
+
from patchi.core import memory as mem
|
|
41
|
+
from patchi.core.agents.base import AgentGroup, AgentInput, BaseAgent, list_agents
|
|
42
|
+
|
|
43
|
+
_log = logging.getLogger("patchi.security.scheduler")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_interval(s: str) -> int:
|
|
47
|
+
"""Parse interval string like '5m', '1h', '30s' into seconds."""
|
|
48
|
+
s = s.strip().lower()
|
|
49
|
+
m = re.match(r"^(\d+)\s*(s|m|h|d)$", s)
|
|
50
|
+
if not m:
|
|
51
|
+
logger.warning("Invalid interval '%s' — defaulting to 1h", s)
|
|
52
|
+
return 3600 # default 1h
|
|
53
|
+
val = int(m.group(1))
|
|
54
|
+
unit = m.group(2)
|
|
55
|
+
return {"s": val, "m": val * 60, "h": val * 3600, "d": val * 86400}[unit]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ScheduledAgent:
|
|
60
|
+
"""A scheduled agent with its interval and last-run tracking."""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
agent_cls: type[BaseAgent]
|
|
64
|
+
interval_sec: int
|
|
65
|
+
last_run: float = 0.0 # time.monotonic()
|
|
66
|
+
consecutive_failures: int = 0
|
|
67
|
+
enabled: bool = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ScheduleResult:
|
|
72
|
+
"""Result of a scheduled scan run."""
|
|
73
|
+
|
|
74
|
+
agent_name: str
|
|
75
|
+
status: str # "started" | "completed" | "skipped" | "failed"
|
|
76
|
+
duration_ms: int = 0
|
|
77
|
+
message: str = ""
|
|
78
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ScanScheduler:
|
|
82
|
+
"""
|
|
83
|
+
Configurable periodic scanner execution.
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
scheduler = ScanScheduler(root, config)
|
|
87
|
+
scheduler.start() # runs in background thread
|
|
88
|
+
scheduler.stop() # stops background thread
|
|
89
|
+
scheduler.run_due() # manually check and run due agents
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
root: Path,
|
|
95
|
+
config: dict | None = None,
|
|
96
|
+
on_result: Callable[[ScheduleResult], None] | None = None,
|
|
97
|
+
):
|
|
98
|
+
self.root = root
|
|
99
|
+
self.config = config or cfg.load(root)
|
|
100
|
+
self.on_result = on_result or (lambda r: None)
|
|
101
|
+
|
|
102
|
+
scheduler_cfg = self.config.get("pipeline", {}).get("scheduler", {})
|
|
103
|
+
self.enabled = scheduler_cfg.get("enabled", False)
|
|
104
|
+
intervals = scheduler_cfg.get("intervals", {})
|
|
105
|
+
default_interval = intervals.get("default", "1h")
|
|
106
|
+
overrides = intervals.get("overrides", {})
|
|
107
|
+
|
|
108
|
+
# Build schedule from registered SECURITY + SCANNER agents
|
|
109
|
+
self._agents: list[ScheduledAgent] = []
|
|
110
|
+
for cls in list_agents(AgentGroup.SECURITY):
|
|
111
|
+
name = getattr(cls, "name", str(cls))
|
|
112
|
+
interval = overrides.get(name, default_interval)
|
|
113
|
+
self._agents.append(
|
|
114
|
+
ScheduledAgent(
|
|
115
|
+
name=name,
|
|
116
|
+
agent_cls=cls,
|
|
117
|
+
interval_sec=_parse_interval(interval),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
for cls in list_agents(AgentGroup.SCANNER):
|
|
121
|
+
name = getattr(cls, "name", str(cls))
|
|
122
|
+
interval = overrides.get(name, default_interval)
|
|
123
|
+
self._agents.append(
|
|
124
|
+
ScheduledAgent(
|
|
125
|
+
name=name,
|
|
126
|
+
agent_cls=cls,
|
|
127
|
+
interval_sec=_parse_interval(interval),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Background thread state
|
|
132
|
+
self._thread: threading.Thread | None = None
|
|
133
|
+
self._stop_event = threading.Event()
|
|
134
|
+
self._lock = threading.Lock()
|
|
135
|
+
|
|
136
|
+
# ── Lifecycle ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def start(self) -> None:
|
|
139
|
+
"""Start the scheduler background thread."""
|
|
140
|
+
if not self.enabled:
|
|
141
|
+
logger.info("ScanScheduler is disabled (pipeline.scheduler.enabled=false)")
|
|
142
|
+
return
|
|
143
|
+
if self._thread and self._thread.is_alive():
|
|
144
|
+
logger.warning("ScanScheduler already running")
|
|
145
|
+
return
|
|
146
|
+
self._stop_event.clear()
|
|
147
|
+
self._thread = threading.Thread(target=self._run_loop, daemon=True, name="ScanScheduler")
|
|
148
|
+
self._thread.start()
|
|
149
|
+
logger.info(f"ScanScheduler started with {len(self._agents)} agents")
|
|
150
|
+
|
|
151
|
+
def stop(self) -> None:
|
|
152
|
+
"""Stop the scheduler background thread."""
|
|
153
|
+
if self._thread and self._thread.is_alive():
|
|
154
|
+
self._stop_event.set()
|
|
155
|
+
self._thread.join(timeout=5)
|
|
156
|
+
logger.info("ScanScheduler stopped")
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_running(self) -> bool:
|
|
160
|
+
return self._thread is not None and self._thread.is_alive()
|
|
161
|
+
|
|
162
|
+
# ── Core loop ─────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def _run_loop(self) -> None:
|
|
165
|
+
"""Background loop: check due agents every 60s."""
|
|
166
|
+
while not self._stop_event.is_set():
|
|
167
|
+
try:
|
|
168
|
+
self.run_due()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"ScanScheduler error: {e}")
|
|
171
|
+
self._stop_event.wait(60)
|
|
172
|
+
|
|
173
|
+
def run_due(self) -> list[ScheduleResult]:
|
|
174
|
+
"""Run all agents whose interval has elapsed. Returns results."""
|
|
175
|
+
results: list[ScheduleResult] = []
|
|
176
|
+
now = time.monotonic()
|
|
177
|
+
|
|
178
|
+
with self._lock:
|
|
179
|
+
due = [a for a in self._agents if a.enabled and (now - a.last_run) >= a.interval_sec]
|
|
180
|
+
|
|
181
|
+
for agent in due:
|
|
182
|
+
result = self._run_agent(agent)
|
|
183
|
+
results.append(result)
|
|
184
|
+
self.on_result(result)
|
|
185
|
+
|
|
186
|
+
with self._lock:
|
|
187
|
+
if result.status == "completed":
|
|
188
|
+
agent.last_run = now
|
|
189
|
+
agent.consecutive_failures = 0
|
|
190
|
+
elif result.status == "failed":
|
|
191
|
+
agent.consecutive_failures += 1
|
|
192
|
+
if agent.consecutive_failures >= 3:
|
|
193
|
+
agent.enabled = False
|
|
194
|
+
logger.warning(f"Agent {agent.name} disabled after 3 consecutive failures")
|
|
195
|
+
|
|
196
|
+
return results
|
|
197
|
+
|
|
198
|
+
# ── Single agent run ──────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def _build_input(self) -> AgentInput:
|
|
201
|
+
try:
|
|
202
|
+
brain = mem.get_brain(self.root)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
_log.warning("ScanScheduler._build_input failed: %s", e)
|
|
205
|
+
brain = {}
|
|
206
|
+
return AgentInput(
|
|
207
|
+
root=self.root,
|
|
208
|
+
scope=[],
|
|
209
|
+
brain=brain,
|
|
210
|
+
config=self.config,
|
|
211
|
+
extra={"source": "scheduler"},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _run_agent(self, agent: ScheduledAgent) -> ScheduleResult:
|
|
215
|
+
"""Run a single scheduled agent and return the result."""
|
|
216
|
+
logger.debug(f"Running scheduled agent: {agent.name}")
|
|
217
|
+
inp = self._build_input()
|
|
218
|
+
instance = agent.agent_cls()
|
|
219
|
+
t0 = time.monotonic()
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
result = instance.run(inp)
|
|
223
|
+
dur = int((time.monotonic() - t0) * 1000)
|
|
224
|
+
|
|
225
|
+
# Save result to history
|
|
226
|
+
try:
|
|
227
|
+
mem.save_scan_result(
|
|
228
|
+
agent.name,
|
|
229
|
+
{
|
|
230
|
+
"status": result.status.value
|
|
231
|
+
if hasattr(result.status, "value")
|
|
232
|
+
else str(result.status),
|
|
233
|
+
"duration_ms": dur,
|
|
234
|
+
"finding_count": len(result.findings),
|
|
235
|
+
"files_scanned": result.files_scanned,
|
|
236
|
+
"source": "scheduler",
|
|
237
|
+
},
|
|
238
|
+
self.root,
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
_log.warning("ScanScheduler._run_agent failed: %s", e)
|
|
242
|
+
|
|
243
|
+
status = "completed" if result.status.value in ("done", "succeeded") else "failed"
|
|
244
|
+
msg = f"{len(result.findings)} findings, {result.files_scanned} files in {dur}ms"
|
|
245
|
+
if result.errors:
|
|
246
|
+
msg += f" ({len(result.errors)} errors)"
|
|
247
|
+
|
|
248
|
+
return ScheduleResult(
|
|
249
|
+
agent_name=agent.name,
|
|
250
|
+
status=status,
|
|
251
|
+
duration_ms=dur,
|
|
252
|
+
message=msg,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
dur = int((time.monotonic() - t0) * 1000)
|
|
257
|
+
return ScheduleResult(
|
|
258
|
+
agent_name=agent.name,
|
|
259
|
+
status="failed",
|
|
260
|
+
duration_ms=dur,
|
|
261
|
+
message=f"Unhandled error: {e}",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# ── Status / introspection ────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
def status(self) -> dict:
|
|
267
|
+
"""Return scheduler status for dashboard."""
|
|
268
|
+
now = time.monotonic()
|
|
269
|
+
with self._lock:
|
|
270
|
+
agents = []
|
|
271
|
+
for a in self._agents:
|
|
272
|
+
next_run = max(0.0, a.interval_sec - (now - a.last_run))
|
|
273
|
+
agents.append(
|
|
274
|
+
{
|
|
275
|
+
"name": a.name,
|
|
276
|
+
"interval_sec": a.interval_sec,
|
|
277
|
+
"next_run_sec": int(next_run),
|
|
278
|
+
"enabled": a.enabled,
|
|
279
|
+
"consecutive_failures": a.consecutive_failures,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
return {
|
|
283
|
+
"enabled": self.enabled,
|
|
284
|
+
"running": self.is_running,
|
|
285
|
+
"agent_count": len(agents),
|
|
286
|
+
"agents": agents,
|
|
287
|
+
}
|