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,298 @@
1
+ """
2
+ Anomaly detection for Patchi hosted mode.
3
+
4
+ Two layers:
5
+ 1. Statistical: rolling windows — request rate spikes, error rate spikes,
6
+ status code distribution shifts, path enumeration patterns.
7
+ 2. ML: IsolationForest on request feature vectors — catches what statistics miss.
8
+
9
+ Detects: brute force, credential stuffing, scanner sweeps, injection probes,
10
+ error spikes, path traversal attempts.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import collections
16
+ import time
17
+ from dataclasses import dataclass, field
18
+
19
+ from patchi.core.hosted.log_parsers import LogEntry
20
+
21
+
22
+ @dataclass
23
+ class AnomalyFinding:
24
+ detector: str
25
+ severity: str # critical / high / medium
26
+ title: str
27
+ detail: str
28
+ ip: str = ""
29
+ evidence: dict = field(default_factory=dict)
30
+ detected_at: float = field(default_factory=time.time)
31
+
32
+
33
+ # ── Statistical detectors ──────────────────────────────────────────────────────
34
+
35
+
36
+ class _RollingWindow:
37
+ """Fixed-size time window for rate tracking."""
38
+
39
+ def __init__(self, window_secs: int = 60) -> None:
40
+ self._window = window_secs
41
+ self._events: collections.deque = collections.deque()
42
+
43
+ def add(self, value: float = 1.0) -> None:
44
+ now = time.time()
45
+ self._events.append((now, value))
46
+ self._trim(now)
47
+
48
+ def _trim(self, now: float) -> None:
49
+ while self._events and now - self._events[0][0] > self._window:
50
+ self._events.popleft()
51
+
52
+ def rate(self) -> float:
53
+ """Events per second in the current window."""
54
+ if not self._events:
55
+ return 0.0
56
+ self._trim(time.time())
57
+ if not self._events:
58
+ return 0.0
59
+ # Compute actual time span of events in window
60
+ span = self._events[-1][0] - self._events[0][0]
61
+ if span <= 0:
62
+ return float(len(self._events))
63
+ return sum(v for _, v in self._events) / span
64
+
65
+ def count(self) -> int:
66
+ self._trim(time.time())
67
+ return len(self._events)
68
+
69
+
70
+ class StatisticalDetector:
71
+ """
72
+ Rolling window detectors for common attack patterns.
73
+ Maintains per-IP and global counters.
74
+ Accepts optional config dict for threshold overrides.
75
+ """
76
+
77
+ # Defaults
78
+ _DEFAULT_RATE_SPIKE_RPS = 50
79
+ _DEFAULT_BRUTE_FORCE_AUTH = 20
80
+ _DEFAULT_SCANNER_PATHS = 30
81
+ _DEFAULT_ERROR_RATE_PCT = 0.40
82
+ _INJECTION_PATTERNS = [
83
+ "../",
84
+ "..\\",
85
+ "%2e%2e",
86
+ "'%20OR%20",
87
+ " OR ",
88
+ "UNION SELECT",
89
+ "<script",
90
+ "javascript:",
91
+ "${jndi:",
92
+ "etc/passwd",
93
+ ]
94
+ _MAX_IP_TRACKERS = 10_000
95
+
96
+ def __init__(self, config: dict | None = None) -> None:
97
+ hosted = (config or {}).get("hosted", {})
98
+ self._RATE_SPIKE_RPS = hosted.get("rate_spike_rps", self._DEFAULT_RATE_SPIKE_RPS)
99
+ self._BRUTE_FORCE_AUTH = hosted.get("brute_force_auth", self._DEFAULT_BRUTE_FORCE_AUTH)
100
+ self._SCANNER_PATHS = hosted.get("scanner_paths", self._DEFAULT_SCANNER_PATHS)
101
+ self._ERROR_RATE_PCT = hosted.get("error_rate_pct", self._DEFAULT_ERROR_RATE_PCT)
102
+ self._whitelist: set[str] = set(hosted.get("ip_whitelist", []))
103
+
104
+ self._global_rate = _RollingWindow(60)
105
+ self._error_count = _RollingWindow(60)
106
+ self._total_count = _RollingWindow(60)
107
+ self._ip_auth_hits: dict[str, _RollingWindow] = {}
108
+ self._ip_paths: dict[str, set] = {}
109
+ self._ip_path_times: dict[str, float] = {}
110
+ self._ip_lru_order: list[str] = []
111
+
112
+ def feed(self, entry: LogEntry) -> list[AnomalyFinding]:
113
+ findings: list[AnomalyFinding] = []
114
+
115
+ # Skip whitelisted IPs
116
+ if entry.ip and entry.ip in self._whitelist:
117
+ return findings
118
+
119
+ self._global_rate.add()
120
+ self._total_count.add()
121
+ if entry.status >= 400:
122
+ self._error_count.add()
123
+
124
+ # Rate spike
125
+ if self._global_rate.rate() > self._RATE_SPIKE_RPS:
126
+ findings.append(
127
+ AnomalyFinding(
128
+ detector="rate_spike",
129
+ severity="high",
130
+ title=f"Request rate spike: {self._global_rate.rate():.0f} req/s",
131
+ detail="Sustained high request rate — possible DDoS or scanner.",
132
+ )
133
+ )
134
+
135
+ # Error rate
136
+ total = self._total_count.count()
137
+ if total > 20:
138
+ err_pct = self._error_count.count() / total
139
+ if err_pct > self._ERROR_RATE_PCT:
140
+ findings.append(
141
+ AnomalyFinding(
142
+ detector="error_spike",
143
+ severity="medium",
144
+ title=f"High error rate: {err_pct:.0%}",
145
+ detail=f"{self._error_count.count()} errors in last 60s.",
146
+ )
147
+ )
148
+
149
+ # Per-IP checks
150
+ if entry.ip:
151
+ findings.extend(self._check_ip(entry))
152
+
153
+ # Injection patterns in path
154
+ path_lower = entry.path.lower()
155
+ for pattern in self._INJECTION_PATTERNS:
156
+ if pattern.lower() in path_lower:
157
+ findings.append(
158
+ AnomalyFinding(
159
+ detector="injection_probe",
160
+ severity="critical",
161
+ title=f"Injection probe detected: {pattern}",
162
+ detail=f"IP {entry.ip} sent probe in path: {entry.path[:120]}",
163
+ ip=entry.ip,
164
+ evidence={"path": entry.path, "pattern": pattern},
165
+ )
166
+ )
167
+
168
+ return findings
169
+
170
+ def _check_ip(self, entry: LogEntry) -> list[AnomalyFinding]:
171
+ findings: list[AnomalyFinding] = []
172
+ ip = entry.ip
173
+
174
+ # LRU eviction if tracking too many IPs
175
+ if ip not in self._ip_auth_hits and len(self._ip_auth_hits) >= self._MAX_IP_TRACKERS:
176
+ self._evict_lru()
177
+
178
+ # Brute force on auth endpoints
179
+ auth_paths = ("/auth/login", "/login", "/signin", "/api/token", "/admin")
180
+ if any(entry.path.startswith(p) for p in auth_paths) and entry.method == "POST":
181
+ if ip not in self._ip_auth_hits:
182
+ self._ip_auth_hits[ip] = _RollingWindow(60)
183
+ self._ip_lru_order.append(ip)
184
+ self._ip_auth_hits[ip].add()
185
+ if self._ip_auth_hits[ip].count() >= self._BRUTE_FORCE_AUTH:
186
+ findings.append(
187
+ AnomalyFinding(
188
+ detector="brute_force",
189
+ severity="critical",
190
+ title=f"Brute force: {ip} → {entry.path}",
191
+ detail=f"{self._ip_auth_hits[ip].count()} auth attempts in 60s.",
192
+ ip=ip,
193
+ evidence={"count": self._ip_auth_hits[ip].count()},
194
+ )
195
+ )
196
+
197
+ # Path enumeration (scanner sweep)
198
+ now = time.time()
199
+ last_reset = self._ip_path_times.get(ip, 0)
200
+ if now - last_reset > 60:
201
+ self._ip_paths[ip] = set()
202
+ self._ip_path_times[ip] = now
203
+ if ip not in self._ip_lru_order:
204
+ self._ip_lru_order.append(ip)
205
+ self._ip_paths[ip].add(entry.path)
206
+ if len(self._ip_paths[ip]) >= self._SCANNER_PATHS:
207
+ findings.append(
208
+ AnomalyFinding(
209
+ detector="scanner_sweep",
210
+ severity="high",
211
+ title=f"Scanner sweep: {ip} probing {len(self._ip_paths[ip])} paths",
212
+ detail="Rapid path enumeration — possible automated scanner.",
213
+ ip=ip,
214
+ evidence={"distinct_paths": len(self._ip_paths[ip])},
215
+ )
216
+ )
217
+
218
+ return findings
219
+
220
+ def _evict_lru(self) -> None:
221
+ """Evict oldest tracked IPs to bound memory usage."""
222
+ while len(self._ip_auth_hits) >= self._MAX_IP_TRACKERS and self._ip_lru_order:
223
+ old_ip = self._ip_lru_order.pop(0)
224
+ self._ip_auth_hits.pop(old_ip, None)
225
+ self._ip_paths.pop(old_ip, None)
226
+ self._ip_path_times.pop(old_ip, None)
227
+
228
+
229
+ class MLDetector:
230
+ """
231
+ IsolationForest anomaly detector on request feature vectors.
232
+ Trains on a baseline window, then flags outliers.
233
+ Requires sklearn — gracefully no-ops if not available.
234
+ """
235
+
236
+ MIN_SAMPLES = 100 # minimum before ML kicks in
237
+ _MAX_SAMPLES = 10_000 # cap memory usage
238
+
239
+ def __init__(self) -> None:
240
+ self._samples: list[list[float]] = []
241
+ self._model = None
242
+ self._trained = False
243
+
244
+ def _to_vector(self, entry: LogEntry) -> list[float]:
245
+ """Convert a log entry to a numeric feature vector."""
246
+ return [
247
+ float(entry.status),
248
+ float(len(entry.path)),
249
+ float(entry.path.count("/")),
250
+ float(1 if entry.method == "POST" else 0),
251
+ float(1 if entry.status >= 500 else 0),
252
+ float(1 if entry.status == 404 else 0),
253
+ float(len(entry.path.split("?")[1]) if "?" in entry.path else 0),
254
+ ]
255
+
256
+ def feed(self, entry: LogEntry) -> list[AnomalyFinding]:
257
+ try:
258
+ from sklearn.ensemble import IsolationForest
259
+ except ImportError:
260
+ return []
261
+
262
+ vec = self._to_vector(entry)
263
+ self._samples.append(vec)
264
+
265
+ # Cap memory usage
266
+ if len(self._samples) > self._MAX_SAMPLES:
267
+ self._samples = self._samples[-self._MAX_SAMPLES :]
268
+
269
+ if len(self._samples) < self.MIN_SAMPLES:
270
+ return []
271
+
272
+ # Retrain every 500 samples
273
+ if len(self._samples) % 500 == 0 or not self._trained:
274
+ self._model = IsolationForest(contamination=0.05, random_state=42, n_estimators=50)
275
+ self._model.fit(self._samples[-1000:])
276
+ self._trained = True
277
+
278
+ if not self._model:
279
+ return []
280
+
281
+ score = self._model.decision_function([vec])[0]
282
+ pred = self._model.predict([vec])[0]
283
+
284
+ if pred == -1 and score < -0.2:
285
+ return [
286
+ AnomalyFinding(
287
+ detector="ml_outlier",
288
+ severity="medium",
289
+ title="ML anomaly detected",
290
+ detail=(
291
+ f"Request to {entry.path} from {entry.ip} has anomaly score {score:.3f}."
292
+ ),
293
+ ip=entry.ip,
294
+ evidence={"score": round(score, 3), "status": entry.status},
295
+ )
296
+ ]
297
+
298
+ return []
@@ -0,0 +1,102 @@
1
+ """
2
+ Rotating audit log for Patchi hosted mode.
3
+
4
+ Writes structured JSON-lines to .patchi/hosted/audit.log.
5
+ Rotates when file exceeds MAX_SIZE_BYTES, keeping MAX_BACKUPS old files.
6
+ Each entry: {timestamp, event, actor, data}.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import time
13
+ from pathlib import Path
14
+
15
+ _LOG_FILE = ".patchi/hosted/audit.log"
16
+ MAX_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB
17
+ MAX_BACKUPS = 3
18
+
19
+
20
+ _log = logging.getLogger("patchi.core.audit_log")
21
+
22
+
23
+ def _log_path(root: Path) -> Path:
24
+ return root / _LOG_FILE
25
+
26
+
27
+ def _rotate(path: Path) -> None:
28
+ """Rotate old log files, keeping MAX_BACKUPS copies."""
29
+ # Shift existing backups down (remove oldest first to avoid conflicts)
30
+ for i in range(MAX_BACKUPS - 1, 0, -1):
31
+ src = Path(f"{path}.{i}")
32
+ dst = Path(f"{path}.{i + 1}")
33
+ if dst.exists():
34
+ dst.unlink()
35
+ if src.exists():
36
+ src.rename(dst)
37
+ # Move current log to .1
38
+ if path.exists():
39
+ dst = Path(f"{path}.1")
40
+ if dst.exists():
41
+ dst.unlink()
42
+ path.rename(dst)
43
+
44
+
45
+ def write(root: Path, event: str, actor: str = "patchi", data: dict | None = None) -> None:
46
+ """
47
+ Append one audit entry. Rotates if log exceeds MAX_SIZE_BYTES.
48
+ Never raises — audit logging must not crash the caller.
49
+ """
50
+ try:
51
+ path = _log_path(root)
52
+ path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ if path.exists() and path.stat().st_size >= MAX_SIZE_BYTES:
55
+ _rotate(path)
56
+
57
+ entry = json.dumps(
58
+ {
59
+ "timestamp": time.time(),
60
+ "event": event,
61
+ "actor": actor,
62
+ "data": data or {},
63
+ }
64
+ )
65
+ with path.open("a", encoding="utf-8") as f:
66
+ f.write(entry + "\n")
67
+ except Exception as e:
68
+ _log.warning("write failed: %s", e)
69
+
70
+
71
+ def read_recent(root: Path, limit: int = 50) -> list[dict]:
72
+ """Return the most recent `limit` audit entries, newest first."""
73
+ path = _log_path(root)
74
+ if not path.exists():
75
+ return []
76
+ try:
77
+ lines = path.read_text(encoding="utf-8").strip().splitlines()
78
+ entries = []
79
+ for line in reversed(lines[-limit * 2 :]):
80
+ try:
81
+ entries.append(json.loads(line))
82
+ if len(entries) >= limit:
83
+ break
84
+ except json.JSONDecodeError:
85
+ continue
86
+ return entries
87
+ except OSError:
88
+ return []
89
+
90
+
91
+ def clear(root: Path) -> int:
92
+ """Delete current log and all backups. Returns number of files removed."""
93
+ path = _log_path(root)
94
+ removed = 0
95
+ for p in [path] + [Path(f"{path}.{i}") for i in range(1, MAX_BACKUPS + 2)]:
96
+ try:
97
+ if p.exists():
98
+ p.unlink()
99
+ removed += 1
100
+ except OSError:
101
+ pass
102
+ return removed
@@ -0,0 +1,148 @@
1
+ """
2
+ Compliance Report Generator — Evidence packs for hosted mode.
3
+
4
+ Aggregates scan results, health score, audit log, and domain control status
5
+ into a single structured report suitable for auditors / customers.
6
+ Deterministic: built entirely from data already on disk (no AI required).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ _log = logging.getLogger("patchi.hosted.compliance")
16
+
17
+ # Map Patchi security agents to compliance framework domains
18
+ _FRAMEWORK_MAPS: dict[str, dict[str, list[str]]] = {
19
+ "owasp-asvs": {
20
+ "V1: Architecture": ["AppMapperAgent", "BlastRadiusAgent", "PlanAuditorAgent"],
21
+ "V2: Authentication": [
22
+ "AuthenticationAuditAgent",
23
+ "JWTSecurityAgent",
24
+ "SessionManagementAgent",
25
+ "SecretScanner",
26
+ ],
27
+ "V3: Session Management": ["SessionManagementAgent", "JWTSecurityAgent"],
28
+ "V4: Access Control": ["AuthZAgent", "InjectionAgent"],
29
+ "V5: Validation & Encoding": ["InjectionAgent", "TaintAnalyzer", "SensitiveDataAgent"],
30
+ "V6: Stored Cryptography": ["CryptoAgent", "SecretsGuard"],
31
+ "V7: Error Handling & Logging": ["HistoryAgent", "GovernanceAgent"],
32
+ "V8: Data Protection": ["PrivacyAgent", "SensitiveDataAgent"],
33
+ "V9: Communications": ["NetworkAgent", "SSRFProtectionAgent", "CORSAuditor"],
34
+ "V10: Malicious Code": ["SupplyChainAgent", "DependencyVulnerabilityAgent"],
35
+ "V12: Files & Resources": ["MisconfigAgent", "ConfigAuditAgent"],
36
+ "V14: Configuration": [
37
+ "ConfigAuditAgent",
38
+ "HeaderAuditAgent",
39
+ "RateLimitAuditor",
40
+ "EnvVarValidator",
41
+ ],
42
+ },
43
+ "pci-dss": {
44
+ "Req 2: Secure Configs": ["ConfigAuditAgent", "MisconfigAgent", "HeaderAuditAgent"],
45
+ "Req 3: Protect Stored Data": ["SecretScanner", "SensitiveDataAgent", "CryptoAgent"],
46
+ "Req 4: Encrypt Transmission": ["NetworkAgent", "CryptoAgent"],
47
+ "Req 5: Malware Protection": ["SupplyChainAgent"],
48
+ "Req 6: Secure Development": [
49
+ "TaintAnalyzer",
50
+ "InjectionAgent",
51
+ "DependencyVulnerabilityAgent",
52
+ ],
53
+ "Req 8: Identify & Auth": ["AuthenticationAuditAgent", "JWTSecurityAgent"],
54
+ "Req 10: Log & Monitor": ["HistoryAgent", "GovernanceAgent", "EnvVarValidator"],
55
+ },
56
+ }
57
+
58
+
59
+ def generate_report(root: Path, standard: str = "owasp-asvs") -> dict:
60
+ """Build a structured compliance evidence report from disk state.
61
+
62
+ Returns {standard, generated_at, controls: [...], summary: {...}} where
63
+ each control row carries pass/fail/pending status plus the agent evidence
64
+ behind it. Never raises — missing data becomes 'pending'.
65
+ """
66
+ from patchi.core import memory as mem
67
+
68
+ framework = _FRAMEWORK_MAPS.get(standard)
69
+ if framework is None:
70
+ return {"error": f"Unknown standard '{standard}'. Supported: {', '.join(_FRAMEWORK_MAPS)}"}
71
+
72
+ scans = mem.get_scan_results(root)
73
+ brain = mem.get_brain(root)
74
+ health = brain.get("health_score", {})
75
+
76
+ def _agent_findings(agent: str) -> list[dict]:
77
+ return [f for f in scans.get(agent, {}).get("findings", []) if isinstance(f, dict)]
78
+
79
+ def _severity_counts(findings: list[dict]) -> dict[str, int]:
80
+ counts: dict[str, int] = {}
81
+ for f in findings:
82
+ sev = f.get("severity", "info")
83
+ counts[sev] = counts.get(sev, 0) + 1
84
+ return counts
85
+
86
+ sections: list[dict] = []
87
+ total_pass = total_fail = total_pending = 0
88
+
89
+ for section_name, agents in framework.items():
90
+ rows = []
91
+ for agent in agents:
92
+ findings = _agent_findings(agent)
93
+ counts = _severity_counts(findings)
94
+
95
+ if agent not in scans:
96
+ status, evidence = "pending", f"{agent} has not run yet"
97
+ elif counts.get("critical", 0) or counts.get("high", 0):
98
+ status = "fail"
99
+ evidence = (
100
+ f"{counts.get('critical', 0)} critical / {counts.get('high', 0)} high "
101
+ f"finding(s) from {agent}"
102
+ )
103
+ elif findings:
104
+ status = "pass_with_notes"
105
+ evidence = f"{len(findings)} low/medium finding(s) from {agent}"
106
+ else:
107
+ status = "pass"
108
+ evidence = f"{agent} ran clean"
109
+
110
+ if status == "fail":
111
+ total_fail += 1
112
+ elif status == "pending":
113
+ total_pending += 1
114
+ else:
115
+ total_pass += 1
116
+
117
+ rows.append({"control_agent": agent, "status": status, "evidence": evidence})
118
+
119
+ sections.append(
120
+ {
121
+ "section": section_name,
122
+ "controls": rows,
123
+ "failed": sum(1 for r in rows if r["status"] == "fail"),
124
+ "total": len(rows),
125
+ }
126
+ )
127
+
128
+ return {
129
+ "standard": standard,
130
+ "generated_at": datetime.now(UTC).isoformat(),
131
+ "project": {
132
+ "purpose": brain.get("project_purpose", ""),
133
+ "domain": brain.get("project_domain", ""),
134
+ "framework": brain.get("framework", ""),
135
+ "file_count": brain.get("file_count", 0),
136
+ },
137
+ "health_score": health.get("total"),
138
+ "summary": {
139
+ "sections": len(sections),
140
+ "controls_total": sum(s["total"] for s in sections),
141
+ "controls_failed": total_fail,
142
+ "controls_passed": total_pass,
143
+ "controls_pending": total_pending,
144
+ # Simple posture rollout: pass ratio over evaluated controls
145
+ "posture_pct": round(total_pass / max(1, total_pass + total_fail) * 100, 1),
146
+ },
147
+ "sections": sections,
148
+ }