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,410 @@
1
+ """
2
+ EnvScanner — secret/credential pattern detection.
3
+
4
+ Scans for potentially sensitive information in:
5
+ - Environment files (.env, .env.local, .env.production, etc.)
6
+ - Configuration files (json, yaml, toml)
7
+ - Source code (hardcoded secrets, API keys, passwords)
8
+ - Build files and scripts
9
+
10
+ Looks for patterns like:
11
+ - AWS keys (AKIA..., AGPA..., AIDA..., etc.)
12
+ - GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
13
+ - Slack tokens (xox[a-z]-...)
14
+ - Password fields in configs
15
+ - Database URLs with credentials
16
+ - Private key headers
17
+
18
+ Does NOT call AI.
19
+ Does NOT write to disk.
20
+ Does NOT touch the queue.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import re
27
+ from pathlib import Path
28
+
29
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
30
+
31
+ from .base import (
32
+ AgentGroup,
33
+ AgentInput,
34
+ AgentResult,
35
+ AgentStatus,
36
+ BaseAgent,
37
+ Finding,
38
+ Severity,
39
+ make_finding,
40
+ register,
41
+ safe_rglob,
42
+ scope_allows,
43
+ )
44
+
45
+ # Directories holding intentional fake secrets (attacker fixtures, test
46
+ # data) — scanning them only produces false positives.
47
+ _FIXTURE_DIRS = ("attack_scenarios/", "tests/fixtures/", "fixtures/")
48
+
49
+ _log = logging.getLogger("patchi.agents.env_scanner")
50
+
51
+
52
+ @register
53
+ class EnvScanner(BaseAgent):
54
+ """Scanner for environment variables and secrets."""
55
+
56
+ group = AgentGroup.SCANNER
57
+ name = "EnvScanner"
58
+ description = "Secret/credential pattern detection"
59
+
60
+ # Common secret patterns
61
+ SECRET_PATTERNS = [
62
+ # AWS keys
63
+ (r"AKIA[0-9A-Z]{16}", "AWS Access Key"),
64
+ (r"AGPA[0-9A-Z]{16}", "AWS Access Key"),
65
+ (r"AIDA[0-9A-Z]{16}", "AWS Access Key"),
66
+ (r"AROA[0-9A-Z]{16}", "AWS Access Key"),
67
+ (r"ASIA[0-9A-Z]{16}", "AWS Access Key"),
68
+ # AWS secret keys
69
+ (r"[0-9a-zA-Z/+]{40}(?=\s|$)", "AWS Secret Key"),
70
+ # GitHub tokens
71
+ (r"ghp_[0-9a-zA-Z]{36}", "GitHub Personal Access Token"),
72
+ (r"gho_[0-9a-zA-Z]{36}", "GitHub OAuth Access Token"),
73
+ (r"ghu_[0-9a-zA-Z]{36}", "GitHub User-to-server Token"),
74
+ (r"ghs_[0-9a-zA-Z]{36}", "GitHub Server-to-Server Token"),
75
+ (r"ghr_[0-9a-zA-Z]{76}", "GitHub Refresh Token"),
76
+ # Slack tokens
77
+ (r"xox[a-zA-Z]-[0-9A-Za-z-]+", "Slack Token"),
78
+ # Slack webhooks
79
+ (
80
+ r"https://hooks\.slack\.com/services/T[A-Z0-9]{8}/B[A-Z0-9]{8}/[a-zA-Z0-9]{24}",
81
+ "Slack Webhook",
82
+ ),
83
+ # Google API keys
84
+ (r"AIza[0-9A-Za-z\\-_]{35}", "Google API Key"),
85
+ # Google OAuth
86
+ (r"[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com", "Google OAuth Client ID"),
87
+ # Facebook access tokens
88
+ (r"EAACEdEose0cBA[0-9A-Za-z]+", "Facebook Access Token"),
89
+ # Passwords in various formats
90
+ (r'password\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
91
+ (r'pwd\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
92
+ (r'passwd\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
93
+ # Database connection strings with credentials
94
+ (r"postgres://[a-zA-Z0-9_%]+:[^@]+@", "PostgreSQL Connection String"),
95
+ (r"mysql://[a-zA-Z0-9_%]+:[^@]+@", "MySQL Connection String"),
96
+ (r"mongodb://[a-zA-Z0-9_%]+:[^@]+@", "MongoDB Connection String"),
97
+ # Private key headers
98
+ (r"-----BEGIN RSA PRIVATE KEY-----", "RSA Private Key"),
99
+ (r"-----BEGIN DSA PRIVATE KEY-----", "DSA Private Key"),
100
+ (r"-----BEGIN EC PRIVATE KEY-----", "EC Private Key"),
101
+ (r"-----BEGIN OPENSSH PRIVATE KEY-----", "OpenSSH Private Key"),
102
+ (r"-----BEGIN PRIVATE KEY-----", "Private Key"),
103
+ # SSH keys
104
+ (r"ssh-rsa [A-Za-z0-9+/\s]+={0,2}", "SSH Public Key"),
105
+ (r"ssh-ed25519 [A-Za-z0-9+/\s]+={0,2}", "SSH Ed25519 Key"),
106
+ # Generic API keys
107
+ (r'api[_-]?key["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic API Key"),
108
+ (r'api[_-]?token["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic API Token"),
109
+ (r'secret["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic Secret"),
110
+ (r'token["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic Token"),
111
+ ]
112
+
113
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
114
+ """Scan for environment variables and secrets."""
115
+ findings = []
116
+
117
+ # Define environment file patterns
118
+ env_patterns = [
119
+ ".env",
120
+ ".env.*",
121
+ ".envrc",
122
+ "*.env",
123
+ "*.config",
124
+ "*.conf",
125
+ "*.ini",
126
+ "*.properties",
127
+ "*.json",
128
+ "*.yaml",
129
+ "*.yml",
130
+ "*.toml",
131
+ "*.py",
132
+ "*.js",
133
+ "*.ts",
134
+ "*.jsx",
135
+ "*.tsx",
136
+ "*.sh",
137
+ "*.bash",
138
+ "*.zsh",
139
+ "*.ps1",
140
+ "*.bat",
141
+ "Dockerfile*",
142
+ "docker-compose*.yml",
143
+ "docker-compose*.yaml",
144
+ "package.json",
145
+ "requirements.txt",
146
+ "Pipfile",
147
+ "Gemfile",
148
+ "composer.json",
149
+ ]
150
+
151
+ # Search for environment/config files
152
+ for pattern in env_patterns:
153
+ for file_path in safe_rglob(inp.root, pattern):
154
+ if file_path.is_file():
155
+ rel_path = file_path.relative_to(inp.root).as_posix()
156
+ if not scope_allows(inp, rel_path):
157
+ continue
158
+ if not self._should_skip_file(rel_path, inp):
159
+ findings.extend(self._scan_env_file(file_path, rel_path))
160
+
161
+ result.status = AgentStatus.SUCCEEDED
162
+ result.findings = findings
163
+ result.data.update(
164
+ {
165
+ "secrets_found": len(
166
+ [
167
+ f
168
+ for f in findings
169
+ if "Secret" in f.title or "Token" in f.title or "Key" in f.title
170
+ ]
171
+ ),
172
+ "passwords_found": len([f for f in findings if "Password" in f.title]),
173
+ "needs_ai": False,
174
+ }
175
+ )
176
+ return
177
+
178
+ def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
179
+ """Check if file should be skipped based on restrictions."""
180
+ # Skip ignored directories (node_modules, .venv, etc.)
181
+ from pathlib import PurePosixPath
182
+
183
+ if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
184
+ return True
185
+
186
+ # Skip intentional-secret fixtures (attacker scenarios, test data),
187
+ # wherever nested in the tree
188
+ parts = PurePosixPath(file_path).parts
189
+ if any(d.strip("/") in parts for d in _FIXTURE_DIRS):
190
+ return True
191
+
192
+ from pathlib import PurePosixPath
193
+
194
+ # Check restrictions
195
+ restrictions = inp.config.get("restrictions", [])
196
+ for r in restrictions:
197
+ if r.get("enabled", True):
198
+ path = r["path"]
199
+ if file_path.startswith(path) or PurePosixPath(file_path).match(path):
200
+ if r["type"] == "NO_TOUCH":
201
+ return True
202
+ elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
203
+ return True
204
+ return False
205
+
206
+ def _scan_env_file(self, file_path: Path, rel_path: str) -> list[Finding]:
207
+ """Scan an environment/config file for secrets."""
208
+ findings = []
209
+
210
+ if file_path.name == "package-lock.json":
211
+ return findings
212
+
213
+ try:
214
+ content = file_path.read_text(encoding="utf-8")
215
+ lines = content.splitlines()
216
+
217
+ # Check each line for secret patterns
218
+ for line_num, line in enumerate(lines, 1):
219
+ stripped = line.strip()
220
+ # Skip comment lines and pattern/regex definitions (not real secrets)
221
+ if stripped.startswith(("#", "//", "*", "/*")):
222
+ continue
223
+ _fp_markers = (
224
+ "re.compile",
225
+ "re.search",
226
+ "re.match",
227
+ "re.finditer",
228
+ "SECRET_PATTERNS",
229
+ "_PATTERN",
230
+ "REGEX",
231
+ "RE_",
232
+ )
233
+ # Self-detection guard: regex pattern strings that LOOK like
234
+ # secrets (e.g. "-----BEGIN RSA PRIVATE KEY-----" inside
235
+ # this file) must not be flagged.
236
+ if "PRIVATE KEY" in line and ('r"' in line or "r'" in line):
237
+ continue
238
+ if any(m in line for m in _fp_markers):
239
+ continue
240
+ for pattern, description in self.SECRET_PATTERNS:
241
+ matches = re.finditer(pattern, line, re.IGNORECASE)
242
+ for match in matches:
243
+ masked_match = self._mask_sensitive_info(match.group(0))
244
+ findings.append(
245
+ make_finding(
246
+ severity=Severity.CRITICAL,
247
+ finding_type="hardcoded_secret",
248
+ file=rel_path,
249
+ line_start=line_num,
250
+ title=f"Potential {description}",
251
+ description=f"Found potential {description} in {file_path.name}",
252
+ evidence=f"Match: {masked_match}",
253
+ )
254
+ )
255
+
256
+ # Special check for .env files - look for common environment variable patterns
257
+ if ".env" in file_path.name.lower():
258
+ findings.extend(self._scan_dotenv_file(file_path, rel_path, lines))
259
+
260
+ except UnicodeDecodeError:
261
+ # If it's a binary file, try to read as binary and decode what we can
262
+ try:
263
+ content = file_path.read_bytes()
264
+ # Try to decode as UTF-8, ignoring errors
265
+ text_content = content.decode("utf-8", errors="ignore")
266
+ lines = text_content.splitlines()
267
+
268
+ for line_num, line in enumerate(lines, 1):
269
+ for pattern, description in self.SECRET_PATTERNS:
270
+ matches = re.finditer(pattern, line, re.IGNORECASE)
271
+ for match in matches:
272
+ masked_match = self._mask_sensitive_info(match.group(0))
273
+
274
+ findings.append(
275
+ make_finding(
276
+ severity=Severity.CRITICAL,
277
+ finding_type="hardcoded_secret",
278
+ file=rel_path,
279
+ line_start=line_num,
280
+ title=f"Potential {description}",
281
+ description=f"Found potential {description} in {file_path.name}",
282
+ evidence=f"Match: {masked_match}",
283
+ )
284
+ )
285
+ except Exception as e:
286
+ _log.warning("EnvScanner._scan_env_file failed: %s", e)
287
+ findings.append(
288
+ make_finding(
289
+ severity=Severity.LOW,
290
+ file=rel_path,
291
+ line_start=0,
292
+ title="File read error",
293
+ description=f"Could not read {file_path.name} for environment variable scanning",
294
+ evidence="Binary or corrupted file",
295
+ )
296
+ )
297
+ except Exception as e:
298
+ findings.append(
299
+ make_finding(
300
+ severity=Severity.LOW,
301
+ file=rel_path,
302
+ line_start=0,
303
+ title="Environment file read error",
304
+ description=f"Could not read environment file {file_path.name}: {str(e)}",
305
+ evidence=str(e),
306
+ )
307
+ )
308
+
309
+ return findings
310
+
311
+ def _scan_dotenv_file(self, file_path: Path, rel_path: str, lines: list[str]) -> list[Finding]:
312
+ """Special scanning for .env files."""
313
+ findings = []
314
+
315
+ # Look for common environment variable patterns that might contain secrets
316
+ for line_num, line in enumerate(lines, 1):
317
+ # Skip comments and empty lines
318
+ if line.strip().startswith("#") or not line.strip():
319
+ continue
320
+
321
+ # Look for assignment patterns
322
+ if "=" in line:
323
+ var_name, var_value = line.split("=", 1)
324
+ var_name = var_name.strip()
325
+ var_value = var_value.strip().strip("\"'") # Remove quotes
326
+
327
+ # Check if the variable name suggests it might be sensitive
328
+ sensitive_keywords = [
329
+ "KEY",
330
+ "SECRET",
331
+ "TOKEN",
332
+ "PASSWORD",
333
+ "PWD",
334
+ "PASSPHRASE",
335
+ "API",
336
+ "AUTH",
337
+ "CREDENTIAL",
338
+ "ACCESS",
339
+ "CLIENT_ID",
340
+ "CLIENT_SECRET",
341
+ "DATABASE_URL",
342
+ "DB_URL",
343
+ "REDIS_URL",
344
+ "MONGO_URL",
345
+ ]
346
+
347
+ if any(keyword in var_name.upper() for keyword in sensitive_keywords):
348
+ # Check if the value looks like a secret
349
+ if self._looks_like_secret(var_value):
350
+ findings.append(
351
+ make_finding(
352
+ severity=Severity.HIGH,
353
+ finding_type="hardcoded_secret",
354
+ file=rel_path,
355
+ line_start=line_num,
356
+ title=f"Potential secret in environment variable: {var_name}",
357
+ description=f"Environment variable '{var_name}' may contain sensitive information",
358
+ evidence=f"Variable: {var_name}, Value (masked): {self._mask_sensitive_info(var_value)}",
359
+ )
360
+ )
361
+
362
+ return findings
363
+
364
+ def _looks_like_secret(self, value: str) -> bool:
365
+ """Check if a value looks like it might be a secret."""
366
+ # Check for common secret characteristics
367
+ if len(value) < 6: # Too short to be a secret
368
+ return False
369
+
370
+ # Check for randomness - if it has high entropy it might be a secret
371
+ if self._has_high_entropy(value):
372
+ return True
373
+
374
+ # Check against our known patterns
375
+ for pattern, _ in self.SECRET_PATTERNS:
376
+ if re.search(pattern, value, re.IGNORECASE):
377
+ return True
378
+
379
+ return False
380
+
381
+ def _has_high_entropy(self, text: str) -> bool:
382
+ """Check if text has high entropy (suggesting it might be a key/token)."""
383
+ if len(text) < 10: # Too short to reliably determine entropy
384
+ return False
385
+
386
+ # Calculate character diversity
387
+ unique_chars = len(set(text))
388
+ total_chars = len(text)
389
+
390
+ # If more than 50% of characters are unique, it might be high entropy
391
+ if unique_chars / total_chars > 0.5:
392
+ return True
393
+
394
+ return False
395
+
396
+ def _mask_sensitive_info(self, text: str) -> str:
397
+ """Mask sensitive information for display."""
398
+ if not text:
399
+ return ""
400
+
401
+ # For API keys and tokens, show first 4 and last 4 characters
402
+ if len(text) > 8:
403
+ return f"{text[:4]}...{text[-4:]}"
404
+
405
+ # For shorter texts, mask the middle
406
+ if len(text) > 4:
407
+ return f"{text[:2]}...{text[-2:]}"
408
+
409
+ # For very short texts, mask completely
410
+ return "***"
@@ -0,0 +1,89 @@
1
+ """
2
+ FeatureFlagArchaeology §2.1.4 — hardcoded boolean flags for >N releases.
3
+
4
+ Detects `if (FEATURE_X)`, `featureFlag === true` where flag value is constant
5
+ across N commits (git log + blame). Flags: true/false, 0/1, "on"/"off".
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ from patchi.core.agents.base import (
16
+ AgentGroup,
17
+ AgentInput,
18
+ AgentResult,
19
+ AgentStatus,
20
+ BaseAgent,
21
+ Severity,
22
+ make_finding,
23
+ register,
24
+ safe_rglob,
25
+ )
26
+
27
+ _log = logging.getLogger("patchi.agents.feature_flag")
28
+
29
+ _FLAG_RE = re.compile(r"(?:feature[_-]?flag|FLAG|ff[_A-Z]+|isEnabled|enabled)\s*[=:]\s*(true|false|1|0|\"on\"|\'on\'|\"off\"|\'off\')", re.I)
30
+ _BOOL_ASSIGN = re.compile(r"^\s*(?:const|let|var)?\s*(\w*(?:flag|enabled|feature)\w*)\s*=\s*(true|false)\b", re.I | re.M)
31
+
32
+
33
+ def _git_log_count(root: Path, rel: str, flag_name: str, commits: int = 20) -> int:
34
+ """How many recent commits keep the same flag value line."""
35
+ try:
36
+ out = subprocess.run(
37
+ ["git", "log", f"-{commits}", "--oneline", "--", rel],
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=5,
41
+ cwd=str(root),
42
+ )
43
+ if out.returncode != 0:
44
+ return 0
45
+ return len([line for line in out.stdout.splitlines() if line.strip()])
46
+ except Exception as exc: # noqa: BLE001
47
+ _log.debug("git log %s failed: %s", flag_name, exc)
48
+ return 0
49
+
50
+
51
+ @register
52
+ class FeatureFlagArchaeologyAgent(BaseAgent):
53
+ group = AgentGroup.SCANNER
54
+ name = "FeatureFlagArchaeologyAgent"
55
+ description = "Hardcoded boolean flags (feature flag archaeology) — §2.1.4"
56
+ timeout = 60
57
+
58
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
59
+ findings = []
60
+ for pattern in ("*.py", "*.js", "*.ts", "*.tsx", "*.go", "*.java"):
61
+ for fp in safe_rglob(inp.root, pattern):
62
+ if not fp.is_file():
63
+ continue
64
+ rel = fp.relative_to(inp.root).as_posix()
65
+ try:
66
+ txt = fp.read_text(encoding="utf-8", errors="replace")
67
+ except OSError:
68
+ continue
69
+ for m in _BOOL_ASSIGN.finditer(txt):
70
+ name = m.group(1)
71
+ val = m.group(2)
72
+ line = txt[: m.start()].count("\n") + 1
73
+ # Check git history depth for this flag line's file
74
+ depth = _git_log_count(inp.root, rel, name, commits=15)
75
+ if depth >= 10: # hardcoded for >10 releases
76
+ findings.append(
77
+ make_finding(
78
+ severity=Severity.LOW,
79
+ file=rel,
80
+ line_start=line,
81
+ title=f"Hardcoded flag {name}={val} for {depth} commits (archaeology)",
82
+ description=f"Boolean flag '{name}' has been {val} for {depth} recent commits touching {rel} — likely dead code. Remove flag or make dynamic. References: uber/piranha for refactoring.",
83
+ evidence=m.group(0).strip()[:120],
84
+ finding_type="hardcoded_flag",
85
+ )
86
+ )
87
+ result.status = AgentStatus.SUCCEEDED
88
+ result.findings = findings[:50]
89
+ result.data["flag_count"] = len(findings)
@@ -0,0 +1,133 @@
1
+ """
2
+ FrontendFrameworkAgent §8.1.1-5 — React/Vue/Svelte/Angular/Solid checks.
3
+
4
+ Uses ESLint plugins where available, else tree-sitter structural queries.
5
+
6
+ Checks:
7
+ React: hooks rules, missing key in list, useEffect dep array
8
+ Vue: v-for key, template type, composition API
9
+ Svelte: store subscription leak ($store without unsubscribe)
10
+ Angular: DI, template type, standalone
11
+ Solid: createEffect without cleanup
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+
18
+ from patchi.core.agents.base import (
19
+ AgentGroup,
20
+ AgentInput,
21
+ AgentResult,
22
+ AgentStatus,
23
+ BaseAgent,
24
+ Severity,
25
+ make_finding,
26
+ register,
27
+ safe_rglob,
28
+ )
29
+ from patchi.core.brain.code_query import (
30
+ extract_script_blocks,
31
+ js_call_names,
32
+ js_calls,
33
+ js_constructor_di_line,
34
+ js_identifier_lines,
35
+ js_jsx_attributes,
36
+ js_jsx_elements,
37
+ lang_for_file,
38
+ parse_js,
39
+ vue_template_attrs,
40
+ )
41
+
42
+ _log = logging.getLogger("patchi.agents.frontend_framework")
43
+
44
+ _HOOKS = {"useEffect", "useState", "useMemo", "useCallback", "useRef"}
45
+
46
+ @register
47
+ class FrontendFrameworkAgent(BaseAgent):
48
+ group = AgentGroup.SCANNER
49
+ name = "FrontendFrameworkAgent"
50
+ description = "React/Vue/Svelte/Angular/Solid framework checks §8.1.1-5"
51
+ timeout = 60
52
+
53
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
54
+ findings=[]
55
+ for pat in ("*.jsx","*.tsx","*.js","*.ts","*.vue","*.svelte"):
56
+ for fp in safe_rglob(inp.root, pat):
57
+ rel=fp.relative_to(inp.root).as_posix()
58
+ if "node_modules" in rel or "tests" in rel:
59
+ continue
60
+ try:
61
+ txt=fp.read_text(encoding="utf-8", errors="replace")
62
+ except OSError:
63
+ continue
64
+ lang = lang_for_file(rel)
65
+ tree = parse_js(txt, lang)
66
+ if tree is None:
67
+ continue
68
+ calls = js_calls(tree, lang)
69
+ names = {c.name for c in calls}
70
+ is_jsx = fp.suffix in (".jsx", ".tsx") or "React" in txt
71
+ # React: useEffect without dep array (single-argument call)
72
+ if is_jsx:
73
+ for call in calls:
74
+ if call.name == "useEffect" and len(call.arg_kinds) < 2:
75
+ findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=call.line, title="React useEffect missing deps", description="Add dependency array or disable exhaustive-deps consciously", finding_type="react_hook"))
76
+ break
77
+ # React: list rendering without key (previously dead branch)
78
+ attrs = js_jsx_attributes(tree, lang)
79
+ if any(c.name in ("map", "forEach") for c in calls):
80
+ if not any(name == "key" for _, name, _ in attrs):
81
+ elems = js_jsx_elements(tree, lang)
82
+ line0 = elems[0][1] if elems else 1
83
+ findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=line0, title="List element without key", description="Add key to list-rendered elements for stable diffing", finding_type="react_key"))
84
+ # Vue: v-for without :key (template markup via html.parser)
85
+ if fp.suffix == ".vue":
86
+ by_el: dict[tuple[str, int], set[str]] = {}
87
+ for tag, attr, line in vue_template_attrs(txt):
88
+ by_el.setdefault((tag, line), set()).add(attr)
89
+ for (_tag, line), attr_set in by_el.items():
90
+ if "v-for" in attr_set and "key" not in attr_set and ":key" not in attr_set:
91
+ findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=line, title="Vue v-for without :key", description="Add :key to v-for for stable diffing", finding_type="vue_key"))
92
+ break
93
+ # Svelte: $store subscription without unsubscribe
94
+ if fp.suffix == ".svelte":
95
+ for block in extract_script_blocks(txt) or [txt]:
96
+ stree = parse_js(block, "javascript")
97
+ if stree is None:
98
+ continue
99
+ snames = js_call_names(stree, "javascript")
100
+ stores = [
101
+ (t, n)
102
+ for t, n in js_identifier_lines(stree, "javascript")
103
+ if t.startswith("$")
104
+ ]
105
+ if not stores or "subscribe" not in snames:
106
+ continue
107
+ if snames & {"unsubscribe", "onDestroy"}:
108
+ continue
109
+ findings.append(make_finding(
110
+ severity=Severity.LOW, file=rel, line_start=stores[0][1],
111
+ title="Svelte store without unsubscribe",
112
+ description=(
113
+ "Store subscription may leak; "
114
+ "use $store auto-sub or onDestroy unsubscribe"
115
+ ),
116
+ finding_type="svelte_store_leak",
117
+ ))
118
+ break
119
+ # Angular: constructor DI (private param)
120
+ if "Angular" in txt or "@Component" in txt:
121
+ line = js_constructor_di_line(tree, lang)
122
+ if line:
123
+ findings.append(make_finding(severity=Severity.INFO, file=rel, line_start=line, title="Angular DI injection", description="Verify DI token provided", finding_type="angular_di"))
124
+ # Solid: createEffect without cleanup
125
+ if "createEffect" in names:
126
+ line = next(c.line for c in calls if c.name == "createEffect")
127
+ findings.append(make_finding(severity=Severity.INFO, file=rel, line_start=line, title="Solid createEffect without cleanup", description="Return cleanup function if needed", finding_type="solid_effect"))
128
+ if len(findings) >= 40:
129
+ break
130
+ if len(findings) >= 40:
131
+ break
132
+ result.status=AgentStatus.SUCCEEDED
133
+ result.findings=findings[:40]