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,315 @@
1
+ """
2
+ Security History & Analytics (Layer 4) — scan history and findings lifecycle tracking.
3
+
4
+ SQLite-backed storage for:
5
+ - Scan history (every security scan recorded)
6
+ - Findings lifecycle (open → fixed → verified → false_positive)
7
+ - Trend analytics (findings over time, mean time-to-fix)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sqlite3
14
+ import time
15
+ import uuid
16
+ from pathlib import Path
17
+
18
+ from patchi.core.agents.base import (
19
+ AgentGroup,
20
+ AgentInput,
21
+ AgentResult,
22
+ BaseAgent,
23
+ Finding,
24
+ Severity,
25
+ register,
26
+ )
27
+
28
+ _DB_NAME = "patchi_history.db"
29
+
30
+ _CREATE_TABLES = """
31
+ CREATE TABLE IF NOT EXISTS scan_history (
32
+ scan_id TEXT PRIMARY KEY,
33
+ timestamp TEXT NOT NULL,
34
+ repo_path TEXT,
35
+ tool TEXT,
36
+ findings_count INTEGER DEFAULT 0,
37
+ severity_breakdown TEXT,
38
+ duration_ms INTEGER DEFAULT 0,
39
+ health_score INTEGER DEFAULT 0
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS findings (
43
+ finding_id TEXT PRIMARY KEY,
44
+ scan_id TEXT,
45
+ rule_id TEXT,
46
+ file TEXT,
47
+ line INTEGER DEFAULT 0,
48
+ severity TEXT,
49
+ status TEXT DEFAULT 'open',
50
+ first_seen TEXT,
51
+ resolved_at TEXT,
52
+ evidence_path TEXT
53
+ );
54
+ """
55
+
56
+
57
+ def _get_db(root: Path) -> sqlite3.Connection:
58
+ db_path = root / ".patchi" / _DB_NAME
59
+ db_path.parent.mkdir(parents=True, exist_ok=True)
60
+ conn = sqlite3.connect(str(db_path))
61
+ conn.executescript(_CREATE_TABLES)
62
+ # Migrate existing DBs that don't have newer columns
63
+ for col_sql in [
64
+ "ALTER TABLE scan_history ADD COLUMN health_score INTEGER DEFAULT 0",
65
+ "ALTER TABLE scan_history ADD COLUMN metrics TEXT DEFAULT '{}'",
66
+ "ALTER TABLE scan_history ADD COLUMN agent_group TEXT DEFAULT ''",
67
+ ]:
68
+ try:
69
+ conn.execute(col_sql)
70
+ conn.commit()
71
+ except sqlite3.OperationalError:
72
+ pass # Column already exists
73
+ return conn
74
+
75
+
76
+ def patchi_record_scan(
77
+ root: Path,
78
+ tool: str,
79
+ findings: list[dict],
80
+ duration_ms: int = 0,
81
+ health_score: int = 0,
82
+ metrics: dict | None = None,
83
+ agent_group: str = "",
84
+ ) -> str:
85
+ """Record a scan. Returns scan_id."""
86
+ scan_id = uuid.uuid4().hex[:12]
87
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
88
+
89
+ by_sev = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
90
+ for f in findings:
91
+ sev = f.get("severity", "info")
92
+ by_sev[sev] = by_sev.get(sev, 0) + 1
93
+
94
+ conn = _get_db(root)
95
+ try:
96
+ conn.execute(
97
+ "INSERT INTO scan_history (scan_id, timestamp, repo_path, tool, findings_count, severity_breakdown, duration_ms, health_score, metrics, agent_group) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
98
+ (
99
+ scan_id,
100
+ now,
101
+ str(root),
102
+ tool,
103
+ len(findings),
104
+ json.dumps(by_sev),
105
+ duration_ms,
106
+ health_score,
107
+ json.dumps(metrics or {}),
108
+ agent_group,
109
+ ),
110
+ )
111
+ for f in findings:
112
+ fid = uuid.uuid4().hex
113
+ conn.execute(
114
+ "INSERT OR REPLACE INTO findings (finding_id, scan_id, rule_id, file, line, severity, status, first_seen) VALUES (?, ?, ?, ?, ?, ?, 'open', ?)",
115
+ (
116
+ fid,
117
+ scan_id,
118
+ f.get("type", ""),
119
+ f.get("file", ""),
120
+ f.get("line", 0),
121
+ f.get("severity", "info"),
122
+ now,
123
+ ),
124
+ )
125
+ conn.commit()
126
+ finally:
127
+ conn.close()
128
+ return scan_id
129
+
130
+
131
+ def patchi_verify_finding(root: Path, finding_id: str) -> None:
132
+ """Mark a finding as verified (fix confirmed)."""
133
+ conn = _get_db(root)
134
+ try:
135
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
136
+ conn.execute(
137
+ "UPDATE findings SET status = 'verified', resolved_at = ? WHERE finding_id = ?",
138
+ (now, finding_id),
139
+ )
140
+ conn.commit()
141
+ finally:
142
+ conn.close()
143
+
144
+
145
+ def patchi_get_history(root: Path, limit: int = 20) -> list[dict]:
146
+ """Get recent scan history."""
147
+ conn = _get_db(root)
148
+ try:
149
+ cursor = conn.execute(
150
+ "SELECT scan_id, timestamp, tool, findings_count, severity_breakdown, duration_ms, health_score FROM scan_history ORDER BY rowid DESC LIMIT ?",
151
+ (limit,),
152
+ )
153
+ return [
154
+ {
155
+ "scan_id": r[0],
156
+ "timestamp": r[1],
157
+ "tool": r[2],
158
+ "findings_count": r[3],
159
+ "severity_breakdown": json.loads(r[4] or "{}"),
160
+ "duration_ms": r[5],
161
+ "health_score": r[6] or 0,
162
+ }
163
+ for r in cursor.fetchall()
164
+ ]
165
+ finally:
166
+ conn.close()
167
+
168
+
169
+ def patchi_get_severity_trends(root: Path, days: int = 90) -> dict:
170
+ """Severity breakdown per day — ready for trend charts.
171
+
172
+ Returns::
173
+ {"labels": ["2025-01-01", ...], "datasets": {"critical": [0, ...], "high": [...], ...}}
174
+ """
175
+ conn = _get_db(root)
176
+ try:
177
+ rows = conn.execute(
178
+ """
179
+ SELECT DATE(timestamp) as day, severity_breakdown
180
+ FROM scan_history
181
+ WHERE timestamp >= DATE('now', ?)
182
+ ORDER BY day ASC
183
+ """,
184
+ (f"-{days} days",),
185
+ ).fetchall()
186
+
187
+ from collections import defaultdict
188
+
189
+ daily: dict[str, dict[str, int]] = defaultdict(
190
+ lambda: {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
191
+ )
192
+ for day, sb_json in rows:
193
+ sb = json.loads(sb_json or "{}")
194
+ for sev in ("critical", "high", "medium", "low", "info"):
195
+ daily[day][sev] += sb.get(sev, 0)
196
+
197
+ labels = sorted(daily.keys())
198
+ datasets = {}
199
+ for sev in ("critical", "high", "medium", "low", "info"):
200
+ datasets[sev] = [daily[d][sev] for d in labels]
201
+ return {"labels": labels, "datasets": datasets}
202
+ finally:
203
+ conn.close()
204
+
205
+
206
+ def patchi_get_health_scores(root: Path, days: int = 180) -> dict:
207
+ """Health score over time for trend charts."""
208
+ conn = _get_db(root)
209
+ try:
210
+ rows = conn.execute(
211
+ """
212
+ SELECT DATE(timestamp) as day, MAX(health_score) as score
213
+ FROM scan_history
214
+ WHERE timestamp >= DATE('now', ?)
215
+ GROUP BY DATE(timestamp)
216
+ ORDER BY day ASC
217
+ """,
218
+ (f"-{days} days",),
219
+ ).fetchall()
220
+ return {"labels": [r[0] for r in rows], "scores": [r[1] for r in rows]}
221
+ finally:
222
+ conn.close()
223
+
224
+
225
+ def patchi_get_metrics_history(root: Path, metric_key: str, days: int = 90) -> dict:
226
+ """Extract a specific metric (e.g. coverage, bundle_size) over time."""
227
+ conn = _get_db(root)
228
+ try:
229
+ rows = conn.execute(
230
+ """
231
+ SELECT DATE(timestamp) as day, metrics
232
+ FROM scan_history
233
+ WHERE timestamp >= DATE('now', ?) AND metrics != '{}'
234
+ ORDER BY timestamp ASC
235
+ """,
236
+ (f"-{days} days",),
237
+ ).fetchall()
238
+ labels: list[str] = []
239
+ values: list[float] = []
240
+ for day, m_json in rows:
241
+ m = json.loads(m_json or "{}")
242
+ val = m.get(metric_key)
243
+ if val is not None:
244
+ labels.append(day)
245
+ values.append(float(val))
246
+ return {"labels": labels, "values": values}
247
+ finally:
248
+ conn.close()
249
+
250
+
251
+ def patchi_get_analytics(root: Path) -> dict:
252
+ """Get trend analytics from scan history."""
253
+ conn = _get_db(root)
254
+ try:
255
+ # Total scans
256
+ total = conn.execute("SELECT COUNT(*) FROM scan_history").fetchone()[0]
257
+
258
+ # Findings by status
259
+ status_counts = {}
260
+ for row in conn.execute("SELECT status, COUNT(*) FROM findings GROUP BY status"):
261
+ status_counts[row[0]] = row[1]
262
+
263
+ # Most common rule violations
264
+ common_rules = []
265
+ for row in conn.execute(
266
+ "SELECT rule_id, COUNT(*) as cnt FROM findings WHERE rule_id != '' GROUP BY rule_id ORDER BY cnt DESC LIMIT 10"
267
+ ):
268
+ common_rules.append({"rule": row[0], "count": row[1]})
269
+
270
+ # Average findings per scan
271
+ avg_findings = 0
272
+ if total > 0:
273
+ total_findings = (
274
+ conn.execute("SELECT SUM(findings_count) FROM scan_history").fetchone()[0] or 0
275
+ )
276
+ avg_findings = round(total_findings / total, 1)
277
+
278
+ return {
279
+ "total_scans": total,
280
+ "findings_by_status": status_counts,
281
+ "common_rules": common_rules,
282
+ "avg_findings_per_scan": avg_findings,
283
+ }
284
+ finally:
285
+ conn.close()
286
+
287
+
288
+ # ── History Agent ─────────────────────────────────────────────────────────────
289
+
290
+
291
+ @register
292
+ class HistoryAgent(BaseAgent):
293
+ """Records scan results to SQLite history and provides analytics."""
294
+
295
+ name = "HistoryAgent"
296
+ group = AgentGroup.GUARD
297
+ timeout = 30
298
+
299
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
300
+ history = patchi_get_history(inp.root, limit=5)
301
+ analytics = patchi_get_analytics(inp.root)
302
+
303
+ result.data["recent_scans"] = history
304
+ result.data["analytics"] = analytics
305
+
306
+ if analytics["total_scans"] == 0:
307
+ result.add_finding(
308
+ Finding(
309
+ agent=self.name,
310
+ type="no_scan_history",
311
+ severity=Severity.INFO,
312
+ file="",
313
+ message="No previous scan history found — this is the first scan",
314
+ )
315
+ )
@@ -0,0 +1,250 @@
1
+ """
2
+ IaC & Container Security Scanner — Dockerfiles, docker-compose, K8s manifests, Terraform.
3
+
4
+ Detects:
5
+ - Dockerfiles running as root, exposed ports, outdated base images
6
+ - docker-compose privileged containers, host network, volume mounts
7
+ - Kubernetes missing securityContext, resource limits, network policies
8
+ - Terraform public S3, open security groups, unencrypted storage
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from patchi.core.agents.base import (
16
+ AgentGroup,
17
+ AgentInput,
18
+ AgentResult,
19
+ BaseAgent,
20
+ Finding,
21
+ Severity,
22
+ register,
23
+ safe_rglob,
24
+ )
25
+
26
+
27
+ @register
28
+ class IaCScannerAgent(BaseAgent):
29
+ """Scans IaC files for security misconfigurations."""
30
+
31
+ name = "IaCScannerAgent"
32
+ group = AgentGroup.SECURITY
33
+ timeout = 60
34
+
35
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
36
+ scanned = 0
37
+
38
+ # Dockerfiles
39
+ for fpath in safe_rglob(inp.root, "Dockerfile*"):
40
+ if fpath.is_file():
41
+ rel = fpath.relative_to(inp.root).as_posix()
42
+ try:
43
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
44
+ except OSError:
45
+ continue
46
+ self._scan_dockerfile(content, rel, result)
47
+ scanned += 1
48
+
49
+ # docker-compose
50
+ for pattern in [
51
+ "docker-compose*.yml",
52
+ "docker-compose*.yaml",
53
+ "compose.yml",
54
+ "compose.yaml",
55
+ ]:
56
+ for fpath in safe_rglob(inp.root, pattern):
57
+ if fpath.is_file():
58
+ rel = fpath.relative_to(inp.root).as_posix()
59
+ try:
60
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
61
+ except OSError:
62
+ continue
63
+ self._scan_docker_compose(content, rel, result)
64
+ scanned += 1
65
+
66
+ # K8s manifests
67
+ for pattern in ["*.yaml", "*.yml"]:
68
+ for fpath in safe_rglob(inp.root, pattern):
69
+ if fpath.is_file():
70
+ rel = fpath.relative_to(inp.root).as_posix()
71
+ try:
72
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
73
+ except OSError:
74
+ continue
75
+ if "apiVersion:" in content and ("kind:" in content):
76
+ self._scan_k8s(content, rel, result)
77
+ scanned += 1
78
+
79
+ # Terraform
80
+ for fpath in safe_rglob(inp.root, "*.tf"):
81
+ if fpath.is_file():
82
+ rel = fpath.relative_to(inp.root).as_posix()
83
+ try:
84
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
85
+ except OSError:
86
+ continue
87
+ self._scan_terraform(content, rel, result)
88
+ scanned += 1
89
+
90
+ result.files_scanned = scanned
91
+
92
+ def _scan_dockerfile(self, content: str, file: str, result: AgentResult) -> None:
93
+ lines = content.splitlines()
94
+ has_user = False
95
+ for i, line in enumerate(lines, 1):
96
+ stripped = line.strip()
97
+ if stripped.startswith("#"):
98
+ continue
99
+ if stripped.upper().startswith("USER "):
100
+ has_user = True
101
+ if "root" in stripped.lower():
102
+ result.add_finding(
103
+ Finding(
104
+ agent=self.name,
105
+ type="container_root",
106
+ severity=Severity.HIGH,
107
+ file=file,
108
+ line=i,
109
+ message="Container runs as root",
110
+ cwe="CWE-250",
111
+ )
112
+ )
113
+ if stripped.upper().startswith("EXPOSE "):
114
+ ports = stripped.split()[1:]
115
+ for p in ports:
116
+ if p in ("22", "3389", "6379", "27017", "5432", "3306"):
117
+ result.add_finding(
118
+ Finding(
119
+ agent=self.name,
120
+ type="exposed_sensitive_port",
121
+ severity=Severity.MEDIUM,
122
+ file=file,
123
+ line=i,
124
+ message=f"Sensitive port exposed: {p}",
125
+ )
126
+ )
127
+ if "latest" in stripped.lower() and stripped.upper().startswith("FROM "):
128
+ result.add_finding(
129
+ Finding(
130
+ agent=self.name,
131
+ type="latest_base_image",
132
+ severity=Severity.LOW,
133
+ file=file,
134
+ line=i,
135
+ message="Using 'latest' base image tag — pin to specific version",
136
+ )
137
+ )
138
+ if not has_user and lines:
139
+ result.add_finding(
140
+ Finding(
141
+ agent=self.name,
142
+ type="no_user_directive",
143
+ severity=Severity.MEDIUM,
144
+ file=file,
145
+ message="No USER directive — container defaults to root",
146
+ )
147
+ )
148
+
149
+ def _scan_docker_compose(self, content: str, file: str, result: AgentResult) -> None:
150
+ if "privileged: true" in content:
151
+ result.add_finding(
152
+ Finding(
153
+ agent=self.name,
154
+ type="privileged_container",
155
+ severity=Severity.CRITICAL,
156
+ file=file,
157
+ message="Privileged container detected",
158
+ cwe="CWE-250",
159
+ )
160
+ )
161
+ if "network_mode: host" in content:
162
+ result.add_finding(
163
+ Finding(
164
+ agent=self.name,
165
+ type="host_network",
166
+ severity=Severity.HIGH,
167
+ file=file,
168
+ message="Host network mode — container shares host network stack",
169
+ )
170
+ )
171
+ if re.search(r'"/etc:/etc"', content) or re.search(r'"/var/run/docker.sock"', content):
172
+ result.add_finding(
173
+ Finding(
174
+ agent=self.name,
175
+ type="dangerous_volume_mount",
176
+ severity=Severity.CRITICAL,
177
+ file=file,
178
+ message="Dangerous volume mount (host system path or Docker socket)",
179
+ cwe="CWE-250",
180
+ )
181
+ )
182
+
183
+ def _scan_k8s(self, content: str, file: str, result: AgentResult) -> None:
184
+ if "kind: Deployment" in content or "kind: StatefulSet" in content:
185
+ if "securityContext" not in content:
186
+ result.add_finding(
187
+ Finding(
188
+ agent=self.name,
189
+ type="missing_security_context",
190
+ severity=Severity.HIGH,
191
+ file=file,
192
+ message="K8s workload missing securityContext",
193
+ cwe="CWE-250",
194
+ )
195
+ )
196
+ if "resources:" not in content:
197
+ result.add_finding(
198
+ Finding(
199
+ agent=self.name,
200
+ type="missing_resource_limits",
201
+ severity=Severity.MEDIUM,
202
+ file=file,
203
+ message="K8s workload missing resource limits",
204
+ )
205
+ )
206
+ if "kind: NetworkPolicy" not in content and "kind: Deployment" in content:
207
+ result.add_finding(
208
+ Finding(
209
+ agent=self.name,
210
+ type="no_network_policy",
211
+ severity=Severity.LOW,
212
+ file=file,
213
+ message="No NetworkPolicy found — all pod-to-pod traffic allowed",
214
+ )
215
+ )
216
+
217
+ def _scan_terraform(self, content: str, file: str, result: AgentResult) -> None:
218
+ if re.search(r'acl\s*=\s*"public"', content, re.I):
219
+ result.add_finding(
220
+ Finding(
221
+ agent=self.name,
222
+ type="public_s3_bucket",
223
+ severity=Severity.CRITICAL,
224
+ file=file,
225
+ message="S3 bucket with public ACL",
226
+ cwe="CWE-200",
227
+ )
228
+ )
229
+ if re.search(r'cidr_blocks\s*=\s*\["0\.0\.0\.0/0"\]', content):
230
+ result.add_finding(
231
+ Finding(
232
+ agent=self.name,
233
+ type="open_security_group",
234
+ severity=Severity.HIGH,
235
+ file=file,
236
+ message="Security group open to 0.0.0.0/0",
237
+ cwe="CWE-284",
238
+ )
239
+ )
240
+ if re.search(r"encrypt\s*=\s*false", content, re.I):
241
+ result.add_finding(
242
+ Finding(
243
+ agent=self.name,
244
+ type="unencrypted_storage",
245
+ severity=Severity.HIGH,
246
+ file=file,
247
+ message="Unencrypted storage resource",
248
+ cwe="CWE-311",
249
+ )
250
+ )
@@ -0,0 +1,111 @@
1
+ """
2
+ Ignore Lists with Expiry §10.2.3 — // patchi-ignore: RULE YYYY-MM-DD
3
+
4
+ Parses inline ignore comments with expiry dates, warns on expired ignores.
5
+ Supports: //, #, <!--, /* */ styles across 11 langs.
6
+
7
+ Usage:
8
+ findings = filter_ignores(findings, root) # returns (kept, suppressed, expired_warnings)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import re
15
+ from datetime import date, datetime
16
+ from pathlib import Path
17
+
18
+ _log = logging.getLogger("patchi.security.ignore_expiry")
19
+
20
+ # Matches: patchi-ignore, patchi_ignore, patchi:ignore optionally with : RULE and date YYYY-MM-DD or YYYY/MM/DD
21
+ _RE = re.compile(
22
+ r"patchi[\-_:]?ignore\s*:?\s*(?P<rule>[\w\-\*]+)?\s*(?P<date>\d{4}[-/]\d{2}[-/]\d{2})?",
23
+ re.IGNORECASE,
24
+ )
25
+
26
+
27
+ def _parse_ignore_line(line: str) -> tuple[str | None, date | None] | None:
28
+ m = _RE.search(line)
29
+ if not m:
30
+ return None
31
+ rule = m.group("rule")
32
+ datestr = m.group("date")
33
+ d = None
34
+ if datestr:
35
+ try:
36
+ d = datetime.strptime(datestr.replace("/", "-"), "%Y-%m-%d").date()
37
+ except ValueError:
38
+ d = None
39
+ # If no rule and no date, still a generic ignore (no expiry)
40
+ if not rule and not d:
41
+ return ("*", None)
42
+ return (rule or "*", d)
43
+
44
+
45
+ def _is_expired(d: date | None) -> bool:
46
+ return d is not None and d < date.today()
47
+
48
+
49
+ def scan_file_ignores(root: Path, rel: str) -> list[dict]:
50
+ """Return list of {line, rule, expiry, expired} for a file."""
51
+ out: list[dict] = []
52
+ try:
53
+ txt = (root / rel).read_text(encoding="utf-8", errors="replace")
54
+ except OSError:
55
+ return out
56
+ for i, line in enumerate(txt.splitlines(), 1):
57
+ parsed = _parse_ignore_line(line)
58
+ if parsed:
59
+ rule, expiry = parsed
60
+ out.append({"line": i, "rule": rule, "expiry": expiry.isoformat() if expiry else None, "expired": _is_expired(expiry)})
61
+ return out
62
+
63
+
64
+ def filter_ignores(findings: list, root: Path) -> tuple[list, list, list]:
65
+ """
66
+ Filter findings suppressed by patchi-ignore comments.
67
+ Returns (kept, suppressed, expired_warnings).
68
+
69
+ A finding is suppressed if its file has a patchi-ignore comment
70
+ on the same line, previous line, or file-header (line 1) with matching rule or "*"
71
+ and not expired. Expired ignores generate warnings but do NOT suppress.
72
+ """
73
+ suppressed: list = []
74
+ expired_warnings: list[dict] = []
75
+ # cache per file ignores
76
+ cache: dict[str, list[dict]] = {}
77
+
78
+ def get_ignores(rel: str) -> list[dict]:
79
+ if rel not in cache:
80
+ cache[rel] = scan_file_ignores(root, rel)
81
+ return cache[rel]
82
+
83
+ kept: list = []
84
+ for f in findings:
85
+ rel = getattr(f, "file", "") or (f.get("file", "") if isinstance(f, dict) else "")
86
+ line = getattr(f, "line", 0) or (f.get("line", 0) if isinstance(f, dict) else 0)
87
+ ftype = getattr(f, "type", "") or (f.get("type", "") if isinstance(f, dict) else "")
88
+ if not rel:
89
+ kept.append(f)
90
+ continue
91
+ ignores = get_ignores(rel)
92
+ matched = None
93
+ for ig in ignores:
94
+ # match rule: "*" or exact type or prefix
95
+ rule = ig["rule"]
96
+ if rule != "*" and rule.lower() not in ftype.lower() and ftype.lower() not in rule.lower():
97
+ continue
98
+ # line proximity: same line, previous line, or header
99
+ if ig["line"] in (line, line - 1, 1):
100
+ if ig["expired"]:
101
+ expired_warnings.append({"file": rel, "line": ig["line"], "rule": rule, "finding_type": ftype, "message": f"Expired ignore {rule} expired {ig['expiry']}"})
102
+ continue
103
+ matched = ig
104
+ break
105
+ if matched:
106
+ suppressed.append(f)
107
+ else:
108
+ kept.append(f)
109
+ if expired_warnings:
110
+ _log.warning("ignore expiry: %d expired ignores (not suppressing)", len(expired_warnings))
111
+ return kept, suppressed, expired_warnings