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,411 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
|
|
5
|
+
"""Product notification dispatcher for scan lifecycle events.
|
|
6
|
+
|
|
7
|
+
Delivers formatted notifications to Slack (Block Kit) and Microsoft Teams
|
|
8
|
+
(Adaptive Cards) via incoming webhooks. Both channels use a plain HTTP POST
|
|
9
|
+
with a JSON payload — no SDK required.
|
|
10
|
+
|
|
11
|
+
Webhook URLs are resolved in this order:
|
|
12
|
+
1. Per-tenant ``notification_slack_webhook_url`` / ``notification_teams_webhook_url``
|
|
13
|
+
stored in the DB (populated via ``PUT /api/v1/notifications/config``).
|
|
14
|
+
2. Global env var fallback: ``NAT_SLACK_WEBHOOK_URL`` / ``NAT_TEAMS_WEBHOOK_URL``.
|
|
15
|
+
|
|
16
|
+
Supported event types (scan lifecycle):
|
|
17
|
+
- ``scan.started``
|
|
18
|
+
- ``scan.completed``
|
|
19
|
+
- ``scan.failed``
|
|
20
|
+
- ``security_scan.completed``
|
|
21
|
+
- ``security_scan.failed``
|
|
22
|
+
- ``autonomous_run.regression``
|
|
23
|
+
- ``autonomous_run.recovery``
|
|
24
|
+
- ``autonomous_run.flake_spike``
|
|
25
|
+
- ``plan.generated``
|
|
26
|
+
- ``plan.approved``
|
|
27
|
+
- ``finding.commented``
|
|
28
|
+
- ``schedule.fired``
|
|
29
|
+
- ``e2e.regression``
|
|
30
|
+
|
|
31
|
+
All delivery errors are caught and logged — notifications must never block or
|
|
32
|
+
break the scan flow.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
import hashlib
|
|
39
|
+
import hmac
|
|
40
|
+
import json
|
|
41
|
+
import logging
|
|
42
|
+
import os
|
|
43
|
+
from typing import Any, Optional
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Event metadata helpers
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
_EVENT_EMOJI: dict[str, str] = {
|
|
52
|
+
"scan.started": "🚀",
|
|
53
|
+
"scan.completed": "✅",
|
|
54
|
+
"scan.failed": "❌",
|
|
55
|
+
"security_scan.completed": "🔒",
|
|
56
|
+
"security_scan.failed": "🚨",
|
|
57
|
+
"autonomous_run.regression": "🔴",
|
|
58
|
+
"autonomous_run.recovery": "🟢",
|
|
59
|
+
"autonomous_run.flake_spike": "⚡",
|
|
60
|
+
"plan.generated": "📋",
|
|
61
|
+
"plan.approved": "🏷️",
|
|
62
|
+
"finding.commented": "💬",
|
|
63
|
+
"schedule.fired": "🗓️",
|
|
64
|
+
"e2e.regression": "🔴",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_EVENT_TITLE: dict[str, str] = {
|
|
68
|
+
"scan.started": "Scan Started",
|
|
69
|
+
"scan.completed": "Scan Completed",
|
|
70
|
+
"scan.failed": "Scan Failed",
|
|
71
|
+
"security_scan.completed": "Security Scan Completed",
|
|
72
|
+
"security_scan.failed": "Security Scan Failed",
|
|
73
|
+
"autonomous_run.regression": "Autonomous Run — New Regression",
|
|
74
|
+
"autonomous_run.recovery": "Autonomous Run — Recovery Detected",
|
|
75
|
+
"autonomous_run.flake_spike": "Autonomous Run — Flakiness Spike",
|
|
76
|
+
"plan.generated": "Test Plan Generated",
|
|
77
|
+
"plan.approved": "Test Plan Approved",
|
|
78
|
+
"finding.commented": "Finding Commented",
|
|
79
|
+
"schedule.fired": "Schedule Fired",
|
|
80
|
+
"e2e.regression": "E2E Regression Detected",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Slack notifier
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SlackNotifier:
|
|
90
|
+
"""Deliver a formatted Slack message via an incoming webhook URL.
|
|
91
|
+
|
|
92
|
+
Uses Slack Block Kit for rich message formatting.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, webhook_url: str) -> None:
|
|
96
|
+
self.webhook_url = webhook_url
|
|
97
|
+
|
|
98
|
+
def _build_payload(self, event_type: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
99
|
+
"""Build a Slack Block Kit message payload."""
|
|
100
|
+
emoji = _EVENT_EMOJI.get(event_type, "🔔")
|
|
101
|
+
title = f"{emoji} NAT — {_EVENT_TITLE.get(event_type, event_type)}"
|
|
102
|
+
|
|
103
|
+
fields = [
|
|
104
|
+
{
|
|
105
|
+
"type": "mrkdwn",
|
|
106
|
+
"text": f"*Event:*\n`{event_type}`",
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
if payload.get("scan_id"):
|
|
110
|
+
fields.append(
|
|
111
|
+
{"type": "mrkdwn", "text": f"*Scan ID:*\n`{payload['scan_id']}`"}
|
|
112
|
+
)
|
|
113
|
+
if payload.get("tenant_id"):
|
|
114
|
+
fields.append(
|
|
115
|
+
{"type": "mrkdwn", "text": f"*Tenant:*\n`{payload['tenant_id']}`"}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
blocks: list[dict[str, Any]] = [
|
|
119
|
+
{
|
|
120
|
+
"type": "header",
|
|
121
|
+
"text": {"type": "plain_text", "text": title, "emoji": True},
|
|
122
|
+
},
|
|
123
|
+
{"type": "section", "fields": fields},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
details = payload.get("details") or {}
|
|
127
|
+
if details:
|
|
128
|
+
detail_lines = [f"*{k}:* {v}" for k, v in details.items()]
|
|
129
|
+
blocks.append(
|
|
130
|
+
{
|
|
131
|
+
"type": "section",
|
|
132
|
+
"text": {"type": "mrkdwn", "text": "\n".join(detail_lines)},
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return {"blocks": blocks}
|
|
137
|
+
|
|
138
|
+
async def send(self, event_type: str, payload: dict[str, Any]) -> None:
|
|
139
|
+
"""POST the formatted payload to the Slack webhook URL."""
|
|
140
|
+
slack_payload = self._build_payload(event_type, payload)
|
|
141
|
+
await _http_post(self.webhook_url, slack_payload)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Microsoft Teams notifier
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TeamsNotifier:
|
|
150
|
+
"""Deliver a formatted Teams message via an incoming webhook connector.
|
|
151
|
+
|
|
152
|
+
Uses Adaptive Cards for rich card formatting.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, webhook_url: str) -> None:
|
|
156
|
+
self.webhook_url = webhook_url
|
|
157
|
+
|
|
158
|
+
def _build_payload(self, event_type: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
159
|
+
"""Build a Teams Adaptive Card message payload."""
|
|
160
|
+
emoji = _EVENT_EMOJI.get(event_type, "🔔")
|
|
161
|
+
title = f"{emoji} NAT — {_EVENT_TITLE.get(event_type, event_type)}"
|
|
162
|
+
|
|
163
|
+
body: list[dict[str, Any]] = [
|
|
164
|
+
{
|
|
165
|
+
"type": "TextBlock",
|
|
166
|
+
"text": title,
|
|
167
|
+
"weight": "Bolder",
|
|
168
|
+
"size": "Medium",
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
facts: list[dict[str, str]] = [{"title": "Event", "value": event_type}]
|
|
173
|
+
if payload.get("scan_id"):
|
|
174
|
+
facts.append({"title": "Scan ID", "value": str(payload["scan_id"])})
|
|
175
|
+
if payload.get("tenant_id"):
|
|
176
|
+
facts.append({"title": "Tenant", "value": str(payload["tenant_id"])})
|
|
177
|
+
|
|
178
|
+
details = payload.get("details") or {}
|
|
179
|
+
for k, v in details.items():
|
|
180
|
+
facts.append({"title": str(k).replace("_", " ").title(), "value": str(v)})
|
|
181
|
+
|
|
182
|
+
body.append({"type": "FactSet", "facts": facts})
|
|
183
|
+
|
|
184
|
+
adaptive_card = {
|
|
185
|
+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
186
|
+
"type": "AdaptiveCard",
|
|
187
|
+
"version": "1.4",
|
|
188
|
+
"body": body,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"type": "message",
|
|
193
|
+
"attachments": [
|
|
194
|
+
{
|
|
195
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
196
|
+
"contentUrl": None,
|
|
197
|
+
"content": adaptive_card,
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async def send(self, event_type: str, payload: dict[str, Any]) -> None:
|
|
203
|
+
"""POST the formatted payload to the Teams webhook URL."""
|
|
204
|
+
teams_payload = self._build_payload(event_type, payload)
|
|
205
|
+
await _http_post(self.webhook_url, teams_payload)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Public dispatcher
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def dispatch_scan_notification(
|
|
214
|
+
event_type: str,
|
|
215
|
+
*,
|
|
216
|
+
tenant_id: Optional[str] = None,
|
|
217
|
+
scan_id: Optional[str] = None,
|
|
218
|
+
details: Optional[dict[str, Any]] = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Dispatch a scan lifecycle notification to all configured channels.
|
|
221
|
+
|
|
222
|
+
Resolves Slack and Teams webhook URLs from:
|
|
223
|
+
1. Per-tenant DB fields (``notification_slack_webhook_url`` /
|
|
224
|
+
``notification_teams_webhook_url`` on the ``tenants`` table).
|
|
225
|
+
2. Global env var fallback (``NAT_SLACK_WEBHOOK_URL`` /
|
|
226
|
+
``NAT_TEAMS_WEBHOOK_URL``).
|
|
227
|
+
|
|
228
|
+
All failures are swallowed and logged — notifications must never break
|
|
229
|
+
the scan flow.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
event_type: Scan event identifier, e.g. ``"scan.completed"``.
|
|
233
|
+
tenant_id: UUID string of the tenant that owns the scan.
|
|
234
|
+
scan_id: UUID string of the affected scan.
|
|
235
|
+
details: Additional event context (scan summary counts, error info).
|
|
236
|
+
"""
|
|
237
|
+
payload: dict[str, Any] = {
|
|
238
|
+
"event_type": event_type,
|
|
239
|
+
"tenant_id": tenant_id,
|
|
240
|
+
"scan_id": scan_id,
|
|
241
|
+
"details": details or {},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
slack_url = await _resolve_webhook_url(tenant_id, "slack")
|
|
245
|
+
teams_url = await _resolve_webhook_url(tenant_id, "teams")
|
|
246
|
+
|
|
247
|
+
tasks: list[Any] = []
|
|
248
|
+
if slack_url:
|
|
249
|
+
tasks.append(SlackNotifier(slack_url).send(event_type, payload))
|
|
250
|
+
if teams_url:
|
|
251
|
+
tasks.append(TeamsNotifier(teams_url).send(event_type, payload))
|
|
252
|
+
|
|
253
|
+
if not tasks:
|
|
254
|
+
logger.debug(
|
|
255
|
+
"No notification channels configured for scan event: %s (tenant=%s)",
|
|
256
|
+
event_type,
|
|
257
|
+
tenant_id,
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
262
|
+
for i, result in enumerate(results):
|
|
263
|
+
if isinstance(result, Exception):
|
|
264
|
+
logger.warning(
|
|
265
|
+
"Scan notification delivery failed (task %d) for event %s: %s",
|
|
266
|
+
i,
|
|
267
|
+
event_type,
|
|
268
|
+
result,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# URL resolution
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def _resolve_webhook_url(
|
|
278
|
+
tenant_id: Optional[str], channel: str
|
|
279
|
+
) -> str:
|
|
280
|
+
"""Resolve the webhook URL for a given channel.
|
|
281
|
+
|
|
282
|
+
Tries the per-tenant DB field first, then falls back to the global env var.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
tenant_id: Tenant UUID string (or None for anonymous scans).
|
|
286
|
+
channel: ``"slack"`` or ``"teams"``.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Webhook URL string, or empty string if not configured.
|
|
290
|
+
"""
|
|
291
|
+
env_var = "NAT_SLACK_WEBHOOK_URL" if channel == "slack" else "NAT_TEAMS_WEBHOOK_URL"
|
|
292
|
+
db_field = (
|
|
293
|
+
"notification_slack_webhook_url"
|
|
294
|
+
if channel == "slack"
|
|
295
|
+
else "notification_teams_webhook_url"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if tenant_id:
|
|
299
|
+
try:
|
|
300
|
+
db_url = await _get_tenant_webhook_url(tenant_id, db_field)
|
|
301
|
+
if db_url:
|
|
302
|
+
return db_url
|
|
303
|
+
except Exception as exc: # noqa: BLE001
|
|
304
|
+
logger.debug(
|
|
305
|
+
"_resolve_webhook_url: DB lookup failed for tenant %s (%s): %s",
|
|
306
|
+
tenant_id,
|
|
307
|
+
channel,
|
|
308
|
+
exc,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return os.environ.get(env_var, "")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _get_tenant_webhook_url(tenant_id: str, field: str) -> str:
|
|
315
|
+
"""Fetch a webhook URL field from the tenant record in the DB.
|
|
316
|
+
|
|
317
|
+
Returns empty string if DB is unavailable or field is null.
|
|
318
|
+
"""
|
|
319
|
+
try:
|
|
320
|
+
from mannf.product.database import _async_session_factory, _build_engine # noqa: PLC0415
|
|
321
|
+
|
|
322
|
+
if _build_engine() is None or _async_session_factory is None:
|
|
323
|
+
return ""
|
|
324
|
+
|
|
325
|
+
import uuid # noqa: PLC0415
|
|
326
|
+
|
|
327
|
+
from mannf.product.models import Tenant # noqa: PLC0415
|
|
328
|
+
|
|
329
|
+
tid = uuid.UUID(tenant_id)
|
|
330
|
+
async with _async_session_factory() as session:
|
|
331
|
+
tenant = await session.get(Tenant, tid)
|
|
332
|
+
if tenant is None:
|
|
333
|
+
return ""
|
|
334
|
+
return getattr(tenant, field, None) or ""
|
|
335
|
+
except Exception: # noqa: BLE001
|
|
336
|
+
return ""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
# HTTP delivery backend
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def _http_post(
|
|
345
|
+
url: str,
|
|
346
|
+
payload: dict[str, Any],
|
|
347
|
+
*,
|
|
348
|
+
timeout: float = 10.0,
|
|
349
|
+
signing_secret: Optional[str] = None,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""POST a JSON payload to the given URL.
|
|
352
|
+
|
|
353
|
+
Optionally adds an HMAC signature header (``X-NAT-Signature``) when
|
|
354
|
+
``NAT_NOTIFICATION_WEBHOOK_SECRET`` is set or ``signing_secret`` is
|
|
355
|
+
provided.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
url: Target webhook endpoint.
|
|
359
|
+
payload: JSON-serialisable message payload.
|
|
360
|
+
timeout: Request timeout in seconds.
|
|
361
|
+
signing_secret: Override for the signing secret (defaults to
|
|
362
|
+
``NAT_NOTIFICATION_WEBHOOK_SECRET`` env var).
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
RuntimeError: on network error or unexpected exception.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
import aiohttp # noqa: PLC0415
|
|
369
|
+
except ImportError:
|
|
370
|
+
logger.warning(
|
|
371
|
+
"_http_post: aiohttp not installed — notification delivery skipped"
|
|
372
|
+
)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
body = json.dumps(payload, default=str)
|
|
377
|
+
headers: dict[str, str] = {
|
|
378
|
+
"Content-Type": "application/json",
|
|
379
|
+
"User-Agent": "NAT-Notifications/1.0",
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
secret = signing_secret or os.environ.get("NAT_NOTIFICATION_WEBHOOK_SECRET", "")
|
|
383
|
+
if secret:
|
|
384
|
+
sig = hmac.new(
|
|
385
|
+
secret.encode(), body.encode(), hashlib.sha256
|
|
386
|
+
).hexdigest()
|
|
387
|
+
headers["X-NAT-Signature"] = f"sha256={sig}"
|
|
388
|
+
|
|
389
|
+
async with aiohttp.ClientSession() as session:
|
|
390
|
+
async with session.post(
|
|
391
|
+
url,
|
|
392
|
+
data=body,
|
|
393
|
+
headers=headers,
|
|
394
|
+
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
395
|
+
) as resp:
|
|
396
|
+
if resp.status >= 400:
|
|
397
|
+
logger.warning(
|
|
398
|
+
"_http_post: server returned %d for event posted to %s",
|
|
399
|
+
resp.status,
|
|
400
|
+
url,
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
logger.debug(
|
|
404
|
+
"_http_post: delivered notification to %s (status=%d)",
|
|
405
|
+
url,
|
|
406
|
+
resp.status,
|
|
407
|
+
)
|
|
408
|
+
except Exception as exc: # noqa: BLE001
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
f"Notification HTTP POST to {url!r} failed: {exc}"
|
|
411
|
+
) from exc
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
"""Onboarding progress tracker for new NAT tenants.
|
|
7
|
+
|
|
8
|
+
Tracks each tenant through their first-time setup journey:
|
|
9
|
+
|
|
10
|
+
ACCOUNT_CREATED → API_KEY_PROVISIONED → FIRST_SCAN_STARTED
|
|
11
|
+
→ FIRST_SCAN_COMPLETED → RESULTS_VIEWED → BILLING_CONFIGURED
|
|
12
|
+
|
|
13
|
+
Progress is stored in-memory (per-process) and keyed by ``tenant_id``.
|
|
14
|
+
When a database is available, progress is also persisted as billing audit
|
|
15
|
+
events so it survives restarts.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
from mannf.product.onboarding import OnboardingStepName, mark_step_complete, get_onboarding_status
|
|
20
|
+
|
|
21
|
+
await mark_step_complete(tenant_id, OnboardingStepName.FIRST_SCAN_STARTED)
|
|
22
|
+
status = get_onboarding_status(tenant_id)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from typing import Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Step definitions
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# Ordered list of all steps — the order here determines progress_percent.
|
|
41
|
+
_ORDERED_STEPS = [
|
|
42
|
+
"ACCOUNT_CREATED",
|
|
43
|
+
"API_KEY_PROVISIONED",
|
|
44
|
+
"FIRST_SCAN_STARTED",
|
|
45
|
+
"FIRST_SCAN_COMPLETED",
|
|
46
|
+
"RESULTS_VIEWED",
|
|
47
|
+
"BILLING_CONFIGURED",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class OnboardingStepName(str, Enum):
|
|
52
|
+
"""Canonical names for each onboarding step."""
|
|
53
|
+
|
|
54
|
+
ACCOUNT_CREATED = "ACCOUNT_CREATED"
|
|
55
|
+
API_KEY_PROVISIONED = "API_KEY_PROVISIONED"
|
|
56
|
+
FIRST_SCAN_STARTED = "FIRST_SCAN_STARTED"
|
|
57
|
+
FIRST_SCAN_COMPLETED = "FIRST_SCAN_COMPLETED"
|
|
58
|
+
RESULTS_VIEWED = "RESULTS_VIEWED"
|
|
59
|
+
BILLING_CONFIGURED = "BILLING_CONFIGURED"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Pydantic response models
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OnboardingStepDetail(BaseModel):
|
|
68
|
+
"""Completion state for a single onboarding step."""
|
|
69
|
+
|
|
70
|
+
step: str
|
|
71
|
+
completed: bool = False
|
|
72
|
+
completed_at: Optional[str] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OnboardingStatusResponse(BaseModel):
|
|
76
|
+
"""Full onboarding progress for a tenant."""
|
|
77
|
+
|
|
78
|
+
tenant_id: str
|
|
79
|
+
completed_steps: List[str] = Field(default_factory=list)
|
|
80
|
+
next_step: Optional[str] = None
|
|
81
|
+
progress_percent: float = 0.0
|
|
82
|
+
all_steps: List[OnboardingStepDetail] = Field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# In-memory progress store
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
# Maps tenant_id (str) → {step_name: completed_at ISO string}
|
|
90
|
+
_progress: Dict[str, Dict[str, str]] = {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _tenant_progress(tenant_id: str) -> Dict[str, str]:
|
|
94
|
+
"""Return (and lazily initialise) the progress dict for *tenant_id*."""
|
|
95
|
+
return _progress.setdefault(tenant_id, {})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Public API
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def mark_step_complete(tenant_id: str, step: OnboardingStepName) -> None:
|
|
104
|
+
"""Record *step* as completed for *tenant_id* (idempotent).
|
|
105
|
+
|
|
106
|
+
If the step is already marked complete, this is a no-op. Progress is
|
|
107
|
+
stored in-memory and, when a database is available, also logged as a
|
|
108
|
+
billing audit event for durability.
|
|
109
|
+
"""
|
|
110
|
+
progress = _tenant_progress(tenant_id)
|
|
111
|
+
if step.value in progress:
|
|
112
|
+
return # already done — idempotent
|
|
113
|
+
|
|
114
|
+
completed_at = datetime.now(timezone.utc).isoformat()
|
|
115
|
+
progress[step.value] = completed_at
|
|
116
|
+
logger.debug("Onboarding step %s completed for tenant %s", step.value, tenant_id)
|
|
117
|
+
|
|
118
|
+
# Fire-and-forget: persist to audit log when DB is available
|
|
119
|
+
try:
|
|
120
|
+
import asyncio # noqa: PLC0415
|
|
121
|
+
from mannf.product.billing.audit import log_billing_event # noqa: PLC0415
|
|
122
|
+
|
|
123
|
+
asyncio.create_task(
|
|
124
|
+
log_billing_event(
|
|
125
|
+
"onboarding.step_completed",
|
|
126
|
+
tenant_id=tenant_id,
|
|
127
|
+
source="system",
|
|
128
|
+
details={"step": step.value, "completed_at": completed_at},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
except Exception: # noqa: BLE001
|
|
132
|
+
pass # DB not configured — in-memory only is fine
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_step_complete(tenant_id: str, step: OnboardingStepName) -> bool:
|
|
136
|
+
"""Return ``True`` if *step* has been completed for *tenant_id*."""
|
|
137
|
+
return step.value in _tenant_progress(tenant_id)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_onboarding_status(tenant_id: str) -> OnboardingStatusResponse:
|
|
141
|
+
"""Build and return the full onboarding status for *tenant_id*."""
|
|
142
|
+
progress = _tenant_progress(tenant_id)
|
|
143
|
+
|
|
144
|
+
all_steps: List[OnboardingStepDetail] = []
|
|
145
|
+
completed_steps: List[str] = []
|
|
146
|
+
next_step: Optional[str] = None
|
|
147
|
+
|
|
148
|
+
for name in _ORDERED_STEPS:
|
|
149
|
+
completed_at = progress.get(name)
|
|
150
|
+
completed = completed_at is not None
|
|
151
|
+
all_steps.append(
|
|
152
|
+
OnboardingStepDetail(
|
|
153
|
+
step=name,
|
|
154
|
+
completed=completed,
|
|
155
|
+
completed_at=completed_at,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
if completed:
|
|
159
|
+
completed_steps.append(name)
|
|
160
|
+
elif next_step is None:
|
|
161
|
+
next_step = name
|
|
162
|
+
|
|
163
|
+
total = len(_ORDERED_STEPS)
|
|
164
|
+
done = len(completed_steps)
|
|
165
|
+
progress_percent = round((done / total) * 100, 1) if total > 0 else 0.0
|
|
166
|
+
|
|
167
|
+
return OnboardingStatusResponse(
|
|
168
|
+
tenant_id=tenant_id,
|
|
169
|
+
completed_steps=completed_steps,
|
|
170
|
+
next_step=next_step,
|
|
171
|
+
progress_percent=progress_percent,
|
|
172
|
+
all_steps=all_steps,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def reset_progress(tenant_id: str) -> None:
|
|
177
|
+
"""Clear all onboarding progress for *tenant_id* (used in tests)."""
|
|
178
|
+
_progress.pop(tenant_id, None)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _mark_step_complete_sync(tenant_id: str, step: OnboardingStepName) -> None:
|
|
182
|
+
"""Synchronously mark *step* as completed (in-memory only, no async).
|
|
183
|
+
|
|
184
|
+
This is intended for use in synchronous test code where calling
|
|
185
|
+
``asyncio.run()`` would interfere with the event loop. It updates the
|
|
186
|
+
in-memory progress dict without attempting to persist to the audit log.
|
|
187
|
+
"""
|
|
188
|
+
progress = _tenant_progress(tenant_id)
|
|
189
|
+
if step.value not in progress:
|
|
190
|
+
progress[step.value] = datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
"""Orchestration package — closed-loop Ingest → Prioritize → Scan → Export → Learn pipeline.
|
|
7
|
+
|
|
8
|
+
Public API::
|
|
9
|
+
|
|
10
|
+
from mannf.product.orchestration import IngestScanOrchestrator, Pipeline
|
|
11
|
+
from mannf.product.orchestrator_models import OrchestrationConfig, OrchestrationResult
|
|
12
|
+
|
|
13
|
+
# Fluent builder
|
|
14
|
+
result = await (
|
|
15
|
+
Pipeline()
|
|
16
|
+
.with_source("api.yaml", format="openapi")
|
|
17
|
+
.with_base_url("http://localhost:8080")
|
|
18
|
+
.with_exporter("github-issues", repo="org/repo")
|
|
19
|
+
.run()
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Direct orchestrator
|
|
23
|
+
config = OrchestrationConfig(
|
|
24
|
+
source_path="api.yaml",
|
|
25
|
+
scan_config={"base_url": "http://localhost:8080"},
|
|
26
|
+
)
|
|
27
|
+
orch = IngestScanOrchestrator(config)
|
|
28
|
+
result = await orch.orchestrate()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from mannf.product.orchestration.ingest_scan_orchestrator import IngestScanOrchestrator
|
|
34
|
+
from mannf.product.orchestration.pipeline import Pipeline
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"IngestScanOrchestrator",
|
|
38
|
+
"Pipeline",
|
|
39
|
+
]
|