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,269 @@
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
+ """Dataclass models for the orchestration pipeline.
7
+
8
+ These models carry configuration and results through the
9
+ ingest → transform → prioritize → scan → learn pipeline implemented
10
+ by :class:`~mannf.product.orchestrator.IngestOrchestrator`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from mannf.product.exporters.base import ExportSummary
20
+ from mannf.product.ingestors.models import IngestedEndpoint, IngestedTestCase
21
+ from mannf.product.security.models import SecurityReport
22
+
23
+
24
+ @dataclass
25
+ class OrchestrationConfig:
26
+ """Top-level configuration for a single orchestration run.
27
+
28
+ Attributes
29
+ ----------
30
+ ingestor_name:
31
+ Name of the ingestor plugin to use (e.g., ``"openapi"``, ``"postman"``).
32
+ If ``None``, auto-detection via :func:`~mannf.product.ingestors.loader.get_ingestor_for_source`
33
+ is attempted.
34
+ source_path:
35
+ File-system path to the source file to ingest.
36
+ source_url:
37
+ URL of the remote source to ingest.
38
+ source_format:
39
+ Format hint for the source (e.g., ``"openapi"``, ``"har"``). Used
40
+ when constructing the :class:`~mannf.product.ingestors.models.IngestorSource`.
41
+ ingestor_config:
42
+ Arbitrary config dict passed verbatim to the ingestor's
43
+ :meth:`~mannf.product.ingestors.base.IngestorPlugin.ingest` method.
44
+ scan_config:
45
+ Scan-level config — must contain at least ``"base_url"`` for the
46
+ :class:`~mannf.product.security.scanner.SecurityScanner` to work.
47
+ May also contain ``"auth_token"``, ``"api_key"``, ``"timeout"``,
48
+ ``"severity_threshold"``, ``"belief_guided"``, ``"enabled_checks"``.
49
+ prioritizer_config:
50
+ Config hints for the belief-guided prioritizer. Currently supports
51
+ ``"seed_belief_scores"`` (dict mapping endpoint keys to float scores)
52
+ and ``"risk_report"`` (list of legacy risk dicts).
53
+ exporter_config:
54
+ Config passed to the exporter plugin after scanning. Must include
55
+ ``"exporter_name"`` if export is desired; ``None`` disables export.
56
+ feedback_config:
57
+ Config for the learning/feedback phase. Supports
58
+ ``"save_path"`` (file path for persisting feedback JSON) and
59
+ ``"enabled"`` (bool, default ``True``).
60
+ llm_config:
61
+ Optional config for LLM-augmented edge-case generation. When set,
62
+ the orchestrator will invoke the LLM to generate additional test cases
63
+ for high-priority endpoints before scanning. Supported keys:
64
+
65
+ - ``"enabled"`` — bool (default ``True`` when this dict is non-empty)
66
+ - ``"provider"`` — ``"openai"`` or ``"anthropic"`` (default ``"openai"``)
67
+ - ``"model"`` — model identifier (e.g. ``"gpt-4o-mini"``)
68
+ - ``"api_key"`` — API key (falls back to env var)
69
+ - ``"priority_threshold"`` — minimum priority_score for a target to
70
+ receive LLM-generated test cases (default ``0.7``)
71
+ - ``"n_cases"`` — number of edge-case test cases to generate per
72
+ high-priority endpoint (default ``3``)
73
+ - Any other keys are passed verbatim to :class:`~mannf.product.llm.config.LLMConfig`.
74
+ """
75
+
76
+ ingestor_name: str | None = None
77
+ source_path: str | None = None
78
+ source_url: str | None = None
79
+ source_format: str | None = None
80
+ ingestor_config: dict = field(default_factory=dict)
81
+ scan_config: dict = field(default_factory=dict)
82
+ prioritizer_config: dict = field(default_factory=dict)
83
+ exporter_config: dict = field(default_factory=dict)
84
+ feedback_config: dict = field(default_factory=dict)
85
+ llm_config: dict = field(default_factory=dict)
86
+
87
+
88
+ @dataclass
89
+ class ScanTarget:
90
+ """A single scan target — one endpoint with associated test intents.
91
+
92
+ Attributes
93
+ ----------
94
+ endpoint:
95
+ The ingested endpoint to scan.
96
+ test_cases:
97
+ Test intents derived from the ingestion phase that are associated
98
+ with this endpoint (matched by path/tags).
99
+ priority_score:
100
+ Composite priority score in ``[0.0, 1.0]`` assigned by the
101
+ belief-guided prioritizer. Higher means test first.
102
+ check_ids:
103
+ OWASP check IDs recommended for this endpoint by the prioritizer.
104
+ metadata:
105
+ Arbitrary additional data (e.g., source ingestor name, raw priority hint).
106
+ """
107
+
108
+ endpoint: IngestedEndpoint
109
+ test_cases: list[IngestedTestCase] = field(default_factory=list)
110
+ priority_score: float = 0.5
111
+ check_ids: list[str] = field(default_factory=list)
112
+ metadata: dict = field(default_factory=dict)
113
+
114
+ def endpoint_key(self) -> str:
115
+ """Return ``"METHOD /path"`` string for logging and lookup."""
116
+ return f"{self.endpoint.method.upper()} {self.endpoint.path}"
117
+
118
+
119
+ @dataclass
120
+ class ScanPlan:
121
+ """A prioritized, ordered list of endpoints to scan.
122
+
123
+ Attributes
124
+ ----------
125
+ targets:
126
+ Scan targets in descending priority order (highest first).
127
+ total_endpoints:
128
+ Number of distinct endpoints in the plan.
129
+ total_test_cases:
130
+ Total number of test intents across all targets.
131
+ ingest_stats:
132
+ Stats returned by the ingestor (passed through for reference).
133
+ metadata:
134
+ Arbitrary metadata (ingestor name, source path, timestamp, etc.).
135
+ """
136
+
137
+ targets: list[ScanTarget] = field(default_factory=list)
138
+ total_endpoints: int = 0
139
+ total_test_cases: int = 0
140
+ ingest_stats: dict = field(default_factory=dict)
141
+ metadata: dict = field(default_factory=dict)
142
+
143
+ def to_dict(self) -> dict:
144
+ """Serialize to a plain dict suitable for JSON output."""
145
+ return {
146
+ "total_endpoints": self.total_endpoints,
147
+ "total_test_cases": self.total_test_cases,
148
+ "ingest_stats": self.ingest_stats,
149
+ "metadata": self.metadata,
150
+ "targets": [
151
+ {
152
+ "endpoint": t.endpoint_key(),
153
+ "priority_score": t.priority_score,
154
+ "check_ids": t.check_ids,
155
+ "test_cases": [tc.name for tc in t.test_cases],
156
+ "metadata": t.metadata,
157
+ }
158
+ for t in self.targets
159
+ ],
160
+ }
161
+
162
+
163
+ @dataclass
164
+ class ScanFeedback:
165
+ """Learning data captured after a completed scan.
166
+
167
+ Attributes
168
+ ----------
169
+ scan_id:
170
+ UUID of the associated security scan.
171
+ total_targets:
172
+ Number of endpoints that were scanned.
173
+ findings_count:
174
+ Total number of security findings across all endpoints.
175
+ per_target:
176
+ Per-endpoint breakdown: list of dicts with keys
177
+ ``endpoint``, ``priority_score``, ``findings_count``,
178
+ ``matched_test_cases`` (names), ``outcome_matched`` (bool).
179
+ priority_accuracy:
180
+ Fraction of findings that came from the top-50 % of endpoints
181
+ by priority score. Value in ``[0.0, 1.0]``; higher is better.
182
+ suggested_adjustments:
183
+ Endpoint keys that should receive a belief-score boost on the
184
+ next run (those that had unexpected findings or missed expected ones).
185
+ """
186
+
187
+ scan_id: str = ""
188
+ total_targets: int = 0
189
+ findings_count: int = 0
190
+ per_target: list[dict] = field(default_factory=list)
191
+ priority_accuracy: float = 0.0
192
+ suggested_adjustments: dict = field(default_factory=dict)
193
+
194
+ def to_dict(self) -> dict:
195
+ """Serialize to a plain dict suitable for JSON output."""
196
+ return {
197
+ "scan_id": self.scan_id,
198
+ "total_targets": self.total_targets,
199
+ "findings_count": self.findings_count,
200
+ "per_target": self.per_target,
201
+ "priority_accuracy": self.priority_accuracy,
202
+ "suggested_adjustments": self.suggested_adjustments,
203
+ }
204
+
205
+
206
+ @dataclass
207
+ class OrchestrationResult:
208
+ """Full result of an orchestration run.
209
+
210
+ Attributes
211
+ ----------
212
+ config:
213
+ The configuration used for this run.
214
+ ingest_stats:
215
+ Raw stats returned by the ingestor plugin.
216
+ scan_plan:
217
+ The prioritized scan plan (populated after the prioritize phase).
218
+ scan_results:
219
+ The full :class:`~mannf.product.security.models.SecurityReport`,
220
+ or ``None`` in dry-run / plan-only mode.
221
+ feedback:
222
+ Learning data from the scan phase, or ``None`` in dry-run mode.
223
+ export_summaries:
224
+ One :class:`~mannf.product.exporters.base.ExportSummary` per exporter
225
+ that was invoked after scanning. Empty when no exporter is configured
226
+ or when running in dry-run / plan-only mode.
227
+ errors:
228
+ Fatal errors encountered during the pipeline (ingestor failures, etc.).
229
+ warnings:
230
+ Non-fatal warnings emitted during the pipeline.
231
+ """
232
+
233
+ config: OrchestrationConfig = field(default_factory=OrchestrationConfig)
234
+ ingest_stats: dict = field(default_factory=dict)
235
+ scan_plan: ScanPlan = field(default_factory=ScanPlan)
236
+ scan_results: SecurityReport | None = None
237
+ feedback: ScanFeedback | None = None
238
+ export_summaries: list[ExportSummary] = field(default_factory=list)
239
+ errors: list[str] = field(default_factory=list)
240
+ warnings: list[str] = field(default_factory=list)
241
+ # LLM root cause suggestions for high-severity findings (Phase 10.3)
242
+ root_cause_suggestions: list[dict] = field(default_factory=list)
243
+
244
+ @property
245
+ def success(self) -> bool:
246
+ """True if there were no fatal errors in the pipeline."""
247
+ return len(self.errors) == 0
248
+
249
+ def to_dict(self) -> dict:
250
+ """Serialize to a plain dict suitable for JSON output."""
251
+ return {
252
+ "success": self.success,
253
+ "ingest_stats": self.ingest_stats,
254
+ "scan_plan": self.scan_plan.to_dict(),
255
+ "scan_results": self.scan_results.to_dict() if self.scan_results else None,
256
+ "feedback": self.feedback.to_dict() if self.feedback else None,
257
+ "root_cause_suggestions": self.root_cause_suggestions,
258
+ "export_summaries": [
259
+ {
260
+ "total": s.total,
261
+ "succeeded": s.succeeded,
262
+ "failed": s.failed,
263
+ "skipped": s.skipped,
264
+ }
265
+ for s in self.export_summaries
266
+ ],
267
+ "errors": self.errors,
268
+ "warnings": self.warnings,
269
+ }
@@ -0,0 +1,36 @@
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
+ """Regression record/replay package for NAT.
7
+
8
+ Provides :class:`~mannf.product.regression.recorder.RegressionRecorder`,
9
+ :class:`~mannf.product.regression.replayer.RegressionReplayer`, and
10
+ :class:`~mannf.product.regression.differ.RegressionDiffer` for recording API
11
+ test runs as portable baselines and comparing future runs against them.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from mannf.product.regression.differ import RegressionDiffer
17
+ from mannf.product.regression.masking import mask_sensitive
18
+ from mannf.product.regression.models import (
19
+ RegressionBaseline,
20
+ RegressionDiff,
21
+ RequestRecord,
22
+ ResponseRecord,
23
+ )
24
+ from mannf.product.regression.recorder import RegressionRecorder
25
+ from mannf.product.regression.replayer import RegressionReplayer
26
+
27
+ __all__ = [
28
+ "RegressionRecorder",
29
+ "RegressionReplayer",
30
+ "RegressionDiffer",
31
+ "RegressionBaseline",
32
+ "RegressionDiff",
33
+ "RequestRecord",
34
+ "ResponseRecord",
35
+ "mask_sensitive",
36
+ ]
@@ -0,0 +1,172 @@
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
+ """RegressionDiffer — compares a baseline recording to a replay recording."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from mannf.product.regression.models import (
13
+ FieldChange,
14
+ RecordDiff,
15
+ RegressionBaseline,
16
+ RegressionDiff,
17
+ RegressionRecord,
18
+ )
19
+
20
+ # Latency regression threshold: flag if replay is ≥ this factor slower than baseline
21
+ _LATENCY_REGRESSION_FACTOR = 2.0
22
+
23
+
24
+ class RegressionDiffer:
25
+ """Compare a baseline :class:`~mannf.product.regression.models.RegressionBaseline` to a
26
+ replay baseline and produce a :class:`~mannf.product.regression.models.RegressionDiff`.
27
+
28
+ Parameters
29
+ ----------
30
+ latency_regression_factor:
31
+ Multiplier applied to the baseline latency. If the replay latency
32
+ exceeds ``baseline_latency * factor``, a latency regression is reported.
33
+ Defaults to ``2.0`` (100 % slower = regression).
34
+ check_headers:
35
+ When ``True``, compare response headers between baseline and replay.
36
+ Defaults to ``False`` (headers are often volatile).
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ latency_regression_factor: float = _LATENCY_REGRESSION_FACTOR,
42
+ check_headers: bool = False,
43
+ ) -> None:
44
+ self.latency_regression_factor = latency_regression_factor
45
+ self.check_headers = check_headers
46
+
47
+ def diff(
48
+ self,
49
+ baseline: RegressionBaseline,
50
+ replay: RegressionBaseline,
51
+ ) -> RegressionDiff:
52
+ """Compute the diff between *baseline* and *replay*.
53
+
54
+ Parameters
55
+ ----------
56
+ baseline:
57
+ The original recording.
58
+ replay:
59
+ The new recording produced by :class:`~mannf.product.regression.replayer.RegressionReplayer`.
60
+
61
+ Returns
62
+ -------
63
+ RegressionDiff
64
+ Structured diff report.
65
+ """
66
+ baseline_by_id: Dict[str, RegressionRecord] = {
67
+ r.test_case_id: r for r in baseline.records
68
+ }
69
+ replay_by_id: Dict[str, RegressionRecord] = {
70
+ r.test_case_id: r for r in replay.records
71
+ }
72
+
73
+ baseline_ids = set(baseline_by_id.keys())
74
+ replay_ids = set(replay_by_id.keys())
75
+
76
+ new_ids = replay_ids - baseline_ids
77
+ removed_ids = baseline_ids - replay_ids
78
+ common_ids = baseline_ids & replay_ids
79
+
80
+ changes: List[RecordDiff] = []
81
+
82
+ for tc_id in sorted(common_ids):
83
+ b_rec = baseline_by_id[tc_id]
84
+ r_rec = replay_by_id[tc_id]
85
+ diffs = self._compare_records(b_rec, r_rec)
86
+ if diffs:
87
+ changes.append(
88
+ RecordDiff(
89
+ test_case_id=tc_id,
90
+ endpoint=f"{b_rec.request.method} {b_rec.request.url}",
91
+ changes=diffs,
92
+ )
93
+ )
94
+
95
+ total = len(baseline_ids) + len(new_ids)
96
+ changed = len(changes)
97
+ unchanged = len(common_ids) - changed
98
+
99
+ summary: Dict[str, int] = {
100
+ "total": total,
101
+ "unchanged": unchanged,
102
+ "changed": changed,
103
+ "new": len(new_ids),
104
+ "removed": len(removed_ids),
105
+ }
106
+
107
+ return RegressionDiff(summary=summary, changes=changes)
108
+
109
+ # ------------------------------------------------------------------
110
+ # Internal helpers
111
+ # ------------------------------------------------------------------
112
+
113
+ def _compare_records(
114
+ self,
115
+ baseline: RegressionRecord,
116
+ replay: RegressionRecord,
117
+ ) -> List[FieldChange]:
118
+ field_changes: List[FieldChange] = []
119
+
120
+ # Status code
121
+ if baseline.response.status_code != replay.response.status_code:
122
+ field_changes.append(
123
+ FieldChange(
124
+ field="response.status_code",
125
+ baseline=baseline.response.status_code,
126
+ replay=replay.response.status_code,
127
+ )
128
+ )
129
+
130
+ # Response body
131
+ if baseline.response.body != replay.response.body:
132
+ field_changes.append(
133
+ FieldChange(
134
+ field="response.body",
135
+ baseline=baseline.response.body,
136
+ replay=replay.response.body,
137
+ )
138
+ )
139
+
140
+ # Pass/fail
141
+ if baseline.passed != replay.passed:
142
+ field_changes.append(
143
+ FieldChange(
144
+ field="passed",
145
+ baseline=baseline.passed,
146
+ replay=replay.passed,
147
+ )
148
+ )
149
+
150
+ # Latency regression
151
+ b_latency = baseline.response.latency_ms
152
+ r_latency = replay.response.latency_ms
153
+ if b_latency > 0 and r_latency > b_latency * self.latency_regression_factor:
154
+ field_changes.append(
155
+ FieldChange(
156
+ field="response.latency_ms",
157
+ baseline=b_latency,
158
+ replay=r_latency,
159
+ )
160
+ )
161
+
162
+ # Response headers (optional)
163
+ if self.check_headers and baseline.response.headers != replay.response.headers:
164
+ field_changes.append(
165
+ FieldChange(
166
+ field="response.headers",
167
+ baseline=baseline.response.headers,
168
+ replay=replay.response.headers,
169
+ )
170
+ )
171
+
172
+ return field_changes
@@ -0,0 +1,100 @@
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
+ """Sensitive data masking for regression recordings.
7
+
8
+ When ``--mask-sensitive`` is used, values in headers such as ``Authorization``,
9
+ ``X-API-Key``, cookie values, and OAuth tokens are replaced with
10
+ ``***MASKED***`` before being written to disk.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from typing import Any, Dict, Optional
17
+
18
+ _MASKED = "***MASKED***"
19
+
20
+ # Header names that carry sensitive data (case-insensitive matching)
21
+ _SENSITIVE_HEADERS = frozenset(
22
+ {
23
+ "authorization",
24
+ "proxy-authorization",
25
+ "x-api-key",
26
+ "x-auth-token",
27
+ "x-access-token",
28
+ "cookie",
29
+ "set-cookie",
30
+ "x-csrf-token",
31
+ "api-key",
32
+ "x-secret",
33
+ "x-token",
34
+ }
35
+ )
36
+
37
+ # Patterns in JSON body / query-param values that look like tokens
38
+ _TOKEN_PATTERNS = [
39
+ re.compile(r"^Bearer\s+\S+", re.IGNORECASE),
40
+ re.compile(r"^Basic\s+\S+", re.IGNORECASE),
41
+ ]
42
+
43
+
44
+ def _is_sensitive_header(name: str) -> bool:
45
+ return name.lower() in _SENSITIVE_HEADERS
46
+
47
+
48
+ def mask_headers(headers: Dict[str, str]) -> Dict[str, str]:
49
+ """Return a copy of *headers* with sensitive values replaced by ``***MASKED***``."""
50
+ return {
51
+ k: (_MASKED if _is_sensitive_header(k) else v)
52
+ for k, v in headers.items()
53
+ }
54
+
55
+
56
+ def _mask_value(value: Any) -> Any:
57
+ """Mask a single string value if it looks like a token."""
58
+ if not isinstance(value, str):
59
+ return value
60
+ for pattern in _TOKEN_PATTERNS:
61
+ if pattern.match(value):
62
+ return _MASKED
63
+ return value
64
+
65
+
66
+ def _mask_dict_values(data: Any) -> Any:
67
+ """Recursively mask token-like string values inside *data*."""
68
+ if isinstance(data, dict):
69
+ return {k: _mask_dict_values(v) for k, v in data.items()}
70
+ if isinstance(data, list):
71
+ return [_mask_dict_values(item) for item in data]
72
+ return _mask_value(data)
73
+
74
+
75
+ def mask_sensitive(
76
+ headers: Optional[Dict[str, str]] = None,
77
+ body: Any = None,
78
+ query_params: Optional[Dict[str, Any]] = None,
79
+ ) -> tuple:
80
+ """Apply all masking rules and return ``(masked_headers, masked_body, masked_params)``.
81
+
82
+ Parameters
83
+ ----------
84
+ headers:
85
+ HTTP headers dict (may be ``None``).
86
+ body:
87
+ Request/response body (any JSON-serialisable type).
88
+ query_params:
89
+ URL query parameters dict (may be ``None``).
90
+
91
+ Returns
92
+ -------
93
+ tuple
94
+ ``(masked_headers, masked_body, masked_query_params)`` with all
95
+ sensitive values replaced by ``***MASKED***``.
96
+ """
97
+ masked_hdrs = mask_headers(headers or {})
98
+ masked_body = _mask_dict_values(body)
99
+ masked_params = _mask_dict_values(query_params) if query_params else query_params
100
+ return masked_hdrs, masked_body, masked_params