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,339 @@
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
+ """IngestScanOrchestrator — closed-loop Ingest → Prioritize → Scan → Export → Learn pipeline.
7
+
8
+ This module extends :class:`~mannf.product.orchestrator.IngestOrchestrator` with:
9
+
10
+ 1. **Direct IngestResult intake** — :meth:`IngestScanOrchestrator.orchestrate_from_result`
11
+ accepts a pre-built :class:`~mannf.product.ingestors.models.IngestResult` produced by
12
+ any ingestor plugin, bypassing the file/URL ingest phase entirely. This is the primary
13
+ entry point when the caller already holds the ingestor output.
14
+
15
+ 2. **Exporter integration** — after scanning, the orchestrator dispatches findings to all
16
+ configured :class:`~mannf.product.exporters.base.ExporterPlugin` instances and records
17
+ the export summaries in :class:`OrchestrationResult`.
18
+
19
+ Usage::
20
+
21
+ from mannf.product.orchestration import IngestScanOrchestrator
22
+ from mannf.product.orchestrator_models import OrchestrationConfig
23
+
24
+ # Run from a file source (same as IngestOrchestrator)
25
+ config = OrchestrationConfig(
26
+ source_path="openapi.yaml",
27
+ scan_config={"base_url": "http://localhost:8080"},
28
+ exporter_config={"exporter_name": "github-issues", "repo": "org/repo"},
29
+ )
30
+ orch = IngestScanOrchestrator(config)
31
+ result = await orch.orchestrate()
32
+
33
+ # --- OR ---
34
+
35
+ # Run from a pre-built IngestResult (e.g., from a custom ingestor)
36
+ from mannf.product.ingestors.openapi_ingestor import OpenAPIIngestor
37
+ from mannf.product.ingestors.models import IngestorSource
38
+
39
+ ingestor = OpenAPIIngestor()
40
+ source = IngestorSource(format="openapi", path="api.yaml")
41
+ ingest_result = await ingestor.ingest(source, {})
42
+
43
+ result = await orch.orchestrate_from_result(ingest_result)
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import logging
49
+ from typing import Any
50
+
51
+ from mannf.product.exporters import BUILTIN_EXPORTERS, get_exporter_by_name
52
+ from mannf.product.exporters.base import ExportSummary, ExporterPlugin
53
+ from mannf.product.ingestors.models import IngestResult
54
+ from mannf.product.orchestrator import IngestOrchestrator
55
+ from mannf.product.orchestrator_models import (
56
+ OrchestrationConfig,
57
+ OrchestrationResult,
58
+ ScanPlan,
59
+ )
60
+ from mannf.product.security.models import SecurityReport
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+
65
+ class IngestScanOrchestrator(IngestOrchestrator):
66
+ """Extended orchestrator that adds direct IngestResult intake and exporter dispatch.
67
+
68
+ Inherits the full Ingest → Prioritize → Scan → Learn pipeline from
69
+ :class:`~mannf.product.orchestrator.IngestOrchestrator` and adds:
70
+
71
+ * :meth:`orchestrate_from_result` — bypass the file/URL ingest phase when
72
+ the caller already has an :class:`~mannf.product.ingestors.models.IngestResult`.
73
+ * Exporter dispatch — automatically calls configured exporter plugins on the
74
+ scan report after scanning completes.
75
+
76
+ Parameters
77
+ ----------
78
+ config:
79
+ Orchestration configuration. The ``exporter_config`` dict controls
80
+ exporter dispatch:
81
+ - ``"exporter_name"`` — name of the built-in exporter to use
82
+ (e.g. ``"github-issues"``, ``"jira"``).
83
+ - ``"min_severity"`` — minimum finding severity to export
84
+ (default ``"low"``).
85
+ - All remaining keys are passed verbatim to the exporter.
86
+ extra_exporters:
87
+ Optional list of additional :class:`~mannf.product.exporters.base.ExporterPlugin`
88
+ instances to invoke alongside the built-in ones. Useful for tests or
89
+ custom plug-ins that are not registered in ``BUILTIN_EXPORTERS``.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ config: OrchestrationConfig,
95
+ extra_exporters: list[ExporterPlugin] | None = None,
96
+ ) -> None:
97
+ super().__init__(config)
98
+ self._extra_exporters: list[ExporterPlugin] = extra_exporters or []
99
+
100
+ # ------------------------------------------------------------------
101
+ # Public API — direct IngestResult entry point
102
+ # ------------------------------------------------------------------
103
+
104
+ async def orchestrate_from_result(
105
+ self,
106
+ ingest_result: IngestResult,
107
+ ) -> OrchestrationResult:
108
+ """Run the full prioritize → scan → export → learn pipeline from an existing result.
109
+
110
+ This method is the primary entry point when the caller has already
111
+ obtained an :class:`~mannf.product.ingestors.models.IngestResult` from
112
+ any ingestor plugin. It skips the file/URL ingest phase entirely.
113
+
114
+ Parameters
115
+ ----------
116
+ ingest_result:
117
+ A completed ingest result produced by any
118
+ :class:`~mannf.product.ingestors.base.IngestorPlugin` implementation.
119
+
120
+ Returns
121
+ -------
122
+ OrchestrationResult
123
+ Complete pipeline result including scan findings, export summaries,
124
+ and feedback for the next run.
125
+ """
126
+ result = OrchestrationResult(config=self.config)
127
+
128
+ if not ingest_result.success:
129
+ for err in ingest_result.errors:
130
+ result.errors.append(f"Ingest error: {err}")
131
+ return result
132
+
133
+ result.ingest_stats = ingest_result.stats
134
+ result.warnings.extend(ingest_result.warnings)
135
+
136
+ # Phase 2 & 3: Transform + Prioritize
137
+ scan_plan = self._build_scan_plan(ingest_result, result)
138
+
139
+ # Phase 3b: LLM edge-case augmentation (optional)
140
+ scan_plan = await self._augment_scan_plan_with_llm(scan_plan, result)
141
+
142
+ result.scan_plan = scan_plan
143
+
144
+ if not scan_plan.targets:
145
+ result.warnings.append(
146
+ "No scan targets produced after transform phase — skipping scan."
147
+ )
148
+ return result
149
+
150
+ base_url = self.config.scan_config.get("base_url", "")
151
+ if not base_url:
152
+ result.errors.append(
153
+ "scan_config['base_url'] is required to run a scan. "
154
+ "Build the scan plan with plan() / plan_from_result() for a dry run."
155
+ )
156
+ return result
157
+
158
+ # Phase 4: Scan
159
+ scan_report = await self._scan(scan_plan, base_url, result)
160
+ result.scan_results = scan_report
161
+
162
+ # Phase 4b: Export
163
+ if scan_report is not None:
164
+ export_summaries = await self._export(scan_report, result)
165
+ result.export_summaries = export_summaries
166
+
167
+ # Phase 5: Learn
168
+ if scan_report is not None:
169
+ feedback = self._compute_feedback(scan_plan, scan_report)
170
+ result.feedback = feedback
171
+ await self._persist_feedback(feedback, result)
172
+
173
+ return result
174
+
175
+ async def plan_from_result(
176
+ self,
177
+ ingest_result: IngestResult,
178
+ ) -> OrchestrationResult:
179
+ """Dry-run mode: prioritize but do not scan.
180
+
181
+ Useful for inspecting the prioritized scan plan before committing to
182
+ a full scan.
183
+
184
+ Parameters
185
+ ----------
186
+ ingest_result:
187
+ A completed ingest result.
188
+
189
+ Returns
190
+ -------
191
+ OrchestrationResult
192
+ Pipeline result with ``scan_plan`` populated but ``scan_results``
193
+ and ``feedback`` left as ``None``.
194
+ """
195
+ result = OrchestrationResult(config=self.config)
196
+
197
+ if not ingest_result.success:
198
+ for err in ingest_result.errors:
199
+ result.errors.append(f"Ingest error: {err}")
200
+ return result
201
+
202
+ result.ingest_stats = ingest_result.stats
203
+ result.warnings.extend(ingest_result.warnings)
204
+
205
+ scan_plan = self._build_scan_plan(ingest_result, result)
206
+ # Phase 3b: LLM edge-case augmentation (optional)
207
+ scan_plan = await self._augment_scan_plan_with_llm(scan_plan, result)
208
+ result.scan_plan = scan_plan
209
+ return result
210
+
211
+ # Override orchestrate() to add exporter dispatch
212
+ async def orchestrate(self) -> OrchestrationResult:
213
+ """Run the full ingest → prioritize → scan → export → learn pipeline.
214
+
215
+ Extends :meth:`~mannf.product.orchestrator.IngestOrchestrator.orchestrate`
216
+ by also dispatching scan results to configured exporter plugins.
217
+
218
+ Returns
219
+ -------
220
+ OrchestrationResult
221
+ Complete pipeline result including scan findings, export summaries,
222
+ and feedback.
223
+ """
224
+ result = await super().orchestrate()
225
+
226
+ # Export findings if a scan was performed
227
+ if result.scan_results is not None and not result.errors:
228
+ export_summaries = await self._export(result.scan_results, result)
229
+ result.export_summaries = export_summaries
230
+
231
+ return result
232
+
233
+ # ------------------------------------------------------------------
234
+ # Phase 4b: Export
235
+ # ------------------------------------------------------------------
236
+
237
+ async def _export(
238
+ self,
239
+ scan_report: SecurityReport,
240
+ result: OrchestrationResult,
241
+ ) -> list[ExportSummary]:
242
+ """Dispatch findings to configured exporter plugins.
243
+
244
+ Parameters
245
+ ----------
246
+ scan_report:
247
+ The completed security scan report.
248
+ result:
249
+ Mutable :class:`OrchestrationResult` for recording warnings.
250
+
251
+ Returns
252
+ -------
253
+ list[ExportSummary]
254
+ One :class:`~mannf.product.exporters.base.ExportSummary` per
255
+ exporter that was invoked. Empty if no exporter is configured.
256
+ """
257
+ exporter_cfg = self.config.exporter_config
258
+ if not exporter_cfg:
259
+ return []
260
+
261
+ exporter_name = exporter_cfg.get("exporter_name")
262
+ if not exporter_name:
263
+ return []
264
+
265
+ # Resolve the exporter: check extra_exporters first, then built-ins
266
+ all_exporters = self._extra_exporters + BUILTIN_EXPORTERS
267
+ exporter = get_exporter_by_name(exporter_name, all_exporters)
268
+ if exporter is None:
269
+ result.warnings.append(
270
+ f"Exporter '{exporter_name}' not found. "
271
+ f"Available: {[e.name for e in all_exporters]}"
272
+ )
273
+ return []
274
+
275
+ # Validate exporter config
276
+ # Build the config to pass — everything except exporter_name
277
+ export_call_config = {k: v for k, v in exporter_cfg.items() if k != "exporter_name"}
278
+
279
+ config_errors = exporter.validate_config(export_call_config)
280
+ if config_errors:
281
+ for err in config_errors:
282
+ result.warnings.append(f"Exporter '{exporter_name}' config warning: {err}")
283
+
284
+ min_severity = exporter_cfg.get("min_severity", "low")
285
+
286
+ logger.info(
287
+ "Exporting %d finding(s) via '%s' (min_severity=%s).",
288
+ len(scan_report.findings),
289
+ exporter_name,
290
+ min_severity,
291
+ )
292
+
293
+ try:
294
+ summary = await exporter.export_findings(
295
+ findings=scan_report.findings,
296
+ report=scan_report,
297
+ config=export_call_config,
298
+ min_severity=min_severity,
299
+ )
300
+ except Exception as exc: # noqa: BLE001
301
+ result.warnings.append(
302
+ f"Exporter '{exporter_name}' raised an exception: {exc}"
303
+ )
304
+ logger.exception("Exporter '%s' raised an exception", exporter_name)
305
+ return []
306
+
307
+ logger.info(
308
+ "Export complete via '%s': %d succeeded, %d failed, %d skipped.",
309
+ exporter_name,
310
+ summary.succeeded,
311
+ summary.failed,
312
+ summary.skipped,
313
+ )
314
+ return [summary]
315
+
316
+ # ------------------------------------------------------------------
317
+ # Factory
318
+ # ------------------------------------------------------------------
319
+
320
+ @classmethod
321
+ def from_cli_args(cls, args: Any) -> "IngestScanOrchestrator": # type: ignore[override]
322
+ """Construct an :class:`IngestScanOrchestrator` from parsed CLI arguments.
323
+
324
+ Delegates to :meth:`~mannf.product.orchestrator.IngestOrchestrator.from_cli_args`
325
+ and returns an :class:`IngestScanOrchestrator` instance.
326
+
327
+ Parameters
328
+ ----------
329
+ args:
330
+ Namespace object as returned by ``argparse.ArgumentParser.parse_args()``.
331
+ See :meth:`~mannf.product.orchestrator.IngestOrchestrator.from_cli_args`
332
+ for the full list of expected attributes.
333
+
334
+ Returns
335
+ -------
336
+ IngestScanOrchestrator
337
+ """
338
+ base = IngestOrchestrator.from_cli_args(args)
339
+ return cls(base.config)