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,114 @@
|
|
|
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
|
+
"""Tenant data isolation dependency for FastAPI route injection.
|
|
6
|
+
|
|
7
|
+
Usage in a route::
|
|
8
|
+
|
|
9
|
+
from mannf.product.middleware.tenant_isolation import get_current_tenant
|
|
10
|
+
|
|
11
|
+
@app.get("/api/v1/some-endpoint")
|
|
12
|
+
async def some_endpoint(
|
|
13
|
+
request: Request,
|
|
14
|
+
tenant: dict = Depends(get_current_tenant),
|
|
15
|
+
) -> ...:
|
|
16
|
+
# tenant is guaranteed to be the authenticated tenant dict
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
When ``NAT_API_KEY`` legacy mode is in use (no DB tenant), this dependency
|
|
20
|
+
returns ``None`` — callers can choose to gate multi-tenant logic on
|
|
21
|
+
``tenant is not None`` to preserve backward compatibility.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from fastapi import HTTPException, Request
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def get_current_tenant(request: Request) -> dict:
|
|
32
|
+
"""FastAPI dependency that extracts the authenticated tenant.
|
|
33
|
+
|
|
34
|
+
Returns the tenant dict from ``request.state.tenant`` when a tenant has
|
|
35
|
+
been resolved by the auth middleware.
|
|
36
|
+
|
|
37
|
+
Raises ``HTTP 401`` if no tenant is authenticated (DB mode with no valid
|
|
38
|
+
API key, or API key not present).
|
|
39
|
+
|
|
40
|
+
In legacy ``NAT_API_KEY`` mode, ``request.state.tenant`` is ``None``.
|
|
41
|
+
Use :func:`get_optional_tenant` if you need backward-compatible behaviour
|
|
42
|
+
where unauthenticated/legacy access is still permitted.
|
|
43
|
+
"""
|
|
44
|
+
tenant = getattr(request.state, "tenant", None)
|
|
45
|
+
if tenant is None:
|
|
46
|
+
raise HTTPException(
|
|
47
|
+
status_code=401,
|
|
48
|
+
detail="Authentication required",
|
|
49
|
+
)
|
|
50
|
+
return tenant
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_optional_tenant(request: Request) -> Optional[dict]:
|
|
54
|
+
"""FastAPI dependency that returns the tenant or ``None`` in legacy mode.
|
|
55
|
+
|
|
56
|
+
Use this on endpoints that must remain backward-compatible with the legacy
|
|
57
|
+
``NAT_API_KEY`` environment variable. The returned value is either:
|
|
58
|
+
|
|
59
|
+
- ``dict`` — the authenticated tenant record, or
|
|
60
|
+
- ``None`` — legacy mode (no DB tenant; auth still validated by middleware).
|
|
61
|
+
"""
|
|
62
|
+
return getattr(request.state, "tenant", None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def get_current_user(request: Request) -> dict:
|
|
66
|
+
"""Return the authenticated user dict from request.state.user.
|
|
67
|
+
|
|
68
|
+
Raises HTTP 401 if no user is set on the request state.
|
|
69
|
+
The user is set by the SAML/SSO auth path in the middleware.
|
|
70
|
+
When using API key auth (not SSO), this returns a synthetic user dict
|
|
71
|
+
derived from the tenant record.
|
|
72
|
+
"""
|
|
73
|
+
user = getattr(request.state, "user", None)
|
|
74
|
+
if user is None:
|
|
75
|
+
# Fallback: synthesize a minimal user from the tenant if available
|
|
76
|
+
tenant = getattr(request.state, "tenant", None)
|
|
77
|
+
if tenant is not None:
|
|
78
|
+
# API-key auth: synthesize a user with admin role for backward compat
|
|
79
|
+
return {
|
|
80
|
+
"id": None,
|
|
81
|
+
"email": tenant.get("email", ""),
|
|
82
|
+
"name": tenant.get("name", ""),
|
|
83
|
+
"role": "admin",
|
|
84
|
+
"tenant_id": tenant.get("tenant_id") or str(tenant.get("id", "")),
|
|
85
|
+
}
|
|
86
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
87
|
+
return user
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def require_role(*allowed_roles: str):
|
|
91
|
+
"""FastAPI dependency factory that enforces RBAC.
|
|
92
|
+
|
|
93
|
+
Usage::
|
|
94
|
+
|
|
95
|
+
@app.post("/api/v1/scan")
|
|
96
|
+
async def trigger_scan(
|
|
97
|
+
user: dict = Depends(require_role("admin", "operator")),
|
|
98
|
+
):
|
|
99
|
+
...
|
|
100
|
+
"""
|
|
101
|
+
async def _check_role(request: Request) -> dict:
|
|
102
|
+
user = await get_current_user(request)
|
|
103
|
+
if user.get("role") not in allowed_roles:
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=403,
|
|
106
|
+
detail={
|
|
107
|
+
"error": {
|
|
108
|
+
"code": "INSUFFICIENT_ROLE",
|
|
109
|
+
"message": f"Role {user.get('role')!r} is not permitted. Required: {list(allowed_roles)}",
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
return user
|
|
114
|
+
return _check_role
|
mannf/product/models.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
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
|
+
"""SQLAlchemy ORM models for the NAT persistence layer.
|
|
6
|
+
|
|
7
|
+
Tables:
|
|
8
|
+
- scans — replaces the ``_scans`` in-memory dict
|
|
9
|
+
- security_scans — replaces ``_security_scans``
|
|
10
|
+
- healing_reports — replaces ``_healing_reports``
|
|
11
|
+
- tenants — SaaS tenant / organisation records
|
|
12
|
+
- api_keys — hashed API keys linked to tenants
|
|
13
|
+
- usage_records — per-month scan usage metering
|
|
14
|
+
- billing_events — auditable log of billing lifecycle events
|
|
15
|
+
- autonomous_runs — replaces the ``_autonomous_runs`` in-memory dict
|
|
16
|
+
- discovery_runs — replaces the ``_discoveries`` in-memory dict
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from mannf.product.database import Base
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text
|
|
27
|
+
from sqlalchemy.dialects.postgresql import JSON, UUID
|
|
28
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
29
|
+
from sqlalchemy.sql import func
|
|
30
|
+
|
|
31
|
+
class Scan(Base):
|
|
32
|
+
"""Persistent record for a functional (NAT) scan."""
|
|
33
|
+
|
|
34
|
+
__tablename__ = "scans"
|
|
35
|
+
|
|
36
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
37
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
38
|
+
)
|
|
39
|
+
status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
|
40
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
41
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
42
|
+
)
|
|
43
|
+
completed_at: Mapped[DateTime | None] = mapped_column(
|
|
44
|
+
DateTime(timezone=True), nullable=True
|
|
45
|
+
)
|
|
46
|
+
spec_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
47
|
+
base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
|
48
|
+
summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
49
|
+
results: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
|
50
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
51
|
+
prioritization: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
52
|
+
llm_cases_generated: Mapped[int] = mapped_column(
|
|
53
|
+
Integer, nullable=False, default=0
|
|
54
|
+
)
|
|
55
|
+
healing_summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
56
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
57
|
+
UUID(as_uuid=True),
|
|
58
|
+
ForeignKey("tenants.id"),
|
|
59
|
+
nullable=True,
|
|
60
|
+
index=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
class SecurityScan(Base):
|
|
64
|
+
"""Persistent record for an OWASP security scan."""
|
|
65
|
+
|
|
66
|
+
__tablename__ = "security_scans"
|
|
67
|
+
|
|
68
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
69
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
70
|
+
)
|
|
71
|
+
status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
|
72
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
73
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
74
|
+
)
|
|
75
|
+
completed_at: Mapped[DateTime | None] = mapped_column(
|
|
76
|
+
DateTime(timezone=True), nullable=True
|
|
77
|
+
)
|
|
78
|
+
spec_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
79
|
+
base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
|
80
|
+
report: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
81
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
82
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
83
|
+
UUID(as_uuid=True),
|
|
84
|
+
ForeignKey("tenants.id"),
|
|
85
|
+
nullable=True,
|
|
86
|
+
index=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
class HealingReport(Base):
|
|
90
|
+
"""Persistent record for a self-healing analysis report."""
|
|
91
|
+
|
|
92
|
+
__tablename__ = "healing_reports"
|
|
93
|
+
|
|
94
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
95
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
96
|
+
)
|
|
97
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
98
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
99
|
+
)
|
|
100
|
+
report_data: Mapped[dict] = mapped_column(JSON, nullable=False)
|
|
101
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
102
|
+
UUID(as_uuid=True),
|
|
103
|
+
ForeignKey("tenants.id"),
|
|
104
|
+
nullable=True,
|
|
105
|
+
index=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
class Tenant(Base):
|
|
109
|
+
"""SaaS tenant / organisation record."""
|
|
110
|
+
|
|
111
|
+
__tablename__ = "tenants"
|
|
112
|
+
|
|
113
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
114
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
115
|
+
)
|
|
116
|
+
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
117
|
+
email: Mapped[str] = mapped_column(String(512), nullable=False, unique=True)
|
|
118
|
+
plan_tier: Mapped[str] = mapped_column(
|
|
119
|
+
String(32), nullable=False, default="free"
|
|
120
|
+
)
|
|
121
|
+
stripe_customer_id: Mapped[str | None] = mapped_column(
|
|
122
|
+
String(256), nullable=True, unique=True
|
|
123
|
+
)
|
|
124
|
+
stripe_subscription_id: Mapped[str | None] = mapped_column(
|
|
125
|
+
String(256), nullable=True
|
|
126
|
+
)
|
|
127
|
+
monthly_scan_quota: Mapped[int] = mapped_column(
|
|
128
|
+
Integer, nullable=False, default=50
|
|
129
|
+
)
|
|
130
|
+
monthly_security_scan_quota: Mapped[int] = mapped_column(
|
|
131
|
+
Integer, nullable=False, default=10
|
|
132
|
+
)
|
|
133
|
+
max_concurrent_scans: Mapped[int] = mapped_column(
|
|
134
|
+
Integer, nullable=False, default=1
|
|
135
|
+
)
|
|
136
|
+
is_active: Mapped[bool] = mapped_column(
|
|
137
|
+
Boolean, nullable=False, default=True
|
|
138
|
+
)
|
|
139
|
+
notification_slack_webhook_url: Mapped[str | None] = mapped_column(
|
|
140
|
+
String(2048), nullable=True
|
|
141
|
+
)
|
|
142
|
+
notification_teams_webhook_url: Mapped[str | None] = mapped_column(
|
|
143
|
+
String(2048), nullable=True
|
|
144
|
+
)
|
|
145
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
146
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
147
|
+
)
|
|
148
|
+
updated_at: Mapped[DateTime] = mapped_column(
|
|
149
|
+
DateTime(timezone=True),
|
|
150
|
+
server_default=func.now(),
|
|
151
|
+
onupdate=func.now(),
|
|
152
|
+
nullable=False,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
class ApiKey(Base):
|
|
156
|
+
"""Hashed API key linked to a tenant."""
|
|
157
|
+
|
|
158
|
+
__tablename__ = "api_keys"
|
|
159
|
+
|
|
160
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
161
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
162
|
+
)
|
|
163
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
|
164
|
+
UUID(as_uuid=True),
|
|
165
|
+
ForeignKey("tenants.id"),
|
|
166
|
+
nullable=False,
|
|
167
|
+
index=True,
|
|
168
|
+
)
|
|
169
|
+
key_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
|
170
|
+
key_prefix: Mapped[str] = mapped_column(String(12), nullable=False)
|
|
171
|
+
label: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
172
|
+
is_active: Mapped[bool] = mapped_column(
|
|
173
|
+
Boolean, nullable=False, default=True
|
|
174
|
+
)
|
|
175
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
176
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
177
|
+
)
|
|
178
|
+
last_used_at: Mapped[DateTime | None] = mapped_column(
|
|
179
|
+
DateTime(timezone=True), nullable=True
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
class UsageRecord(Base):
|
|
183
|
+
"""Per-month scan usage record for quota metering."""
|
|
184
|
+
|
|
185
|
+
__tablename__ = "usage_records"
|
|
186
|
+
|
|
187
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
188
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
189
|
+
)
|
|
190
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
|
191
|
+
UUID(as_uuid=True),
|
|
192
|
+
ForeignKey("tenants.id"),
|
|
193
|
+
nullable=False,
|
|
194
|
+
index=True,
|
|
195
|
+
)
|
|
196
|
+
scan_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
197
|
+
scan_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
198
|
+
UUID(as_uuid=True), nullable=True
|
|
199
|
+
)
|
|
200
|
+
recorded_at: Mapped[DateTime] = mapped_column(
|
|
201
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
202
|
+
)
|
|
203
|
+
period_year: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
204
|
+
period_month: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
205
|
+
|
|
206
|
+
__table_args__ = (
|
|
207
|
+
Index(
|
|
208
|
+
"ix_usage_records_tenant_period",
|
|
209
|
+
"tenant_id",
|
|
210
|
+
"period_year",
|
|
211
|
+
"period_month",
|
|
212
|
+
"scan_type",
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
class BillingEvent(Base):
|
|
217
|
+
"""Auditable log of billing lifecycle events."""
|
|
218
|
+
|
|
219
|
+
__tablename__ = "billing_events"
|
|
220
|
+
|
|
221
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
222
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
223
|
+
)
|
|
224
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
225
|
+
UUID(as_uuid=True),
|
|
226
|
+
ForeignKey("tenants.id"),
|
|
227
|
+
nullable=True,
|
|
228
|
+
index=True,
|
|
229
|
+
)
|
|
230
|
+
event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
231
|
+
source: Mapped[str] = mapped_column(
|
|
232
|
+
String(32), nullable=False, default="system"
|
|
233
|
+
)
|
|
234
|
+
details: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
235
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
236
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
class AutonomousRun(Base):
|
|
240
|
+
"""Persistent record for an autonomous test-loop run."""
|
|
241
|
+
|
|
242
|
+
__tablename__ = "autonomous_runs"
|
|
243
|
+
|
|
244
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
245
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
246
|
+
)
|
|
247
|
+
status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
|
248
|
+
target_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
|
249
|
+
strategy: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
250
|
+
report: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
251
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
252
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
253
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
254
|
+
)
|
|
255
|
+
completed_at: Mapped[DateTime | None] = mapped_column(
|
|
256
|
+
DateTime(timezone=True), nullable=True
|
|
257
|
+
)
|
|
258
|
+
last_heartbeat_at: Mapped[DateTime | None] = mapped_column(
|
|
259
|
+
DateTime(timezone=True), nullable=True
|
|
260
|
+
)
|
|
261
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
262
|
+
UUID(as_uuid=True),
|
|
263
|
+
ForeignKey("tenants.id"),
|
|
264
|
+
nullable=True,
|
|
265
|
+
index=True,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
class DiscoveryRun(Base):
|
|
269
|
+
"""Persistent record for a discovery crawl + scenario generation run."""
|
|
270
|
+
|
|
271
|
+
__tablename__ = "discovery_runs"
|
|
272
|
+
|
|
273
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
274
|
+
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
275
|
+
)
|
|
276
|
+
status: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
|
277
|
+
base_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
|
278
|
+
scenarios: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
|
279
|
+
stats: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
280
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
281
|
+
created_at: Mapped[DateTime] = mapped_column(
|
|
282
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
283
|
+
)
|
|
284
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
285
|
+
UUID(as_uuid=True),
|
|
286
|
+
ForeignKey("tenants.id"),
|
|
287
|
+
nullable=True,
|
|
288
|
+
index=True,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
class Workspace(Base):
|
|
292
|
+
__tablename__ = "workspaces"
|
|
293
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
294
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
|
|
295
|
+
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
296
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
297
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
298
|
+
created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
299
|
+
updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
300
|
+
|
|
301
|
+
class Project(Base):
|
|
302
|
+
__tablename__ = "projects"
|
|
303
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
304
|
+
workspace_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=False, index=True)
|
|
305
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
|
|
306
|
+
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
307
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
308
|
+
parent_project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=True)
|
|
309
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
310
|
+
created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
311
|
+
updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
312
|
+
|
|
313
|
+
class User(Base):
|
|
314
|
+
__tablename__ = "users"
|
|
315
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
316
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
|
|
317
|
+
email: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
318
|
+
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
319
|
+
sso_subject: Mapped[str | None] = mapped_column(String(512), nullable=True, unique=True)
|
|
320
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
321
|
+
created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
322
|
+
updated_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
323
|
+
__table_args__ = (Index("uq_users_email_tenant", "email", "tenant_id", unique=True),)
|
|
324
|
+
|
|
325
|
+
class UserRole(Base):
|
|
326
|
+
__tablename__ = "user_roles"
|
|
327
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
328
|
+
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
|
|
329
|
+
tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True)
|
|
330
|
+
workspace_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=True)
|
|
331
|
+
role: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
332
|
+
created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
333
|
+
|
|
334
|
+
class ApiAuditLog(Base):
|
|
335
|
+
__tablename__ = "api_audit_log"
|
|
336
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
337
|
+
tenant_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True)
|
|
338
|
+
user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
|
339
|
+
method: Mapped[str] = mapped_column(String(10), nullable=False)
|
|
340
|
+
path: Mapped[str] = mapped_column(String(2048), nullable=False)
|
|
341
|
+
status_code: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
342
|
+
request_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
343
|
+
source_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
344
|
+
created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
345
|
+
|
|
346
|
+
except ImportError: # pragma: no cover — SQLAlchemy not installed
|
|
347
|
+
pass
|
|
@@ -0,0 +1,24 @@
|
|
|
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 service for scan lifecycle events.
|
|
6
|
+
|
|
7
|
+
Delivers real-time notifications to Slack and Microsoft Teams via incoming
|
|
8
|
+
webhooks whenever key scan or platform events occur.
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
from mannf.product.notifications import dispatch_scan_notification
|
|
13
|
+
|
|
14
|
+
await dispatch_scan_notification(
|
|
15
|
+
"scan.completed",
|
|
16
|
+
tenant_id="<uuid>",
|
|
17
|
+
scan_id="<uuid>",
|
|
18
|
+
details={"total": 10, "passed": 9, "failed": 1},
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from mannf.product.notifications.dispatcher import dispatch_scan_notification
|
|
23
|
+
|
|
24
|
+
__all__ = ["dispatch_scan_notification"]
|