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,216 @@
1
+ """
2
+ App Mapper — crawls a live deployed app and builds a structured map.
3
+
4
+ Uses urllib (no external deps) or Crawl4AI when available.
5
+ Returns structured map of pages, forms, links, and input fields.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ import urllib.error
13
+ import urllib.request
14
+ from html.parser import HTMLParser
15
+ from urllib.parse import urljoin, urlparse
16
+
17
+ from patchi.core.agents.base import (
18
+ AgentGroup,
19
+ AgentInput,
20
+ AgentResult,
21
+ BaseAgent,
22
+ Finding,
23
+ Severity,
24
+ register,
25
+ )
26
+ from patchi.core.constants import is_offline
27
+
28
+ _log = logging.getLogger("patchi.security.app_mapper")
29
+
30
+
31
+ class _LinkFormParser(HTMLParser):
32
+ """Extract links and forms from HTML."""
33
+
34
+ def __init__(self, base_url: str):
35
+ super().__init__()
36
+ self.base_url = base_url
37
+ self.links: list[str] = []
38
+ self.forms: list[dict] = []
39
+ self._in_form = False
40
+ self._form_action = ""
41
+ self._form_method = "GET"
42
+ self._form_fields: list[dict] = []
43
+ self._title = ""
44
+
45
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
46
+ attr_dict = {k: v or "" for k, v in attrs}
47
+
48
+ if tag == "title":
49
+ pass # handled in data
50
+
51
+ if tag == "a":
52
+ href = attr_dict.get("href", "")
53
+ if href and not href.startswith(("#", "javascript:", "mailto:")):
54
+ full = urljoin(self.base_url, href)
55
+ self.links.append(full)
56
+
57
+ if tag == "form":
58
+ self._in_form = True
59
+ self._form_action = urljoin(self.base_url, attr_dict.get("action", self.base_url))
60
+ self._form_method = attr_dict.get("method", "GET").upper()
61
+ self._form_fields = []
62
+
63
+ if tag == "input" and self._in_form:
64
+ self._form_fields.append(
65
+ {
66
+ "name": attr_dict.get("name", ""),
67
+ "type": attr_dict.get("type", "text"),
68
+ "placeholder": attr_dict.get("placeholder", ""),
69
+ }
70
+ )
71
+
72
+ if tag == "select" and self._in_form:
73
+ self._form_fields.append(
74
+ {
75
+ "name": attr_dict.get("name", ""),
76
+ "type": "select",
77
+ "placeholder": "",
78
+ }
79
+ )
80
+
81
+ if tag == "textarea" and self._in_form:
82
+ self._form_fields.append(
83
+ {
84
+ "name": attr_dict.get("name", ""),
85
+ "type": "textarea",
86
+ "placeholder": "",
87
+ }
88
+ )
89
+
90
+ def handle_endtag(self, tag: str):
91
+ if tag == "form" and self._in_form:
92
+ self._in_form = False
93
+ self.forms.append(
94
+ {
95
+ "action": self._form_action,
96
+ "method": self._form_method,
97
+ "fields": self._form_fields,
98
+ }
99
+ )
100
+
101
+ def handle_data(self, data: str):
102
+ pass
103
+
104
+
105
+ def patchi_map_app(url: str, max_pages: int = 20) -> dict:
106
+ """Crawl a live app and return structured map."""
107
+ if is_offline():
108
+ return {"pages": [], "total_pages": 0, "total_entry_points": 0}
109
+ visited: set[str] = set()
110
+ pages: list[dict] = []
111
+ queue = [url]
112
+
113
+ while queue and len(pages) < max_pages:
114
+ current = queue.pop(0)
115
+ if current in visited:
116
+ continue
117
+ visited.add(current)
118
+
119
+ try:
120
+ req = urllib.request.Request(current, headers={"User-Agent": "Patchi-AppMapper/1.0"})
121
+ with urllib.request.urlopen(req, timeout=10) as resp:
122
+ content_type = resp.headers.get("Content-Type", "")
123
+ if "text/html" not in content_type:
124
+ continue
125
+ html = resp.read().decode("utf-8", errors="ignore")
126
+ except (urllib.error.URLError, urllib.error.HTTPError, OSError):
127
+ continue
128
+
129
+ parser = _LinkFormParser(current)
130
+ try:
131
+ parser.feed(html)
132
+ except Exception as e:
133
+ _log.warning("patchi_map_app failed: %s", e)
134
+ continue
135
+
136
+ # Extract title
137
+ title_match = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.DOTALL)
138
+ title = title_match.group(1).strip() if title_match else ""
139
+
140
+ pages.append(
141
+ {
142
+ "url": current,
143
+ "title": title,
144
+ "forms": parser.forms,
145
+ "links": list(set(parser.links)),
146
+ }
147
+ )
148
+
149
+ # Queue new links (same origin only)
150
+ origin = urlparse(url).netloc
151
+ for link in parser.links:
152
+ if urlparse(link).netloc == origin and link not in visited:
153
+ queue.append(link)
154
+
155
+ total_entry_points = sum(len(p["forms"]) + len(p["links"]) for p in pages)
156
+
157
+ return {
158
+ "pages": pages,
159
+ "total_pages": len(pages),
160
+ "total_entry_points": total_entry_points,
161
+ }
162
+
163
+
164
+ # ── App Mapper Agent ──────────────────────────────────────────────────────────
165
+
166
+
167
+ @register
168
+ class AppMapperAgent(BaseAgent):
169
+ """Crawls live app and maps pages, forms, and entry points."""
170
+
171
+ name = "AppMapperAgent"
172
+ group = AgentGroup.SECURITY
173
+ timeout = 60
174
+
175
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
176
+ app_url = (
177
+ inp.brain.get("app_url", "")
178
+ or inp.config.get("app_url", "")
179
+ or inp.extra.get("app_url", "")
180
+ )
181
+
182
+ if not app_url:
183
+ result.add_finding(
184
+ Finding(
185
+ agent=self.name,
186
+ type="no_target_url",
187
+ severity=Severity.INFO,
188
+ file="",
189
+ message="No app URL configured — set 'app_url' in config to enable app mapping",
190
+ )
191
+ )
192
+ return
193
+
194
+ app_map = patchi_map_app(app_url)
195
+ result.data["app_map"] = app_map
196
+ result.data["total_pages"] = app_map["total_pages"]
197
+ result.data["total_entry_points"] = app_map["total_entry_points"]
198
+
199
+ # Flag forms without CSRF protection indicators
200
+ for page in app_map["pages"]:
201
+ for form in page["forms"]:
202
+ if form["method"] == "POST":
203
+ field_names = [f["name"].lower() for f in form["fields"]]
204
+ if not any("csrf" in n or "token" in n or "_token" in n for n in field_names):
205
+ result.add_finding(
206
+ Finding(
207
+ agent=self.name,
208
+ type="no_csrf_token",
209
+ severity=Severity.MEDIUM,
210
+ file=page["url"],
211
+ message=f"POST form at {form['action']} has no CSRF token field",
212
+ cwe="CWE-352",
213
+ )
214
+ )
215
+
216
+ result.files_scanned = app_map["total_pages"]
@@ -0,0 +1,464 @@
1
+ """
2
+ App Profile — component-level project profiling with domain scoring.
3
+
4
+ Detects what kind of project this is (CLI binary, backend API, frontend web,
5
+ library, mobile, etc.) and activates/deactivates security domains accordingly.
6
+
7
+ Three activation states per domain:
8
+ - Active: Domain applies to this project type
9
+ - N/A: Domain does not apply (e.g. web-frontend for a CLI tool)
10
+ - Unclear: Cannot determine — falls to heuristic/AI
11
+
12
+ Scoring:
13
+ - Only Active domains contribute to the weighted score
14
+ - context_score = Σ(domain_weight × domain_score) / Σ(domain_weight)
15
+ - N/A domains are excluded from both numerator and denominator
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from patchi.core.security.domain_loader import Domain, DomainLoader, normalize_component_type
26
+
27
+ _log = logging.getLogger("patchi.security.app_profile")
28
+
29
+
30
+ class ActivationState(str):
31
+ ACTIVE = "active"
32
+ NA = "n/a"
33
+ UNCLEAR = "unclear"
34
+
35
+
36
+ @dataclass
37
+ class DomainActivation:
38
+ domain_id: str
39
+ display_name: str
40
+ state: str # active | n/a | unclear
41
+ weight: float
42
+ score: float = 0.0
43
+ reason: str = ""
44
+
45
+ def to_dict(self) -> dict:
46
+ return {
47
+ "domain_id": self.domain_id,
48
+ "display_name": self.display_name,
49
+ "state": self.state,
50
+ "weight": self.weight,
51
+ "score": round(self.score, 1),
52
+ "reason": self.reason,
53
+ }
54
+
55
+
56
+ @dataclass
57
+ class ComponentProfile:
58
+ component_type: (
59
+ str # cli-binary | backend-api | frontend-web | library | mobile | embedded | unknown
60
+ )
61
+ display_name: str = ""
62
+ frameworks: list[str] = field(default_factory=list)
63
+ languages: list[str] = field(default_factory=list)
64
+ has_database: bool = False
65
+ has_authentication: bool = False
66
+ has_network: bool = False
67
+ has_user_input: bool = False
68
+ is_packaged: bool = False
69
+ detection_signals: dict[str, Any] = field(default_factory=dict)
70
+
71
+ def to_dict(self) -> dict:
72
+ return {
73
+ "component_type": self.component_type,
74
+ "display_name": self.display_name,
75
+ "frameworks": self.frameworks,
76
+ "languages": self.languages,
77
+ "has_database": self.has_database,
78
+ "has_authentication": self.has_authentication,
79
+ "has_network": self.has_network,
80
+ "has_user_input": self.has_user_input,
81
+ "is_packaged": self.is_packaged,
82
+ }
83
+
84
+
85
+ _COMPONENT_SIGNALS: dict[str, dict] = {
86
+ "backend-api": {
87
+ "frameworks": {
88
+ "fastapi",
89
+ "flask",
90
+ "django",
91
+ "express",
92
+ "koa",
93
+ "spring",
94
+ "actix",
95
+ "rocket",
96
+ "gin",
97
+ "echo",
98
+ },
99
+ "files": {"routes/", "api/", "controllers/", "middleware/", "serializers/"},
100
+ "deps": {"fastapi", "flask", "django", "express", "axios", "rest"},
101
+ },
102
+ "frontend-web": {
103
+ "frameworks": {"react", "vue", "angular", "svelte", "next", "nuxt", "solid"},
104
+ "files": {"components/", "pages/", "views/", "templates/"},
105
+ "deps": {"react", "vue", "angular", "svelte"},
106
+ },
107
+ "cli-binary": {
108
+ "frameworks": {"click", "typer", "argparse", "clap", "cobra", "commander"},
109
+ "files": {"cli/", "commands/"},
110
+ "deps": {"click", "typer", "argparse", "clap"},
111
+ },
112
+ "library": {
113
+ "frameworks": set(),
114
+ "files": {"src/", "lib/"},
115
+ "deps": set(),
116
+ },
117
+ "mobile": {
118
+ "frameworks": {"react-native", "flutter", "swiftui", "jetpack"},
119
+ "files": {"android/", "ios/", "lib/"},
120
+ "deps": {"react-native", "flutter"},
121
+ },
122
+ "embedded": {
123
+ "frameworks": set(),
124
+ "files": {"firmware/", "hal/", "drivers/"},
125
+ "deps": set(),
126
+ },
127
+ "infra": {
128
+ "frameworks": {
129
+ "docker",
130
+ "kubernetes",
131
+ "terraform",
132
+ "ansible",
133
+ "pulumi",
134
+ "helm",
135
+ "kustomize",
136
+ },
137
+ "files": {
138
+ "Dockerfile",
139
+ "docker-compose",
140
+ ".github/workflows/",
141
+ ".gitlab-ci/",
142
+ "k8s/",
143
+ "deploy/",
144
+ "infra/",
145
+ },
146
+ "deps": {"docker", "docker-compose", "kubernetes", "terraform"},
147
+ },
148
+ "llm-integration": {
149
+ "frameworks": {"langchain", "llamaindex", "openai", "anthropic", "cohere", "transformers"},
150
+ "files": {"llm/", "ai/", "prompts/", "embeddings/", "vector-store/", "agents/"},
151
+ "deps": {"openai", "langchain", "anthropic", "cohere", "transformers"},
152
+ },
153
+ }
154
+
155
+ _COMPONENT_DOMAIN_MAP: dict[str, list[str]] = {
156
+ "backend-api": ["backend-api", "web-frontend"],
157
+ "frontend-web": ["web-frontend"],
158
+ "frontend": ["web-frontend"],
159
+ "cli-binary": [],
160
+ "library": [],
161
+ "mobile": ["web-frontend"],
162
+ "infra": ["cicd-pipeline", "container-infra", "supply-chain-local-tool"],
163
+ "infrastructure": ["cicd-pipeline", "container-infra", "supply-chain-local-tool"],
164
+ "llm-integration": ["llm-integration"],
165
+ "embedded": [],
166
+ }
167
+
168
+
169
+ class AppProfileBuilder:
170
+ """Builds an AppProfile from project files and brain data."""
171
+
172
+ def __init__(self, root: Path):
173
+ self.root = root
174
+
175
+ def build(self, brain: dict | None = None) -> ComponentProfile:
176
+ """Detect component type from project files and brain data."""
177
+ profile = ComponentProfile(component_type="unknown")
178
+ brain = brain or {}
179
+
180
+ # Languages from brain
181
+ lang_data = brain.get("languages", {})
182
+ if isinstance(lang_data, dict):
183
+ profile.languages = list(lang_data.keys())
184
+ elif isinstance(lang_data, list):
185
+ profile.languages = [str(lang) for lang in lang_data]
186
+
187
+ # Frameworks from brain
188
+ fw_raw = brain.get("frameworks", brain.get("framework", ""))
189
+ if isinstance(fw_raw, list):
190
+ profile.frameworks = [
191
+ f if isinstance(f, str) else (f.get("name", "") or "") for f in fw_raw
192
+ ]
193
+ elif isinstance(fw_raw, dict):
194
+ profile.frameworks = [fw_raw.get("name", "")] if fw_raw.get("name") else []
195
+ elif isinstance(fw_raw, str):
196
+ profile.frameworks = [fw_raw] if fw_raw else []
197
+
198
+ profile.frameworks = [f.lower() for f in profile.frameworks if f]
199
+
200
+ # Detection signals from file system
201
+ profile.detection_signals = self._scan_project_files()
202
+
203
+ # Component type inference
204
+ profile.component_type = self._infer_component_type(profile)
205
+ profile.display_name = (
206
+ brain.get("project_purpose", brain.get("project_name", "")) or profile.component_type
207
+ )
208
+
209
+ profile.has_database = profile.detection_signals.get("has_database", False)
210
+ profile.has_authentication = profile.detection_signals.get("has_authentication", False)
211
+ profile.has_network = profile.detection_signals.get("has_network", False)
212
+ profile.has_user_input = profile.detection_signals.get("has_user_input", False)
213
+ profile.is_packaged = profile.detection_signals.get("is_packaged", False)
214
+
215
+ return profile
216
+
217
+ def _scan_project_files(self) -> dict[str, Any]:
218
+ """Scan root for file-level signals (fast, no full AST)."""
219
+ signals: dict[str, Any] = {
220
+ "has_database": False,
221
+ "has_authentication": False,
222
+ "has_network": False,
223
+ "has_user_input": False,
224
+ "is_packaged": False,
225
+ "found_dirs": set(),
226
+ "found_deps": set(),
227
+ }
228
+
229
+ pyproject = self.root / "pyproject.toml"
230
+ if pyproject.exists():
231
+ signals["is_packaged"] = True
232
+ try:
233
+ content = pyproject.read_text(encoding="utf-8", errors="ignore")
234
+ if "django" in content:
235
+ signals["found_deps"].add("django")
236
+ if "fastapi" in content:
237
+ signals["found_deps"].add("fastapi")
238
+ if "click" in content or "typer" in content:
239
+ signals["found_deps"].add("click")
240
+ except Exception as e:
241
+ _log.warning("AppProfileBuilder._scan_project_files failed: %s", e)
242
+
243
+ package = self.root / "package.json"
244
+ if package.exists():
245
+ signals["is_packaged"] = True
246
+ try:
247
+ import json
248
+
249
+ data = json.loads(package.read_text(encoding="utf-8", errors="ignore"))
250
+ deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
251
+ signals["found_deps"].update(deps.keys())
252
+ except Exception as e:
253
+ _log.warning("AppProfileBuilder._scan_project_files failed: %s", e)
254
+
255
+ # Directory structure scan (limited depth)
256
+ for entry in self.root.iterdir():
257
+ if entry.is_dir() and not entry.name.startswith("."):
258
+ name = entry.name.lower()
259
+ if name in ("routes", "api", "controllers", "middleware"):
260
+ signals["found_dirs"].add(name)
261
+ signals["has_network"] = True
262
+ if name in ("components", "pages", "views", "templates"):
263
+ signals["found_dirs"].add(name)
264
+ signals["has_user_input"] = True
265
+ if name in ("cli", "commands"):
266
+ signals["found_dirs"].add(name)
267
+ if name in ("models", "migrations", "db", "database"):
268
+ signals["found_dirs"].add(name)
269
+ signals["has_database"] = True
270
+ if name in ("auth", "login", "sessions"):
271
+ signals["found_dirs"].add(name)
272
+ signals["has_authentication"] = True
273
+
274
+ return signals
275
+
276
+ def _infer_component_type(self, profile: ComponentProfile) -> str:
277
+ """Score each component type and return the best match."""
278
+ scores: dict[str, int] = dict.fromkeys(_COMPONENT_SIGNALS, 0)
279
+ signals = profile.detection_signals
280
+
281
+ for ct, sig in _COMPONENT_SIGNALS.items():
282
+ # Framework matches
283
+ for fw in profile.frameworks:
284
+ if fw in sig.get("frameworks", set()):
285
+ scores[ct] += 3
286
+
287
+ # Directory matches
288
+ for d in signals.get("found_dirs", set()):
289
+ if d + "/" in sig.get("files", set()):
290
+ scores[ct] += 2
291
+
292
+ # Dependency matches
293
+ for dep in signals.get("found_deps", set()):
294
+ if any(k in dep for k in sig.get("deps", set())):
295
+ scores[ct] += 1
296
+
297
+ best = max(scores, key=scores.get)
298
+ return best if scores[best] > 0 else "unknown"
299
+
300
+
301
+ class AppProfileScorer:
302
+ """Computes weighted domain scores from an AppProfile."""
303
+
304
+ def __init__(self, root: Path):
305
+ self.root = root
306
+ self._loader = DomainLoader(root)
307
+
308
+ def score(
309
+ self, profile: ComponentProfile, existing_scores: dict[str, float] | None = None
310
+ ) -> list[DomainActivation]:
311
+ """Score all domains against the component profile."""
312
+ existing_scores = existing_scores or {}
313
+ domain_ids = self._get_applicable_domains(profile)
314
+ results: list[DomainActivation] = []
315
+
316
+ for did in domain_ids:
317
+ domain = self._loader.get_domain(did)
318
+ if not domain:
319
+ continue
320
+
321
+ state, reason = self._determine_activation(domain, profile)
322
+ score = existing_scores.get(did, 50.0)
323
+
324
+ results.append(
325
+ DomainActivation(
326
+ domain_id=did,
327
+ display_name=domain.display_name,
328
+ state=state,
329
+ weight=domain.weight,
330
+ score=score if state == ActivationState.ACTIVE else 0.0,
331
+ reason=reason,
332
+ )
333
+ )
334
+
335
+ return results
336
+
337
+ def compute_context_score(self, activations: list[DomainActivation]) -> float:
338
+ """Weighted sum of active domain scores."""
339
+ total_weight = 0.0
340
+ weighted_sum = 0.0
341
+ for a in activations:
342
+ if a.state == ActivationState.ACTIVE:
343
+ total_weight += a.weight
344
+ weighted_sum += a.weight * a.score
345
+
346
+ if total_weight == 0:
347
+ return 50.0
348
+ return weighted_sum / total_weight
349
+
350
+ _TYPE_ALIASES: dict[str, str] = {
351
+ "infrastructure": "infra",
352
+ "frontend": "frontend-web",
353
+ "web-frontend": "frontend-web",
354
+ "backend-api,frontend-web": "backend-api",
355
+ }
356
+
357
+ def _normalize_type(self, ct: str) -> str:
358
+ # Delegate to the loader's canonical normalization (same alias table)
359
+ # so scoped loading and applicability checks can never disagree.
360
+ ct = (ct or "").strip().lower()
361
+ if ct in self._TYPE_ALIASES:
362
+ return self._TYPE_ALIASES[ct]
363
+ return normalize_component_type(ct)
364
+
365
+ def _get_applicable_domains(self, profile: ComponentProfile) -> list[str]:
366
+ """Return domain IDs that may apply to this component type.
367
+
368
+ The loader is scoped to the profile's component type before enumerating,
369
+ so a narrow profile only ever parses the domain files that could match
370
+ (plus the explicit component->domain map entries) instead of the full
371
+ taxonomy.
372
+ """
373
+ profile_type = self._normalize_type(profile.component_type)
374
+ explicit = list(_COMPONENT_DOMAIN_MAP.get(profile.component_type, []))
375
+
376
+ # Both axes: component-type match AND explicit domain ids (e.g. a
377
+ # backend-api profile always loads the web-frontend domain).
378
+ self._loader.set_component_scope(
379
+ component_types=[profile.component_type], domain_ids=explicit
380
+ )
381
+
382
+ all_domains = self._loader.list_domains()
383
+ for d in all_domains:
384
+ if d not in explicit:
385
+ domain = self._loader.get_domain(d)
386
+ if domain and domain.component_type:
387
+ comp_types = [
388
+ self._normalize_type(ct.strip()) for ct in domain.component_type.split(",")
389
+ ]
390
+ if profile_type in comp_types:
391
+ explicit.append(d)
392
+
393
+ return explicit
394
+
395
+ def _determine_activation(self, domain: Domain, profile: ComponentProfile) -> tuple[str, str]:
396
+ """Determine if a domain is Active, N/A, or Unclear."""
397
+ sig = domain.activation_signals or {}
398
+
399
+ # Check exclusion signals first
400
+ excluded_if = sig.get("excluded_if", [])
401
+ for cond in excluded_if:
402
+ if self._check_signal(cond, profile):
403
+ return ActivationState.NA, f"Excluded by: {cond}"
404
+
405
+ # Check required signals
406
+ required_any = sig.get("required_any", [])
407
+ if required_any:
408
+ for cond in required_any:
409
+ if self._check_signal(cond, profile):
410
+ return ActivationState.ACTIVE, f"Matched: {cond}"
411
+ return ActivationState.UNCLEAR, f"No required signal matched: {required_any}"
412
+
413
+ # Check framework match
414
+ fw = sig.get("framework", "")
415
+ if fw and fw.lower() in [f.lower() for f in profile.frameworks]:
416
+ return ActivationState.ACTIVE, f"Framework matched: {fw}"
417
+
418
+ # Default: active if component type is relevant (handles comma-separated composite types)
419
+ comp_type = sig.get("component_type", "")
420
+ if comp_type and profile.component_type in [ct.strip() for ct in comp_type.split(",")]:
421
+ return ActivationState.ACTIVE, f"Component type: {comp_type}"
422
+
423
+ return ActivationState.UNCLEAR, "No signals matched — unable to determine activation"
424
+
425
+ def _check_signal(self, condition: str, profile: ComponentProfile) -> bool:
426
+ """Check a single activation condition string."""
427
+ cond = condition.lower()
428
+
429
+ # Framework checks — check if any framework name appears as a keyword in the signal
430
+ for fw in profile.frameworks:
431
+ fw_key = fw.lower().replace("-", "").replace("_", "")
432
+ cond_key = cond.replace("-", "").replace("_", "").replace("(", "").replace(")", "")
433
+ if fw_key in cond_key:
434
+ return True
435
+
436
+ # Dependency checks — check known dependency keywords
437
+ for dep in profile.detection_signals.get("found_deps", set()):
438
+ dep_key = dep.lower().replace("-", "").replace("_", "")
439
+ if dep_key in cond:
440
+ return True
441
+
442
+ # Component type check
443
+ if profile.component_type in cond:
444
+ return True
445
+
446
+ # Directory structure checks
447
+ for d in profile.detection_signals.get("found_dirs", set()):
448
+ if d in cond:
449
+ return True
450
+
451
+ # Feature checks
452
+ feature_map = {
453
+ "database": profile.has_database,
454
+ "auth": profile.has_authentication,
455
+ "authentication": profile.has_authentication,
456
+ "network": profile.has_network,
457
+ "user_input": profile.has_user_input,
458
+ "packaged": profile.is_packaged,
459
+ }
460
+ for feature, present in feature_map.items():
461
+ if feature in cond and present:
462
+ return True
463
+
464
+ return False