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,541 @@
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
+ """Enhanced Anomaly Detection with adaptive thresholds and classification.
7
+
8
+ Learns the *normal* response profile for each endpoint and scores incoming
9
+ responses on how anomalous they are. Anomalies are classified into:
10
+
11
+ * ``latency_spike`` — Response time significantly above learned baseline.
12
+ * ``status_code_shift`` — Unexpected status code pattern change.
13
+ * ``payload_anomaly`` — Response body structure deviates from learned norm.
14
+ * ``error_rate_surge`` — Sudden increase in error rate.
15
+
16
+ Thresholds adapt automatically based on the rolling baseline rather than being
17
+ hardcoded, which prevents false positives on naturally slow endpoints.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import math
24
+ import statistics
25
+ from collections import defaultdict
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime, timezone
28
+ from enum import Enum
29
+ from typing import Any, Dict, List, Optional, Set, Tuple
30
+
31
+
32
+ class AnomalyType(str, Enum):
33
+ """Taxonomy of detectable anomaly types."""
34
+
35
+ LATENCY_SPIKE = "latency_spike"
36
+ STATUS_CODE_SHIFT = "status_code_shift"
37
+ PAYLOAD_ANOMALY = "payload_anomaly"
38
+ ERROR_RATE_SURGE = "error_rate_surge"
39
+
40
+
41
+ class Severity(str, Enum):
42
+ """Alert severity levels."""
43
+
44
+ INFO = "info"
45
+ WARNING = "warning"
46
+ CRITICAL = "critical"
47
+
48
+
49
+ @dataclass
50
+ class AnomalyAlert:
51
+ """A structured anomaly alert produced by the detector.
52
+
53
+ Attributes
54
+ ----------
55
+ endpoint:
56
+ The API endpoint where the anomaly was detected.
57
+ anomaly_type:
58
+ Classification of the detected anomaly.
59
+ severity:
60
+ How severe the anomaly is (info / warning / critical).
61
+ score:
62
+ Anomaly score in [0.0, 1.0]; 1.0 = maximally anomalous.
63
+ description:
64
+ Human-readable explanation of what was detected.
65
+ detected_at:
66
+ ISO 8601 timestamp of when the alert was generated.
67
+ scan_id:
68
+ Optional scan identifier linking the alert to a specific run.
69
+ details:
70
+ Additional structured information about the anomaly.
71
+ """
72
+
73
+ endpoint: str
74
+ anomaly_type: AnomalyType
75
+ severity: Severity
76
+ score: float
77
+ description: str
78
+ detected_at: str = field(
79
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
80
+ )
81
+ scan_id: Optional[str] = None
82
+ details: Dict[str, Any] = field(default_factory=dict)
83
+
84
+ def to_dict(self) -> Dict[str, Any]:
85
+ return {
86
+ "endpoint": self.endpoint,
87
+ "anomaly_type": self.anomaly_type,
88
+ "severity": self.severity,
89
+ "score": round(self.score, 4),
90
+ "description": self.description,
91
+ "detected_at": self.detected_at,
92
+ "scan_id": self.scan_id,
93
+ "details": self.details,
94
+ }
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Per-endpoint baseline statistics
99
+ # ---------------------------------------------------------------------------
100
+
101
+ @dataclass
102
+ class _EndpointBaseline:
103
+ """Rolling baseline stats for a single endpoint."""
104
+
105
+ latencies: List[float] = field(default_factory=list)
106
+ status_codes: List[int] = field(default_factory=list)
107
+ payload_sizes: List[int] = field(default_factory=list)
108
+ error_flags: List[bool] = field(default_factory=list)
109
+ seen_status_codes: Set[int] = field(default_factory=set)
110
+
111
+ # Adaptive thresholds (recomputed periodically)
112
+ latency_mean: float = 0.0
113
+ latency_std: float = 0.0
114
+ error_rate: float = 0.0
115
+ payload_size_mean: float = 0.0
116
+ payload_size_std: float = 0.0
117
+
118
+ _window: int = 50 # rolling window size
119
+
120
+ def update(
121
+ self,
122
+ latency_ms: float,
123
+ status_code: int,
124
+ payload_size: int,
125
+ is_error: bool,
126
+ ) -> None:
127
+ def _push(lst: list, val: Any) -> None:
128
+ lst.append(val)
129
+ if len(lst) > self._window:
130
+ lst.pop(0)
131
+
132
+ _push(self.latencies, latency_ms)
133
+ _push(self.status_codes, status_code)
134
+ _push(self.payload_sizes, payload_size)
135
+ _push(self.error_flags, is_error)
136
+ self.seen_status_codes.add(status_code)
137
+
138
+ if len(self.latencies) >= 3:
139
+ self.latency_mean = statistics.mean(self.latencies)
140
+ self.latency_std = statistics.stdev(self.latencies) if len(self.latencies) > 1 else 0.0
141
+ if self.payload_sizes:
142
+ self.payload_size_mean = statistics.mean(self.payload_sizes)
143
+ self.payload_size_std = (
144
+ statistics.stdev(self.payload_sizes) if len(self.payload_sizes) > 1 else 0.0
145
+ )
146
+ if self.error_flags:
147
+ self.error_rate = sum(self.error_flags) / len(self.error_flags)
148
+
149
+ @property
150
+ def is_warmed_up(self) -> bool:
151
+ """True once the baseline has enough data to compute thresholds."""
152
+ return len(self.latencies) >= 5
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Main detector
157
+ # ---------------------------------------------------------------------------
158
+
159
+ class EnhancedAnomalyDetector:
160
+ """Adaptive anomaly detector for API test responses.
161
+
162
+ Learns per-endpoint baseline response profiles and scores each new
163
+ response on how anomalous it is. Anomaly thresholds adapt automatically
164
+ as the baseline evolves.
165
+
166
+ Parameters
167
+ ----------
168
+ latency_sigma_threshold:
169
+ Number of standard deviations above the mean latency to trigger a
170
+ ``latency_spike`` warning. Default is 2σ (≈95th percentile).
171
+ latency_critical_sigma:
172
+ Standard deviations threshold for a *critical* latency spike.
173
+ error_rate_warning_threshold:
174
+ Error rate (0–1) at which an ``error_rate_surge`` warning fires.
175
+ error_rate_critical_threshold:
176
+ Error rate at which a *critical* surge fires.
177
+ payload_sigma_threshold:
178
+ Standard deviations from mean payload size to flag a payload anomaly.
179
+ alert_history_limit:
180
+ Maximum number of alerts retained in memory.
181
+ """
182
+
183
+ def __init__(
184
+ self,
185
+ latency_sigma_threshold: float = 2.0,
186
+ latency_critical_sigma: float = 4.0,
187
+ error_rate_warning_threshold: float = 0.3,
188
+ error_rate_critical_threshold: float = 0.6,
189
+ payload_sigma_threshold: float = 3.0,
190
+ alert_history_limit: int = 500,
191
+ ) -> None:
192
+ self._lat_warn_sigma = latency_sigma_threshold
193
+ self._lat_crit_sigma = latency_critical_sigma
194
+ self._err_warn = error_rate_warning_threshold
195
+ self._err_crit = error_rate_critical_threshold
196
+ self._payload_sigma = payload_sigma_threshold
197
+ self._alert_limit = alert_history_limit
198
+
199
+ self._baselines: Dict[str, _EndpointBaseline] = defaultdict(
200
+ _EndpointBaseline
201
+ )
202
+ self._alerts: List[AnomalyAlert] = []
203
+
204
+ # ------------------------------------------------------------------
205
+ # Core observe API
206
+ # ------------------------------------------------------------------
207
+
208
+ def observe(
209
+ self,
210
+ endpoint: str,
211
+ latency_ms: float,
212
+ status_code: int,
213
+ payload: Any = None,
214
+ is_error: bool = False,
215
+ scan_id: Optional[str] = None,
216
+ ) -> Tuple[float, List[AnomalyAlert]]:
217
+ """Record a new response observation and return anomaly signals.
218
+
219
+ Parameters
220
+ ----------
221
+ endpoint:
222
+ Target endpoint identifier.
223
+ latency_ms:
224
+ Response latency in milliseconds.
225
+ status_code:
226
+ HTTP status code returned.
227
+ payload:
228
+ Response body (used for payload size estimation).
229
+ is_error:
230
+ Whether this response counts as an error.
231
+ scan_id:
232
+ Optional scan identifier for alert attribution.
233
+
234
+ Returns
235
+ -------
236
+ (anomaly_score, new_alerts)
237
+ *anomaly_score* is in [0.0, 1.0].
238
+ *new_alerts* contains any new :class:`AnomalyAlert` objects
239
+ generated by this observation.
240
+ """
241
+ payload_size = self._estimate_payload_size(payload)
242
+ baseline = self._baselines[endpoint]
243
+
244
+ # Compute anomaly scores *before* updating baseline to avoid
245
+ # the new observation polluting its own threshold check
246
+ new_alerts: List[AnomalyAlert] = []
247
+ composite_score = 0.0
248
+
249
+ if baseline.is_warmed_up:
250
+ lat_score, lat_alert = self._check_latency(
251
+ endpoint, latency_ms, baseline, scan_id
252
+ )
253
+ sc_score, sc_alert = self._check_status_code(
254
+ endpoint, status_code, baseline, scan_id
255
+ )
256
+ pl_score, pl_alert = self._check_payload(
257
+ endpoint, payload_size, baseline, scan_id
258
+ )
259
+ err_score, err_alert = self._check_error_rate(
260
+ endpoint, is_error, baseline, scan_id
261
+ )
262
+
263
+ for alert in [lat_alert, sc_alert, pl_alert, err_alert]:
264
+ if alert is not None:
265
+ new_alerts.append(alert)
266
+ self._store_alert(alert)
267
+
268
+ composite_score = min(
269
+ 1.0, (lat_score + sc_score + pl_score + err_score) / 4.0
270
+ )
271
+
272
+ # Now update baseline
273
+ baseline.update(latency_ms, status_code, payload_size, is_error)
274
+
275
+ return composite_score, new_alerts
276
+
277
+ # ------------------------------------------------------------------
278
+ # Anomaly score (no alert side-effect)
279
+ # ------------------------------------------------------------------
280
+
281
+ def score(
282
+ self,
283
+ endpoint: str,
284
+ latency_ms: float,
285
+ status_code: int,
286
+ payload: Any = None,
287
+ is_error: bool = False,
288
+ ) -> float:
289
+ """Return anomaly score for a response without updating the baseline."""
290
+ baseline = self._baselines.get(endpoint)
291
+ if baseline is None or not baseline.is_warmed_up:
292
+ return 0.0
293
+
294
+ lat_score, _ = self._check_latency(endpoint, latency_ms, baseline, None)
295
+ sc_score, _ = self._check_status_code(endpoint, status_code, baseline, None)
296
+ pl_score, _ = self._check_payload(
297
+ endpoint, self._estimate_payload_size(payload), baseline, None
298
+ )
299
+ err_score, _ = self._check_error_rate(endpoint, is_error, baseline, None)
300
+
301
+ return min(1.0, (lat_score + sc_score + pl_score + err_score) / 4.0)
302
+
303
+ # ------------------------------------------------------------------
304
+ # Latency check
305
+ # ------------------------------------------------------------------
306
+
307
+ def _check_latency(
308
+ self,
309
+ endpoint: str,
310
+ latency_ms: float,
311
+ baseline: _EndpointBaseline,
312
+ scan_id: Optional[str],
313
+ ) -> Tuple[float, Optional[AnomalyAlert]]:
314
+ mean = baseline.latency_mean
315
+ std = baseline.latency_std
316
+
317
+ # When std is zero (all observations identical) use a relative floor
318
+ # so minor fluctuations don't trigger false positives.
319
+ if std < 1e-6:
320
+ # When all observations are identical (std = 0), apply a relative
321
+ # floor of 10 % of the mean (min 1 ms) to prevent every minor
322
+ # fluctuation from triggering a false positive.
323
+ _RELATIVE_FLOOR = 0.10 # 10 % of mean
324
+ _ABS_MIN_MS = 1.0 # absolute minimum floor (milliseconds)
325
+ std = max(_ABS_MIN_MS, mean * _RELATIVE_FLOOR)
326
+
327
+ deviation = (latency_ms - mean) / std
328
+
329
+ if deviation < self._lat_warn_sigma:
330
+ return 0.0, None
331
+
332
+ # Normalize: at warning threshold → 0.3, at critical → 0.8, beyond → 1.0
333
+ score = min(
334
+ 1.0,
335
+ 0.3 + 0.5 * (deviation - self._lat_warn_sigma)
336
+ / max(1.0, self._lat_crit_sigma - self._lat_warn_sigma),
337
+ )
338
+ severity = (
339
+ Severity.CRITICAL if deviation >= self._lat_crit_sigma else Severity.WARNING
340
+ )
341
+ alert = AnomalyAlert(
342
+ endpoint=endpoint,
343
+ anomaly_type=AnomalyType.LATENCY_SPIKE,
344
+ severity=severity,
345
+ score=round(score, 4),
346
+ description=(
347
+ f"Latency {latency_ms:.1f}ms is {deviation:.1f}σ above the "
348
+ f"baseline mean of {mean:.1f}ms (σ={std:.1f}ms)"
349
+ ),
350
+ scan_id=scan_id,
351
+ details={
352
+ "latency_ms": latency_ms,
353
+ "baseline_mean_ms": round(mean, 2),
354
+ "baseline_std_ms": round(std, 2),
355
+ "sigma_deviation": round(deviation, 2),
356
+ },
357
+ )
358
+ return score, alert
359
+
360
+ # ------------------------------------------------------------------
361
+ # Status code check
362
+ # ------------------------------------------------------------------
363
+
364
+ def _check_status_code(
365
+ self,
366
+ endpoint: str,
367
+ status_code: int,
368
+ baseline: _EndpointBaseline,
369
+ scan_id: Optional[str],
370
+ ) -> Tuple[float, Optional[AnomalyAlert]]:
371
+ if status_code in baseline.seen_status_codes:
372
+ return 0.0, None
373
+
374
+ # Novel status code — severity depends on the code family
375
+ if status_code >= 500:
376
+ score, severity = 0.9, Severity.CRITICAL
377
+ elif status_code >= 400:
378
+ score, severity = 0.6, Severity.WARNING
379
+ else:
380
+ score, severity = 0.3, Severity.INFO
381
+
382
+ alert = AnomalyAlert(
383
+ endpoint=endpoint,
384
+ anomaly_type=AnomalyType.STATUS_CODE_SHIFT,
385
+ severity=severity,
386
+ score=score,
387
+ description=(
388
+ f"Unexpected status code {status_code} — not seen in baseline "
389
+ f"(known codes: {sorted(baseline.seen_status_codes)})"
390
+ ),
391
+ scan_id=scan_id,
392
+ details={
393
+ "status_code": status_code,
394
+ "known_status_codes": sorted(baseline.seen_status_codes),
395
+ },
396
+ )
397
+ return score, alert
398
+
399
+ # ------------------------------------------------------------------
400
+ # Payload size check
401
+ # ------------------------------------------------------------------
402
+
403
+ def _check_payload(
404
+ self,
405
+ endpoint: str,
406
+ payload_size: int,
407
+ baseline: _EndpointBaseline,
408
+ scan_id: Optional[str],
409
+ ) -> Tuple[float, Optional[AnomalyAlert]]:
410
+ mean = baseline.payload_size_mean
411
+ std = baseline.payload_size_std if baseline.payload_size_std > 0 else 1.0
412
+ deviation = abs(payload_size - mean) / std
413
+
414
+ if deviation < self._payload_sigma:
415
+ return 0.0, None
416
+
417
+ score = min(1.0, 0.4 + 0.3 * (deviation - self._payload_sigma) / self._payload_sigma)
418
+ severity = Severity.CRITICAL if deviation >= self._payload_sigma * 2 else Severity.WARNING
419
+ alert = AnomalyAlert(
420
+ endpoint=endpoint,
421
+ anomaly_type=AnomalyType.PAYLOAD_ANOMALY,
422
+ severity=severity,
423
+ score=round(score, 4),
424
+ description=(
425
+ f"Payload size {payload_size} bytes deviates {deviation:.1f}σ from "
426
+ f"baseline mean {mean:.0f}±{std:.0f} bytes"
427
+ ),
428
+ scan_id=scan_id,
429
+ details={
430
+ "payload_size": payload_size,
431
+ "baseline_mean_bytes": round(mean, 0),
432
+ "baseline_std_bytes": round(std, 0),
433
+ "sigma_deviation": round(deviation, 2),
434
+ },
435
+ )
436
+ return score, alert
437
+
438
+ # ------------------------------------------------------------------
439
+ # Error rate check
440
+ # ------------------------------------------------------------------
441
+
442
+ def _check_error_rate(
443
+ self,
444
+ endpoint: str,
445
+ is_error: bool,
446
+ baseline: _EndpointBaseline,
447
+ scan_id: Optional[str],
448
+ ) -> Tuple[float, Optional[AnomalyAlert]]:
449
+ if not is_error:
450
+ return 0.0, None
451
+
452
+ # Compute a provisional error rate including this observation
453
+ n = len(baseline.error_flags) + 1
454
+ errors = sum(baseline.error_flags) + 1
455
+ current_rate = errors / n
456
+
457
+ if current_rate < self._err_warn:
458
+ return 0.0, None
459
+
460
+ if current_rate >= self._err_crit:
461
+ score, severity = 0.9, Severity.CRITICAL
462
+ else:
463
+ score = 0.3 + 0.6 * (current_rate - self._err_warn) / max(
464
+ 0.01, self._err_crit - self._err_warn
465
+ )
466
+ severity = Severity.WARNING
467
+
468
+ alert = AnomalyAlert(
469
+ endpoint=endpoint,
470
+ anomaly_type=AnomalyType.ERROR_RATE_SURGE,
471
+ severity=severity,
472
+ score=round(score, 4),
473
+ description=(
474
+ f"Error rate surging: {current_rate:.1%} "
475
+ f"(baseline: {baseline.error_rate:.1%}, "
476
+ f"threshold: {self._err_warn:.0%})"
477
+ ),
478
+ scan_id=scan_id,
479
+ details={
480
+ "current_error_rate": round(current_rate, 4),
481
+ "baseline_error_rate": round(baseline.error_rate, 4),
482
+ "warning_threshold": self._err_warn,
483
+ "critical_threshold": self._err_crit,
484
+ },
485
+ )
486
+ return score, alert
487
+
488
+ # ------------------------------------------------------------------
489
+ # Alert management
490
+ # ------------------------------------------------------------------
491
+
492
+ def _store_alert(self, alert: AnomalyAlert) -> None:
493
+ self._alerts.append(alert)
494
+ if len(self._alerts) > self._alert_limit:
495
+ self._alerts.pop(0)
496
+
497
+ def get_alerts(
498
+ self,
499
+ endpoint: Optional[str] = None,
500
+ scan_id: Optional[str] = None,
501
+ severity: Optional[Severity] = None,
502
+ ) -> List[AnomalyAlert]:
503
+ """Return stored anomaly alerts, optionally filtered."""
504
+ alerts = self._alerts
505
+ if endpoint is not None:
506
+ alerts = [a for a in alerts if a.endpoint == endpoint]
507
+ if scan_id is not None:
508
+ alerts = [a for a in alerts if a.scan_id == scan_id]
509
+ if severity is not None:
510
+ alerts = [a for a in alerts if a.severity == severity]
511
+ return list(alerts)
512
+
513
+ def get_alerts_dict(
514
+ self,
515
+ endpoint: Optional[str] = None,
516
+ scan_id: Optional[str] = None,
517
+ severity: Optional[Severity] = None,
518
+ ) -> List[Dict[str, Any]]:
519
+ """Return alerts as a list of JSON-serializable dicts."""
520
+ return [a.to_dict() for a in self.get_alerts(endpoint, scan_id, severity)]
521
+
522
+ def clear_alerts(self) -> None:
523
+ """Remove all stored alerts."""
524
+ self._alerts.clear()
525
+
526
+ # ------------------------------------------------------------------
527
+ # Helpers
528
+ # ------------------------------------------------------------------
529
+
530
+ @staticmethod
531
+ def _estimate_payload_size(payload: Any) -> int:
532
+ if payload is None:
533
+ return 0
534
+ if isinstance(payload, (bytes, bytearray)):
535
+ return len(payload)
536
+ if isinstance(payload, str):
537
+ return len(payload.encode("utf-8"))
538
+ try:
539
+ return len(json.dumps(payload).encode("utf-8"))
540
+ except (TypeError, ValueError):
541
+ return len(str(payload).encode("utf-8"))
@@ -0,0 +1,63 @@
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 agent utilities for functional/UI testing."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from mannf.core.browser.dom_snapshot import DOMElement, DOMSnapshot, build_selector, capture_dom_snapshot
11
+ from mannf.core.browser.visual_comparer import (
12
+ VisualComparisonResult,
13
+ compare_screenshots,
14
+ load_baseline,
15
+ save_baseline,
16
+ )
17
+ from mannf.core.browser.accessibility_scanner import (
18
+ AccessibilityScanResult,
19
+ AccessibilityViolation,
20
+ scan_accessibility,
21
+ )
22
+ from mannf.core.browser.performance_metrics import (
23
+ PerformanceMetric,
24
+ PerformanceResult,
25
+ evaluate_metrics,
26
+ get_threshold_violations,
27
+ DEFAULT_THRESHOLDS,
28
+ )
29
+ from mannf.core.browser.scenario_generator import ScenarioConfigGenerator
30
+ from mannf.core.browser.security_scenario_generator import SecurityScenarioGenerator
31
+ from mannf.core.browser.reflection_analyzer import ReflectionAnalyzer, analyze_security_results
32
+ from mannf.core.browser.ingestor_bridge import IngestorBridge
33
+
34
+ __all__ = [
35
+ "DOMElement",
36
+ "DOMSnapshot",
37
+ "build_selector",
38
+ "capture_dom_snapshot",
39
+ # Visual comparison
40
+ "VisualComparisonResult",
41
+ "compare_screenshots",
42
+ "load_baseline",
43
+ "save_baseline",
44
+ # Accessibility scanning
45
+ "AccessibilityScanResult",
46
+ "AccessibilityViolation",
47
+ "scan_accessibility",
48
+ # Performance metrics
49
+ "PerformanceMetric",
50
+ "PerformanceResult",
51
+ "evaluate_metrics",
52
+ "get_threshold_violations",
53
+ "DEFAULT_THRESHOLDS",
54
+ # Scenario generation
55
+ "ScenarioConfigGenerator",
56
+ # Security scenario generation
57
+ "SecurityScenarioGenerator",
58
+ # Reflection analysis
59
+ "ReflectionAnalyzer",
60
+ "analyze_security_results",
61
+ # Ingestor bridge
62
+ "IngestorBridge",
63
+ ]