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,70 @@
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
+ """API4:2023 — Unrestricted Resource Consumption (rate limiting)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
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
+ _PROBE_COUNT = 10
18
+
19
+
20
+ class ResourceConsumptionCheck(SecurityCheck):
21
+ owasp_id = "API4:2023"
22
+ name = "Unrestricted Resource Consumption"
23
+ description = "Tests whether the endpoint enforces rate limiting."
24
+ severity_default = "medium"
25
+
26
+ async def run(
27
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
28
+ ) -> list[SecurityFinding]:
29
+ url = endpoint.full_url
30
+ method = endpoint.method.upper()
31
+
32
+ async def _probe() -> int:
33
+ try:
34
+ resp = await client.request(method, url)
35
+ return resp.status_code
36
+ except httpx.RequestError:
37
+ return -1
38
+
39
+ results = await asyncio.gather(*[_probe() for _ in range(_PROBE_COUNT)])
40
+
41
+ rate_limited = any(s == 429 for s in results)
42
+ if rate_limited:
43
+ return []
44
+
45
+ successful = sum(1 for s in results if 200 <= s < 300)
46
+ if successful < _PROBE_COUNT:
47
+ return []
48
+
49
+ return [
50
+ self._make_finding(
51
+ endpoint,
52
+ title="No rate limiting detected",
53
+ description=(
54
+ f"All {_PROBE_COUNT} rapid consecutive requests to this endpoint "
55
+ "succeeded without receiving a 429 Too Many Requests response. "
56
+ "This may indicate missing rate limiting controls."
57
+ ),
58
+ evidence=(
59
+ f"{_PROBE_COUNT}/{_PROBE_COUNT} requests returned success "
60
+ f"(status codes: {list(results)})"
61
+ ),
62
+ remediation=(
63
+ "Implement rate limiting (e.g., token bucket, sliding window) "
64
+ "at the API gateway or application layer. Return HTTP 429 with "
65
+ "Retry-After header when limits are exceeded."
66
+ ),
67
+ cwe_id="CWE-770",
68
+ confidence=0.5,
69
+ )
70
+ ]
@@ -0,0 +1,80 @@
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
+ """API6:2023 — Unrestricted Access to Sensitive Business Flows."""
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
+ _SENSITIVE_KEYWORDS = (
16
+ "password-reset", "forgot-password", "forgot_password", "reset-password",
17
+ "reset_password", "payment", "checkout", "transfer", "withdraw",
18
+ "register", "signup", "sign-up", "login", "sign-in",
19
+ )
20
+
21
+ _RATE_LIMIT_INDICATORS = ("x-ratelimit", "x-rate-limit", "retry-after")
22
+ _CAPTCHA_INDICATORS = ("captcha", "recaptcha", "hcaptcha", "cf-turnstile")
23
+
24
+
25
+ class SensitiveFlowsCheck(SecurityCheck):
26
+ owasp_id = "API6:2023"
27
+ name = "Unrestricted Access to Sensitive Business Flows"
28
+ description = "Detects sensitive business flows that may lack adequate protections."
29
+ severity_default = "low"
30
+
31
+ async def run(
32
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
33
+ ) -> list[SecurityFinding]:
34
+ lower_path = endpoint.path.lower()
35
+ matched = [kw for kw in _SENSITIVE_KEYWORDS if kw in lower_path]
36
+ if not matched:
37
+ return []
38
+
39
+ url = endpoint.full_url
40
+ method = endpoint.method.upper()
41
+
42
+ try:
43
+ resp = await client.request(method, url)
44
+ except httpx.RequestError:
45
+ return []
46
+
47
+ response_headers_lower = {k.lower(): v for k, v in resp.headers.items()}
48
+ body_lower = resp.text.lower()
49
+
50
+ has_rate_limit = any(h in response_headers_lower for h in _RATE_LIMIT_INDICATORS)
51
+ has_captcha = any(ind in body_lower for ind in _CAPTCHA_INDICATORS)
52
+
53
+ if has_rate_limit or has_captcha:
54
+ return []
55
+
56
+ protection_hints = []
57
+ if not has_rate_limit:
58
+ protection_hints.append("no rate-limit headers detected")
59
+ if not has_captcha:
60
+ protection_hints.append("no CAPTCHA indicators detected")
61
+
62
+ return [
63
+ self._make_finding(
64
+ endpoint,
65
+ title=f"Sensitive business flow may lack bot/abuse protections",
66
+ description=(
67
+ f"The endpoint path matches sensitive keyword(s): {matched}. "
68
+ "No rate-limiting headers or CAPTCHA indicators were found in "
69
+ "the response, suggesting it may be susceptible to automated abuse."
70
+ ),
71
+ evidence=f"{method} {url} → {resp.status_code}; {'; '.join(protection_hints)}",
72
+ remediation=(
73
+ "Add rate limiting, CAPTCHA challenges, and/or account lockout "
74
+ "mechanisms to protect sensitive flows from automated abuse."
75
+ ),
76
+ cwe_id="CWE-799",
77
+ severity="low",
78
+ confidence=0.5,
79
+ )
80
+ ]
@@ -0,0 +1,101 @@
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
+ """API7:2023 — Server Side Request Forgery (SSRF)."""
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
+ _URL_PARAM_NAMES = {
16
+ "url", "uri", "href", "link", "src", "source", "redirect",
17
+ "callback", "webhook", "endpoint", "target", "host", "domain",
18
+ }
19
+
20
+ _SSRF_PAYLOADS = [
21
+ "http://localhost:80",
22
+ "http://127.0.0.1",
23
+ "http://169.254.169.254/latest/meta-data/",
24
+ "http://[::1]",
25
+ "http://0.0.0.0",
26
+ ]
27
+
28
+
29
+ def _url_params(endpoint: EndpointInfo) -> list[dict]:
30
+ return [
31
+ p for p in endpoint.parameters
32
+ if p.get("name", "").lower() in _URL_PARAM_NAMES
33
+ ]
34
+
35
+
36
+ class SSRFCheck(SecurityCheck):
37
+ owasp_id = "API7:2023"
38
+ name = "Server Side Request Forgery"
39
+ description = "Tests for SSRF by injecting internal URLs into URL-like parameters."
40
+ severity_default = "high"
41
+
42
+ async def run(
43
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
44
+ ) -> list[SecurityFinding]:
45
+ ssrf_params = _url_params(endpoint)
46
+ if not ssrf_params:
47
+ return []
48
+
49
+ method = endpoint.method.upper()
50
+ findings: list[SecurityFinding] = []
51
+
52
+ for param in ssrf_params:
53
+ param_name = param["name"]
54
+ param_in = param.get("in", "query")
55
+
56
+ for payload in _SSRF_PAYLOADS:
57
+ try:
58
+ if param_in == "query":
59
+ resp = await client.request(
60
+ method, endpoint.full_url, params={param_name: payload}
61
+ )
62
+ elif param_in == "path":
63
+ # replace placeholder in path
64
+ path = endpoint.path.replace(
65
+ "{" + param_name + "}", payload
66
+ )
67
+ url = f"{endpoint.base_url.rstrip('/')}{path}"
68
+ resp = await client.request(method, url)
69
+ else:
70
+ continue
71
+ except httpx.RequestError:
72
+ continue
73
+
74
+ if resp.status_code == 200 and resp.text.strip():
75
+ findings.append(
76
+ self._make_finding(
77
+ endpoint,
78
+ title="Possible SSRF: internal URL payload returned data",
79
+ description=(
80
+ f"Injecting '{payload}' into parameter '{param_name}' "
81
+ f"returned HTTP 200 with a non-empty body, which may "
82
+ "indicate the server is fetching the provided URL."
83
+ ),
84
+ evidence=(
85
+ f"param={param_name}, payload={payload}, "
86
+ f"status={resp.status_code}, "
87
+ f"body_snippet={resp.text[:200]}"
88
+ ),
89
+ remediation=(
90
+ "Validate and sanitize all URL parameters against an "
91
+ "allowlist of permitted domains/schemes. Use a dedicated "
92
+ "HTTP client with no access to internal networks for "
93
+ "outbound requests."
94
+ ),
95
+ cwe_id="CWE-918",
96
+ confidence=0.7,
97
+ )
98
+ )
99
+ break # one finding per param is enough
100
+
101
+ return findings
@@ -0,0 +1,120 @@
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
+ """API10:2023 — Unsafe Consumption of APIs (injection)."""
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
+ _INJECTION_PAYLOADS: list[tuple[str, str]] = [
16
+ ("sql", "' OR '1'='1"),
17
+ ("sql_drop", "'; DROP TABLE users; --"),
18
+ ("nosql", '{"$gt": ""}'),
19
+ ("xss", "<script>alert(1)</script>"),
20
+ ("cmd", "; ls -la"),
21
+ ]
22
+
23
+ _STACK_TRACE_MARKERS = [
24
+ "Traceback", "at line", "Exception in thread",
25
+ "NullPointerException", "stack trace", "ORA-", "SQL syntax",
26
+ ]
27
+
28
+
29
+ class UnsafeConsumptionCheck(SecurityCheck):
30
+ owasp_id = "API10:2023"
31
+ name = "Unsafe Consumption of APIs"
32
+ description = "Tests for injection vulnerabilities in string parameters."
33
+ severity_default = "high"
34
+
35
+ async def run(
36
+ self, endpoint: EndpointInfo, client: httpx.AsyncClient
37
+ ) -> list[SecurityFinding]:
38
+ string_params = [
39
+ p for p in endpoint.parameters
40
+ if p.get("schema", {}).get("type", "string") == "string"
41
+ or "type" not in p.get("schema", {})
42
+ ]
43
+
44
+ if not string_params:
45
+ return []
46
+
47
+ method = endpoint.method.upper()
48
+ findings: list[SecurityFinding] = []
49
+ seen_endpoints: set[str] = set()
50
+
51
+ for param in string_params:
52
+ param_name = param.get("name", "")
53
+ param_in = param.get("in", "query")
54
+ if not param_name:
55
+ continue
56
+
57
+ for payload_type, payload in _INJECTION_PAYLOADS:
58
+ try:
59
+ if param_in == "query":
60
+ resp = await client.request(
61
+ method,
62
+ endpoint.full_url,
63
+ params={param_name: payload},
64
+ )
65
+ elif param_in == "path":
66
+ path = endpoint.path.replace(
67
+ "{" + param_name + "}", payload
68
+ )
69
+ url = f"{endpoint.base_url.rstrip('/')}{path}"
70
+ resp = await client.request(method, url)
71
+ else:
72
+ continue
73
+ except httpx.RequestError:
74
+ continue
75
+
76
+ body = resp.text
77
+ reflected = payload in body
78
+ has_trace = any(m in body for m in _STACK_TRACE_MARKERS)
79
+ is_500 = resp.status_code == 500
80
+
81
+ if not (reflected or has_trace or is_500):
82
+ continue
83
+
84
+ key = f"{param_name}:{payload_type}"
85
+ if key in seen_endpoints:
86
+ continue
87
+ seen_endpoints.add(key)
88
+
89
+ reasons = []
90
+ if reflected:
91
+ reasons.append("payload reflected in response")
92
+ if has_trace:
93
+ reasons.append("stack trace detected")
94
+ if is_500:
95
+ reasons.append("HTTP 500 returned")
96
+
97
+ findings.append(
98
+ self._make_finding(
99
+ endpoint,
100
+ title=f"Possible {payload_type.upper()} injection in parameter '{param_name}'",
101
+ description=(
102
+ f"Injecting a {payload_type} payload into parameter "
103
+ f"'{param_name}' triggered: {', '.join(reasons)}."
104
+ ),
105
+ evidence=(
106
+ f"param={param_name}, payload={payload!r}, "
107
+ f"status={resp.status_code}, reasons={reasons}, "
108
+ f"snippet={body[:200]}"
109
+ ),
110
+ remediation=(
111
+ "Use parameterized queries and prepared statements for database "
112
+ "access. Validate and sanitize all input. Encode output before "
113
+ "rendering. Never pass user input to shell commands."
114
+ ),
115
+ cwe_id="CWE-74",
116
+ confidence=0.7 if (reflected or has_trace) else 0.5,
117
+ )
118
+ )
119
+
120
+ return findings
@@ -0,0 +1,92 @@
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 finding and report dataclasses."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ _severity_order: dict[str, int] = {
13
+ "critical": 0,
14
+ "high": 1,
15
+ "medium": 2,
16
+ "low": 3,
17
+ "info": 4,
18
+ }
19
+
20
+
21
+ @dataclass
22
+ class SecurityFinding:
23
+ check_id: str
24
+ check_name: str
25
+ severity: str
26
+ endpoint: str
27
+ title: str
28
+ description: str
29
+ evidence: str
30
+ remediation: str
31
+ cwe_id: str
32
+ confidence: float
33
+
34
+
35
+ @dataclass
36
+ class EndpointSecurityPriority:
37
+ """Per-endpoint prioritization metadata produced by the belief-guided scorer."""
38
+
39
+ endpoint: str
40
+ priority_score: float
41
+ belief_score: float
42
+ risk_score: float
43
+ check_ids: list[str]
44
+ checks_run: list[str]
45
+
46
+ def to_dict(self) -> dict:
47
+ return {
48
+ "endpoint": self.endpoint,
49
+ "priority_score": self.priority_score,
50
+ "belief_score": self.belief_score,
51
+ "risk_score": self.risk_score,
52
+ "check_ids": self.check_ids,
53
+ "checks_run": self.checks_run,
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class SecurityReport:
59
+ scan_id: str
60
+ timestamp: str
61
+ target_url: str
62
+ total_endpoints_scanned: int
63
+ findings: list[SecurityFinding] = field(default_factory=list)
64
+ summary: dict = field(default_factory=dict)
65
+ belief_guided: bool = False
66
+ prioritization: list[EndpointSecurityPriority] = field(default_factory=list)
67
+
68
+ def to_dict(self) -> dict:
69
+ return {
70
+ "scan_id": self.scan_id,
71
+ "timestamp": self.timestamp,
72
+ "target_url": self.target_url,
73
+ "total_endpoints_scanned": self.total_endpoints_scanned,
74
+ "findings": [
75
+ {
76
+ "check_id": f.check_id,
77
+ "check_name": f.check_name,
78
+ "severity": f.severity,
79
+ "endpoint": f.endpoint,
80
+ "title": f.title,
81
+ "description": f.description,
82
+ "evidence": f.evidence,
83
+ "remediation": f.remediation,
84
+ "cwe_id": f.cwe_id,
85
+ "confidence": f.confidence,
86
+ }
87
+ for f in self.findings
88
+ ],
89
+ "summary": self.summary,
90
+ "belief_guided": self.belief_guided,
91
+ "prioritization": [p.to_dict() for p in self.prioritization],
92
+ }
@@ -0,0 +1,182 @@
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
+ """Plugin loader for custom SecurityCheck implementations.
7
+
8
+ Provides two discovery mechanisms:
9
+ - Directory scanning: load .py files from a directory path.
10
+ - Entry-point discovery: use importlib.metadata to find registered plugins.
11
+
12
+ Both mechanisms validate that each discovered class has the required attributes
13
+ and the ``run`` method before instantiating it. Invalid plugins are skipped
14
+ with a warning — they will never cause the scanner to crash.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import importlib
20
+ import importlib.util
21
+ import inspect
22
+ import logging
23
+ import sys
24
+ from importlib.metadata import entry_points
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING
27
+
28
+ if TYPE_CHECKING:
29
+ pass
30
+
31
+ from mannf.product.security.checks.base import SecurityCheck
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ _REQUIRED_ATTRS = ("owasp_id", "name", "description", "severity_default")
36
+
37
+
38
+ def _is_valid_plugin(cls: type) -> bool:
39
+ """Return True if *cls* is a valid, concrete SecurityCheck subclass."""
40
+ if not (isinstance(cls, type) and issubclass(cls, SecurityCheck) and cls is not SecurityCheck):
41
+ return False
42
+ for attr in _REQUIRED_ATTRS:
43
+ if not hasattr(cls, attr):
44
+ logger.warning("Plugin %s is missing required attribute '%s' — skipping.", cls.__name__, attr)
45
+ return False
46
+ if not callable(getattr(cls, "run", None)):
47
+ logger.warning("Plugin %s has no callable 'run' method — skipping.", cls.__name__)
48
+ return False
49
+ return True
50
+
51
+
52
+ def _instantiate(cls: type) -> SecurityCheck | None:
53
+ """Try to instantiate *cls*; log a warning and return None on failure."""
54
+ try:
55
+ return cls()
56
+ except Exception as exc: # noqa: BLE001
57
+ logger.warning("Plugin %s raised %s during __init__ — skipping.", cls.__name__, exc)
58
+ return None
59
+
60
+
61
+ def load_plugins_from_directory(path: str) -> list[SecurityCheck]:
62
+ """Discover and load SecurityCheck subclasses from all ``.py`` files in *path*.
63
+
64
+ Parameters
65
+ ----------
66
+ path:
67
+ Filesystem path to a directory containing plugin ``.py`` files.
68
+
69
+ Returns
70
+ -------
71
+ list[SecurityCheck]
72
+ Instantiated check objects for every valid plugin found.
73
+ """
74
+ plugins: list[SecurityCheck] = []
75
+ directory = Path(path)
76
+
77
+ if not directory.exists():
78
+ logger.warning("Plugin directory '%s' does not exist — no plugins loaded.", path)
79
+ return plugins
80
+
81
+ if not directory.is_dir():
82
+ logger.warning("Plugin path '%s' is not a directory — no plugins loaded.", path)
83
+ return plugins
84
+
85
+ for py_file in sorted(directory.glob("*.py")):
86
+ module_name = f"_nat_plugin_{py_file.stem}"
87
+ try:
88
+ spec = importlib.util.spec_from_file_location(module_name, py_file)
89
+ if spec is None or spec.loader is None:
90
+ logger.warning("Could not create module spec for '%s' — skipping.", py_file)
91
+ continue
92
+ module = importlib.util.module_from_spec(spec)
93
+ sys.modules[module_name] = module
94
+ spec.loader.exec_module(module) # type: ignore[union-attr]
95
+ except Exception as exc: # noqa: BLE001
96
+ logger.warning("Failed to import plugin file '%s': %s — skipping.", py_file, exc)
97
+ continue
98
+
99
+ for _name, obj in inspect.getmembers(module, inspect.isclass):
100
+ if obj.__module__ != module_name:
101
+ # Avoid re-processing imported base classes
102
+ continue
103
+ if _is_valid_plugin(obj):
104
+ instance = _instantiate(obj)
105
+ if instance is not None:
106
+ plugins.append(instance)
107
+
108
+ return plugins
109
+
110
+
111
+ def load_plugins_from_entry_points(group: str = "nat.security_checks") -> list[SecurityCheck]:
112
+ """Discover and load SecurityCheck subclasses registered as package entry points.
113
+
114
+ Parameters
115
+ ----------
116
+ group:
117
+ The entry-point group name to scan (default ``"nat.security_checks"``).
118
+
119
+ Returns
120
+ -------
121
+ list[SecurityCheck]
122
+ Instantiated check objects for every valid entry point found.
123
+ """
124
+ plugins: list[SecurityCheck] = []
125
+
126
+ try:
127
+ eps = entry_points(group=group)
128
+ except Exception as exc: # noqa: BLE001
129
+ logger.warning("Failed to query entry points for group '%s': %s", group, exc)
130
+ return plugins
131
+
132
+ for ep in eps:
133
+ try:
134
+ obj = ep.load()
135
+ except Exception as exc: # noqa: BLE001
136
+ logger.warning("Failed to load entry point '%s': %s — skipping.", ep.name, exc)
137
+ continue
138
+
139
+ if _is_valid_plugin(obj):
140
+ instance = _instantiate(obj)
141
+ if instance is not None:
142
+ plugins.append(instance)
143
+ else:
144
+ logger.warning(
145
+ "Entry point '%s' does not point to a valid SecurityCheck subclass — skipping.",
146
+ ep.name,
147
+ )
148
+
149
+ return plugins
150
+
151
+
152
+ def load_all_plugins(
153
+ plugins_dir: str | None = None,
154
+ entry_point_group: str = "nat.security_checks",
155
+ ) -> list[SecurityCheck]:
156
+ """Load plugins from both a directory and registered entry points, deduplicating by class name.
157
+
158
+ Parameters
159
+ ----------
160
+ plugins_dir:
161
+ Optional filesystem path to a directory of plugin ``.py`` files.
162
+ entry_point_group:
163
+ Entry-point group to scan (default ``"nat.security_checks"``).
164
+
165
+ Returns
166
+ -------
167
+ list[SecurityCheck]
168
+ Merged, deduplicated list of instantiated check objects.
169
+ """
170
+ seen_class_names: set[str] = set()
171
+ combined: list[SecurityCheck] = []
172
+
173
+ dir_plugins = load_plugins_from_directory(plugins_dir) if plugins_dir else []
174
+ ep_plugins = load_plugins_from_entry_points(entry_point_group)
175
+
176
+ for plugin in dir_plugins + ep_plugins:
177
+ class_name = type(plugin).__name__
178
+ if class_name not in seen_class_names:
179
+ seen_class_names.add(class_name)
180
+ combined.append(plugin)
181
+
182
+ return combined