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,442 @@
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
+ """Reflection and canary analysis for browser-level security scenarios.
7
+
8
+ After the :class:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator`
9
+ executes a security scenario, this module inspects the task results to detect:
10
+
11
+ 1. **XSS reflection** — the raw payload appears in the DOM response.
12
+ 2. **SQL/error-pattern detection** — database error strings (reusing
13
+ ``_STACK_TRACE_MARKERS`` from ``unsafe_consumption.py``) appear in the page.
14
+ 3. **Canary token reflection** — the unique canary embedded alongside the
15
+ payload appears in unexpected response locations (headers, body, error text).
16
+ 4. **SSRF indicators** — internal service response signatures in the output.
17
+
18
+ Results are returned as :class:`~mannf.product.security.models.SecurityFinding`
19
+ instances so they slot directly into the existing security report pipeline.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import re
26
+ from typing import Any
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Error / stack-trace markers (mirrors unsafe_consumption._STACK_TRACE_MARKERS)
32
+ # ---------------------------------------------------------------------------
33
+
34
+ _STACK_TRACE_MARKERS: list[str] = [
35
+ "Traceback",
36
+ "at line",
37
+ "Exception in thread",
38
+ "NullPointerException",
39
+ "stack trace",
40
+ "ORA-",
41
+ "SQL syntax",
42
+ "mysql_fetch",
43
+ "pg_query",
44
+ "SQLiteException",
45
+ "SQLSTATE",
46
+ "syntax error",
47
+ "unterminated quoted string",
48
+ ]
49
+
50
+ # DOM patterns that signal successful XSS execution
51
+ _XSS_EXECUTION_INDICATORS: list[str] = [
52
+ "alert(",
53
+ "onerror=",
54
+ "onload=",
55
+ "<script>",
56
+ "javascript:",
57
+ "eval(",
58
+ ]
59
+
60
+ # Patterns that suggest SSRF success
61
+ _SSRF_RESPONSE_MARKERS: list[str] = [
62
+ "instance-id",
63
+ "ami-id",
64
+ "169.254.169.254",
65
+ "localhost responded",
66
+ "127.0.0.1",
67
+ "Connection refused",
68
+ ]
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # ReflectionAnalyzer
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ class ReflectionAnalyzer:
77
+ """Inspect orchestrator task results for security payload execution evidence.
78
+
79
+ Parameters
80
+ ----------
81
+ security_scenarios:
82
+ The list of security scenario task dicts that were executed (each
83
+ should have a ``metadata`` key with ``canary``, ``payload_value``,
84
+ ``payload_type``, ``target_field``, ``cwe_id``, and
85
+ ``owasp_category``).
86
+ task_results:
87
+ The raw result items from the orchestrator report (list of dicts with
88
+ at least ``task_id``, ``passed``, ``error``, and optionally ``dom_snapshot``
89
+ and ``page_url``).
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ security_scenarios: list[dict[str, Any]],
95
+ task_results: list[dict[str, Any]],
96
+ ) -> None:
97
+ self._scenarios = security_scenarios
98
+ self._results = task_results
99
+ # Index results by task_id for fast lookup
100
+ self._result_map: dict[str, dict[str, Any]] = {
101
+ r.get("task_id", ""): r for r in task_results
102
+ }
103
+
104
+ # ------------------------------------------------------------------
105
+ # Public API
106
+ # ------------------------------------------------------------------
107
+
108
+ def analyze(self) -> list[dict[str, Any]]:
109
+ """Run all reflection checks and return a list of finding dicts.
110
+
111
+ Each finding dict matches the :class:`~mannf.product.security.models.SecurityFinding`
112
+ dataclass field layout so it can be instantiated directly.
113
+
114
+ Returns
115
+ -------
116
+ list[dict]
117
+ Zero or more security finding dicts, each with fields:
118
+ ``check_id``, ``check_name``, ``severity``, ``endpoint``,
119
+ ``title``, ``description``, ``evidence``, ``remediation``,
120
+ ``cwe_id``, ``confidence``.
121
+ """
122
+ findings: list[dict[str, Any]] = []
123
+
124
+ for scenario in self._scenarios:
125
+ meta = scenario.get("metadata", {})
126
+ if meta.get("scenario_type") != "security":
127
+ continue
128
+
129
+ canary = meta.get("canary", "")
130
+ payload_value = meta.get("payload_value", "")
131
+ payload_type = meta.get("payload_type", "")
132
+ target_field = meta.get("target_field", "")
133
+ cwe_id = meta.get("cwe_id", "CWE-74")
134
+ owasp_category = meta.get("owasp_category", "")
135
+ check_id = meta.get("check_id", "BROWSER-SECURITY")
136
+ page_url = scenario.get("url", "")
137
+
138
+ # Find the matching result (match by url + target_field or by order)
139
+ result = self._find_result(scenario)
140
+ if result is None:
141
+ continue
142
+
143
+ dom = result.get("dom_snapshot", "")
144
+ error_text = result.get("error", "") or ""
145
+ combined_text = f"{dom}\n{error_text}"
146
+
147
+ finding = self._check_scenario(
148
+ check_id=check_id,
149
+ page_url=page_url,
150
+ target_field=target_field,
151
+ payload_type=payload_type,
152
+ payload_value=payload_value,
153
+ canary=canary,
154
+ cwe_id=cwe_id,
155
+ owasp_category=owasp_category,
156
+ combined_text=combined_text,
157
+ result=result,
158
+ )
159
+ if finding:
160
+ findings.append(finding)
161
+
162
+ return findings
163
+
164
+ # ------------------------------------------------------------------
165
+ # Internal checks
166
+ # ------------------------------------------------------------------
167
+
168
+ def _check_scenario(
169
+ self,
170
+ check_id: str,
171
+ page_url: str,
172
+ target_field: str,
173
+ payload_type: str,
174
+ payload_value: str,
175
+ canary: str,
176
+ cwe_id: str,
177
+ owasp_category: str,
178
+ combined_text: str,
179
+ result: dict[str, Any],
180
+ ) -> dict[str, Any] | None:
181
+ """Evaluate a single scenario result for security evidence."""
182
+ evidence_parts: list[str] = []
183
+ confidence = 0.0
184
+ severity = "medium"
185
+
186
+ # 1. Canary reflection check
187
+ if canary and canary in combined_text:
188
+ evidence_parts.append(f"Canary token '{canary}' reflected in DOM/error output")
189
+ confidence = max(confidence, 0.7)
190
+
191
+ # 2. Raw payload reflection
192
+ if payload_value and payload_value in combined_text:
193
+ evidence_parts.append(f"Payload reflected verbatim: {payload_value[:80]!r}")
194
+ confidence = max(confidence, 0.75)
195
+
196
+ # 3. XSS execution indicators
197
+ if payload_type.startswith("xss"):
198
+ for indicator in _XSS_EXECUTION_INDICATORS:
199
+ if indicator.lower() in combined_text.lower():
200
+ evidence_parts.append(
201
+ f"XSS execution indicator found: {indicator!r}"
202
+ )
203
+ confidence = max(confidence, 0.85)
204
+ severity = "high"
205
+ break
206
+
207
+ # 4. SQL / stack-trace error detection
208
+ if payload_type.startswith("sql") or payload_type.startswith("nosql"):
209
+ for marker in _STACK_TRACE_MARKERS:
210
+ if marker.lower() in combined_text.lower():
211
+ evidence_parts.append(f"Database error marker: {marker!r}")
212
+ confidence = max(confidence, 0.80)
213
+ severity = "high"
214
+ break
215
+
216
+ # 5. SSRF indicators
217
+ if payload_type.startswith("ssrf"):
218
+ for marker in _SSRF_RESPONSE_MARKERS:
219
+ if marker.lower() in combined_text.lower():
220
+ evidence_parts.append(f"SSRF response marker: {marker!r}")
221
+ confidence = max(confidence, 0.90)
222
+ severity = "critical"
223
+ break
224
+
225
+ # 6. HTTP 500 on injection payload
226
+ status_code = result.get("status_code")
227
+ if status_code == 500 and payload_type.startswith(("sql", "nosql", "xss")):
228
+ evidence_parts.append("HTTP 500 returned after injection payload")
229
+ confidence = max(confidence, 0.60)
230
+
231
+ if not evidence_parts:
232
+ return None
233
+
234
+ label_hint = target_field or page_url
235
+ title = _make_title(payload_type, label_hint)
236
+ description = _make_description(payload_type, target_field, page_url, owasp_category)
237
+ remediation = _make_remediation(payload_type)
238
+
239
+ return {
240
+ "check_id": check_id,
241
+ "check_name": _check_name(payload_type),
242
+ "severity": severity,
243
+ "endpoint": f"BROWSER {page_url}#{target_field}",
244
+ "title": title,
245
+ "description": description,
246
+ "evidence": "; ".join(evidence_parts),
247
+ "remediation": remediation,
248
+ "cwe_id": cwe_id,
249
+ "confidence": round(confidence, 2),
250
+ }
251
+
252
+ # ------------------------------------------------------------------
253
+ # Result lookup
254
+ # ------------------------------------------------------------------
255
+
256
+ def _find_result(self, scenario: dict[str, Any]) -> dict[str, Any] | None:
257
+ """Find the task result that corresponds to *scenario*."""
258
+ task_id = scenario.get("task_id", "")
259
+ if task_id and task_id in self._result_map:
260
+ return self._result_map[task_id]
261
+
262
+ # Fall back to matching by url + target_field metadata
263
+ meta = scenario.get("metadata", {})
264
+ target_field = meta.get("target_field", "")
265
+ page_url = scenario.get("url", "")
266
+
267
+ for result in self._results:
268
+ r_url = result.get("page_url", result.get("url", ""))
269
+ r_field = result.get("target_field", "")
270
+ if r_url == page_url and r_field == target_field:
271
+ return result
272
+
273
+ # Last resort: return first result if only one scenario was run
274
+ if len(self._results) == 1:
275
+ return self._results[0]
276
+
277
+ return None
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # Text-generation helpers
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ def _check_name(payload_type: str) -> str:
286
+ """Return a human-readable check name for a payload type."""
287
+ names: dict[str, str] = {
288
+ "xss": "Cross-Site Scripting (XSS)",
289
+ "sql": "SQL Injection",
290
+ "nosql": "NoSQL Injection",
291
+ "path_traversal": "Path Traversal",
292
+ "crlf": "CRLF Injection",
293
+ "proto_pollution": "Prototype Pollution",
294
+ "unicode": "Unicode Normalization Attack",
295
+ "ssrf": "Server-Side Request Forgery (SSRF)",
296
+ "llm_security": "LLM-Generated Security Test",
297
+ }
298
+ for prefix, name in names.items():
299
+ if payload_type.startswith(prefix):
300
+ return name
301
+ return "Security Injection Test"
302
+
303
+
304
+ def _make_title(payload_type: str, label_hint: str) -> str:
305
+ """Return a short finding title."""
306
+ name = _check_name(payload_type)
307
+ hint = label_hint[:60] if label_hint else "unknown field"
308
+ return f"Possible {name} in '{hint}'"
309
+
310
+
311
+ def _make_description(
312
+ payload_type: str, target_field: str, page_url: str, owasp_category: str
313
+ ) -> str:
314
+ """Return a detailed finding description."""
315
+ descriptions: dict[str, str] = {
316
+ "xss": (
317
+ "An XSS payload injected into the form field was reflected in the page "
318
+ "DOM without sanitization. An attacker could use this to execute arbitrary "
319
+ "JavaScript in the context of other users' browsers."
320
+ ),
321
+ "sql": (
322
+ "A SQL injection payload triggered an observable difference (reflection, "
323
+ "error message, or status code change). The application may be passing "
324
+ "user input directly to a database query without parameterization."
325
+ ),
326
+ "nosql": (
327
+ "A NoSQL injection payload produced an observable response change, "
328
+ "suggesting the application may be vulnerable to operator injection attacks."
329
+ ),
330
+ "path_traversal": (
331
+ "A path traversal sequence was accepted by the form and may have been "
332
+ "processed by the server, potentially allowing unauthorized file access."
333
+ ),
334
+ "crlf": (
335
+ "A CRLF injection sequence was reflected in the response, which could "
336
+ "allow header injection, HTTP response splitting, or log injection."
337
+ ),
338
+ "proto_pollution": (
339
+ "A prototype pollution payload was submitted via the form. If the server "
340
+ "merges user-supplied objects without sanitization, this could modify "
341
+ "shared object prototypes and affect application logic."
342
+ ),
343
+ "unicode": (
344
+ "A Unicode normalization or homoglyph attack payload was reflected without "
345
+ "normalization. This can be used to bypass security filters or cause "
346
+ "unexpected behavior in string comparison operations."
347
+ ),
348
+ "ssrf": (
349
+ "An SSRF payload submitted via a URL-accepting field returned response "
350
+ "content indicating the server made an outbound request to an internal "
351
+ "or metadata endpoint."
352
+ ),
353
+ }
354
+ for prefix, desc in descriptions.items():
355
+ if payload_type.startswith(prefix):
356
+ return (
357
+ f"{desc}\n\nField: {target_field}, Page: {page_url}, "
358
+ f"Category: {owasp_category}"
359
+ )
360
+ return (
361
+ f"A security payload in '{target_field}' on {page_url} produced evidence "
362
+ f"of a potential vulnerability. Category: {owasp_category}"
363
+ )
364
+
365
+
366
+ def _make_remediation(payload_type: str) -> str:
367
+ """Return remediation advice for a payload type."""
368
+ remediations: dict[str, str] = {
369
+ "xss": (
370
+ "Encode all user-supplied data before rendering it in HTML (use "
371
+ "context-aware output encoding). Implement a strict Content-Security-Policy "
372
+ "header. Validate and sanitize input server-side."
373
+ ),
374
+ "sql": (
375
+ "Use parameterized queries or prepared statements for all database access. "
376
+ "Never concatenate user input into SQL strings. Apply the principle of "
377
+ "least privilege to database accounts."
378
+ ),
379
+ "nosql": (
380
+ "Validate and sanitize input before passing to database queries. "
381
+ "Use an allowlist for operator keys. Avoid directly deserializing user "
382
+ "input into query objects."
383
+ ),
384
+ "path_traversal": (
385
+ "Canonicalize file paths before use and validate they reside within the "
386
+ "expected directory. Reject paths containing '..' sequences. Use an "
387
+ "allowlist of permitted file names."
388
+ ),
389
+ "crlf": (
390
+ "Sanitize all user-supplied input before including it in HTTP headers or "
391
+ "log entries. Strip or encode CR (\\r) and LF (\\n) characters."
392
+ ),
393
+ "proto_pollution": (
394
+ "Avoid using Object.assign or spread operators with user-supplied objects "
395
+ "without sanitization. Use Object.create(null) for lookup maps. Freeze "
396
+ "Object.prototype in security-sensitive contexts."
397
+ ),
398
+ "unicode": (
399
+ "Normalize all user input to a canonical Unicode form (NFC/NFKC) before "
400
+ "comparison or storage. Apply security-aware string comparisons that are "
401
+ "not susceptible to homoglyph attacks."
402
+ ),
403
+ "ssrf": (
404
+ "Validate and allowlist URLs accepted from user input. Block requests to "
405
+ "loopback, link-local, and metadata service addresses. Use a dedicated "
406
+ "egress proxy that enforces network-level controls."
407
+ ),
408
+ }
409
+ for prefix, rem in remediations.items():
410
+ if payload_type.startswith(prefix):
411
+ return rem
412
+ return (
413
+ "Validate, sanitize, and encode all user-supplied input. Follow OWASP "
414
+ "Top 10 guidelines for the relevant vulnerability category."
415
+ )
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # Convenience function for quick analysis
420
+ # ---------------------------------------------------------------------------
421
+
422
+
423
+ def analyze_security_results(
424
+ security_scenarios: list[dict[str, Any]],
425
+ task_results: list[dict[str, Any]],
426
+ ) -> list[dict[str, Any]]:
427
+ """Convenience wrapper around :class:`ReflectionAnalyzer`.
428
+
429
+ Parameters
430
+ ----------
431
+ security_scenarios:
432
+ Security scenario task dicts (with security ``metadata``).
433
+ task_results:
434
+ Orchestrator result items.
435
+
436
+ Returns
437
+ -------
438
+ list[dict]
439
+ Security finding dicts ready for use with
440
+ :class:`~mannf.product.security.models.SecurityFinding`.
441
+ """
442
+ return ReflectionAnalyzer(security_scenarios, task_results).analyze()