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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- 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)
|