nat-engine 1__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 (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,695 @@
1
+ # Copyright (C) 2026 Brad Guider
2
+ # This file is part of NAT (Neural Agent Testing Framework).
3
+ # Licensed under the AGPL-3.0. See LICENSE for details.
4
+ # Commercial licensing available — see COMMERCIAL_LICENSE.md.
5
+
6
+ """Browser-level security scenario generator.
7
+
8
+ Produces task dicts compatible with
9
+ :meth:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator.run`
10
+ that exercise XSS, SQL injection, path traversal, CRLF injection, prototype
11
+ pollution, Unicode normalization attacks, and SSRF payloads through the
12
+ browser form submission path.
13
+
14
+ Reuses existing payload sets from the API-level security checks
15
+ (``mannf.product.security.checks``) and adds new browser-specific categories.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import uuid
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Payload catalogues
28
+ # ---------------------------------------------------------------------------
29
+
30
+ # XSS payloads — various bypass vectors
31
+ _XSS_PAYLOADS: list[tuple[str, str]] = [
32
+ ("xss_basic", "<script>alert('xss')</script>"),
33
+ ("xss_img", "<img src=x onerror=alert(1)>"),
34
+ ("xss_svg", "<svg onload=alert(1)>"),
35
+ ("xss_javascript_uri", "javascript:alert(document.domain)"),
36
+ ("xss_event", "\" onmouseover=\"alert(1)"),
37
+ ("xss_template", "{{7*7}}"), # server-side template injection probe
38
+ ("xss_html_entity", "&lt;script&gt;alert(1)&lt;/script&gt;"),
39
+ ]
40
+
41
+ # SQL injection payloads
42
+ _SQL_PAYLOADS: list[tuple[str, str]] = [
43
+ ("sql_or", "' OR '1'='1"),
44
+ ("sql_drop", "'; DROP TABLE users; --"),
45
+ ("sql_union", "' UNION SELECT null,null,null--"),
46
+ ("sql_time", "' OR SLEEP(5)--"),
47
+ ("sql_blind", "' AND 1=1--"),
48
+ ("nosql", '{"$gt": ""}'),
49
+ ]
50
+
51
+ # Path traversal payloads
52
+ _PATH_TRAVERSAL_PAYLOADS: list[tuple[str, str]] = [
53
+ ("path_traversal_unix", "../../etc/passwd"),
54
+ ("path_traversal_win", "..\\..\\windows\\system32\\drivers\\etc\\hosts"),
55
+ ("path_traversal_url", "%2e%2e%2f%2e%2e%2fetc%2fpasswd"),
56
+ ("path_traversal_null", "../../etc/passwd\x00.jpg"),
57
+ ]
58
+
59
+ # CRLF injection payloads
60
+ _CRLF_PAYLOADS: list[tuple[str, str]] = [
61
+ ("crlf_basic", "foo\r\nSet-Cookie: injected=1"),
62
+ ("crlf_url_encoded", "foo%0d%0aSet-Cookie:%20injected=1"),
63
+ ("crlf_header_split", "value\r\nX-Injected: header"),
64
+ ]
65
+
66
+ # Prototype pollution payloads (relevant for JSON-consuming endpoints)
67
+ _PROTOTYPE_POLLUTION_PAYLOADS: list[tuple[str, str]] = [
68
+ ("proto_pollution_proto", '__proto__[admin]=true'),
69
+ ("proto_pollution_constructor", 'constructor[prototype][admin]=true'),
70
+ ("proto_pollution_json", '{"__proto__":{"admin":true}}'),
71
+ ]
72
+
73
+ # Unicode normalization attack payloads
74
+ _UNICODE_PAYLOADS: list[tuple[str, str]] = [
75
+ ("unicode_rtl", "\u202e\u0041\u202c"), # RTL override
76
+ ("unicode_fullwidth", "\uff1cscript\uff1ealert(1)\uff1c/script\uff1e"), # fullwidth
77
+ ("unicode_zero_width", "adm\u200bin"), # zero-width space inside "admin"
78
+ ("unicode_homoglyph", "\u0430\u0440\u0433"), # Cyrillic lookalikes
79
+ ]
80
+
81
+ # SSRF payloads (for URL-like fields)
82
+ _SSRF_PAYLOADS: list[tuple[str, str]] = [
83
+ ("ssrf_localhost", "http://localhost:80"),
84
+ ("ssrf_127", "http://127.0.0.1"),
85
+ ("ssrf_metadata", "http://169.254.169.254/latest/meta-data/"),
86
+ ("ssrf_ipv6", "http://[::1]"),
87
+ ("ssrf_0000", "http://0.0.0.0"),
88
+ ]
89
+
90
+ # Mapping of OWASP categories to payload groups
91
+ _OWASP_CATEGORIES: dict[str, str] = {
92
+ "xss": "A03:2021 — Injection / CWE-79",
93
+ "sql": "A03:2021 — Injection / CWE-89",
94
+ "nosql": "A03:2021 — Injection / CWE-943",
95
+ "path_traversal": "A01:2021 — Path Traversal / CWE-22",
96
+ "crlf": "A03:2021 — CRLF Injection / CWE-93",
97
+ "proto_pollution": "A08:2021 — Prototype Pollution / CWE-1321",
98
+ "unicode": "A03:2021 — Unicode Attack / CWE-116",
99
+ "ssrf": "A10:2021 — SSRF / CWE-918",
100
+ "grpc_malformed": "A03:2021 — Injection / CWE-20 (gRPC Malformed Protobuf)",
101
+ "grpc_oversized": "A05:2021 — Resource Exhaustion / CWE-400 (gRPC Oversized Message)",
102
+ "grpc_missing": "A03:2021 — Input Validation / CWE-20 (gRPC Missing Required Fields)",
103
+ "grpc_type_confusion": "A03:2021 — Type Confusion / CWE-843 (gRPC Type Mismatch)",
104
+ }
105
+
106
+ # CWE IDs per payload category prefix
107
+ _CWE_MAP: dict[str, str] = {
108
+ "xss": "CWE-79",
109
+ "sql": "CWE-89",
110
+ "nosql": "CWE-943",
111
+ "path_traversal": "CWE-22",
112
+ "crlf": "CWE-93",
113
+ "proto_pollution": "CWE-1321",
114
+ "unicode": "CWE-116",
115
+ "ssrf": "CWE-918",
116
+ "grpc_malformed": "CWE-20",
117
+ "grpc_oversized": "CWE-400",
118
+ "grpc_missing": "CWE-20",
119
+ "grpc_type_confusion": "CWE-843",
120
+ }
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # gRPC-specific fuzz payloads
124
+ # ---------------------------------------------------------------------------
125
+
126
+ # Malformed protobuf wire bytes — truncated, invalid wire types, etc.
127
+ _GRPC_MALFORMED_PAYLOADS: list[tuple[str, bytes]] = [
128
+ ("grpc_malformed_truncated", b"\x0a\x05he"), # truncated length-delimited
129
+ ("grpc_malformed_invalid_wire", b"\xff\xff\xff\xff"), # invalid wire type
130
+ ("grpc_malformed_varint_overflow", b"\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"), # overlong varint
131
+ ("grpc_malformed_empty", b""), # empty payload
132
+ ("grpc_malformed_random", bytes(range(256))), # all byte values
133
+ ]
134
+
135
+ # Oversized message payloads (serialised as JSON-encoded strings for the dynamic stub)
136
+ _GRPC_OVERSIZED_PAYLOADS: list[tuple[str, str]] = [
137
+ ("grpc_oversized_string", "A" * 1024 * 1024), # 1 MB string field
138
+ ("grpc_oversized_nested", '{"nested": ' + '{"a":1}' * 10000 + '}'), # deep nesting
139
+ ("grpc_oversized_array", str(list(range(100_000)))), # large repeated field
140
+ ]
141
+
142
+ # Missing required fields — empty dict triggers server-side validation
143
+ _GRPC_MISSING_FIELD_PAYLOAD = {}
144
+
145
+ # Type confusion payloads — send wrong types for fields
146
+ _GRPC_TYPE_CONFUSION_PAYLOADS: list[tuple[str, dict]] = [
147
+ ("grpc_type_confusion_string_as_int", {"id": "not_an_integer"}),
148
+ ("grpc_type_confusion_bool_as_string", {"active": "yes"}),
149
+ ("grpc_type_confusion_null_required", {"name": None}),
150
+ ("grpc_type_confusion_array_as_scalar", {"count": [1, 2, 3]}),
151
+ ]
152
+
153
+
154
+ # URL-like field names that should receive SSRF payloads
155
+ _URL_FIELD_HINTS = {
156
+ "url", "uri", "href", "link", "src", "source", "redirect",
157
+ "callback", "webhook", "endpoint", "target", "host", "domain", "website",
158
+ }
159
+
160
+ # Fields that should NOT receive injection payloads (hidden, file, etc.)
161
+ _SKIP_TYPES = {"hidden", "file", "submit", "button", "reset", "image"}
162
+
163
+
164
+ def _payload_category(payload_type: str) -> str:
165
+ """Return the OWASP category label for a given payload type prefix."""
166
+ for prefix, category in _OWASP_CATEGORIES.items():
167
+ if payload_type.startswith(prefix):
168
+ return category
169
+ return "A03:2021 — Injection"
170
+
171
+
172
+ def _cwe_for(payload_type: str) -> str:
173
+ """Return the CWE identifier for a given payload type prefix."""
174
+ for prefix, cwe in _CWE_MAP.items():
175
+ if payload_type.startswith(prefix):
176
+ return cwe
177
+ return "CWE-74"
178
+
179
+
180
+ def _is_url_field(field: Any) -> bool:
181
+ """Return True when a field appears to accept URLs (for SSRF probing)."""
182
+ if field.type in ("url",):
183
+ return True
184
+ # Check if any hint keyword appears as a substring in the field metadata
185
+ candidates = [
186
+ (field.name or "").lower(),
187
+ (field.label or "").lower(),
188
+ (field.placeholder or "").lower(),
189
+ ]
190
+ for candidate in candidates:
191
+ for hint in _URL_FIELD_HINTS:
192
+ if hint in candidate:
193
+ return True
194
+ return False
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # SecurityScenarioGenerator
199
+ # ---------------------------------------------------------------------------
200
+
201
+
202
+ class SecurityScenarioGenerator:
203
+ """Generate browser-level security test scenarios from a :class:`DiscoveryModel`.
204
+
205
+ Each scenario targets a single form field with a specific attack payload.
206
+ After the form is submitted the :class:`~mannf.core.browser.reflection_analyzer.ReflectionAnalyzer`
207
+ post-processes the result to determine whether the payload executed, was
208
+ reflected, or triggered an error pattern.
209
+
210
+ Parameters
211
+ ----------
212
+ discovery_model:
213
+ The structured page graph produced by :class:`WebCrawlerAgent`.
214
+ llm_provider:
215
+ Optional LLM provider for adaptive payload generation. When set,
216
+ additional field-specific payloads are generated via
217
+ ``UI_SECURITY_PAYLOAD_PROMPT``.
218
+ max_payloads_per_field:
219
+ Maximum number of attack payloads to inject into a single field.
220
+ include_ssrf:
221
+ Whether to include SSRF payloads for URL-like fields.
222
+ include_path_traversal:
223
+ Whether to include path traversal payloads.
224
+ include_crlf:
225
+ Whether to include CRLF injection payloads.
226
+ include_proto_pollution:
227
+ Whether to include prototype pollution payloads.
228
+ include_unicode:
229
+ Whether to include Unicode normalization attack payloads.
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ discovery_model: Any,
235
+ *,
236
+ llm_provider: Any | None = None,
237
+ max_payloads_per_field: int = 5,
238
+ include_ssrf: bool = True,
239
+ include_path_traversal: bool = True,
240
+ include_crlf: bool = True,
241
+ include_proto_pollution: bool = True,
242
+ include_unicode: bool = True,
243
+ ) -> None:
244
+ self._model = discovery_model
245
+ self._llm = llm_provider
246
+ self._max_per_field = max_payloads_per_field
247
+ self._include_ssrf = include_ssrf
248
+ self._include_path_traversal = include_path_traversal
249
+ self._include_crlf = include_crlf
250
+ self._include_proto_pollution = include_proto_pollution
251
+ self._include_unicode = include_unicode
252
+
253
+ # ------------------------------------------------------------------
254
+ # Public API
255
+ # ------------------------------------------------------------------
256
+
257
+ def generate(self) -> list[dict[str, Any]]:
258
+ """Generate security test scenarios from the discovery model.
259
+
260
+ Returns
261
+ -------
262
+ list[dict]
263
+ Task dicts compatible with
264
+ :meth:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator.run`.
265
+ """
266
+ scenarios: list[dict[str, Any]] = []
267
+ for page in self._model.pages:
268
+ for form in page.forms:
269
+ scenarios.extend(self._generate_form_security_scenarios(page, form))
270
+ return scenarios
271
+
272
+ async def generate_async(self) -> list[dict[str, Any]]:
273
+ """Generate security scenarios, optionally augmented with LLM payloads.
274
+
275
+ LLM calls are best-effort — failures fall back to heuristic payloads.
276
+ """
277
+ scenarios = self.generate()
278
+
279
+ if self._llm is None or not self._llm.is_available():
280
+ return scenarios
281
+
282
+ llm_scenarios: list[dict[str, Any]] = []
283
+ for page in self._model.pages:
284
+ for form in page.forms:
285
+ try:
286
+ extra = await self._llm_security_payloads(page, form)
287
+ if extra:
288
+ llm_scenarios.extend(
289
+ self._convert_llm_security_to_scenarios(page, form, extra)
290
+ )
291
+ except Exception as exc: # noqa: BLE001
292
+ logger.warning(
293
+ "LLM security-payload call failed for %s: %s", page.url, exc
294
+ )
295
+
296
+ return scenarios + llm_scenarios
297
+
298
+ # ------------------------------------------------------------------
299
+ # Coverage helpers
300
+ # ------------------------------------------------------------------
301
+
302
+ @staticmethod
303
+ def coverage_categories() -> list[str]:
304
+ """Return all OWASP categories covered by this generator."""
305
+ return list(_OWASP_CATEGORIES.values())
306
+
307
+ # ------------------------------------------------------------------
308
+ # Internal — form-level security scenarios
309
+ # ------------------------------------------------------------------
310
+
311
+ def _generate_form_security_scenarios(
312
+ self, page: Any, form: Any
313
+ ) -> list[dict[str, Any]]:
314
+ """Produce security scenarios for every injectable field in *form*."""
315
+ scenarios: list[dict[str, Any]] = []
316
+ fillable = [f for f in form.fields if f.type not in _SKIP_TYPES]
317
+
318
+ for field in fillable:
319
+ payloads = self._payloads_for_field(field)
320
+ for payload_type, payload_value in payloads[: self._max_per_field]:
321
+ canary = self._make_canary()
322
+ scenario = self._make_security_scenario(
323
+ page=page,
324
+ form=form,
325
+ fillable=fillable,
326
+ target_field=field,
327
+ payload_type=payload_type,
328
+ payload_value=payload_value,
329
+ canary=canary,
330
+ )
331
+ scenarios.append(scenario)
332
+
333
+ return scenarios
334
+
335
+ def _payloads_for_field(self, field: Any) -> list[tuple[str, str]]:
336
+ """Return the list of (payload_type, payload_value) pairs for *field*."""
337
+ payloads: list[tuple[str, str]] = []
338
+
339
+ # XSS and SQL are broadly applicable to text-like fields
340
+ if field.type not in ("number", "date", "time", "month", "week", "color"):
341
+ payloads.extend(_XSS_PAYLOADS)
342
+ payloads.extend(_SQL_PAYLOADS)
343
+
344
+ # SSRF for URL-like fields
345
+ if self._include_ssrf and _is_url_field(field):
346
+ payloads.extend(_SSRF_PAYLOADS)
347
+
348
+ # Path traversal for fields that might accept file paths or names
349
+ if self._include_path_traversal and field.type in (
350
+ "text", "textarea", "search", "url"
351
+ ):
352
+ payloads.extend(_PATH_TRAVERSAL_PAYLOADS)
353
+
354
+ # CRLF for all text-like fields
355
+ if self._include_crlf and field.type in ("text", "textarea", "search", "email"):
356
+ payloads.extend(_CRLF_PAYLOADS)
357
+
358
+ # Prototype pollution for text fields that might feed JSON
359
+ if self._include_proto_pollution and field.type in ("text", "textarea", "search"):
360
+ payloads.extend(_PROTOTYPE_POLLUTION_PAYLOADS)
361
+
362
+ # Unicode attacks for all text-like fields
363
+ if self._include_unicode and field.type in (
364
+ "text", "textarea", "search", "email", "password"
365
+ ):
366
+ payloads.extend(_UNICODE_PAYLOADS)
367
+
368
+ return payloads
369
+
370
+ # ------------------------------------------------------------------
371
+ # Internal — scenario construction
372
+ # ------------------------------------------------------------------
373
+
374
+ def _make_security_scenario(
375
+ self,
376
+ page: Any,
377
+ form: Any,
378
+ fillable: list[Any],
379
+ target_field: Any,
380
+ payload_type: str,
381
+ payload_value: str,
382
+ canary: str,
383
+ ) -> dict[str, Any]:
384
+ """Build a single security test scenario task dict."""
385
+ steps: list[dict[str, Any]] = [
386
+ {"action": "navigate", "selector": page.url, "value": ""},
387
+ ]
388
+
389
+ for field in fillable:
390
+ if field.selector == target_field.selector:
391
+ # Embed canary inside the payload so reflection detection is reliable
392
+ value = f"{payload_value}<!--{canary}-->"
393
+ else:
394
+ value = _safe_fill_value(field)
395
+ if value:
396
+ steps.append({"action": "fill", "selector": field.selector, "value": value})
397
+
398
+ steps.append({"action": "click", "selector": form.submit_selector})
399
+
400
+ label_hint = target_field.label or target_field.name or target_field.selector
401
+ cwe = _cwe_for(payload_type)
402
+ owasp = _payload_category(payload_type)
403
+
404
+ return {
405
+ "url": page.url,
406
+ "steps": steps,
407
+ "metadata": {
408
+ "scenario_type": "security",
409
+ "source_page": page.url,
410
+ "source_form_action": form.action,
411
+ "description": (
412
+ f"Security: {payload_type} payload in '{label_hint}' "
413
+ f"({owasp})"
414
+ ),
415
+ "generator": "security",
416
+ "payload_type": payload_type,
417
+ "payload_value": payload_value,
418
+ "canary": canary,
419
+ "target_field": target_field.selector,
420
+ "cwe_id": cwe,
421
+ "owasp_category": owasp,
422
+ "check_id": f"BROWSER-{payload_type.upper().split('_')[0]}",
423
+ },
424
+ }
425
+
426
+ # ------------------------------------------------------------------
427
+ # Internal — canary tokens
428
+ # ------------------------------------------------------------------
429
+
430
+ @staticmethod
431
+ def _make_canary() -> str:
432
+ """Generate a unique canary token for reflection detection."""
433
+ return f"NAT-CANARY-{uuid.uuid4().hex[:12].upper()}"
434
+
435
+ # ------------------------------------------------------------------
436
+ # Internal — LLM adaptive payload generation
437
+ # ------------------------------------------------------------------
438
+
439
+ async def _llm_security_payloads(
440
+ self, page: Any, form: Any, n: int = 3
441
+ ) -> list[dict[str, Any]] | None:
442
+ """Call the LLM for field-specific attack payloads."""
443
+ if self._llm is None:
444
+ return None
445
+ from mannf.product.llm.prompts import UI_SECURITY_PAYLOAD_PROMPT # noqa: PLC0415
446
+
447
+ fields_desc = _format_fields_for_llm(form.fields)
448
+ prompt = UI_SECURITY_PAYLOAD_PROMPT.format(
449
+ n=n,
450
+ page_url=page.url,
451
+ form_action=form.action,
452
+ form_method=form.method,
453
+ fields_description=fields_desc,
454
+ )
455
+ import json # noqa: PLC0415
456
+ try:
457
+ raw = await self._llm.generate(prompt)
458
+ return json.loads(raw)
459
+ except Exception as exc: # noqa: BLE001
460
+ logger.warning("LLM security-payload call failed: %s", exc)
461
+ return None
462
+
463
+ def _convert_llm_security_to_scenarios(
464
+ self, page: Any, form: Any, results: list[dict[str, Any]]
465
+ ) -> list[dict[str, Any]]:
466
+ """Convert LLM-generated security payload dicts to scenario task dicts."""
467
+ scenarios: list[dict[str, Any]] = []
468
+ fillable = [f for f in form.fields if f.type not in _SKIP_TYPES]
469
+ field_map = {f.selector: f for f in fillable}
470
+
471
+ for item in results:
472
+ if not isinstance(item, dict):
473
+ continue
474
+ inputs = item.get("inputs", {})
475
+ payload_type = item.get("payload_type", "llm_security")
476
+ if not inputs:
477
+ continue
478
+
479
+ steps: list[dict[str, Any]] = [
480
+ {"action": "navigate", "selector": page.url, "value": ""},
481
+ ]
482
+ canary = self._make_canary()
483
+ target_selector = ""
484
+
485
+ for selector, value in inputs.items():
486
+ if selector not in field_map:
487
+ continue
488
+ if not target_selector:
489
+ target_selector = selector
490
+ value = f"{value}<!--{canary}-->"
491
+ steps.append({"action": "fill", "selector": selector, "value": str(value)})
492
+
493
+ if not target_selector:
494
+ continue
495
+
496
+ steps.append({"action": "click", "selector": form.submit_selector})
497
+ cwe = _cwe_for(payload_type)
498
+ owasp = _payload_category(payload_type)
499
+
500
+ scenarios.append({
501
+ "url": page.url,
502
+ "steps": steps,
503
+ "metadata": {
504
+ "scenario_type": "security",
505
+ "source_page": page.url,
506
+ "source_form_action": form.action,
507
+ "description": (
508
+ f"LLM-security: {payload_type} — "
509
+ f"{item.get('description', '')}"
510
+ ),
511
+ "generator": "llm_security",
512
+ "payload_type": payload_type,
513
+ "canary": canary,
514
+ "target_field": target_selector,
515
+ "cwe_id": cwe,
516
+ "owasp_category": owasp,
517
+ "check_id": f"BROWSER-{payload_type.upper().split('_')[0]}",
518
+ },
519
+ })
520
+
521
+ return scenarios
522
+
523
+ # ------------------------------------------------------------------
524
+ # gRPC-specific fuzz scenario generation
525
+ # ------------------------------------------------------------------
526
+
527
+ def generate_grpc_fuzz_scenarios(
528
+ self,
529
+ grpc_method: str,
530
+ rpc_type: str = "unary",
531
+ include_malformed: bool = True,
532
+ include_oversized: bool = True,
533
+ include_missing_fields: bool = True,
534
+ include_type_confusion: bool = True,
535
+ max_per_category: int = 3,
536
+ ) -> list[dict[str, Any]]:
537
+ """Generate gRPC-specific security fuzz scenarios for a single RPC method.
538
+
539
+ Produces test case dicts compatible with :class:`GrpcSUT.execute` that
540
+ exercise malformed protobuf bytes, oversized messages, missing required
541
+ fields, and type-confused payloads.
542
+
543
+ Parameters
544
+ ----------
545
+ grpc_method:
546
+ Full gRPC method path (e.g. ``"/helloworld.Greeter/SayHello"``).
547
+ rpc_type:
548
+ RPC type: ``"unary"``, ``"server_streaming"``,
549
+ ``"client_streaming"``, or ``"bidi_streaming"`` (default: unary).
550
+ include_malformed:
551
+ Include malformed protobuf wire-byte payloads.
552
+ include_oversized:
553
+ Include oversized message payloads.
554
+ include_missing_fields:
555
+ Include empty/missing-required-field payloads.
556
+ include_type_confusion:
557
+ Include type-confused field payloads.
558
+ max_per_category:
559
+ Maximum payloads per category (default: 3).
560
+
561
+ Returns
562
+ -------
563
+ list of dict
564
+ Scenario dicts with ``grpc_method``, ``request``, and ``metadata``.
565
+ """
566
+ scenarios: list[dict[str, Any]] = []
567
+
568
+ if include_malformed:
569
+ for payload_type, raw_bytes in _GRPC_MALFORMED_PAYLOADS[:max_per_category]:
570
+ cwe = _cwe_for(payload_type)
571
+ owasp = _payload_category(payload_type)
572
+ scenarios.append({
573
+ "grpc_method": grpc_method,
574
+ "rpc_type": rpc_type,
575
+ "request_raw": list(raw_bytes), # JSON-serialisable
576
+ "metadata": {
577
+ "scenario_type": "grpc_security",
578
+ "generator": "security",
579
+ "payload_type": payload_type,
580
+ "description": (
581
+ f"gRPC security: {payload_type} — malformed protobuf bytes "
582
+ f"for {grpc_method}"
583
+ ),
584
+ "grpc_method": grpc_method,
585
+ "cwe_id": cwe,
586
+ "owasp_category": owasp,
587
+ "check_id": f"GRPC-{payload_type.upper().split('_')[0]}",
588
+ },
589
+ })
590
+
591
+ if include_oversized:
592
+ for payload_type, oversized_value in _GRPC_OVERSIZED_PAYLOADS[:max_per_category]:
593
+ cwe = _cwe_for(payload_type)
594
+ owasp = _payload_category(payload_type)
595
+ scenarios.append({
596
+ "grpc_method": grpc_method,
597
+ "rpc_type": rpc_type,
598
+ "request": {"data": oversized_value},
599
+ "metadata": {
600
+ "scenario_type": "grpc_security",
601
+ "generator": "security",
602
+ "payload_type": payload_type,
603
+ "description": (
604
+ f"gRPC security: {payload_type} — oversized message "
605
+ f"for {grpc_method}"
606
+ ),
607
+ "grpc_method": grpc_method,
608
+ "cwe_id": cwe,
609
+ "owasp_category": owasp,
610
+ "check_id": "GRPC-OVERSIZED",
611
+ },
612
+ })
613
+
614
+ if include_missing_fields:
615
+ cwe = _cwe_for("grpc_missing")
616
+ owasp = _payload_category("grpc_missing")
617
+ scenarios.append({
618
+ "grpc_method": grpc_method,
619
+ "rpc_type": rpc_type,
620
+ "request": _GRPC_MISSING_FIELD_PAYLOAD,
621
+ "metadata": {
622
+ "scenario_type": "grpc_security",
623
+ "generator": "security",
624
+ "payload_type": "grpc_missing_fields",
625
+ "description": (
626
+ f"gRPC security: missing required fields for {grpc_method}"
627
+ ),
628
+ "grpc_method": grpc_method,
629
+ "cwe_id": cwe,
630
+ "owasp_category": owasp,
631
+ "check_id": "GRPC-MISSING-FIELDS",
632
+ },
633
+ })
634
+
635
+ if include_type_confusion:
636
+ for payload_type, request_dict in _GRPC_TYPE_CONFUSION_PAYLOADS[:max_per_category]:
637
+ cwe = _cwe_for(payload_type)
638
+ owasp = _payload_category(payload_type)
639
+ scenarios.append({
640
+ "grpc_method": grpc_method,
641
+ "rpc_type": rpc_type,
642
+ "request": request_dict,
643
+ "metadata": {
644
+ "scenario_type": "grpc_security",
645
+ "generator": "security",
646
+ "payload_type": payload_type,
647
+ "description": (
648
+ f"gRPC security: {payload_type} — type confusion "
649
+ f"for {grpc_method}"
650
+ ),
651
+ "grpc_method": grpc_method,
652
+ "cwe_id": cwe,
653
+ "owasp_category": owasp,
654
+ "check_id": "GRPC-TYPE-CONFUSION",
655
+ },
656
+ })
657
+
658
+ return scenarios
659
+
660
+
661
+
662
+
663
+ def _safe_fill_value(field: Any) -> str:
664
+ """Return a safe, benign fill value for a non-target field."""
665
+ type_defaults: dict[str, str] = {
666
+ "email": "test@example.com",
667
+ "password": "TestP@ss123!",
668
+ "tel": "555-0100",
669
+ "number": "1",
670
+ "date": "2024-01-01",
671
+ "url": "https://example.com",
672
+ "checkbox": "true",
673
+ "select": "",
674
+ "textarea": "test input",
675
+ }
676
+ return type_defaults.get(field.type, "test")
677
+
678
+
679
+ def _format_fields_for_llm(fields: list[Any]) -> str:
680
+ """Format field descriptors for LLM prompts."""
681
+ lines: list[str] = []
682
+ for f in fields:
683
+ parts = [
684
+ f" - selector={f.selector!r}",
685
+ f"type={f.type!r}",
686
+ f"required={f.required}",
687
+ ]
688
+ if f.label:
689
+ parts.append(f"label={f.label!r}")
690
+ if f.name:
691
+ parts.append(f"name={f.name!r}")
692
+ if f.pattern:
693
+ parts.append(f"pattern={f.pattern!r}")
694
+ lines.append(", ".join(parts))
695
+ return "\n".join(lines)