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,235 @@
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
+ """In-memory schedule store with per-tier limit enforcement.
6
+
7
+ The store is a simple dict-backed registry that mirrors the pattern used
8
+ by ``_scans`` and ``_security_scans`` in ``mannf.product.server``.
9
+
10
+ Tier schedule limits
11
+ --------------------
12
+ +------------+------------------+
13
+ | Plan | Max schedules |
14
+ +============+==================+
15
+ | free | 0 (blocked) |
16
+ +------------+------------------+
17
+ | pro | 3 |
18
+ +------------+------------------+
19
+ | team | 10 |
20
+ +------------+------------------+
21
+ | enterprise | 999 999 |
22
+ +------------+------------------+
23
+
24
+ Usage::
25
+
26
+ from mannf.product.scheduling.store import ScheduleStore, TierLimitExceeded
27
+
28
+ store = ScheduleStore()
29
+ schedule = store.create(schedule, plan_tier="pro")
30
+ schedules = store.list_for_tenant("tenant-uuid")
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ from typing import Iterator
37
+
38
+ from mannf.product.scheduling.models import ScanSchedule
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Per-tier limit constants
44
+ # ---------------------------------------------------------------------------
45
+
46
+ TIER_SCHEDULE_LIMITS: dict[str, int] = {
47
+ "free": 0,
48
+ "pro": 3,
49
+ "team": 10,
50
+ "enterprise": 999_999,
51
+ }
52
+
53
+ _DEFAULT_TIER = "free"
54
+
55
+
56
+ def get_schedule_limit(plan_tier: str) -> int:
57
+ """Return the maximum number of schedules allowed for *plan_tier*.
58
+
59
+ Unknown tiers default to the ``"free"`` limit (0 — blocked).
60
+ """
61
+ return TIER_SCHEDULE_LIMITS.get(plan_tier, TIER_SCHEDULE_LIMITS[_DEFAULT_TIER])
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Custom exceptions
66
+ # ---------------------------------------------------------------------------
67
+
68
+ class TierLimitExceeded(Exception):
69
+ """Raised when a tenant tries to exceed the schedule limit for their plan.
70
+
71
+ Attributes:
72
+ plan_tier: The tenant's plan tier name.
73
+ limit: The maximum number of schedules allowed.
74
+ current: The current number of schedules the tenant already has.
75
+ """
76
+
77
+ def __init__(self, plan_tier: str, limit: int, current: int) -> None:
78
+ self.plan_tier = plan_tier
79
+ self.limit = limit
80
+ self.current = current
81
+ super().__init__(
82
+ f"Schedule limit exceeded for plan '{plan_tier}': "
83
+ f"limit is {limit}, current count is {current}"
84
+ )
85
+
86
+
87
+ class ScheduleNotFound(KeyError):
88
+ """Raised when a schedule_id does not exist in the store."""
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Store implementation
93
+ # ---------------------------------------------------------------------------
94
+
95
+ class ScheduleStore:
96
+ """Thread-safe, in-memory store for :class:`~mannf.product.scheduling.models.ScanSchedule` objects.
97
+
98
+ All mutating operations enforce per-tier schedule limits. Pass
99
+ ``plan_tier`` on :meth:`create` to have the limit checked; use
100
+ ``"enterprise"`` for privileged callers that bypass gating.
101
+
102
+ The store is intentionally simple — persistence and cross-process
103
+ sharing are out of scope for PR 1.
104
+ """
105
+
106
+ def __init__(self) -> None:
107
+ self._schedules: dict[str, ScanSchedule] = {}
108
+
109
+ # ------------------------------------------------------------------
110
+ # CRUD helpers
111
+ # ------------------------------------------------------------------
112
+
113
+ def create(self, schedule: ScanSchedule, plan_tier: str = _DEFAULT_TIER) -> ScanSchedule:
114
+ """Persist a new schedule, enforcing the per-tier limit.
115
+
116
+ Args:
117
+ schedule: A fully-populated :class:`ScanSchedule` instance. The
118
+ ``schedule_id`` must be unique within the store.
119
+ plan_tier: The owning tenant's plan tier name. Defaults to
120
+ ``"free"`` (which blocks scheduling).
121
+
122
+ Returns:
123
+ The stored :class:`ScanSchedule` (same object, unchanged).
124
+
125
+ Raises:
126
+ TierLimitExceeded: When the tenant already has the maximum number
127
+ of schedules allowed for their plan.
128
+ ValueError: When a schedule with the same ``schedule_id`` already
129
+ exists.
130
+ """
131
+ limit = get_schedule_limit(plan_tier)
132
+ current = sum(
133
+ 1 for s in self._schedules.values()
134
+ if s.tenant_id == schedule.tenant_id
135
+ )
136
+ if current >= limit:
137
+ raise TierLimitExceeded(plan_tier, limit, current)
138
+
139
+ if schedule.schedule_id in self._schedules:
140
+ raise ValueError(
141
+ f"Schedule with id '{schedule.schedule_id}' already exists"
142
+ )
143
+
144
+ # Stamp the tier limit so API callers can surface it.
145
+ schedule.max_schedules_for_tier = limit
146
+
147
+ self._schedules[schedule.schedule_id] = schedule
148
+ logger.debug(
149
+ "Created schedule %s for tenant %s (tier=%s, %d/%d used)",
150
+ schedule.schedule_id,
151
+ schedule.tenant_id,
152
+ plan_tier,
153
+ current + 1,
154
+ limit,
155
+ )
156
+ return schedule
157
+
158
+ def get(self, schedule_id: str) -> ScanSchedule:
159
+ """Return the schedule with *schedule_id*.
160
+
161
+ Raises:
162
+ ScheduleNotFound: When no schedule with that ID exists.
163
+ """
164
+ try:
165
+ return self._schedules[schedule_id]
166
+ except KeyError:
167
+ raise ScheduleNotFound(schedule_id)
168
+
169
+ def list_for_tenant(self, tenant_id: str) -> list[ScanSchedule]:
170
+ """Return all schedules owned by *tenant_id*, sorted by name."""
171
+ return sorted(
172
+ (s for s in self._schedules.values() if s.tenant_id == tenant_id),
173
+ key=lambda s: s.name,
174
+ )
175
+
176
+ def update(self, schedule: ScanSchedule) -> ScanSchedule:
177
+ """Replace the stored schedule entry for ``schedule.schedule_id``.
178
+
179
+ Args:
180
+ schedule: The updated :class:`ScanSchedule`. The ``schedule_id``
181
+ must already exist in the store.
182
+
183
+ Returns:
184
+ The updated :class:`ScanSchedule`.
185
+
186
+ Raises:
187
+ ScheduleNotFound: When the schedule does not exist.
188
+ """
189
+ if schedule.schedule_id not in self._schedules:
190
+ raise ScheduleNotFound(schedule.schedule_id)
191
+ self._schedules[schedule.schedule_id] = schedule
192
+ return schedule
193
+
194
+ def delete(self, schedule_id: str) -> None:
195
+ """Remove the schedule with *schedule_id* from the store.
196
+
197
+ Raises:
198
+ ScheduleNotFound: When no schedule with that ID exists.
199
+ """
200
+ if schedule_id not in self._schedules:
201
+ raise ScheduleNotFound(schedule_id)
202
+ del self._schedules[schedule_id]
203
+
204
+ # ------------------------------------------------------------------
205
+ # Iteration helpers (used by the engine)
206
+ # ------------------------------------------------------------------
207
+
208
+ def iter_enabled(self) -> Iterator[ScanSchedule]:
209
+ """Yield all enabled schedules across all tenants."""
210
+ for schedule in list(self._schedules.values()):
211
+ if schedule.enabled:
212
+ yield schedule
213
+
214
+ def __len__(self) -> int:
215
+ return len(self._schedules)
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Module-level singleton (for CLI access)
220
+ # ---------------------------------------------------------------------------
221
+
222
+ _store_singleton: ScheduleStore | None = None
223
+
224
+
225
+ def get_store() -> ScheduleStore:
226
+ """Return the module-level :class:`ScheduleStore` singleton.
227
+
228
+ In the server context the store is managed by ``server.py``; this helper
229
+ is primarily useful for CLI commands that need to interact with the store
230
+ without importing the full server module.
231
+ """
232
+ global _store_singleton
233
+ if _store_singleton is None:
234
+ _store_singleton = ScheduleStore()
235
+ return _store_singleton
@@ -0,0 +1,21 @@
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
+ """Security package for OWASP API Security Top 10 testing."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from mannf.product.security.belief_guided import prioritize_for_security
11
+ from mannf.product.security.models import SecurityFinding, SecurityReport
12
+ from mannf.product.security.reporter import SecurityReporter
13
+ from mannf.product.security.scanner import SecurityScanner
14
+
15
+ __all__ = [
16
+ "SecurityScanner",
17
+ "SecurityFinding",
18
+ "SecurityReport",
19
+ "SecurityReporter",
20
+ "prioritize_for_security",
21
+ ]
@@ -0,0 +1,143 @@
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
+ """Belief-guided endpoint prioritization for security scanning."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from mannf.product.security.checks.base import EndpointInfo
13
+
14
+ if TYPE_CHECKING:
15
+ from mannf.core.agents.belief_state import BeliefState
16
+ from mannf.core.prioritization.risk_scorer import RiskScorer
17
+
18
+ _HIGH_VALUE_PATH_KEYWORDS = (
19
+ "admin", "payment", "user", "account", "auth", "token",
20
+ "secret", "key", "password", "config", "management", "internal",
21
+ )
22
+
23
+ _CHECK_HINTS: dict[str, list[str]] = {
24
+ "admin": ["API5:2023", "API2:2023"],
25
+ "payment": ["API6:2023", "API4:2023", "API10:2023"],
26
+ "user": ["API1:2023", "API2:2023", "API3:2023"],
27
+ "account": ["API1:2023", "API2:2023"],
28
+ "auth": ["API2:2023"],
29
+ "token": ["API2:2023"],
30
+ "password": ["API2:2023", "API6:2023"],
31
+ "config": ["API5:2023", "API8:2023"],
32
+ "management": ["API5:2023"],
33
+ "internal": ["API5:2023", "API8:2023"],
34
+ }
35
+
36
+ _DEFAULT_CHECKS = [
37
+ "API1:2023", "API2:2023", "API3:2023", "API4:2023", "API5:2023",
38
+ "API6:2023", "API7:2023", "API8:2023", "API9:2023", "API10:2023",
39
+ ]
40
+
41
+
42
+ def prioritize_for_security(
43
+ endpoints: list[EndpointInfo],
44
+ risk_report: list[dict] | None = None,
45
+ belief_states: list[BeliefState] | None = None,
46
+ risk_scorer: RiskScorer | None = None,
47
+ fp_rates: dict[str, float] | None = None,
48
+ ) -> list[tuple[EndpointInfo, float, list[str], float, float]]:
49
+ """Return ``(endpoint, priority_score, check_ids, belief_score, risk_score)`` tuples sorted by descending priority.
50
+
51
+ Priority logic:
52
+ - Base score 0.5 for all endpoints
53
+ - If *belief_states* are provided, average ``fault_likelihood`` across agents is used as the primary score
54
+ - If *risk_scorer* is provided, its composite score is used as the primary score (overrides belief_states base)
55
+ - If *risk_report* is provided (legacy), use ``risk_score`` from the matching report entry
56
+ - Endpoints with high-value keywords in the path get +0.2
57
+ - POST/PUT/DELETE methods get +0.1 over GET
58
+ - If *fp_rates* is provided, the FP rate for the endpoint is used as a downward modifier:
59
+ ``score *= (1 - fp_rate)``
60
+ - ``check_ids`` lists the OWASP IDs most relevant for this endpoint
61
+ - ``belief_score`` is the raw belief-derived score (0.5 if not available)
62
+ - ``risk_score`` is the raw risk-scorer composite score (0.5 if not available)
63
+ """
64
+ risk_map: dict[str, float] = {}
65
+ if risk_report:
66
+ for entry in risk_report:
67
+ path = entry.get("path") or entry.get("endpoint", "")
68
+ score = entry.get("risk_score", 0.5)
69
+ if path:
70
+ risk_map[path] = float(score)
71
+
72
+ results: list[tuple[EndpointInfo, float, list[str], float, float]] = []
73
+
74
+ for ep in endpoints:
75
+ endpoint_key = ep.path
76
+
77
+ # Compute belief score: average fault_likelihood across all BeliefState agents
78
+ belief_score = 0.5
79
+ if belief_states:
80
+ likelihoods = []
81
+ for bs in belief_states:
82
+ fl = bs.fault_likelihood.get(endpoint_key)
83
+ if fl is None:
84
+ # Try matching by method+path key format
85
+ method_key = f"{ep.method.upper()} {endpoint_key}"
86
+ fl = bs.fault_likelihood.get(method_key)
87
+ if fl is not None:
88
+ likelihoods.append(fl)
89
+ if likelihoods:
90
+ belief_score = sum(likelihoods) / len(likelihoods)
91
+
92
+ # Compute risk scorer composite score
93
+ risk_score_val = 0.5
94
+ if risk_scorer is not None:
95
+ try:
96
+ risk_score_val = risk_scorer.score(endpoint_key, belief_states or [])
97
+ except Exception:
98
+ pass
99
+
100
+ # Determine the primary base score:
101
+ # 1. risk_scorer (highest fidelity composite) if available
102
+ # 2. belief_states (direct BDI signal) if available
103
+ # 3. legacy risk_report
104
+ # 4. default 0.5
105
+ if risk_scorer is not None and risk_score_val != 0.5:
106
+ score = risk_score_val
107
+ elif belief_states:
108
+ score = belief_score
109
+ else:
110
+ score = risk_map.get(ep.path, 0.5)
111
+
112
+ lower_path = ep.path.lower()
113
+ matched_keywords = [kw for kw in _HIGH_VALUE_PATH_KEYWORDS if kw in lower_path]
114
+ if matched_keywords:
115
+ score = min(1.0, score + 0.2)
116
+
117
+ if ep.method.upper() in ("POST", "PUT", "DELETE"):
118
+ score = min(1.0, score + 0.1)
119
+
120
+ # Apply FP rate as a downward modifier: score *= (1 - fp_rate)
121
+ if fp_rates:
122
+ ep_fp_rate = fp_rates.get(endpoint_key)
123
+ if ep_fp_rate is None:
124
+ method_key = f"{ep.method.upper()} {endpoint_key}"
125
+ ep_fp_rate = fp_rates.get(method_key)
126
+ if ep_fp_rate is not None:
127
+ fp_rate_clamped = max(0.0, min(1.0, float(ep_fp_rate)))
128
+ score = max(0.0, score * (1.0 - fp_rate_clamped))
129
+
130
+ check_ids: list[str] = []
131
+ seen: set[str] = set()
132
+ for kw in matched_keywords:
133
+ for cid in _CHECK_HINTS.get(kw, []):
134
+ if cid not in seen:
135
+ check_ids.append(cid)
136
+ seen.add(cid)
137
+ if not check_ids:
138
+ check_ids = list(_DEFAULT_CHECKS)
139
+
140
+ results.append((ep, score, check_ids, belief_score, risk_score_val))
141
+
142
+ results.sort(key=lambda t: t[1], reverse=True)
143
+ return results
@@ -0,0 +1,55 @@
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
+ """Security checks package — all 10 OWASP API Security Top 10 checks."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
11
+ from mannf.product.security.checks.bfla import BFLACheck
12
+ from mannf.product.security.checks.bola import BOLACheck
13
+ from mannf.product.security.checks.bopla import BOPLACheck
14
+ from mannf.product.security.checks.broken_auth import BrokenAuthCheck
15
+ from mannf.product.security.checks.graphql_security import (
16
+ GraphQLAliasDoSCheck,
17
+ GraphQLBatchQueryCheck,
18
+ GraphQLDepthAttackCheck,
19
+ GraphQLFieldSuggestionCheck,
20
+ GraphQLIntrospectionCheck,
21
+ )
22
+ from mannf.product.security.checks.inventory import InventoryCheck
23
+ from mannf.product.security.checks.misconfig import MisconfigCheck
24
+ from mannf.product.security.checks.resource_consumption import ResourceConsumptionCheck
25
+ from mannf.product.security.checks.sensitive_flows import SensitiveFlowsCheck
26
+ from mannf.product.security.checks.ssrf import SSRFCheck
27
+ from mannf.product.security.checks.unsafe_consumption import UnsafeConsumptionCheck
28
+ from mannf.product.security.plugin_loader import (
29
+ load_all_plugins,
30
+ load_plugins_from_directory,
31
+ load_plugins_from_entry_points,
32
+ )
33
+
34
+ __all__ = [
35
+ "SecurityCheck",
36
+ "EndpointInfo",
37
+ "BOLACheck",
38
+ "BrokenAuthCheck",
39
+ "BOPLACheck",
40
+ "ResourceConsumptionCheck",
41
+ "BFLACheck",
42
+ "SensitiveFlowsCheck",
43
+ "SSRFCheck",
44
+ "MisconfigCheck",
45
+ "InventoryCheck",
46
+ "UnsafeConsumptionCheck",
47
+ "GraphQLIntrospectionCheck",
48
+ "GraphQLDepthAttackCheck",
49
+ "GraphQLBatchQueryCheck",
50
+ "GraphQLFieldSuggestionCheck",
51
+ "GraphQLAliasDoSCheck",
52
+ "load_plugins_from_directory",
53
+ "load_plugins_from_entry_points",
54
+ "load_all_plugins",
55
+ ]
@@ -0,0 +1,69 @@
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
+ """Abstract base for OWASP security checks."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+
13
+ import httpx
14
+
15
+ from mannf.product.security.models import SecurityFinding
16
+
17
+
18
+ @dataclass
19
+ class EndpointInfo:
20
+ """Lightweight endpoint descriptor for security checks."""
21
+
22
+ method: str
23
+ path: str
24
+ base_url: str
25
+ parameters: list[dict] = field(default_factory=list)
26
+ request_body: dict | None = None
27
+
28
+ @property
29
+ def full_url(self) -> str:
30
+ base = self.base_url.rstrip("/")
31
+ path = self.path if self.path.startswith("/") else f"/{self.path}"
32
+ return f"{base}{path}"
33
+
34
+
35
+ class SecurityCheck(ABC):
36
+ owasp_id: str
37
+ name: str
38
+ description: str
39
+ severity_default: str
40
+
41
+ @abstractmethod
42
+ async def run(
43
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
44
+ ) -> list[SecurityFinding]:
45
+ ...
46
+
47
+ def _make_finding(
48
+ self,
49
+ endpoint: EndpointInfo,
50
+ title: str,
51
+ description: str,
52
+ evidence: str,
53
+ remediation: str,
54
+ cwe_id: str,
55
+ severity: str | None = None,
56
+ confidence: float = 0.8,
57
+ ) -> SecurityFinding:
58
+ return SecurityFinding(
59
+ check_id=self.owasp_id,
60
+ check_name=self.name,
61
+ severity=severity if severity is not None else self.severity_default,
62
+ endpoint=f"{endpoint.method.upper()} {endpoint.path}",
63
+ title=title,
64
+ description=description,
65
+ evidence=evidence,
66
+ remediation=remediation,
67
+ cwe_id=cwe_id,
68
+ confidence=confidence,
69
+ )
@@ -0,0 +1,77 @@
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
+ """API5:2023 — Broken Function Level Authorization (BFLA)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import httpx
11
+
12
+ from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
13
+ from mannf.product.security.models import SecurityFinding
14
+
15
+ _ADMIN_PATH_HINTS = (
16
+ "/admin", "/management", "/internal", "/config",
17
+ "/settings", "/users", "/roles",
18
+ )
19
+
20
+ _PRIVILEGED_METHODS = {"DELETE", "PUT"}
21
+
22
+
23
+ def _is_admin_like(path: str) -> bool:
24
+ lower = path.lower()
25
+ return any(hint in lower for hint in _ADMIN_PATH_HINTS)
26
+
27
+
28
+ class BFLACheck(SecurityCheck):
29
+ owasp_id = "API5:2023"
30
+ name = "Broken Function Level Authorization"
31
+ description = "Tests whether function-level access controls are enforced."
32
+ severity_default = "high"
33
+
34
+ async def run(
35
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
36
+ ) -> list[SecurityFinding]:
37
+ method = endpoint.method.upper()
38
+ is_admin = _is_admin_like(endpoint.path)
39
+ is_privileged_method = method in _PRIVILEGED_METHODS
40
+
41
+ if not is_admin and not is_privileged_method:
42
+ return []
43
+
44
+ url = endpoint.full_url
45
+ headers = {} # no auth header intentionally
46
+
47
+ try:
48
+ resp = await client.request(method, url, headers=headers)
49
+ except httpx.RequestError:
50
+ return []
51
+
52
+ if resp.status_code not in (200, 201):
53
+ return []
54
+
55
+ label_parts = []
56
+ if is_admin:
57
+ label_parts.append("admin-like path")
58
+ if is_privileged_method:
59
+ label_parts.append(f"privileged method ({method})")
60
+
61
+ return [
62
+ self._make_finding(
63
+ endpoint,
64
+ title="Admin/privileged endpoint accessible without authorization",
65
+ description=(
66
+ f"The endpoint ({', '.join(label_parts)}) returned "
67
+ f"HTTP {resp.status_code} without an Authorization header."
68
+ ),
69
+ evidence=f"{method} {url} (no auth) → {resp.status_code}",
70
+ remediation=(
71
+ "Enforce role-based access control (RBAC) on all admin and "
72
+ "privileged endpoints. Reject unauthorized access with HTTP 401/403."
73
+ ),
74
+ cwe_id="CWE-285",
75
+ confidence=0.7,
76
+ )
77
+ ]
@@ -0,0 +1,77 @@
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
+ """API1:2023 — Broken Object Level Authorization (BOLA)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ import httpx
13
+
14
+ from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
15
+ from mannf.product.security.models import SecurityFinding
16
+
17
+ _UUID_RE = re.compile(
18
+ r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE
19
+ )
20
+ _NUM_RE = re.compile(r"/(\d+)(?=/|$)")
21
+ _ALT_UUID = "00000000-0000-0000-0000-000000000099"
22
+
23
+
24
+ class BOLACheck(SecurityCheck):
25
+ owasp_id = "API1:2023"
26
+ name = "Broken Object Level Authorization"
27
+ description = "Tests whether object-level access controls are enforced per request."
28
+ severity_default = "high"
29
+
30
+ async def run(
31
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
32
+ ) -> list[SecurityFinding]:
33
+ findings: list[SecurityFinding] = []
34
+ path = endpoint.path
35
+
36
+ has_uuid = bool(_UUID_RE.search(path))
37
+ has_num = bool(_NUM_RE.search(path))
38
+ if not has_uuid and not has_num:
39
+ return findings
40
+
41
+ probes: list[str] = []
42
+ if has_uuid:
43
+ probes.append(_UUID_RE.sub(_ALT_UUID, path))
44
+
45
+ if has_num:
46
+ for replacement in ("2", "99999", "0", "-1"):
47
+ probes.append(_NUM_RE.sub(f"/{replacement}", path, count=1))
48
+
49
+ for probe_path in probes:
50
+ if probe_path == path:
51
+ continue
52
+ url = f"{endpoint.base_url.rstrip('/')}{probe_path}"
53
+ try:
54
+ resp = await client.request(endpoint.method.upper(), url)
55
+ except httpx.RequestError:
56
+ continue
57
+ if resp.status_code == 200:
58
+ findings.append(
59
+ self._make_finding(
60
+ endpoint,
61
+ title="Possible BOLA: Resource accessible with alternate ID",
62
+ description=(
63
+ "A request to a modified resource ID returned HTTP 200. "
64
+ "This may indicate missing object-level authorization checks."
65
+ ),
66
+ evidence=f"GET {url} → {resp.status_code}",
67
+ remediation=(
68
+ "Validate that the authenticated user owns or has permission "
69
+ "to access the requested resource ID on every request."
70
+ ),
71
+ cwe_id="CWE-639",
72
+ confidence=0.5,
73
+ )
74
+ )
75
+ break # one finding per endpoint is enough
76
+
77
+ return findings