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,114 @@
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
+
5
+ """Tenant data isolation dependency for FastAPI route injection.
6
+
7
+ Usage in a route::
8
+
9
+ from mannf.product.middleware.tenant_isolation import get_current_tenant
10
+
11
+ @app.get("/api/v1/some-endpoint")
12
+ async def some_endpoint(
13
+ request: Request,
14
+ tenant: dict = Depends(get_current_tenant),
15
+ ) -> ...:
16
+ # tenant is guaranteed to be the authenticated tenant dict
17
+ ...
18
+
19
+ When ``NAT_API_KEY`` legacy mode is in use (no DB tenant), this dependency
20
+ returns ``None`` — callers can choose to gate multi-tenant logic on
21
+ ``tenant is not None`` to preserve backward compatibility.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Optional
27
+
28
+ from fastapi import HTTPException, Request
29
+
30
+
31
+ async def get_current_tenant(request: Request) -> dict:
32
+ """FastAPI dependency that extracts the authenticated tenant.
33
+
34
+ Returns the tenant dict from ``request.state.tenant`` when a tenant has
35
+ been resolved by the auth middleware.
36
+
37
+ Raises ``HTTP 401`` if no tenant is authenticated (DB mode with no valid
38
+ API key, or API key not present).
39
+
40
+ In legacy ``NAT_API_KEY`` mode, ``request.state.tenant`` is ``None``.
41
+ Use :func:`get_optional_tenant` if you need backward-compatible behaviour
42
+ where unauthenticated/legacy access is still permitted.
43
+ """
44
+ tenant = getattr(request.state, "tenant", None)
45
+ if tenant is None:
46
+ raise HTTPException(
47
+ status_code=401,
48
+ detail="Authentication required",
49
+ )
50
+ return tenant
51
+
52
+
53
+ async def get_optional_tenant(request: Request) -> Optional[dict]:
54
+ """FastAPI dependency that returns the tenant or ``None`` in legacy mode.
55
+
56
+ Use this on endpoints that must remain backward-compatible with the legacy
57
+ ``NAT_API_KEY`` environment variable. The returned value is either:
58
+
59
+ - ``dict`` — the authenticated tenant record, or
60
+ - ``None`` — legacy mode (no DB tenant; auth still validated by middleware).
61
+ """
62
+ return getattr(request.state, "tenant", None)
63
+
64
+
65
+ async def get_current_user(request: Request) -> dict:
66
+ """Return the authenticated user dict from request.state.user.
67
+
68
+ Raises HTTP 401 if no user is set on the request state.
69
+ The user is set by the SAML/SSO auth path in the middleware.
70
+ When using API key auth (not SSO), this returns a synthetic user dict
71
+ derived from the tenant record.
72
+ """
73
+ user = getattr(request.state, "user", None)
74
+ if user is None:
75
+ # Fallback: synthesize a minimal user from the tenant if available
76
+ tenant = getattr(request.state, "tenant", None)
77
+ if tenant is not None:
78
+ # API-key auth: synthesize a user with admin role for backward compat
79
+ return {
80
+ "id": None,
81
+ "email": tenant.get("email", ""),
82
+ "name": tenant.get("name", ""),
83
+ "role": "admin",
84
+ "tenant_id": tenant.get("tenant_id") or str(tenant.get("id", "")),
85
+ }
86
+ raise HTTPException(status_code=401, detail="Authentication required")
87
+ return user
88
+
89
+
90
+ def require_role(*allowed_roles: str):
91
+ """FastAPI dependency factory that enforces RBAC.
92
+
93
+ Usage::
94
+
95
+ @app.post("/api/v1/scan")
96
+ async def trigger_scan(
97
+ user: dict = Depends(require_role("admin", "operator")),
98
+ ):
99
+ ...
100
+ """
101
+ async def _check_role(request: Request) -> dict:
102
+ user = await get_current_user(request)
103
+ if user.get("role") not in allowed_roles:
104
+ raise HTTPException(
105
+ status_code=403,
106
+ detail={
107
+ "error": {
108
+ "code": "INSUFFICIENT_ROLE",
109
+ "message": f"Role {user.get('role')!r} is not permitted. Required: {list(allowed_roles)}",
110
+ }
111
+ },
112
+ )
113
+ return user
114
+ return _check_role
@@ -0,0 +1,347 @@
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
+
5
+ """SQLAlchemy ORM models for the NAT persistence layer.
6
+
7
+ Tables:
8
+ - scans — replaces the ``_scans`` in-memory dict
9
+ - security_scans — replaces ``_security_scans``
10
+ - healing_reports — replaces ``_healing_reports``
11
+ - tenants — SaaS tenant / organisation records
12
+ - api_keys — hashed API keys linked to tenants
13
+ - usage_records — per-month scan usage metering
14
+ - billing_events — auditable log of billing lifecycle events
15
+ - autonomous_runs — replaces the ``_autonomous_runs`` in-memory dict
16
+ - discovery_runs — replaces the ``_discoveries`` in-memory dict
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import uuid
22
+
23
+ from mannf.product.database import Base
24
+
25
+ try:
26
+ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text
27
+ from sqlalchemy.dialects.postgresql import JSON, UUID
28
+ from sqlalchemy.orm import Mapped, mapped_column
29
+ from sqlalchemy.sql import func
30
+
31
+ class Scan(Base):
32
+ """Persistent record for a functional (NAT) scan."""
33
+
34
+ __tablename__ = "scans"
35
+
36
+ id: Mapped[uuid.UUID] = mapped_column(
37
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
38
+ )
39
+ status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
40
+ created_at: Mapped[DateTime] = mapped_column(
41
+ DateTime(timezone=True), server_default=func.now(), nullable=False
42
+ )
43
+ completed_at: Mapped[DateTime | None] = mapped_column(
44
+ DateTime(timezone=True), nullable=True
45
+ )
46
+ spec_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
47
+ base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
48
+ summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
49
+ results: Mapped[list | None] = mapped_column(JSON, nullable=True)
50
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
51
+ prioritization: Mapped[dict | None] = mapped_column(JSON, nullable=True)
52
+ llm_cases_generated: Mapped[int] = mapped_column(
53
+ Integer, nullable=False, default=0
54
+ )
55
+ healing_summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
56
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
57
+ UUID(as_uuid=True),
58
+ ForeignKey("tenants.id"),
59
+ nullable=True,
60
+ index=True,
61
+ )
62
+
63
+ class SecurityScan(Base):
64
+ """Persistent record for an OWASP security scan."""
65
+
66
+ __tablename__ = "security_scans"
67
+
68
+ id: Mapped[uuid.UUID] = mapped_column(
69
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
70
+ )
71
+ status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
72
+ created_at: Mapped[DateTime] = mapped_column(
73
+ DateTime(timezone=True), server_default=func.now(), nullable=False
74
+ )
75
+ completed_at: Mapped[DateTime | None] = mapped_column(
76
+ DateTime(timezone=True), nullable=True
77
+ )
78
+ spec_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
79
+ base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
80
+ report: Mapped[dict | None] = mapped_column(JSON, nullable=True)
81
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
82
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
83
+ UUID(as_uuid=True),
84
+ ForeignKey("tenants.id"),
85
+ nullable=True,
86
+ index=True,
87
+ )
88
+
89
+ class HealingReport(Base):
90
+ """Persistent record for a self-healing analysis report."""
91
+
92
+ __tablename__ = "healing_reports"
93
+
94
+ id: Mapped[uuid.UUID] = mapped_column(
95
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
96
+ )
97
+ created_at: Mapped[DateTime] = mapped_column(
98
+ DateTime(timezone=True), server_default=func.now(), nullable=False
99
+ )
100
+ report_data: Mapped[dict] = mapped_column(JSON, nullable=False)
101
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
102
+ UUID(as_uuid=True),
103
+ ForeignKey("tenants.id"),
104
+ nullable=True,
105
+ index=True,
106
+ )
107
+
108
+ class Tenant(Base):
109
+ """SaaS tenant / organisation record."""
110
+
111
+ __tablename__ = "tenants"
112
+
113
+ id: Mapped[uuid.UUID] = mapped_column(
114
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
115
+ )
116
+ name: Mapped[str] = mapped_column(String(256), nullable=False)
117
+ email: Mapped[str] = mapped_column(String(512), nullable=False, unique=True)
118
+ plan_tier: Mapped[str] = mapped_column(
119
+ String(32), nullable=False, default="free"
120
+ )
121
+ stripe_customer_id: Mapped[str | None] = mapped_column(
122
+ String(256), nullable=True, unique=True
123
+ )
124
+ stripe_subscription_id: Mapped[str | None] = mapped_column(
125
+ String(256), nullable=True
126
+ )
127
+ monthly_scan_quota: Mapped[int] = mapped_column(
128
+ Integer, nullable=False, default=50
129
+ )
130
+ monthly_security_scan_quota: Mapped[int] = mapped_column(
131
+ Integer, nullable=False, default=10
132
+ )
133
+ max_concurrent_scans: Mapped[int] = mapped_column(
134
+ Integer, nullable=False, default=1
135
+ )
136
+ is_active: Mapped[bool] = mapped_column(
137
+ Boolean, nullable=False, default=True
138
+ )
139
+ notification_slack_webhook_url: Mapped[str | None] = mapped_column(
140
+ String(2048), nullable=True
141
+ )
142
+ notification_teams_webhook_url: Mapped[str | None] = mapped_column(
143
+ String(2048), nullable=True
144
+ )
145
+ created_at: Mapped[DateTime] = mapped_column(
146
+ DateTime(timezone=True), server_default=func.now(), nullable=False
147
+ )
148
+ updated_at: Mapped[DateTime] = mapped_column(
149
+ DateTime(timezone=True),
150
+ server_default=func.now(),
151
+ onupdate=func.now(),
152
+ nullable=False,
153
+ )
154
+
155
+ class ApiKey(Base):
156
+ """Hashed API key linked to a tenant."""
157
+
158
+ __tablename__ = "api_keys"
159
+
160
+ id: Mapped[uuid.UUID] = mapped_column(
161
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
162
+ )
163
+ tenant_id: Mapped[uuid.UUID] = mapped_column(
164
+ UUID(as_uuid=True),
165
+ ForeignKey("tenants.id"),
166
+ nullable=False,
167
+ index=True,
168
+ )
169
+ key_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
170
+ key_prefix: Mapped[str] = mapped_column(String(12), nullable=False)
171
+ label: Mapped[str | None] = mapped_column(String(256), nullable=True)
172
+ is_active: Mapped[bool] = mapped_column(
173
+ Boolean, nullable=False, default=True
174
+ )
175
+ created_at: Mapped[DateTime] = mapped_column(
176
+ DateTime(timezone=True), server_default=func.now(), nullable=False
177
+ )
178
+ last_used_at: Mapped[DateTime | None] = mapped_column(
179
+ DateTime(timezone=True), nullable=True
180
+ )
181
+
182
+ class UsageRecord(Base):
183
+ """Per-month scan usage record for quota metering."""
184
+
185
+ __tablename__ = "usage_records"
186
+
187
+ id: Mapped[uuid.UUID] = mapped_column(
188
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
189
+ )
190
+ tenant_id: Mapped[uuid.UUID] = mapped_column(
191
+ UUID(as_uuid=True),
192
+ ForeignKey("tenants.id"),
193
+ nullable=False,
194
+ index=True,
195
+ )
196
+ scan_type: Mapped[str] = mapped_column(String(32), nullable=False)
197
+ scan_id: Mapped[uuid.UUID | None] = mapped_column(
198
+ UUID(as_uuid=True), nullable=True
199
+ )
200
+ recorded_at: Mapped[DateTime] = mapped_column(
201
+ DateTime(timezone=True), server_default=func.now(), nullable=False
202
+ )
203
+ period_year: Mapped[int] = mapped_column(Integer, nullable=False)
204
+ period_month: Mapped[int] = mapped_column(Integer, nullable=False)
205
+
206
+ __table_args__ = (
207
+ Index(
208
+ "ix_usage_records_tenant_period",
209
+ "tenant_id",
210
+ "period_year",
211
+ "period_month",
212
+ "scan_type",
213
+ ),
214
+ )
215
+
216
+ class BillingEvent(Base):
217
+ """Auditable log of billing lifecycle events."""
218
+
219
+ __tablename__ = "billing_events"
220
+
221
+ id: Mapped[uuid.UUID] = mapped_column(
222
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
223
+ )
224
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
225
+ UUID(as_uuid=True),
226
+ ForeignKey("tenants.id"),
227
+ nullable=True,
228
+ index=True,
229
+ )
230
+ event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
231
+ source: Mapped[str] = mapped_column(
232
+ String(32), nullable=False, default="system"
233
+ )
234
+ details: Mapped[dict | None] = mapped_column(JSON, nullable=True)
235
+ created_at: Mapped[DateTime] = mapped_column(
236
+ DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
237
+ )
238
+
239
+ class AutonomousRun(Base):
240
+ """Persistent record for an autonomous test-loop run."""
241
+
242
+ __tablename__ = "autonomous_runs"
243
+
244
+ id: Mapped[uuid.UUID] = mapped_column(
245
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
246
+ )
247
+ status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
248
+ target_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
249
+ strategy: Mapped[str | None] = mapped_column(String(64), nullable=True)
250
+ report: Mapped[dict | None] = mapped_column(JSON, nullable=True)
251
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
252
+ created_at: Mapped[DateTime] = mapped_column(
253
+ DateTime(timezone=True), server_default=func.now(), nullable=False
254
+ )
255
+ completed_at: Mapped[DateTime | None] = mapped_column(
256
+ DateTime(timezone=True), nullable=True
257
+ )
258
+ last_heartbeat_at: Mapped[DateTime | None] = mapped_column(
259
+ DateTime(timezone=True), nullable=True
260
+ )
261
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
262
+ UUID(as_uuid=True),
263
+ ForeignKey("tenants.id"),
264
+ nullable=True,
265
+ index=True,
266
+ )
267
+
268
+ class DiscoveryRun(Base):
269
+ """Persistent record for a discovery crawl + scenario generation run."""
270
+
271
+ __tablename__ = "discovery_runs"
272
+
273
+ id: Mapped[uuid.UUID] = mapped_column(
274
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
275
+ )
276
+ status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
277
+ base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
278
+ scenarios: Mapped[list | None] = mapped_column(JSON, nullable=True)
279
+ stats: Mapped[dict | None] = mapped_column(JSON, nullable=True)
280
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
281
+ created_at: Mapped[DateTime] = mapped_column(
282
+ DateTime(timezone=True), server_default=func.now(), nullable=False
283
+ )
284
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(
285
+ UUID(as_uuid=True),
286
+ ForeignKey("tenants.id"),
287
+ nullable=True,
288
+ index=True,
289
+ )
290
+
291
+ class Workspace(Base):
292
+ __tablename__ = "workspaces"
293
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
294
+ tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
295
+ name: Mapped[str] = mapped_column(String(256), nullable=False)
296
+ description: Mapped[str | None] = mapped_column(Text, nullable=True)
297
+ is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
298
+ created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
299
+ updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
300
+
301
+ class Project(Base):
302
+ __tablename__ = "projects"
303
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
304
+ workspace_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=False, index=True)
305
+ tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
306
+ name: Mapped[str] = mapped_column(String(256), nullable=False)
307
+ description: Mapped[str | None] = mapped_column(Text, nullable=True)
308
+ parent_project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True)
309
+ is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
310
+ created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
311
+ updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
312
+
313
+ class User(Base):
314
+ __tablename__ = "users"
315
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
316
+ tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
317
+ email: Mapped[str] = mapped_column(String(512), nullable=False)
318
+ name: Mapped[str] = mapped_column(String(256), nullable=False)
319
+ sso_subject: Mapped[str | None] = mapped_column(String(512), nullable=True, unique=True)
320
+ is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
321
+ created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
322
+ updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
323
+ __table_args__ = (Index("uq_users_email_tenant", "email", "tenant_id", unique=True),)
324
+
325
+ class UserRole(Base):
326
+ __tablename__ = "user_roles"
327
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
328
+ user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
329
+ tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
330
+ workspace_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=True)
331
+ role: Mapped[str] = mapped_column(String(32), nullable=False)
332
+ created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
333
+
334
+ class ApiAuditLog(Base):
335
+ __tablename__ = "api_audit_log"
336
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
337
+ tenant_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True)
338
+ user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
339
+ method: Mapped[str] = mapped_column(String(10), nullable=False)
340
+ path: Mapped[str] = mapped_column(String(2048), nullable=False)
341
+ status_code: Mapped[int] = mapped_column(Integer, nullable=False)
342
+ request_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
343
+ source_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
344
+ created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
345
+
346
+ except ImportError: # pragma: no cover — SQLAlchemy not installed
347
+ pass
@@ -0,0 +1,24 @@
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
+
5
+ """Product notification service for scan lifecycle events.
6
+
7
+ Delivers real-time notifications to Slack and Microsoft Teams via incoming
8
+ webhooks whenever key scan or platform events occur.
9
+
10
+ Usage::
11
+
12
+ from mannf.product.notifications import dispatch_scan_notification
13
+
14
+ await dispatch_scan_notification(
15
+ "scan.completed",
16
+ tenant_id="<uuid>",
17
+ scan_id="<uuid>",
18
+ details={"total": 10, "passed": 9, "failed": 1},
19
+ )
20
+ """
21
+
22
+ from mannf.product.notifications.dispatcher import dispatch_scan_notification
23
+
24
+ __all__ = ["dispatch_scan_notification"]