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
mannf/product/demo.py ADDED
@@ -0,0 +1,844 @@
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
+ """Demo orchestrator for the ``nat demo`` command.
7
+
8
+ Populates the NAT server's in-memory state with a curated set of
9
+ microservice scan results, live agent telemetry, and pre-seeded OWASP
10
+ security findings so that every dashboard tab has content the moment
11
+ the browser opens.
12
+
13
+ Also exposes a standalone ``run_demo()`` entry-point (Issue #140) that
14
+ loads pre-generated sample report data from ``docs/demo/sample-reports/``
15
+ and renders a formatted console summary plus an HTML report.
16
+
17
+ Issue #58 — ``nat demo`` CLI subcommand
18
+ Issue #59 — Curated demo data & "wow factor" scenarios
19
+ Issue #140 — Demo & Landing Page: Marketing-Ready Reporting Examples
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ import logging
27
+ import pathlib
28
+ import time
29
+ import uuid
30
+ from datetime import datetime, timezone
31
+ from typing import Any, Dict, List, Optional
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Curated demo service topology (Issue #59)
37
+ # ---------------------------------------------------------------------------
38
+ # Service names are recognisable to sales prospects; failure/error/latency
39
+ # values are tuned so the risk heatmap tells a compelling story.
40
+
41
+ _DEMO_SERVICES_CONFIG: List[Dict[str, Any]] = [
42
+ {
43
+ "name": "checkout-api",
44
+ "failure_rate": 0.18,
45
+ "error_rate": 0.04,
46
+ "latency_ms": 25,
47
+ "latency_jitter_ms": 5,
48
+ },
49
+ {
50
+ # latency spikes → anomaly detection demo
51
+ "name": "auth-gateway",
52
+ "failure_rate": 0.12,
53
+ "error_rate": 0.03,
54
+ "latency_ms": 35,
55
+ "latency_jitter_ms": 20,
56
+ },
57
+ {
58
+ "name": "user-profile-svc",
59
+ "failure_rate": 0.03,
60
+ "error_rate": 0.01,
61
+ "latency_ms": 10,
62
+ "latency_jitter_ms": 5,
63
+ },
64
+ {
65
+ # highest risk → highlighted in risk heatmap
66
+ "name": "payment-processor",
67
+ "failure_rate": 0.22,
68
+ "error_rate": 0.06,
69
+ "latency_ms": 30,
70
+ "latency_jitter_ms": 10,
71
+ },
72
+ {
73
+ "name": "inventory-service",
74
+ "failure_rate": 0.08,
75
+ "error_rate": 0.02,
76
+ "latency_ms": 12,
77
+ "latency_jitter_ms": 5,
78
+ },
79
+ {
80
+ "name": "notification-svc",
81
+ "failure_rate": 0.04,
82
+ "error_rate": 0.01,
83
+ "latency_ms": 8,
84
+ "latency_jitter_ms": 5,
85
+ },
86
+ {
87
+ "name": "search-api",
88
+ "failure_rate": 0.05,
89
+ "error_rate": 0.01,
90
+ "latency_ms": 15,
91
+ "latency_jitter_ms": 5,
92
+ },
93
+ {
94
+ "name": "analytics-pipeline",
95
+ "failure_rate": 0.06,
96
+ "error_rate": 0.01,
97
+ "latency_ms": 50,
98
+ "latency_jitter_ms": 10,
99
+ },
100
+ ]
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Curated OWASP security findings (Issue #59)
104
+ # ---------------------------------------------------------------------------
105
+ # Pre-seeded so the Security tab has content immediately on browser open.
106
+
107
+ _DEMO_SECURITY_FINDINGS: List[Dict[str, Any]] = [
108
+ {
109
+ "check_id": "API1",
110
+ "check_name": "Broken Object Level Authorization",
111
+ "severity": "critical",
112
+ "endpoint": "GET /checkout-api/orders/{order_id}",
113
+ "title": "BOLA — Order enumeration allows cross-user data access",
114
+ "description": (
115
+ "The order retrieval endpoint does not verify that the "
116
+ "requesting user owns the queried order. Any authenticated "
117
+ "user can access orders belonging to other users by "
118
+ "incrementing the order_id path parameter."
119
+ ),
120
+ "evidence": (
121
+ "User A successfully retrieved order data owned by User B "
122
+ "by requesting GET /checkout-api/orders/10042 with User A "
123
+ "credentials. HTTP 200 with full order payload returned."
124
+ ),
125
+ "remediation": (
126
+ "Add an ownership check in the order retrieval handler: "
127
+ "verify that order.user_id == authenticated_user.id before "
128
+ "returning the response. Return HTTP 403 on mismatch."
129
+ ),
130
+ "cwe_id": "CWE-639",
131
+ "confidence": 0.90,
132
+ },
133
+ {
134
+ "check_id": "API2",
135
+ "check_name": "Broken Authentication",
136
+ "severity": "critical",
137
+ "endpoint": "POST /auth-gateway/token",
138
+ "title": "Expired JWT tokens accepted without expiry validation",
139
+ "description": (
140
+ "The auth-gateway accepts bearer tokens that have passed "
141
+ "their expiry timestamp (exp claim). Expired tokens remain "
142
+ "valid indefinitely, allowing long-term session hijacking "
143
+ "after credential theft."
144
+ ),
145
+ "evidence": (
146
+ "HTTP 200 returned for a request authenticated with a JWT "
147
+ "whose exp claim was set 24 hours in the past. The token "
148
+ "was accepted and a new session was created."
149
+ ),
150
+ "remediation": (
151
+ "Enforce strict JWT expiry validation on every inbound "
152
+ "request. Reject tokens with exp < now() with HTTP 401. "
153
+ "Consider adding a short-lived refresh token flow."
154
+ ),
155
+ "cwe_id": "CWE-287",
156
+ "confidence": 0.92,
157
+ },
158
+ {
159
+ "check_id": "API3",
160
+ "check_name": "Broken Object Property Level Authorization",
161
+ "severity": "high",
162
+ "endpoint": "GET /user-profile-svc/users/{id}",
163
+ "title": "Sensitive fields exposed to unprivileged users",
164
+ "description": (
165
+ "The user profile endpoint returns sensitive internal fields "
166
+ "(ssn_hash, internal_id, admin_flags) that should only be "
167
+ "visible to administrators. Standard authenticated users "
168
+ "receive these fields in the response body."
169
+ ),
170
+ "evidence": (
171
+ "Standard user GET /user-profile-svc/users/42 response body "
172
+ "contains: internal_id, ssn_hash, admin_flags=false. "
173
+ "Fields are present regardless of caller role."
174
+ ),
175
+ "remediation": (
176
+ "Apply field-level access control using a role-based field "
177
+ "filter. Return only permitted fields per caller role. "
178
+ "Use an allowlist approach rather than a blocklist."
179
+ ),
180
+ "cwe_id": "CWE-213",
181
+ "confidence": 0.85,
182
+ },
183
+ {
184
+ "check_id": "API4",
185
+ "check_name": "Unrestricted Resource Consumption",
186
+ "severity": "medium",
187
+ "endpoint": "POST /payment-processor/charges",
188
+ "title": "No rate limiting on payment charge endpoint",
189
+ "description": (
190
+ "The payment processor charge endpoint has no rate limiting "
191
+ "or request throttling. An attacker can submit unlimited "
192
+ "charge attempts in rapid succession, enabling DoS attacks "
193
+ "and brute-force card validation."
194
+ ),
195
+ "evidence": (
196
+ "500 sequential POST /payment-processor/charges requests "
197
+ "completed without any 429 Too Many Requests response or "
198
+ "observable throttling. All requests processed at full speed."
199
+ ),
200
+ "remediation": (
201
+ "Implement per-user rate limiting on the payment endpoint "
202
+ "(e.g. 10 requests/minute). Return HTTP 429 with "
203
+ "Retry-After header on breach. Add exponential backoff "
204
+ "for repeated failures."
205
+ ),
206
+ "cwe_id": "CWE-770",
207
+ "confidence": 0.88,
208
+ },
209
+ {
210
+ "check_id": "API8",
211
+ "check_name": "Security Misconfiguration",
212
+ "severity": "high",
213
+ "endpoint": "GET /analytics-pipeline/health",
214
+ "title": "Debug mode enabled — stack traces in production responses",
215
+ "description": (
216
+ "The analytics-pipeline service has debug mode enabled in "
217
+ "production. Error responses include full Python stack "
218
+ "traces, sys.path contents, and environment variable names, "
219
+ "leaking sensitive infrastructure details."
220
+ ),
221
+ "evidence": (
222
+ "Triggered a 500 error by sending a malformed request. "
223
+ "Response body contained: full traceback, module paths "
224
+ "revealing framework versions, and partial env var names."
225
+ ),
226
+ "remediation": (
227
+ "Disable debug mode in production (DEBUG=False). Configure "
228
+ "a generic error handler that returns sanitised messages "
229
+ "and logs the full detail server-side only."
230
+ ),
231
+ "cwe_id": "CWE-1004",
232
+ "confidence": 0.95,
233
+ },
234
+ ]
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Browser-level security findings (Phase 6.2 — SecurityScenarioGenerator)
238
+ # ---------------------------------------------------------------------------
239
+ # Pre-seeded so the Browser Security tab has content immediately on browser open.
240
+
241
+ _DEMO_BROWSER_SECURITY_FINDINGS: List[Dict[str, Any]] = [
242
+ {
243
+ "check_id": "BROWSER-XSS",
244
+ "check_name": "Cross-Site Scripting (XSS)",
245
+ "severity": "high",
246
+ "endpoint": "BROWSER /login#input[name='username']",
247
+ "title": "Possible Cross-Site Scripting (XSS) in 'input[name='username']'",
248
+ "description": (
249
+ "An XSS payload injected into the username form field was reflected "
250
+ "in the page DOM without sanitization. An attacker could use this to "
251
+ "execute arbitrary JavaScript in the context of other users' browsers.\n\n"
252
+ "Field: input[name='username'], Page: /login, "
253
+ "Category: A03:2021 — Injection / CWE-79"
254
+ ),
255
+ "evidence": (
256
+ "Payload reflected verbatim: '<script>alert(\\'xss\\')</script>'; "
257
+ "XSS execution indicator found: '<script>'"
258
+ ),
259
+ "remediation": (
260
+ "Encode all user-supplied data before rendering it in HTML (use "
261
+ "context-aware output encoding). Implement a strict Content-Security-Policy "
262
+ "header. Validate and sanitize input server-side."
263
+ ),
264
+ "cwe_id": "CWE-79",
265
+ "confidence": 0.85,
266
+ },
267
+ {
268
+ "check_id": "BROWSER-SQL",
269
+ "check_name": "SQL Injection",
270
+ "severity": "high",
271
+ "endpoint": "BROWSER /search#input[name='q']",
272
+ "title": "Possible SQL Injection in 'input[name='q']'",
273
+ "description": (
274
+ "A SQL injection payload triggered an observable difference in the "
275
+ "page response. The application may be passing user input directly "
276
+ "to a database query without parameterization.\n\n"
277
+ "Field: input[name='q'], Page: /search, "
278
+ "Category: A03:2021 — Injection / CWE-89"
279
+ ),
280
+ "evidence": (
281
+ "Database error marker: 'SQL syntax'; "
282
+ "Canary token 'NAT-CANARY-A1B2C3D4E5F6' reflected in DOM/error output"
283
+ ),
284
+ "remediation": (
285
+ "Use parameterized queries or prepared statements for all database access. "
286
+ "Never concatenate user input into SQL strings. Apply the principle of "
287
+ "least privilege to database accounts."
288
+ ),
289
+ "cwe_id": "CWE-89",
290
+ "confidence": 0.80,
291
+ },
292
+ {
293
+ "check_id": "BROWSER-PATH_TRAVERSAL",
294
+ "check_name": "Path Traversal",
295
+ "severity": "medium",
296
+ "endpoint": "BROWSER /upload#input[name='filename']",
297
+ "title": "Possible Path Traversal in 'input[name='filename']'",
298
+ "description": (
299
+ "A path traversal sequence was accepted by the form and may have been "
300
+ "processed by the server, potentially allowing unauthorized file access.\n\n"
301
+ "Field: input[name='filename'], Page: /upload, "
302
+ "Category: A01:2021 — Path Traversal / CWE-22"
303
+ ),
304
+ "evidence": (
305
+ "Payload reflected verbatim: '../../etc/passwd'; "
306
+ "Canary token 'NAT-CANARY-F6E5D4C3B2A1' reflected in DOM/error output"
307
+ ),
308
+ "remediation": (
309
+ "Canonicalize file paths before use and validate they reside within the "
310
+ "expected directory. Reject paths containing '..' sequences. Use an "
311
+ "allowlist of permitted file names."
312
+ ),
313
+ "cwe_id": "CWE-22",
314
+ "confidence": 0.70,
315
+ },
316
+ {
317
+ "check_id": "BROWSER-SSRF",
318
+ "check_name": "Server-Side Request Forgery (SSRF)",
319
+ "severity": "critical",
320
+ "endpoint": "BROWSER /profile#input[name='avatar_url']",
321
+ "title": "Possible Server-Side Request Forgery (SSRF) in 'input[name='avatar_url']'",
322
+ "description": (
323
+ "An SSRF payload submitted via a URL-accepting field returned response "
324
+ "content indicating the server made an outbound request to an internal "
325
+ "or metadata endpoint.\n\n"
326
+ "Field: input[name='avatar_url'], Page: /profile, "
327
+ "Category: A10:2021 — SSRF / CWE-918"
328
+ ),
329
+ "evidence": (
330
+ "SSRF response marker: '169.254.169.254'; "
331
+ "Canary token 'NAT-CANARY-1A2B3C4D5E6F' reflected in DOM/error output"
332
+ ),
333
+ "remediation": (
334
+ "Validate and allowlist URLs accepted from user input. Block requests to "
335
+ "loopback, link-local, and metadata service addresses. Use a dedicated "
336
+ "egress proxy that enforces network-level controls."
337
+ ),
338
+ "cwe_id": "CWE-918",
339
+ "confidence": 0.90,
340
+ },
341
+ ]
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Demo scan runner
346
+ # ---------------------------------------------------------------------------
347
+
348
+
349
+ async def run_demo_scan(port: int = 8080) -> None:
350
+ """Populate the server's in-memory state with curated demo data.
351
+
352
+ This coroutine is designed to run inside the **same** asyncio event loop
353
+ as the uvicorn server so that WebSocket telemetry broadcasts reach any
354
+ connected browser clients in real-time.
355
+
356
+ Parameters
357
+ ----------
358
+ port:
359
+ The port the NAT server is listening on (used for base_url labels).
360
+ """
361
+ import mannf.product.server as _srv
362
+ from mannf.core.distributed.system_under_test import (
363
+ MockDistributedSystem,
364
+ _ServiceConfig,
365
+ )
366
+ from mannf.core.nat_orchestrator import NATOrchestrator
367
+ from mannf.product.dashboard import telemetry
368
+
369
+ now_iso = datetime.now(timezone.utc).isoformat()
370
+ base_url = f"http://localhost:{port}"
371
+
372
+ # ----------------------------------------------------------------
373
+ # 1. Register a functional scan entry (History + Overview tabs)
374
+ # ----------------------------------------------------------------
375
+ scan_id = str(uuid.uuid4())
376
+ _srv._scans[scan_id] = {
377
+ "scan_id": scan_id,
378
+ "status": _srv.ScanStatus.RUNNING,
379
+ "created_at": now_iso,
380
+ "spec_name": "demo-microservices",
381
+ "base_url": base_url,
382
+ "summary": None,
383
+ "results": [],
384
+ "error": None,
385
+ "completed_at": None,
386
+ }
387
+
388
+ # ----------------------------------------------------------------
389
+ # 2. Pre-seed OWASP security findings (Security tab)
390
+ # ----------------------------------------------------------------
391
+ sec_scan_id = str(uuid.uuid4())
392
+ _srv._security_scans[sec_scan_id] = {
393
+ "scan_id": sec_scan_id,
394
+ "status": "completed",
395
+ "created_at": now_iso,
396
+ "target_url": base_url,
397
+ "report": {
398
+ "scan_id": sec_scan_id,
399
+ "timestamp": now_iso,
400
+ "target_url": base_url,
401
+ "total_endpoints_scanned": len(_DEMO_SERVICES_CONFIG),
402
+ "findings": _DEMO_SECURITY_FINDINGS,
403
+ "summary": {
404
+ "total_findings": len(_DEMO_SECURITY_FINDINGS),
405
+ "critical": sum(
406
+ 1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "critical"
407
+ ),
408
+ "high": sum(
409
+ 1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "high"
410
+ ),
411
+ "medium": sum(
412
+ 1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "medium"
413
+ ),
414
+ "low": 0,
415
+ "info": 0,
416
+ },
417
+ "belief_guided": True,
418
+ "prioritization": [],
419
+ },
420
+ "error": None,
421
+ "completed_at": now_iso,
422
+ }
423
+
424
+ # ----------------------------------------------------------------
425
+ # 3. Build demo MockDistributedSystem with curated services
426
+ # ----------------------------------------------------------------
427
+ demo_services = [_ServiceConfig(**cfg) for cfg in _DEMO_SERVICES_CONFIG]
428
+ sut = MockDistributedSystem(services=demo_services, seed=42)
429
+
430
+ # ----------------------------------------------------------------
431
+ # 4. Create NATOrchestrator wired to server's shared data layer
432
+ # ----------------------------------------------------------------
433
+ risk_scorer = _srv._get_risk_scorer()
434
+ anomaly_detector = _srv._get_anomaly_detector()
435
+
436
+ orch = NATOrchestrator(
437
+ sut,
438
+ num_planners=2,
439
+ num_executors=4,
440
+ num_analyzers=2,
441
+ max_contracts=120,
442
+ suite_name="NAT Demo — Microservices Platform",
443
+ log_interval=20,
444
+ bid_timeout_s=0.03,
445
+ anomaly_detector=anomaly_detector,
446
+ )
447
+
448
+ # Broadcast initial agent state so the Agents tab populates before
449
+ # the scan completes (first snapshot; beliefs start at 0.5 priors).
450
+ all_agents = (
451
+ orch._planners + orch._executors + orch._analyzers + [orch._coordinator]
452
+ )
453
+ for agent in all_agents:
454
+ await telemetry.broadcast_agent_state(agent)
455
+
456
+ # ----------------------------------------------------------------
457
+ # 5. Run the orchestrator — agents collaborate via ECNP, results
458
+ # flow through the shared anomaly detector and risk scorer.
459
+ # ----------------------------------------------------------------
460
+ logger.info("Demo scan %s: starting NATOrchestrator", scan_id)
461
+ suite = await orch.run()
462
+
463
+ # ----------------------------------------------------------------
464
+ # 6. Broadcast final agent states (beliefs updated after full run)
465
+ # ----------------------------------------------------------------
466
+ for agent in all_agents:
467
+ await telemetry.broadcast_agent_state(agent)
468
+
469
+ # Also feed orchestrator's risk scorer results into the server's
470
+ # shared risk scorer so the risk heatmap is populated.
471
+ for entry in orch.get_risk_report():
472
+ endpoint = entry.get("endpoint", "")
473
+ risk_score = entry.get("risk_score", 0.0)
474
+ if endpoint:
475
+ # Replay synthetic observations to seed the server scorer
476
+ _srv._get_risk_scorer().record(
477
+ endpoint,
478
+ _make_synthetic_result(endpoint, risk_score),
479
+ )
480
+
481
+ # ----------------------------------------------------------------
482
+ # 7. Build raw results list and update scan to COMPLETED
483
+ # ----------------------------------------------------------------
484
+ tc_by_id = {tc.id: tc for tc in suite.cases}
485
+ raw_results = []
486
+ for result in suite.results:
487
+ tc = tc_by_id.get(result.test_case_id)
488
+ target = tc.target if tc is not None else "unknown"
489
+ raw_results.append(
490
+ {
491
+ "test_id": result.test_case_id,
492
+ "target": target,
493
+ "passed": result.passed,
494
+ "execution_time_ms": result.execution_time_ms,
495
+ "error": result.error,
496
+ "actual_output": result.actual_output,
497
+ }
498
+ )
499
+
500
+ s = suite.summary()
501
+ completed_at = datetime.now(timezone.utc).isoformat()
502
+ _srv._scans[scan_id].update(
503
+ {
504
+ "status": _srv.ScanStatus.COMPLETED,
505
+ "completed_at": completed_at,
506
+ "summary": s,
507
+ "results": raw_results,
508
+ }
509
+ )
510
+
511
+ logger.info(
512
+ "Demo scan %s complete: total=%d passed=%d failed=%d",
513
+ scan_id,
514
+ s["total"],
515
+ s["passed"],
516
+ s["failed"],
517
+ )
518
+
519
+ # Broadcast scan completion to any connected WebSocket clients
520
+ from mannf.product.dashboard.app import manager as _ws_mgr
521
+
522
+ await _ws_mgr.broadcast(
523
+ {
524
+ "type": "scan_update",
525
+ "data": {
526
+ "scan_id": scan_id,
527
+ "status": _srv.ScanStatus.COMPLETED,
528
+ "total": s.get("total", 0),
529
+ "passed": s.get("passed", 0),
530
+ "failed": s.get("failed", 0),
531
+ },
532
+ }
533
+ )
534
+
535
+
536
+ # ---------------------------------------------------------------------------
537
+ # Internal helpers
538
+ # ---------------------------------------------------------------------------
539
+
540
+
541
+ def _make_synthetic_result(endpoint: str, risk_score: float) -> Any:
542
+ """Create a minimal :class:`~mannf.core.testing.models.TestResult` that
543
+ encodes the given *risk_score* as a passed/failed outcome for seeding
544
+ the server's shared :class:`~mannf.core.prioritization.risk_scorer.RiskScorer`.
545
+ """
546
+ from mannf.core.testing.models import TestResult
547
+
548
+ # risk_score ≥ 0.5 → treat as a failure signal
549
+ passed = risk_score < 0.5
550
+ return TestResult(
551
+ test_case_id=str(uuid.uuid4()),
552
+ passed=passed,
553
+ execution_time_ms=10.0,
554
+ )
555
+
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # Standalone demo runner (Issue #140)
559
+ # ---------------------------------------------------------------------------
560
+
561
+ #: Directory that contains pre-generated sample reports.
562
+ _SAMPLE_REPORTS_DIR: pathlib.Path = (
563
+ pathlib.Path(__file__).parent.parent.parent.parent # repo root
564
+ / "docs"
565
+ / "demo"
566
+ / "sample-reports"
567
+ )
568
+
569
+ #: HTML report template path.
570
+ _HTML_TEMPLATE_PATH: pathlib.Path = (
571
+ pathlib.Path(__file__).parent.parent.parent.parent # repo root
572
+ / "docs"
573
+ / "demo"
574
+ / "report-template.html"
575
+ )
576
+
577
+
578
+ def _load_sample_report(filename: str) -> Optional[Dict[str, Any]]:
579
+ """Load a single JSON sample report file. Returns *None* on failure."""
580
+ path = _SAMPLE_REPORTS_DIR / filename
581
+ try:
582
+ with path.open(encoding="utf-8") as fh:
583
+ return json.load(fh)
584
+ except FileNotFoundError:
585
+ logger.warning("Sample report not found: %s", path)
586
+ return None
587
+ except json.JSONDecodeError as exc:
588
+ logger.warning("Failed to parse %s: %s", path, exc)
589
+ return None
590
+
591
+
592
+ def _severity_counts(findings: List[Dict[str, Any]]) -> Dict[str, int]:
593
+ """Return a dict of severity → count for a list of security findings."""
594
+ counts: Dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
595
+ for f in findings:
596
+ sev = str(f.get("severity", "")).lower()
597
+ if sev in counts:
598
+ counts[sev] += 1
599
+ return counts
600
+
601
+
602
+ def _print_section(title: str, *, width: int = 70) -> None:
603
+ """Print a formatted section separator to stdout."""
604
+ bar = "─" * width
605
+ print(f"\n{bar}")
606
+ print(f" {title}")
607
+ print(bar)
608
+
609
+
610
+ def _print_kv(key: str, value: str, indent: int = 4) -> None:
611
+ pad = " " * indent
612
+ print(f"{pad}{key:<30} {value}")
613
+
614
+
615
+ def run_demo(
616
+ mode: str = "quick",
617
+ output_html: Optional[str] = None,
618
+ ) -> None:
619
+ """Load pre-generated sample report data and print a formatted demo.
620
+
621
+ Parameters
622
+ ----------
623
+ mode:
624
+ One of:
625
+
626
+ * ``"quick"`` – Loads data and prints results instantly (default).
627
+ * ``"simulated"`` – Adds realistic delays between phases to mimic a
628
+ live scan in progress (useful for screen recordings).
629
+ * ``"interactive"`` – Pauses between each phase so a presenter can
630
+ narrate before pressing Enter to continue.
631
+
632
+ output_html:
633
+ Optional file path. When provided the standalone HTML report
634
+ template is copied to this path (ready to open in a browser or
635
+ send to a prospect). If *None*, no HTML file is written.
636
+ """
637
+ delays: Dict[str, float]
638
+ if mode == "simulated":
639
+ delays = {
640
+ "header": 0.5,
641
+ "security": 2.0,
642
+ "functional": 1.5,
643
+ "visual": 1.0,
644
+ "accessibility": 1.2,
645
+ "performance": 1.0,
646
+ "summary": 0.8,
647
+ }
648
+ else:
649
+ delays = {k: 0.0 for k in ("header", "security", "functional", "visual", "accessibility", "performance", "summary")}
650
+
651
+ def _pause(phase: str) -> None:
652
+ delay = delays.get(phase, 0.0)
653
+ if mode == "interactive":
654
+ input(f"\n ↵ Press Enter to continue to {phase}…")
655
+ elif delay > 0:
656
+ time.sleep(delay)
657
+
658
+ # ------------------------------------------------------------------
659
+ # Header
660
+ # ------------------------------------------------------------------
661
+ _pause("header")
662
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
663
+ print("\n" + "═" * 70)
664
+ print(" NAT — Neural Agent Testing Platform")
665
+ print(" Demo Report Viewer · Issue #140")
666
+ print(f" {now}")
667
+ print("═" * 70)
668
+ print(" Target: https://www.demo-store.example.com")
669
+ print(" Suite: Full Platform Audit")
670
+ print(" Mode: " + mode)
671
+
672
+ # ------------------------------------------------------------------
673
+ # Load all reports
674
+ # ------------------------------------------------------------------
675
+ unified = _load_sample_report("unified-report.json") or {}
676
+ security = _load_sample_report("security-scan-report.json") or {}
677
+ functional = _load_sample_report("functional-test-report.json") or {}
678
+ visual = _load_sample_report("visual-regression-report.json") or {}
679
+ a11y = _load_sample_report("accessibility-report.json") or {}
680
+ performance = _load_sample_report("performance-report.json") or {}
681
+
682
+ # ------------------------------------------------------------------
683
+ # Executive summary
684
+ # ------------------------------------------------------------------
685
+ health = unified.get("overall_health_score", "N/A")
686
+ grade = unified.get("health_grade", "?")
687
+ _print_section(f"📋 EXECUTIVE SUMMARY Health Score: {health}/100 ({grade})")
688
+ exec_sum = unified.get("executive_summary", {})
689
+ headline = exec_sum.get("headline", "")
690
+ if headline:
691
+ # Word-wrap at ~64 chars
692
+ words = headline.split()
693
+ line = " "
694
+ for word in words:
695
+ if len(line) + len(word) + 1 > 68:
696
+ print(line)
697
+ line = " " + word + " "
698
+ else:
699
+ line += word + " "
700
+ if line.strip():
701
+ print(line)
702
+ print()
703
+ for action in exec_sum.get("critical_actions", []):
704
+ print(f" ⚠ {action}")
705
+
706
+ # ------------------------------------------------------------------
707
+ # Security
708
+ # ------------------------------------------------------------------
709
+ _pause("security")
710
+ sec_sum = security.get("summary", {})
711
+ _print_section(
712
+ "🔒 SECURITY SCAN"
713
+ f" {sec_sum.get('total_findings', 0)} findings · "
714
+ f"Risk score: {sec_sum.get('risk_score', 'N/A')}"
715
+ )
716
+ _print_kv("Endpoints scanned:", str(security.get("total_endpoints_scanned", "?")))
717
+ _print_kv("Scan duration:", f"{security.get('scan_duration_seconds', '?')}s")
718
+ _print_kv("Critical:", str(sec_sum.get("critical", 0)))
719
+ _print_kv("High:", str(sec_sum.get("high", 0)))
720
+ _print_kv("Medium:", str(sec_sum.get("medium", 0)))
721
+ _print_kv("Low:", str(sec_sum.get("low", 0)))
722
+ print()
723
+ for finding in security.get("findings", [])[:5]:
724
+ sev = finding.get("severity", "?").upper()
725
+ title = finding.get("title", "")
726
+ ep = finding.get("affected_endpoint", "")
727
+ cvss = finding.get("cvss_score", "")
728
+ print(f" [{sev:8s}] CVSS {cvss:4} {title}")
729
+ print(f" {ep}")
730
+ remaining = len(security.get("findings", [])) - 5
731
+ if remaining > 0:
732
+ print(f" … and {remaining} more findings (see security-scan-report.json)")
733
+
734
+ # ------------------------------------------------------------------
735
+ # Functional
736
+ # ------------------------------------------------------------------
737
+ _pause("functional")
738
+ func_sum = functional.get("summary", {})
739
+ total = func_sum.get("total", 0)
740
+ passed = func_sum.get("passed", 0)
741
+ failed = func_sum.get("failed", 0)
742
+ skipped = func_sum.get("skipped", 0)
743
+ pass_rate = func_sum.get("pass_rate", 0)
744
+ _print_section(
745
+ f"✅ FUNCTIONAL TESTS"
746
+ f" {passed}/{total} passed · {failed} failed · {skipped} skipped"
747
+ )
748
+ _print_kv("Pass rate:", f"{pass_rate * 100:.1f}%")
749
+ _print_kv("Duration:", f"{functional.get('duration_seconds', '?')}s")
750
+ print()
751
+ failed_tests = [t for t in functional.get("tests", []) if t.get("status") == "failed"]
752
+ for t in failed_tests:
753
+ print(f" [FAIL] {t['id']} {t['name']}")
754
+ fail = t.get("failure", {})
755
+ if fail.get("reason"):
756
+ print(f" {fail['reason']}")
757
+
758
+ # ------------------------------------------------------------------
759
+ # Visual regression
760
+ # ------------------------------------------------------------------
761
+ _pause("visual")
762
+ vis_sum = visual.get("summary", {})
763
+ _print_section(
764
+ f"🖼 VISUAL REGRESSION"
765
+ f" {vis_sum.get('pages_compared', 0)} pages · "
766
+ f"{vis_sum.get('diffs_detected', 0)} diffs detected"
767
+ )
768
+ for page in visual.get("pages", []):
769
+ if page.get("status") == "diff_detected":
770
+ pct = page.get("diff_percent", 0)
771
+ print(f" [DIFF] {page['name']} ({pct}% pixel diff)")
772
+ for region in page.get("diff_regions", []):
773
+ print(f" · {region['label']}: {region['description'][:60]}…")
774
+
775
+ # ------------------------------------------------------------------
776
+ # Accessibility
777
+ # ------------------------------------------------------------------
778
+ _pause("accessibility")
779
+ a11y_sum = a11y.get("summary", {})
780
+ _print_section(
781
+ f"♿ ACCESSIBILITY (WCAG {a11y.get('wcag_version', '2.1')} {a11y.get('wcag_level', 'AA')})"
782
+ f" Score: {a11y.get('overall_score', '?')}%"
783
+ )
784
+ _print_kv("Violations:", str(a11y_sum.get("critical", 0)))
785
+ _print_kv("Warnings:", str(a11y_sum.get("warnings", 0)))
786
+ _print_kv("Passed checks:", str(a11y_sum.get("passed_checks", 0)))
787
+ print()
788
+ for v in a11y.get("violations", []):
789
+ print(f" [VIOLATION] WCAG {v['wcag_criterion']} {v['issue']}")
790
+ print(f" {v['affected_page']}")
791
+
792
+ # ------------------------------------------------------------------
793
+ # Performance
794
+ # ------------------------------------------------------------------
795
+ _pause("performance")
796
+ perf_sum = performance.get("summary", {})
797
+ _print_section(
798
+ f"⚡ PERFORMANCE"
799
+ f" Overall: {performance.get('overall_performance_score', '?')}/100"
800
+ )
801
+ _print_kv("Pages tested:", str(performance.get("pages", []).__len__()))
802
+ _print_kv("Avg LCP:", f"{perf_sum.get('avg_lcp_ms', '?')} ms")
803
+ _print_kv("Avg CLS:", str(perf_sum.get("avg_cls", "?")))
804
+ _print_kv("Avg TTFB:", f"{perf_sum.get('avg_ttfb_ms', '?')} ms")
805
+ print()
806
+ for page in performance.get("pages", []):
807
+ score = page.get("performance_score", 0)
808
+ grade_char = "✅" if score >= 85 else ("⚠️" if score >= 70 else "❌")
809
+ print(f" {grade_char} {score:3d}/100 {page['page']}")
810
+
811
+ # ------------------------------------------------------------------
812
+ # Final summary
813
+ # ------------------------------------------------------------------
814
+ _pause("summary")
815
+ _print_section("🔗 CROSS-CUTTING INSIGHTS")
816
+ for insight in unified.get("cross_cutting_insights", []):
817
+ sev = insight.get("severity", "info").upper()
818
+ print(f" [{sev}] {insight['insight'][:72]}…")
819
+
820
+ print()
821
+ print("═" * 70)
822
+ print(" ✅ Demo complete. Full data in docs/demo/sample-reports/")
823
+ if output_html:
824
+ print(f" 📄 HTML report → {output_html}")
825
+ print("═" * 70 + "\n")
826
+
827
+ # ------------------------------------------------------------------
828
+ # HTML output
829
+ # ------------------------------------------------------------------
830
+ if output_html:
831
+ _write_html_report(output_html)
832
+
833
+
834
+ def _write_html_report(dest: str) -> None:
835
+ """Copy the standalone HTML report template to *dest*."""
836
+ import shutil
837
+
838
+ dest_path = pathlib.Path(dest)
839
+ try:
840
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
841
+ shutil.copy2(_HTML_TEMPLATE_PATH, dest_path)
842
+ logger.info("HTML report written to %s", dest_path)
843
+ except OSError as exc:
844
+ logger.warning("Could not write HTML report to %s: %s", dest_path, exc)