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,269 @@
|
|
|
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
|
+
"""Dataclass models for the orchestration pipeline.
|
|
7
|
+
|
|
8
|
+
These models carry configuration and results through the
|
|
9
|
+
ingest → transform → prioritize → scan → learn pipeline implemented
|
|
10
|
+
by :class:`~mannf.product.orchestrator.IngestOrchestrator`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mannf.product.exporters.base import ExportSummary
|
|
20
|
+
from mannf.product.ingestors.models import IngestedEndpoint, IngestedTestCase
|
|
21
|
+
from mannf.product.security.models import SecurityReport
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class OrchestrationConfig:
|
|
26
|
+
"""Top-level configuration for a single orchestration run.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
ingestor_name:
|
|
31
|
+
Name of the ingestor plugin to use (e.g., ``"openapi"``, ``"postman"``).
|
|
32
|
+
If ``None``, auto-detection via :func:`~mannf.product.ingestors.loader.get_ingestor_for_source`
|
|
33
|
+
is attempted.
|
|
34
|
+
source_path:
|
|
35
|
+
File-system path to the source file to ingest.
|
|
36
|
+
source_url:
|
|
37
|
+
URL of the remote source to ingest.
|
|
38
|
+
source_format:
|
|
39
|
+
Format hint for the source (e.g., ``"openapi"``, ``"har"``). Used
|
|
40
|
+
when constructing the :class:`~mannf.product.ingestors.models.IngestorSource`.
|
|
41
|
+
ingestor_config:
|
|
42
|
+
Arbitrary config dict passed verbatim to the ingestor's
|
|
43
|
+
:meth:`~mannf.product.ingestors.base.IngestorPlugin.ingest` method.
|
|
44
|
+
scan_config:
|
|
45
|
+
Scan-level config — must contain at least ``"base_url"`` for the
|
|
46
|
+
:class:`~mannf.product.security.scanner.SecurityScanner` to work.
|
|
47
|
+
May also contain ``"auth_token"``, ``"api_key"``, ``"timeout"``,
|
|
48
|
+
``"severity_threshold"``, ``"belief_guided"``, ``"enabled_checks"``.
|
|
49
|
+
prioritizer_config:
|
|
50
|
+
Config hints for the belief-guided prioritizer. Currently supports
|
|
51
|
+
``"seed_belief_scores"`` (dict mapping endpoint keys to float scores)
|
|
52
|
+
and ``"risk_report"`` (list of legacy risk dicts).
|
|
53
|
+
exporter_config:
|
|
54
|
+
Config passed to the exporter plugin after scanning. Must include
|
|
55
|
+
``"exporter_name"`` if export is desired; ``None`` disables export.
|
|
56
|
+
feedback_config:
|
|
57
|
+
Config for the learning/feedback phase. Supports
|
|
58
|
+
``"save_path"`` (file path for persisting feedback JSON) and
|
|
59
|
+
``"enabled"`` (bool, default ``True``).
|
|
60
|
+
llm_config:
|
|
61
|
+
Optional config for LLM-augmented edge-case generation. When set,
|
|
62
|
+
the orchestrator will invoke the LLM to generate additional test cases
|
|
63
|
+
for high-priority endpoints before scanning. Supported keys:
|
|
64
|
+
|
|
65
|
+
- ``"enabled"`` — bool (default ``True`` when this dict is non-empty)
|
|
66
|
+
- ``"provider"`` — ``"openai"`` or ``"anthropic"`` (default ``"openai"``)
|
|
67
|
+
- ``"model"`` — model identifier (e.g. ``"gpt-4o-mini"``)
|
|
68
|
+
- ``"api_key"`` — API key (falls back to env var)
|
|
69
|
+
- ``"priority_threshold"`` — minimum priority_score for a target to
|
|
70
|
+
receive LLM-generated test cases (default ``0.7``)
|
|
71
|
+
- ``"n_cases"`` — number of edge-case test cases to generate per
|
|
72
|
+
high-priority endpoint (default ``3``)
|
|
73
|
+
- Any other keys are passed verbatim to :class:`~mannf.product.llm.config.LLMConfig`.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
ingestor_name: str | None = None
|
|
77
|
+
source_path: str | None = None
|
|
78
|
+
source_url: str | None = None
|
|
79
|
+
source_format: str | None = None
|
|
80
|
+
ingestor_config: dict = field(default_factory=dict)
|
|
81
|
+
scan_config: dict = field(default_factory=dict)
|
|
82
|
+
prioritizer_config: dict = field(default_factory=dict)
|
|
83
|
+
exporter_config: dict = field(default_factory=dict)
|
|
84
|
+
feedback_config: dict = field(default_factory=dict)
|
|
85
|
+
llm_config: dict = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class ScanTarget:
|
|
90
|
+
"""A single scan target — one endpoint with associated test intents.
|
|
91
|
+
|
|
92
|
+
Attributes
|
|
93
|
+
----------
|
|
94
|
+
endpoint:
|
|
95
|
+
The ingested endpoint to scan.
|
|
96
|
+
test_cases:
|
|
97
|
+
Test intents derived from the ingestion phase that are associated
|
|
98
|
+
with this endpoint (matched by path/tags).
|
|
99
|
+
priority_score:
|
|
100
|
+
Composite priority score in ``[0.0, 1.0]`` assigned by the
|
|
101
|
+
belief-guided prioritizer. Higher means test first.
|
|
102
|
+
check_ids:
|
|
103
|
+
OWASP check IDs recommended for this endpoint by the prioritizer.
|
|
104
|
+
metadata:
|
|
105
|
+
Arbitrary additional data (e.g., source ingestor name, raw priority hint).
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
endpoint: IngestedEndpoint
|
|
109
|
+
test_cases: list[IngestedTestCase] = field(default_factory=list)
|
|
110
|
+
priority_score: float = 0.5
|
|
111
|
+
check_ids: list[str] = field(default_factory=list)
|
|
112
|
+
metadata: dict = field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
def endpoint_key(self) -> str:
|
|
115
|
+
"""Return ``"METHOD /path"`` string for logging and lookup."""
|
|
116
|
+
return f"{self.endpoint.method.upper()} {self.endpoint.path}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class ScanPlan:
|
|
121
|
+
"""A prioritized, ordered list of endpoints to scan.
|
|
122
|
+
|
|
123
|
+
Attributes
|
|
124
|
+
----------
|
|
125
|
+
targets:
|
|
126
|
+
Scan targets in descending priority order (highest first).
|
|
127
|
+
total_endpoints:
|
|
128
|
+
Number of distinct endpoints in the plan.
|
|
129
|
+
total_test_cases:
|
|
130
|
+
Total number of test intents across all targets.
|
|
131
|
+
ingest_stats:
|
|
132
|
+
Stats returned by the ingestor (passed through for reference).
|
|
133
|
+
metadata:
|
|
134
|
+
Arbitrary metadata (ingestor name, source path, timestamp, etc.).
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
targets: list[ScanTarget] = field(default_factory=list)
|
|
138
|
+
total_endpoints: int = 0
|
|
139
|
+
total_test_cases: int = 0
|
|
140
|
+
ingest_stats: dict = field(default_factory=dict)
|
|
141
|
+
metadata: dict = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> dict:
|
|
144
|
+
"""Serialize to a plain dict suitable for JSON output."""
|
|
145
|
+
return {
|
|
146
|
+
"total_endpoints": self.total_endpoints,
|
|
147
|
+
"total_test_cases": self.total_test_cases,
|
|
148
|
+
"ingest_stats": self.ingest_stats,
|
|
149
|
+
"metadata": self.metadata,
|
|
150
|
+
"targets": [
|
|
151
|
+
{
|
|
152
|
+
"endpoint": t.endpoint_key(),
|
|
153
|
+
"priority_score": t.priority_score,
|
|
154
|
+
"check_ids": t.check_ids,
|
|
155
|
+
"test_cases": [tc.name for tc in t.test_cases],
|
|
156
|
+
"metadata": t.metadata,
|
|
157
|
+
}
|
|
158
|
+
for t in self.targets
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ScanFeedback:
|
|
165
|
+
"""Learning data captured after a completed scan.
|
|
166
|
+
|
|
167
|
+
Attributes
|
|
168
|
+
----------
|
|
169
|
+
scan_id:
|
|
170
|
+
UUID of the associated security scan.
|
|
171
|
+
total_targets:
|
|
172
|
+
Number of endpoints that were scanned.
|
|
173
|
+
findings_count:
|
|
174
|
+
Total number of security findings across all endpoints.
|
|
175
|
+
per_target:
|
|
176
|
+
Per-endpoint breakdown: list of dicts with keys
|
|
177
|
+
``endpoint``, ``priority_score``, ``findings_count``,
|
|
178
|
+
``matched_test_cases`` (names), ``outcome_matched`` (bool).
|
|
179
|
+
priority_accuracy:
|
|
180
|
+
Fraction of findings that came from the top-50 % of endpoints
|
|
181
|
+
by priority score. Value in ``[0.0, 1.0]``; higher is better.
|
|
182
|
+
suggested_adjustments:
|
|
183
|
+
Endpoint keys that should receive a belief-score boost on the
|
|
184
|
+
next run (those that had unexpected findings or missed expected ones).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
scan_id: str = ""
|
|
188
|
+
total_targets: int = 0
|
|
189
|
+
findings_count: int = 0
|
|
190
|
+
per_target: list[dict] = field(default_factory=list)
|
|
191
|
+
priority_accuracy: float = 0.0
|
|
192
|
+
suggested_adjustments: dict = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
def to_dict(self) -> dict:
|
|
195
|
+
"""Serialize to a plain dict suitable for JSON output."""
|
|
196
|
+
return {
|
|
197
|
+
"scan_id": self.scan_id,
|
|
198
|
+
"total_targets": self.total_targets,
|
|
199
|
+
"findings_count": self.findings_count,
|
|
200
|
+
"per_target": self.per_target,
|
|
201
|
+
"priority_accuracy": self.priority_accuracy,
|
|
202
|
+
"suggested_adjustments": self.suggested_adjustments,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class OrchestrationResult:
|
|
208
|
+
"""Full result of an orchestration run.
|
|
209
|
+
|
|
210
|
+
Attributes
|
|
211
|
+
----------
|
|
212
|
+
config:
|
|
213
|
+
The configuration used for this run.
|
|
214
|
+
ingest_stats:
|
|
215
|
+
Raw stats returned by the ingestor plugin.
|
|
216
|
+
scan_plan:
|
|
217
|
+
The prioritized scan plan (populated after the prioritize phase).
|
|
218
|
+
scan_results:
|
|
219
|
+
The full :class:`~mannf.product.security.models.SecurityReport`,
|
|
220
|
+
or ``None`` in dry-run / plan-only mode.
|
|
221
|
+
feedback:
|
|
222
|
+
Learning data from the scan phase, or ``None`` in dry-run mode.
|
|
223
|
+
export_summaries:
|
|
224
|
+
One :class:`~mannf.product.exporters.base.ExportSummary` per exporter
|
|
225
|
+
that was invoked after scanning. Empty when no exporter is configured
|
|
226
|
+
or when running in dry-run / plan-only mode.
|
|
227
|
+
errors:
|
|
228
|
+
Fatal errors encountered during the pipeline (ingestor failures, etc.).
|
|
229
|
+
warnings:
|
|
230
|
+
Non-fatal warnings emitted during the pipeline.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
config: OrchestrationConfig = field(default_factory=OrchestrationConfig)
|
|
234
|
+
ingest_stats: dict = field(default_factory=dict)
|
|
235
|
+
scan_plan: ScanPlan = field(default_factory=ScanPlan)
|
|
236
|
+
scan_results: SecurityReport | None = None
|
|
237
|
+
feedback: ScanFeedback | None = None
|
|
238
|
+
export_summaries: list[ExportSummary] = field(default_factory=list)
|
|
239
|
+
errors: list[str] = field(default_factory=list)
|
|
240
|
+
warnings: list[str] = field(default_factory=list)
|
|
241
|
+
# LLM root cause suggestions for high-severity findings (Phase 10.3)
|
|
242
|
+
root_cause_suggestions: list[dict] = field(default_factory=list)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def success(self) -> bool:
|
|
246
|
+
"""True if there were no fatal errors in the pipeline."""
|
|
247
|
+
return len(self.errors) == 0
|
|
248
|
+
|
|
249
|
+
def to_dict(self) -> dict:
|
|
250
|
+
"""Serialize to a plain dict suitable for JSON output."""
|
|
251
|
+
return {
|
|
252
|
+
"success": self.success,
|
|
253
|
+
"ingest_stats": self.ingest_stats,
|
|
254
|
+
"scan_plan": self.scan_plan.to_dict(),
|
|
255
|
+
"scan_results": self.scan_results.to_dict() if self.scan_results else None,
|
|
256
|
+
"feedback": self.feedback.to_dict() if self.feedback else None,
|
|
257
|
+
"root_cause_suggestions": self.root_cause_suggestions,
|
|
258
|
+
"export_summaries": [
|
|
259
|
+
{
|
|
260
|
+
"total": s.total,
|
|
261
|
+
"succeeded": s.succeeded,
|
|
262
|
+
"failed": s.failed,
|
|
263
|
+
"skipped": s.skipped,
|
|
264
|
+
}
|
|
265
|
+
for s in self.export_summaries
|
|
266
|
+
],
|
|
267
|
+
"errors": self.errors,
|
|
268
|
+
"warnings": self.warnings,
|
|
269
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
"""Regression record/replay package for NAT.
|
|
7
|
+
|
|
8
|
+
Provides :class:`~mannf.product.regression.recorder.RegressionRecorder`,
|
|
9
|
+
:class:`~mannf.product.regression.replayer.RegressionReplayer`, and
|
|
10
|
+
:class:`~mannf.product.regression.differ.RegressionDiffer` for recording API
|
|
11
|
+
test runs as portable baselines and comparing future runs against them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from mannf.product.regression.differ import RegressionDiffer
|
|
17
|
+
from mannf.product.regression.masking import mask_sensitive
|
|
18
|
+
from mannf.product.regression.models import (
|
|
19
|
+
RegressionBaseline,
|
|
20
|
+
RegressionDiff,
|
|
21
|
+
RequestRecord,
|
|
22
|
+
ResponseRecord,
|
|
23
|
+
)
|
|
24
|
+
from mannf.product.regression.recorder import RegressionRecorder
|
|
25
|
+
from mannf.product.regression.replayer import RegressionReplayer
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"RegressionRecorder",
|
|
29
|
+
"RegressionReplayer",
|
|
30
|
+
"RegressionDiffer",
|
|
31
|
+
"RegressionBaseline",
|
|
32
|
+
"RegressionDiff",
|
|
33
|
+
"RequestRecord",
|
|
34
|
+
"ResponseRecord",
|
|
35
|
+
"mask_sensitive",
|
|
36
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
"""RegressionDiffer — compares a baseline recording to a replay recording."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from mannf.product.regression.models import (
|
|
13
|
+
FieldChange,
|
|
14
|
+
RecordDiff,
|
|
15
|
+
RegressionBaseline,
|
|
16
|
+
RegressionDiff,
|
|
17
|
+
RegressionRecord,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Latency regression threshold: flag if replay is ≥ this factor slower than baseline
|
|
21
|
+
_LATENCY_REGRESSION_FACTOR = 2.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegressionDiffer:
|
|
25
|
+
"""Compare a baseline :class:`~mannf.product.regression.models.RegressionBaseline` to a
|
|
26
|
+
replay baseline and produce a :class:`~mannf.product.regression.models.RegressionDiff`.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
latency_regression_factor:
|
|
31
|
+
Multiplier applied to the baseline latency. If the replay latency
|
|
32
|
+
exceeds ``baseline_latency * factor``, a latency regression is reported.
|
|
33
|
+
Defaults to ``2.0`` (100 % slower = regression).
|
|
34
|
+
check_headers:
|
|
35
|
+
When ``True``, compare response headers between baseline and replay.
|
|
36
|
+
Defaults to ``False`` (headers are often volatile).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
latency_regression_factor: float = _LATENCY_REGRESSION_FACTOR,
|
|
42
|
+
check_headers: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.latency_regression_factor = latency_regression_factor
|
|
45
|
+
self.check_headers = check_headers
|
|
46
|
+
|
|
47
|
+
def diff(
|
|
48
|
+
self,
|
|
49
|
+
baseline: RegressionBaseline,
|
|
50
|
+
replay: RegressionBaseline,
|
|
51
|
+
) -> RegressionDiff:
|
|
52
|
+
"""Compute the diff between *baseline* and *replay*.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
baseline:
|
|
57
|
+
The original recording.
|
|
58
|
+
replay:
|
|
59
|
+
The new recording produced by :class:`~mannf.product.regression.replayer.RegressionReplayer`.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
RegressionDiff
|
|
64
|
+
Structured diff report.
|
|
65
|
+
"""
|
|
66
|
+
baseline_by_id: Dict[str, RegressionRecord] = {
|
|
67
|
+
r.test_case_id: r for r in baseline.records
|
|
68
|
+
}
|
|
69
|
+
replay_by_id: Dict[str, RegressionRecord] = {
|
|
70
|
+
r.test_case_id: r for r in replay.records
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
baseline_ids = set(baseline_by_id.keys())
|
|
74
|
+
replay_ids = set(replay_by_id.keys())
|
|
75
|
+
|
|
76
|
+
new_ids = replay_ids - baseline_ids
|
|
77
|
+
removed_ids = baseline_ids - replay_ids
|
|
78
|
+
common_ids = baseline_ids & replay_ids
|
|
79
|
+
|
|
80
|
+
changes: List[RecordDiff] = []
|
|
81
|
+
|
|
82
|
+
for tc_id in sorted(common_ids):
|
|
83
|
+
b_rec = baseline_by_id[tc_id]
|
|
84
|
+
r_rec = replay_by_id[tc_id]
|
|
85
|
+
diffs = self._compare_records(b_rec, r_rec)
|
|
86
|
+
if diffs:
|
|
87
|
+
changes.append(
|
|
88
|
+
RecordDiff(
|
|
89
|
+
test_case_id=tc_id,
|
|
90
|
+
endpoint=f"{b_rec.request.method} {b_rec.request.url}",
|
|
91
|
+
changes=diffs,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
total = len(baseline_ids) + len(new_ids)
|
|
96
|
+
changed = len(changes)
|
|
97
|
+
unchanged = len(common_ids) - changed
|
|
98
|
+
|
|
99
|
+
summary: Dict[str, int] = {
|
|
100
|
+
"total": total,
|
|
101
|
+
"unchanged": unchanged,
|
|
102
|
+
"changed": changed,
|
|
103
|
+
"new": len(new_ids),
|
|
104
|
+
"removed": len(removed_ids),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return RegressionDiff(summary=summary, changes=changes)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Internal helpers
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _compare_records(
|
|
114
|
+
self,
|
|
115
|
+
baseline: RegressionRecord,
|
|
116
|
+
replay: RegressionRecord,
|
|
117
|
+
) -> List[FieldChange]:
|
|
118
|
+
field_changes: List[FieldChange] = []
|
|
119
|
+
|
|
120
|
+
# Status code
|
|
121
|
+
if baseline.response.status_code != replay.response.status_code:
|
|
122
|
+
field_changes.append(
|
|
123
|
+
FieldChange(
|
|
124
|
+
field="response.status_code",
|
|
125
|
+
baseline=baseline.response.status_code,
|
|
126
|
+
replay=replay.response.status_code,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Response body
|
|
131
|
+
if baseline.response.body != replay.response.body:
|
|
132
|
+
field_changes.append(
|
|
133
|
+
FieldChange(
|
|
134
|
+
field="response.body",
|
|
135
|
+
baseline=baseline.response.body,
|
|
136
|
+
replay=replay.response.body,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Pass/fail
|
|
141
|
+
if baseline.passed != replay.passed:
|
|
142
|
+
field_changes.append(
|
|
143
|
+
FieldChange(
|
|
144
|
+
field="passed",
|
|
145
|
+
baseline=baseline.passed,
|
|
146
|
+
replay=replay.passed,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Latency regression
|
|
151
|
+
b_latency = baseline.response.latency_ms
|
|
152
|
+
r_latency = replay.response.latency_ms
|
|
153
|
+
if b_latency > 0 and r_latency > b_latency * self.latency_regression_factor:
|
|
154
|
+
field_changes.append(
|
|
155
|
+
FieldChange(
|
|
156
|
+
field="response.latency_ms",
|
|
157
|
+
baseline=b_latency,
|
|
158
|
+
replay=r_latency,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Response headers (optional)
|
|
163
|
+
if self.check_headers and baseline.response.headers != replay.response.headers:
|
|
164
|
+
field_changes.append(
|
|
165
|
+
FieldChange(
|
|
166
|
+
field="response.headers",
|
|
167
|
+
baseline=baseline.response.headers,
|
|
168
|
+
replay=replay.response.headers,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return field_changes
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
"""Sensitive data masking for regression recordings.
|
|
7
|
+
|
|
8
|
+
When ``--mask-sensitive`` is used, values in headers such as ``Authorization``,
|
|
9
|
+
``X-API-Key``, cookie values, and OAuth tokens are replaced with
|
|
10
|
+
``***MASKED***`` before being written to disk.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
_MASKED = "***MASKED***"
|
|
19
|
+
|
|
20
|
+
# Header names that carry sensitive data (case-insensitive matching)
|
|
21
|
+
_SENSITIVE_HEADERS = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"authorization",
|
|
24
|
+
"proxy-authorization",
|
|
25
|
+
"x-api-key",
|
|
26
|
+
"x-auth-token",
|
|
27
|
+
"x-access-token",
|
|
28
|
+
"cookie",
|
|
29
|
+
"set-cookie",
|
|
30
|
+
"x-csrf-token",
|
|
31
|
+
"api-key",
|
|
32
|
+
"x-secret",
|
|
33
|
+
"x-token",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Patterns in JSON body / query-param values that look like tokens
|
|
38
|
+
_TOKEN_PATTERNS = [
|
|
39
|
+
re.compile(r"^Bearer\s+\S+", re.IGNORECASE),
|
|
40
|
+
re.compile(r"^Basic\s+\S+", re.IGNORECASE),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_sensitive_header(name: str) -> bool:
|
|
45
|
+
return name.lower() in _SENSITIVE_HEADERS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def mask_headers(headers: Dict[str, str]) -> Dict[str, str]:
|
|
49
|
+
"""Return a copy of *headers* with sensitive values replaced by ``***MASKED***``."""
|
|
50
|
+
return {
|
|
51
|
+
k: (_MASKED if _is_sensitive_header(k) else v)
|
|
52
|
+
for k, v in headers.items()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _mask_value(value: Any) -> Any:
|
|
57
|
+
"""Mask a single string value if it looks like a token."""
|
|
58
|
+
if not isinstance(value, str):
|
|
59
|
+
return value
|
|
60
|
+
for pattern in _TOKEN_PATTERNS:
|
|
61
|
+
if pattern.match(value):
|
|
62
|
+
return _MASKED
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _mask_dict_values(data: Any) -> Any:
|
|
67
|
+
"""Recursively mask token-like string values inside *data*."""
|
|
68
|
+
if isinstance(data, dict):
|
|
69
|
+
return {k: _mask_dict_values(v) for k, v in data.items()}
|
|
70
|
+
if isinstance(data, list):
|
|
71
|
+
return [_mask_dict_values(item) for item in data]
|
|
72
|
+
return _mask_value(data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mask_sensitive(
|
|
76
|
+
headers: Optional[Dict[str, str]] = None,
|
|
77
|
+
body: Any = None,
|
|
78
|
+
query_params: Optional[Dict[str, Any]] = None,
|
|
79
|
+
) -> tuple:
|
|
80
|
+
"""Apply all masking rules and return ``(masked_headers, masked_body, masked_params)``.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
headers:
|
|
85
|
+
HTTP headers dict (may be ``None``).
|
|
86
|
+
body:
|
|
87
|
+
Request/response body (any JSON-serialisable type).
|
|
88
|
+
query_params:
|
|
89
|
+
URL query parameters dict (may be ``None``).
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
tuple
|
|
94
|
+
``(masked_headers, masked_body, masked_query_params)`` with all
|
|
95
|
+
sensitive values replaced by ``***MASKED***``.
|
|
96
|
+
"""
|
|
97
|
+
masked_hdrs = mask_headers(headers or {})
|
|
98
|
+
masked_body = _mask_dict_values(body)
|
|
99
|
+
masked_params = _mask_dict_values(query_params) if query_params else query_params
|
|
100
|
+
return masked_hdrs, masked_body, masked_params
|