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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. 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
+ }