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,376 @@
1
+ """
2
+ NetworkAgent — network security configuration issues.
3
+
4
+ Detects network-related security issues:
5
+ - Insecure SSL/TLS configurations
6
+ - Missing HTTPS redirects
7
+ - Weak cipher suites
8
+ - Open ports/services
9
+ - Missing security headers
10
+ - Improper firewall rules
11
+ - Unencrypted communication
12
+
13
+ Uses pattern matching and configuration analysis.
14
+ Does NOT call AI.
15
+ Does NOT write to disk.
16
+ Does NOT touch the queue.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from pathlib import Path
23
+
24
+ from ..agents.base import (
25
+ AgentDomain,
26
+ AgentGroup,
27
+ AgentInput,
28
+ AgentResult,
29
+ AgentStatus,
30
+ BaseAgent,
31
+ Finding,
32
+ Severity,
33
+ make_finding,
34
+ register,
35
+ safe_rglob,
36
+ )
37
+
38
+
39
+ @register
40
+ class NetworkAgent(BaseAgent):
41
+ """Agent for detecting network security issues."""
42
+
43
+ group = AgentGroup.SECURITY
44
+ domain = AgentDomain.SECURITY
45
+ name = "NetworkAgent"
46
+ description = "Network security: SSL/TLS, HTTPS redirects, cipher suites, security headers"
47
+
48
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
49
+ """Run network security configuration detection."""
50
+ findings = []
51
+
52
+ # Define configuration and source file patterns to scan
53
+ patterns = [
54
+ "*.py",
55
+ "*.js",
56
+ "*.jsx",
57
+ "*.ts",
58
+ "*.tsx",
59
+ "*.java",
60
+ "*.php",
61
+ "*.rb",
62
+ "*.go",
63
+ "*.rs",
64
+ "*.cpp",
65
+ "*.cxx",
66
+ "*.cc",
67
+ "*.c",
68
+ "*.h",
69
+ "*.hpp",
70
+ "*.cs",
71
+ "*.json",
72
+ "*.yml",
73
+ "*.yaml",
74
+ "*.toml",
75
+ "*.config",
76
+ "*.conf",
77
+ "*.ini",
78
+ "*.xml",
79
+ "Dockerfile*",
80
+ "docker-compose*.yml",
81
+ "**/config/**",
82
+ "**/nginx/**",
83
+ "**/apache/**",
84
+ "**/ssl/**",
85
+ "**/tls/**",
86
+ ]
87
+
88
+ # Search for files
89
+ for pattern in patterns:
90
+ for file_path in safe_rglob(inp.root, pattern):
91
+ if file_path.is_file():
92
+ rel_path = file_path.relative_to(inp.root).as_posix()
93
+ if not self._should_skip_file(rel_path, inp):
94
+ findings.extend(self._scan_file_network_security(file_path, rel_path))
95
+
96
+ result.status = AgentStatus.SUCCEEDED
97
+ result.findings = findings
98
+ result.data.update(
99
+ {
100
+ "network_findings": len(
101
+ [
102
+ f
103
+ for f in findings
104
+ if any(
105
+ word in f.title.lower()
106
+ for word in [
107
+ "ssl",
108
+ "tls",
109
+ "https",
110
+ "cipher",
111
+ "header",
112
+ "redirect",
113
+ "encryption",
114
+ ]
115
+ )
116
+ ]
117
+ ),
118
+ "needs_ai": False,
119
+ }
120
+ )
121
+ return
122
+
123
+ def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
124
+ """Check if file should be skipped based on restrictions."""
125
+ from pathlib import PurePosixPath
126
+
127
+ # Check restrictions
128
+ restrictions = inp.config.get("restrictions", [])
129
+ for r in restrictions:
130
+ if r.get("enabled", True):
131
+ path = r["path"]
132
+ if file_path.startswith(path) or PurePosixPath(file_path).match(path):
133
+ if r["type"] == "NO_TOUCH":
134
+ return True
135
+ elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
136
+ return True
137
+ return False
138
+
139
+ def _scan_file_network_security(self, file_path: Path, rel_path: str) -> list[Finding]:
140
+ """Scan a file for network security issues."""
141
+ findings = []
142
+
143
+ try:
144
+ content = file_path.read_text(encoding="utf-8")
145
+
146
+ # Check for SSL/TLS issues
147
+ findings.extend(self._scan_ssl_tls_issues(content, rel_path))
148
+
149
+ # Check for HTTPS redirect issues
150
+ findings.extend(self._scan_https_redirects(content, rel_path))
151
+
152
+ # Check for weak cipher suites
153
+ findings.extend(self._scan_cipher_suites(content, rel_path))
154
+
155
+ # Check for missing security headers
156
+ findings.extend(self._scan_security_headers(content, rel_path))
157
+
158
+ # Check for other network security issues
159
+ findings.extend(self._scan_other_network_issues(content, rel_path))
160
+
161
+ except Exception as e:
162
+ findings.append(
163
+ make_finding(
164
+ severity=Severity.LOW,
165
+ file=rel_path,
166
+ line_start=0,
167
+ title="Network security scanner file read error",
168
+ description=f"Could not analyze {file_path.name} for network security issues: {str(e)}",
169
+ evidence=str(e),
170
+ )
171
+ )
172
+
173
+ return findings
174
+
175
+ def _scan_ssl_tls_issues(self, content: str, rel_path: str) -> list[Finding]:
176
+ """Scan for SSL/TLS configuration issues."""
177
+ findings = []
178
+
179
+ # Look for weak SSL/TLS protocol versions
180
+ ssl_patterns = [
181
+ (r"SSLv2|SSLv3|TLSv1(\.0)?", "Weak SSL/TLS Protocol", Severity.CRITICAL),
182
+ (r"ssl_version.*TLSv1(_0|0)?", "Weak TLS Version", Severity.CRITICAL),
183
+ (r"tls_min_version.*TLSv1(_0|0)?", "Weak TLS Minimum Version", Severity.CRITICAL),
184
+ (
185
+ r"protocol.*SSLv2|protocol.*SSLv3|protocol.*TLSv1(\.0)?",
186
+ "Weak Protocol Configuration",
187
+ Severity.CRITICAL,
188
+ ),
189
+ ]
190
+
191
+ lines = content.splitlines()
192
+ for i, line in enumerate(lines, 1):
193
+ for pattern, description, severity in ssl_patterns:
194
+ matches = re.finditer(pattern, line, re.IGNORECASE)
195
+ for match in matches:
196
+ findings.append(
197
+ make_finding(
198
+ severity=severity,
199
+ file=rel_path,
200
+ line_start=i,
201
+ title=description,
202
+ description=f"Weak SSL/TLS protocol configuration found: {match.group(0)}",
203
+ evidence=line.strip(),
204
+ )
205
+ )
206
+
207
+ return findings
208
+
209
+ def _scan_https_redirects(self, content: str, rel_path: str) -> list[Finding]:
210
+ """Scan for HTTPS redirect issues."""
211
+ findings = []
212
+
213
+ has_https_enforcement = bool(
214
+ re.search(
215
+ r"redirect.*https|enable.*https|hsts|strict-transport-security",
216
+ content,
217
+ re.IGNORECASE,
218
+ )
219
+ )
220
+ has_plain_http = bool(
221
+ re.search(
222
+ r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)",
223
+ content,
224
+ )
225
+ )
226
+
227
+ if has_plain_http and not has_https_enforcement:
228
+ lines = content.splitlines()
229
+ for i, line in enumerate(lines, 1):
230
+ if re.search(r"http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)", line):
231
+ findings.append(
232
+ make_finding(
233
+ severity=Severity.MEDIUM,
234
+ file=rel_path,
235
+ line_start=i,
236
+ title="Plain HTTP without HTTPS enforcement",
237
+ description="Plain HTTP URL found without HTTPS redirect/enforcement",
238
+ evidence=line.strip(),
239
+ )
240
+ )
241
+
242
+ return findings
243
+
244
+ def _scan_cipher_suites(self, content: str, rel_path: str) -> list[Finding]:
245
+ """Scan for weak cipher suite configurations."""
246
+ findings = []
247
+
248
+ # Look for weak cipher suites
249
+ weak_cipher_patterns = [
250
+ (r"\bRC4\b|\bDES\b|\b3DES\b|\bMD5\b|\bSHA1\b", "Weak Cipher Suite", Severity.HIGH),
251
+ (
252
+ r"cipher.*\bRC4\b|cipher.*\bDES\b|cipher.*\b3DES\b|cipher.*\bMD5\b",
253
+ "Weak Cipher Configuration",
254
+ Severity.HIGH,
255
+ ),
256
+ (r"\baes-128\b|\bdes-cbc\b", "Potentially Weak Cipher", Severity.MEDIUM),
257
+ ]
258
+
259
+ lines = content.splitlines()
260
+ for i, line in enumerate(lines, 1):
261
+ for pattern, description, severity in weak_cipher_patterns:
262
+ matches = re.finditer(pattern, line, re.IGNORECASE)
263
+ for match in matches:
264
+ findings.append(
265
+ make_finding(
266
+ severity=severity,
267
+ file=rel_path,
268
+ line_start=i,
269
+ title=description,
270
+ description=f"Weak cipher suite configuration found: {match.group(0)}",
271
+ evidence=line.strip(),
272
+ )
273
+ )
274
+
275
+ return findings
276
+
277
+ def _scan_security_headers(self, content: str, rel_path: str) -> list[Finding]:
278
+ """Scan for missing security headers."""
279
+ findings = []
280
+
281
+ # Look for missing security headers
282
+ security_headers = [
283
+ "Strict-Transport-Security",
284
+ "X-Frame-Options",
285
+ "X-Content-Type-Options",
286
+ "X-XSS-Protection",
287
+ "Content-Security-Policy",
288
+ ]
289
+
290
+ # Check if security headers are set
291
+ lines = content.splitlines()
292
+ for _i, line in enumerate(lines, 1):
293
+ # Look for response header setting
294
+ if any(header.lower() in line.lower() for header in security_headers):
295
+ continue # Header is being set, no issue here
296
+ elif any(
297
+ setting_pattern in line.lower()
298
+ for setting_pattern in ["header(", "setheader", "response.header"]
299
+ ):
300
+ # Check if it's setting a security header
301
+ has_security_header = False
302
+ for header in security_headers:
303
+ if header.lower() in line.lower():
304
+ has_security_header = True
305
+ break
306
+ if not has_security_header:
307
+ # This line sets a header but not a security header
308
+ # This alone isn't necessarily bad, but let's check for missing headers in context
309
+ pass
310
+
311
+ # Look for common web frameworks where security headers should be set
312
+ framework_indicators = ["express", "django", "flask", "fastapi", "spring", "laravel"]
313
+ if any(indicator in content.lower() for indicator in framework_indicators):
314
+ # Check if security headers are configured
315
+ has_hsts = any(
316
+ hsts_pattern in content.lower()
317
+ for hsts_pattern in ["strict-transport-security", "hsts"]
318
+ )
319
+ has_xfo = any(
320
+ xfo_pattern in content.lower()
321
+ for xfo_pattern in ["x-frame-options", "frame-options"]
322
+ )
323
+ has_xcto = any(
324
+ xcto_pattern in content.lower()
325
+ for xcto_pattern in ["x-content-type-options", "nosniff"]
326
+ )
327
+
328
+ missing_headers = []
329
+ if not has_hsts:
330
+ missing_headers.append("HSTS")
331
+ if not has_xfo:
332
+ missing_headers.append("X-Frame-Options")
333
+ if not has_xcto:
334
+ missing_headers.append("X-Content-Type-Options")
335
+
336
+ if missing_headers:
337
+ findings.append(
338
+ make_finding(
339
+ severity=Severity.MEDIUM,
340
+ file=rel_path,
341
+ line_start=0,
342
+ title=f"Missing Security Headers: {', '.join(missing_headers)}",
343
+ description=f"Common security headers not configured: {', '.join(missing_headers)}",
344
+ evidence=f"Detected web framework but missing security headers: {', '.join(missing_headers)}",
345
+ )
346
+ )
347
+
348
+ return findings
349
+
350
+ def _scan_other_network_issues(self, content: str, rel_path: str) -> list[Finding]:
351
+ """Scan for other network security issues."""
352
+ findings = []
353
+
354
+ # Look for plain HTTP URLs in secure contexts
355
+ http_patterns = [
356
+ (r'http://[^s][^"\'\s]+', "Plain HTTP URL", Severity.MEDIUM),
357
+ (r'"http://|\'http://', "Plain HTTP Reference", Severity.MEDIUM),
358
+ ]
359
+
360
+ lines = content.splitlines()
361
+ for i, line in enumerate(lines, 1):
362
+ for pattern, description, severity in http_patterns:
363
+ matches = re.finditer(pattern, line, re.IGNORECASE)
364
+ for match in matches:
365
+ findings.append(
366
+ make_finding(
367
+ severity=severity,
368
+ file=rel_path,
369
+ line_start=i,
370
+ title=description,
371
+ description=f"Plain HTTP URL found in potentially secure context: {match.group(0)}",
372
+ evidence=line.strip(),
373
+ )
374
+ )
375
+
376
+ return findings
@@ -0,0 +1,274 @@
1
+ """
2
+ Noise Filter — classifies and suppresses findings from non-source files.
3
+
4
+ The scanner's largest false-positive source is not weak detection logic —
5
+ it is scanning files whose contents are *supposed* to look dangerous:
6
+ test fixtures with fake secrets, migration SQL strings, lockfile blobs,
7
+ minified bundles, generated protobuf stubs.
8
+
9
+ NoiseFilter assigns every finding's file path to a category and either
10
+ discards or severity-caps the finding per config (``noise_filter``):
11
+
12
+ enabled default true
13
+ mode "cap" (default: downgrade to info + tag) | "discard"
14
+ skip_tests default true — test_*, *_test.*, conftest, spec/, fixtures/
15
+ skip_locks default true — package-lock.json, yarn.lock, poetry.lock, ...
16
+ skip_generated default true — *.min.js, *_pb2.py, *.map, *_pb2_grpc.py, ...
17
+ skip_docs default true — *.md, *.rst, *.txt, *.csv
18
+
19
+ Honesty rule: capped findings are never deleted silently — they keep their
20
+ data, marked ``noise_category`` so the user can see exactly what was muted
21
+ and why.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import fnmatch
27
+ import logging
28
+ from dataclasses import dataclass, field
29
+ from pathlib import PurePosixPath
30
+
31
+ _log = logging.getLogger("patchi.noise_filter")
32
+
33
+ # ── Category definitions ─────────────────────────────────────────────────────
34
+
35
+ _LOCKFILE_NAMES = frozenset(
36
+ {
37
+ "package-lock.json",
38
+ "npm-shrinkwrap.json",
39
+ "yarn.lock",
40
+ "pnpm-lock.yaml",
41
+ "poetry.lock",
42
+ "Pipfile.lock",
43
+ "Cargo.lock",
44
+ "composer.lock",
45
+ "Gemfile.lock",
46
+ "packages.locker",
47
+ "mix.lock",
48
+ "go.sum",
49
+ "bun.lockb",
50
+ }
51
+ )
52
+
53
+ _GENERATED_PATTERNS = (
54
+ "*.min.js",
55
+ "*.min.css",
56
+ "*.min.mjs",
57
+ "*.map",
58
+ "*.bundle.js",
59
+ "*_pb2.py",
60
+ "*_pb2_grpc.py",
61
+ "*_pb.py",
62
+ "*.pb.go",
63
+ "*_pb2.pyi",
64
+ "*-gen.py",
65
+ "*_gen.go",
66
+ "*.generated.*",
67
+ "*.g.cs",
68
+ "*.Designer.cs",
69
+ "*.snap",
70
+ "__snapshots__/*",
71
+ )
72
+
73
+ _DOC_PATTERNS = ("*.md", "*.mdx", "*.rst", "*.txt", "*.csv", "*.log")
74
+
75
+ _TEST_FILE_PATTERNS = (
76
+ "test_*.py",
77
+ "*_test.py",
78
+ "*_test.go",
79
+ "*_test.dart",
80
+ "conftest.py",
81
+ "test_utils.py",
82
+ "*.spec.ts",
83
+ "*.spec.tsx",
84
+ "*.spec.js",
85
+ "*.spec.jsx",
86
+ "*.spec.mjs",
87
+ "*.test.ts",
88
+ "*.test.tsx",
89
+ "*.test.js",
90
+ "*.test.jsx",
91
+ "*.test.mjs",
92
+ "*_test.rb",
93
+ "*.spec.rb",
94
+ )
95
+
96
+ _TEST_DIR_MARKERS = frozenset(
97
+ {
98
+ "test",
99
+ "tests",
100
+ "spec",
101
+ "specs",
102
+ "fixtures",
103
+ "fixture",
104
+ "testdata",
105
+ "test_data",
106
+ "mock",
107
+ "mocks",
108
+ "stubs",
109
+ "__snapshots__",
110
+ "__tests__",
111
+ "testing",
112
+ }
113
+ )
114
+
115
+
116
+ def classify(path: str) -> str | None:
117
+ """Return the noise category for a path, or None if it looks like source.
118
+
119
+ Categories: "lockfile", "generated", "docs", "tests".
120
+ Order matters: lockfile > generated > docs > tests.
121
+ """
122
+ p = PurePosixPath(path.replace("\\", "/"))
123
+ name = p.name
124
+
125
+ # 1. Lockfiles — exact name match anywhere in the tree
126
+ if name in _LOCKFILE_NAMES:
127
+ return "lockfile"
128
+
129
+ # 2. Generated/minified — pattern match on filename
130
+ for pat in _GENERATED_PATTERNS:
131
+ if "/" not in pat and fnmatch.fnmatch(name, pat):
132
+ return "generated"
133
+
134
+ # 3. Docs — extension-based
135
+ for pat in _DOC_PATTERNS:
136
+ if fnmatch.fnmatch(name, pat):
137
+ return "docs"
138
+
139
+ # 4. Tests — filename pattern OR directory marker in path
140
+ for pat in _TEST_FILE_PATTERNS:
141
+ if fnmatch.fnmatch(name, pat):
142
+ return "tests"
143
+ parts = {seg.lower() for seg in p.parts[:-1]}
144
+ if parts & _TEST_DIR_MARKERS:
145
+ return "tests"
146
+
147
+ return None
148
+
149
+
150
+ @dataclass
151
+ class NoiseReport:
152
+ """Summary of what the filter suppressed and why."""
153
+
154
+ total_in: int = 0
155
+ kept: int = 0
156
+ capped: int = 0
157
+ discarded: int = 0
158
+ by_category: dict = field(default_factory=dict)
159
+
160
+ def to_dict(self) -> dict:
161
+ return {
162
+ "total_in": self.total_in,
163
+ "kept": self.kept,
164
+ "capped": self.capped,
165
+ "discarded": self.discarded,
166
+ "by_category": dict(self.by_category),
167
+ "statement": (
168
+ f"{self.kept}/{self.total_in} findings kept "
169
+ f"({self.capped} noise-capped, {self.discarded} discarded)"
170
+ ),
171
+ }
172
+
173
+
174
+ class NoiseFilter:
175
+ """Filters or severity-caps findings originating from noise files."""
176
+
177
+ def __init__(self, root=None, config: dict | None = None):
178
+ cfg = (config or {}).get("noise_filter", {})
179
+ self.enabled: bool = bool(cfg.get("enabled", True))
180
+ self.mode: str = cfg.get("mode", "cap") # "cap" | "discard"
181
+ self.skip_tests: bool = bool(cfg.get("skip_tests", True))
182
+ self.skip_locks: bool = bool(cfg.get("skip_locks", True))
183
+ self.skip_generated: bool = bool(cfg.get("skip_generated", True))
184
+ self.skip_docs: bool = bool(cfg.get("skip_docs", True))
185
+
186
+ # ── Single-path API ─────────────────────────────────────────────────────
187
+
188
+ def category_for(self, path: str) -> str | None:
189
+ """Category for this path, honoring the skip_* toggles."""
190
+ cat = classify(path)
191
+ if cat == "lockfile" and not self.skip_locks:
192
+ return None
193
+ if cat == "generated" and not self.skip_generated:
194
+ return None
195
+ if cat == "docs" and not self.skip_docs:
196
+ return None
197
+ if cat == "tests" and not self.skip_tests:
198
+ return None
199
+ return cat
200
+
201
+ def is_noise(self, path: str) -> bool:
202
+ return self.category_for(path) is not None
203
+
204
+ # ── Finding-list API ────────────────────────────────────────────────────
205
+
206
+ def apply(self, findings: list) -> tuple[list, NoiseReport]:
207
+ """Split findings into (kept, report).
208
+
209
+ Accepts Finding objects *or* plain dicts (as produced by
210
+ ``merge_results``); each entry needs a ``file`` key/attribute.
211
+ In "cap" mode noisy findings stay in the kept list but their
212
+ severity is downgraded to info and they gain a ``noise_category``
213
+ annotation. In "discard" mode they are removed entirely.
214
+ """
215
+ report = NoiseReport(total_in=len(findings))
216
+ kept: list = []
217
+ for f in findings:
218
+ path = _get(f, "file", "") or ""
219
+ cat = self.category_for(path) if (self.enabled and path) else None
220
+ if cat is None:
221
+ kept.append(f)
222
+ report.kept += 1
223
+ continue
224
+ report.by_category[cat] = report.by_category.get(cat, 0) + 1
225
+ if self.mode == "discard":
226
+ report.discarded += 1
227
+ continue
228
+ # cap mode: downgrade + annotate, never delete silently
229
+ _set_severity_info(f)
230
+ _set(f, "noise_category", cat)
231
+ kept.append(f)
232
+ report.capped += 1
233
+ report.kept += 1
234
+ if report.total_in and (report.capped or report.discarded):
235
+ _log.info(
236
+ "Noise filter: %d/%d findings from noise files (%s)",
237
+ report.capped + report.discarded,
238
+ report.total_in,
239
+ ", ".join(f"{k}={v}" for k, v in sorted(report.by_category.items())),
240
+ )
241
+ return kept, report
242
+
243
+
244
+ # ── Polymorphic accessors (Finding objects vs merged dicts) ─────────────
245
+
246
+
247
+ def _get(f, key: str, default=None):
248
+ if isinstance(f, dict):
249
+ return f.get(key, default)
250
+ return getattr(f, key, default)
251
+
252
+
253
+ def _set(f, key: str, value) -> None:
254
+ if isinstance(f, dict):
255
+ f[key] = value
256
+ return
257
+ try:
258
+ setattr(f, key, value)
259
+ except Exception as _exc: # noqa: BLE001 — frozen/sealed objects: skip silently
260
+ _log.debug('_set skipped: %s', _exc)
261
+
262
+
263
+ def _set_severity_info(f) -> None:
264
+ """Downgrade severity to INFO, matching the container's own encoding."""
265
+ if isinstance(f, dict):
266
+ # merge_results dicts carry severity as a plain string
267
+ f["severity"] = "info"
268
+ return
269
+ try:
270
+ from patchi.core.agents.base import Severity
271
+
272
+ f.severity = Severity.INFO
273
+ except Exception as _exc: # noqa: BLE001 — frozen/sealed objects: annotate only
274
+ _log.debug('_set_severity_info skipped: %s', _exc)