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
mannf/product/demo.py
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
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
|
+
"""Demo orchestrator for the ``nat demo`` command.
|
|
7
|
+
|
|
8
|
+
Populates the NAT server's in-memory state with a curated set of
|
|
9
|
+
microservice scan results, live agent telemetry, and pre-seeded OWASP
|
|
10
|
+
security findings so that every dashboard tab has content the moment
|
|
11
|
+
the browser opens.
|
|
12
|
+
|
|
13
|
+
Also exposes a standalone ``run_demo()`` entry-point (Issue #140) that
|
|
14
|
+
loads pre-generated sample report data from ``docs/demo/sample-reports/``
|
|
15
|
+
and renders a formatted console summary plus an HTML report.
|
|
16
|
+
|
|
17
|
+
Issue #58 — ``nat demo`` CLI subcommand
|
|
18
|
+
Issue #59 — Curated demo data & "wow factor" scenarios
|
|
19
|
+
Issue #140 — Demo & Landing Page: Marketing-Ready Reporting Examples
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import pathlib
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Curated demo service topology (Issue #59)
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Service names are recognisable to sales prospects; failure/error/latency
|
|
39
|
+
# values are tuned so the risk heatmap tells a compelling story.
|
|
40
|
+
|
|
41
|
+
_DEMO_SERVICES_CONFIG: List[Dict[str, Any]] = [
|
|
42
|
+
{
|
|
43
|
+
"name": "checkout-api",
|
|
44
|
+
"failure_rate": 0.18,
|
|
45
|
+
"error_rate": 0.04,
|
|
46
|
+
"latency_ms": 25,
|
|
47
|
+
"latency_jitter_ms": 5,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
# latency spikes → anomaly detection demo
|
|
51
|
+
"name": "auth-gateway",
|
|
52
|
+
"failure_rate": 0.12,
|
|
53
|
+
"error_rate": 0.03,
|
|
54
|
+
"latency_ms": 35,
|
|
55
|
+
"latency_jitter_ms": 20,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "user-profile-svc",
|
|
59
|
+
"failure_rate": 0.03,
|
|
60
|
+
"error_rate": 0.01,
|
|
61
|
+
"latency_ms": 10,
|
|
62
|
+
"latency_jitter_ms": 5,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
# highest risk → highlighted in risk heatmap
|
|
66
|
+
"name": "payment-processor",
|
|
67
|
+
"failure_rate": 0.22,
|
|
68
|
+
"error_rate": 0.06,
|
|
69
|
+
"latency_ms": 30,
|
|
70
|
+
"latency_jitter_ms": 10,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "inventory-service",
|
|
74
|
+
"failure_rate": 0.08,
|
|
75
|
+
"error_rate": 0.02,
|
|
76
|
+
"latency_ms": 12,
|
|
77
|
+
"latency_jitter_ms": 5,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "notification-svc",
|
|
81
|
+
"failure_rate": 0.04,
|
|
82
|
+
"error_rate": 0.01,
|
|
83
|
+
"latency_ms": 8,
|
|
84
|
+
"latency_jitter_ms": 5,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "search-api",
|
|
88
|
+
"failure_rate": 0.05,
|
|
89
|
+
"error_rate": 0.01,
|
|
90
|
+
"latency_ms": 15,
|
|
91
|
+
"latency_jitter_ms": 5,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "analytics-pipeline",
|
|
95
|
+
"failure_rate": 0.06,
|
|
96
|
+
"error_rate": 0.01,
|
|
97
|
+
"latency_ms": 50,
|
|
98
|
+
"latency_jitter_ms": 10,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Curated OWASP security findings (Issue #59)
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Pre-seeded so the Security tab has content immediately on browser open.
|
|
106
|
+
|
|
107
|
+
_DEMO_SECURITY_FINDINGS: List[Dict[str, Any]] = [
|
|
108
|
+
{
|
|
109
|
+
"check_id": "API1",
|
|
110
|
+
"check_name": "Broken Object Level Authorization",
|
|
111
|
+
"severity": "critical",
|
|
112
|
+
"endpoint": "GET /checkout-api/orders/{order_id}",
|
|
113
|
+
"title": "BOLA — Order enumeration allows cross-user data access",
|
|
114
|
+
"description": (
|
|
115
|
+
"The order retrieval endpoint does not verify that the "
|
|
116
|
+
"requesting user owns the queried order. Any authenticated "
|
|
117
|
+
"user can access orders belonging to other users by "
|
|
118
|
+
"incrementing the order_id path parameter."
|
|
119
|
+
),
|
|
120
|
+
"evidence": (
|
|
121
|
+
"User A successfully retrieved order data owned by User B "
|
|
122
|
+
"by requesting GET /checkout-api/orders/10042 with User A "
|
|
123
|
+
"credentials. HTTP 200 with full order payload returned."
|
|
124
|
+
),
|
|
125
|
+
"remediation": (
|
|
126
|
+
"Add an ownership check in the order retrieval handler: "
|
|
127
|
+
"verify that order.user_id == authenticated_user.id before "
|
|
128
|
+
"returning the response. Return HTTP 403 on mismatch."
|
|
129
|
+
),
|
|
130
|
+
"cwe_id": "CWE-639",
|
|
131
|
+
"confidence": 0.90,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"check_id": "API2",
|
|
135
|
+
"check_name": "Broken Authentication",
|
|
136
|
+
"severity": "critical",
|
|
137
|
+
"endpoint": "POST /auth-gateway/token",
|
|
138
|
+
"title": "Expired JWT tokens accepted without expiry validation",
|
|
139
|
+
"description": (
|
|
140
|
+
"The auth-gateway accepts bearer tokens that have passed "
|
|
141
|
+
"their expiry timestamp (exp claim). Expired tokens remain "
|
|
142
|
+
"valid indefinitely, allowing long-term session hijacking "
|
|
143
|
+
"after credential theft."
|
|
144
|
+
),
|
|
145
|
+
"evidence": (
|
|
146
|
+
"HTTP 200 returned for a request authenticated with a JWT "
|
|
147
|
+
"whose exp claim was set 24 hours in the past. The token "
|
|
148
|
+
"was accepted and a new session was created."
|
|
149
|
+
),
|
|
150
|
+
"remediation": (
|
|
151
|
+
"Enforce strict JWT expiry validation on every inbound "
|
|
152
|
+
"request. Reject tokens with exp < now() with HTTP 401. "
|
|
153
|
+
"Consider adding a short-lived refresh token flow."
|
|
154
|
+
),
|
|
155
|
+
"cwe_id": "CWE-287",
|
|
156
|
+
"confidence": 0.92,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"check_id": "API3",
|
|
160
|
+
"check_name": "Broken Object Property Level Authorization",
|
|
161
|
+
"severity": "high",
|
|
162
|
+
"endpoint": "GET /user-profile-svc/users/{id}",
|
|
163
|
+
"title": "Sensitive fields exposed to unprivileged users",
|
|
164
|
+
"description": (
|
|
165
|
+
"The user profile endpoint returns sensitive internal fields "
|
|
166
|
+
"(ssn_hash, internal_id, admin_flags) that should only be "
|
|
167
|
+
"visible to administrators. Standard authenticated users "
|
|
168
|
+
"receive these fields in the response body."
|
|
169
|
+
),
|
|
170
|
+
"evidence": (
|
|
171
|
+
"Standard user GET /user-profile-svc/users/42 response body "
|
|
172
|
+
"contains: internal_id, ssn_hash, admin_flags=false. "
|
|
173
|
+
"Fields are present regardless of caller role."
|
|
174
|
+
),
|
|
175
|
+
"remediation": (
|
|
176
|
+
"Apply field-level access control using a role-based field "
|
|
177
|
+
"filter. Return only permitted fields per caller role. "
|
|
178
|
+
"Use an allowlist approach rather than a blocklist."
|
|
179
|
+
),
|
|
180
|
+
"cwe_id": "CWE-213",
|
|
181
|
+
"confidence": 0.85,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"check_id": "API4",
|
|
185
|
+
"check_name": "Unrestricted Resource Consumption",
|
|
186
|
+
"severity": "medium",
|
|
187
|
+
"endpoint": "POST /payment-processor/charges",
|
|
188
|
+
"title": "No rate limiting on payment charge endpoint",
|
|
189
|
+
"description": (
|
|
190
|
+
"The payment processor charge endpoint has no rate limiting "
|
|
191
|
+
"or request throttling. An attacker can submit unlimited "
|
|
192
|
+
"charge attempts in rapid succession, enabling DoS attacks "
|
|
193
|
+
"and brute-force card validation."
|
|
194
|
+
),
|
|
195
|
+
"evidence": (
|
|
196
|
+
"500 sequential POST /payment-processor/charges requests "
|
|
197
|
+
"completed without any 429 Too Many Requests response or "
|
|
198
|
+
"observable throttling. All requests processed at full speed."
|
|
199
|
+
),
|
|
200
|
+
"remediation": (
|
|
201
|
+
"Implement per-user rate limiting on the payment endpoint "
|
|
202
|
+
"(e.g. 10 requests/minute). Return HTTP 429 with "
|
|
203
|
+
"Retry-After header on breach. Add exponential backoff "
|
|
204
|
+
"for repeated failures."
|
|
205
|
+
),
|
|
206
|
+
"cwe_id": "CWE-770",
|
|
207
|
+
"confidence": 0.88,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"check_id": "API8",
|
|
211
|
+
"check_name": "Security Misconfiguration",
|
|
212
|
+
"severity": "high",
|
|
213
|
+
"endpoint": "GET /analytics-pipeline/health",
|
|
214
|
+
"title": "Debug mode enabled — stack traces in production responses",
|
|
215
|
+
"description": (
|
|
216
|
+
"The analytics-pipeline service has debug mode enabled in "
|
|
217
|
+
"production. Error responses include full Python stack "
|
|
218
|
+
"traces, sys.path contents, and environment variable names, "
|
|
219
|
+
"leaking sensitive infrastructure details."
|
|
220
|
+
),
|
|
221
|
+
"evidence": (
|
|
222
|
+
"Triggered a 500 error by sending a malformed request. "
|
|
223
|
+
"Response body contained: full traceback, module paths "
|
|
224
|
+
"revealing framework versions, and partial env var names."
|
|
225
|
+
),
|
|
226
|
+
"remediation": (
|
|
227
|
+
"Disable debug mode in production (DEBUG=False). Configure "
|
|
228
|
+
"a generic error handler that returns sanitised messages "
|
|
229
|
+
"and logs the full detail server-side only."
|
|
230
|
+
),
|
|
231
|
+
"cwe_id": "CWE-1004",
|
|
232
|
+
"confidence": 0.95,
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Browser-level security findings (Phase 6.2 — SecurityScenarioGenerator)
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Pre-seeded so the Browser Security tab has content immediately on browser open.
|
|
240
|
+
|
|
241
|
+
_DEMO_BROWSER_SECURITY_FINDINGS: List[Dict[str, Any]] = [
|
|
242
|
+
{
|
|
243
|
+
"check_id": "BROWSER-XSS",
|
|
244
|
+
"check_name": "Cross-Site Scripting (XSS)",
|
|
245
|
+
"severity": "high",
|
|
246
|
+
"endpoint": "BROWSER /login#input[name='username']",
|
|
247
|
+
"title": "Possible Cross-Site Scripting (XSS) in 'input[name='username']'",
|
|
248
|
+
"description": (
|
|
249
|
+
"An XSS payload injected into the username form field was reflected "
|
|
250
|
+
"in the page DOM without sanitization. An attacker could use this to "
|
|
251
|
+
"execute arbitrary JavaScript in the context of other users' browsers.\n\n"
|
|
252
|
+
"Field: input[name='username'], Page: /login, "
|
|
253
|
+
"Category: A03:2021 — Injection / CWE-79"
|
|
254
|
+
),
|
|
255
|
+
"evidence": (
|
|
256
|
+
"Payload reflected verbatim: '<script>alert(\\'xss\\')</script>'; "
|
|
257
|
+
"XSS execution indicator found: '<script>'"
|
|
258
|
+
),
|
|
259
|
+
"remediation": (
|
|
260
|
+
"Encode all user-supplied data before rendering it in HTML (use "
|
|
261
|
+
"context-aware output encoding). Implement a strict Content-Security-Policy "
|
|
262
|
+
"header. Validate and sanitize input server-side."
|
|
263
|
+
),
|
|
264
|
+
"cwe_id": "CWE-79",
|
|
265
|
+
"confidence": 0.85,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"check_id": "BROWSER-SQL",
|
|
269
|
+
"check_name": "SQL Injection",
|
|
270
|
+
"severity": "high",
|
|
271
|
+
"endpoint": "BROWSER /search#input[name='q']",
|
|
272
|
+
"title": "Possible SQL Injection in 'input[name='q']'",
|
|
273
|
+
"description": (
|
|
274
|
+
"A SQL injection payload triggered an observable difference in the "
|
|
275
|
+
"page response. The application may be passing user input directly "
|
|
276
|
+
"to a database query without parameterization.\n\n"
|
|
277
|
+
"Field: input[name='q'], Page: /search, "
|
|
278
|
+
"Category: A03:2021 — Injection / CWE-89"
|
|
279
|
+
),
|
|
280
|
+
"evidence": (
|
|
281
|
+
"Database error marker: 'SQL syntax'; "
|
|
282
|
+
"Canary token 'NAT-CANARY-A1B2C3D4E5F6' reflected in DOM/error output"
|
|
283
|
+
),
|
|
284
|
+
"remediation": (
|
|
285
|
+
"Use parameterized queries or prepared statements for all database access. "
|
|
286
|
+
"Never concatenate user input into SQL strings. Apply the principle of "
|
|
287
|
+
"least privilege to database accounts."
|
|
288
|
+
),
|
|
289
|
+
"cwe_id": "CWE-89",
|
|
290
|
+
"confidence": 0.80,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"check_id": "BROWSER-PATH_TRAVERSAL",
|
|
294
|
+
"check_name": "Path Traversal",
|
|
295
|
+
"severity": "medium",
|
|
296
|
+
"endpoint": "BROWSER /upload#input[name='filename']",
|
|
297
|
+
"title": "Possible Path Traversal in 'input[name='filename']'",
|
|
298
|
+
"description": (
|
|
299
|
+
"A path traversal sequence was accepted by the form and may have been "
|
|
300
|
+
"processed by the server, potentially allowing unauthorized file access.\n\n"
|
|
301
|
+
"Field: input[name='filename'], Page: /upload, "
|
|
302
|
+
"Category: A01:2021 — Path Traversal / CWE-22"
|
|
303
|
+
),
|
|
304
|
+
"evidence": (
|
|
305
|
+
"Payload reflected verbatim: '../../etc/passwd'; "
|
|
306
|
+
"Canary token 'NAT-CANARY-F6E5D4C3B2A1' reflected in DOM/error output"
|
|
307
|
+
),
|
|
308
|
+
"remediation": (
|
|
309
|
+
"Canonicalize file paths before use and validate they reside within the "
|
|
310
|
+
"expected directory. Reject paths containing '..' sequences. Use an "
|
|
311
|
+
"allowlist of permitted file names."
|
|
312
|
+
),
|
|
313
|
+
"cwe_id": "CWE-22",
|
|
314
|
+
"confidence": 0.70,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"check_id": "BROWSER-SSRF",
|
|
318
|
+
"check_name": "Server-Side Request Forgery (SSRF)",
|
|
319
|
+
"severity": "critical",
|
|
320
|
+
"endpoint": "BROWSER /profile#input[name='avatar_url']",
|
|
321
|
+
"title": "Possible Server-Side Request Forgery (SSRF) in 'input[name='avatar_url']'",
|
|
322
|
+
"description": (
|
|
323
|
+
"An SSRF payload submitted via a URL-accepting field returned response "
|
|
324
|
+
"content indicating the server made an outbound request to an internal "
|
|
325
|
+
"or metadata endpoint.\n\n"
|
|
326
|
+
"Field: input[name='avatar_url'], Page: /profile, "
|
|
327
|
+
"Category: A10:2021 — SSRF / CWE-918"
|
|
328
|
+
),
|
|
329
|
+
"evidence": (
|
|
330
|
+
"SSRF response marker: '169.254.169.254'; "
|
|
331
|
+
"Canary token 'NAT-CANARY-1A2B3C4D5E6F' reflected in DOM/error output"
|
|
332
|
+
),
|
|
333
|
+
"remediation": (
|
|
334
|
+
"Validate and allowlist URLs accepted from user input. Block requests to "
|
|
335
|
+
"loopback, link-local, and metadata service addresses. Use a dedicated "
|
|
336
|
+
"egress proxy that enforces network-level controls."
|
|
337
|
+
),
|
|
338
|
+
"cwe_id": "CWE-918",
|
|
339
|
+
"confidence": 0.90,
|
|
340
|
+
},
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Demo scan runner
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
async def run_demo_scan(port: int = 8080) -> None:
|
|
350
|
+
"""Populate the server's in-memory state with curated demo data.
|
|
351
|
+
|
|
352
|
+
This coroutine is designed to run inside the **same** asyncio event loop
|
|
353
|
+
as the uvicorn server so that WebSocket telemetry broadcasts reach any
|
|
354
|
+
connected browser clients in real-time.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
port:
|
|
359
|
+
The port the NAT server is listening on (used for base_url labels).
|
|
360
|
+
"""
|
|
361
|
+
import mannf.product.server as _srv
|
|
362
|
+
from mannf.core.distributed.system_under_test import (
|
|
363
|
+
MockDistributedSystem,
|
|
364
|
+
_ServiceConfig,
|
|
365
|
+
)
|
|
366
|
+
from mannf.core.nat_orchestrator import NATOrchestrator
|
|
367
|
+
from mannf.product.dashboard import telemetry
|
|
368
|
+
|
|
369
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
370
|
+
base_url = f"http://localhost:{port}"
|
|
371
|
+
|
|
372
|
+
# ----------------------------------------------------------------
|
|
373
|
+
# 1. Register a functional scan entry (History + Overview tabs)
|
|
374
|
+
# ----------------------------------------------------------------
|
|
375
|
+
scan_id = str(uuid.uuid4())
|
|
376
|
+
_srv._scans[scan_id] = {
|
|
377
|
+
"scan_id": scan_id,
|
|
378
|
+
"status": _srv.ScanStatus.RUNNING,
|
|
379
|
+
"created_at": now_iso,
|
|
380
|
+
"spec_name": "demo-microservices",
|
|
381
|
+
"base_url": base_url,
|
|
382
|
+
"summary": None,
|
|
383
|
+
"results": [],
|
|
384
|
+
"error": None,
|
|
385
|
+
"completed_at": None,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# ----------------------------------------------------------------
|
|
389
|
+
# 2. Pre-seed OWASP security findings (Security tab)
|
|
390
|
+
# ----------------------------------------------------------------
|
|
391
|
+
sec_scan_id = str(uuid.uuid4())
|
|
392
|
+
_srv._security_scans[sec_scan_id] = {
|
|
393
|
+
"scan_id": sec_scan_id,
|
|
394
|
+
"status": "completed",
|
|
395
|
+
"created_at": now_iso,
|
|
396
|
+
"target_url": base_url,
|
|
397
|
+
"report": {
|
|
398
|
+
"scan_id": sec_scan_id,
|
|
399
|
+
"timestamp": now_iso,
|
|
400
|
+
"target_url": base_url,
|
|
401
|
+
"total_endpoints_scanned": len(_DEMO_SERVICES_CONFIG),
|
|
402
|
+
"findings": _DEMO_SECURITY_FINDINGS,
|
|
403
|
+
"summary": {
|
|
404
|
+
"total_findings": len(_DEMO_SECURITY_FINDINGS),
|
|
405
|
+
"critical": sum(
|
|
406
|
+
1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "critical"
|
|
407
|
+
),
|
|
408
|
+
"high": sum(
|
|
409
|
+
1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "high"
|
|
410
|
+
),
|
|
411
|
+
"medium": sum(
|
|
412
|
+
1 for f in _DEMO_SECURITY_FINDINGS if f["severity"] == "medium"
|
|
413
|
+
),
|
|
414
|
+
"low": 0,
|
|
415
|
+
"info": 0,
|
|
416
|
+
},
|
|
417
|
+
"belief_guided": True,
|
|
418
|
+
"prioritization": [],
|
|
419
|
+
},
|
|
420
|
+
"error": None,
|
|
421
|
+
"completed_at": now_iso,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# ----------------------------------------------------------------
|
|
425
|
+
# 3. Build demo MockDistributedSystem with curated services
|
|
426
|
+
# ----------------------------------------------------------------
|
|
427
|
+
demo_services = [_ServiceConfig(**cfg) for cfg in _DEMO_SERVICES_CONFIG]
|
|
428
|
+
sut = MockDistributedSystem(services=demo_services, seed=42)
|
|
429
|
+
|
|
430
|
+
# ----------------------------------------------------------------
|
|
431
|
+
# 4. Create NATOrchestrator wired to server's shared data layer
|
|
432
|
+
# ----------------------------------------------------------------
|
|
433
|
+
risk_scorer = _srv._get_risk_scorer()
|
|
434
|
+
anomaly_detector = _srv._get_anomaly_detector()
|
|
435
|
+
|
|
436
|
+
orch = NATOrchestrator(
|
|
437
|
+
sut,
|
|
438
|
+
num_planners=2,
|
|
439
|
+
num_executors=4,
|
|
440
|
+
num_analyzers=2,
|
|
441
|
+
max_contracts=120,
|
|
442
|
+
suite_name="NAT Demo — Microservices Platform",
|
|
443
|
+
log_interval=20,
|
|
444
|
+
bid_timeout_s=0.03,
|
|
445
|
+
anomaly_detector=anomaly_detector,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Broadcast initial agent state so the Agents tab populates before
|
|
449
|
+
# the scan completes (first snapshot; beliefs start at 0.5 priors).
|
|
450
|
+
all_agents = (
|
|
451
|
+
orch._planners + orch._executors + orch._analyzers + [orch._coordinator]
|
|
452
|
+
)
|
|
453
|
+
for agent in all_agents:
|
|
454
|
+
await telemetry.broadcast_agent_state(agent)
|
|
455
|
+
|
|
456
|
+
# ----------------------------------------------------------------
|
|
457
|
+
# 5. Run the orchestrator — agents collaborate via ECNP, results
|
|
458
|
+
# flow through the shared anomaly detector and risk scorer.
|
|
459
|
+
# ----------------------------------------------------------------
|
|
460
|
+
logger.info("Demo scan %s: starting NATOrchestrator", scan_id)
|
|
461
|
+
suite = await orch.run()
|
|
462
|
+
|
|
463
|
+
# ----------------------------------------------------------------
|
|
464
|
+
# 6. Broadcast final agent states (beliefs updated after full run)
|
|
465
|
+
# ----------------------------------------------------------------
|
|
466
|
+
for agent in all_agents:
|
|
467
|
+
await telemetry.broadcast_agent_state(agent)
|
|
468
|
+
|
|
469
|
+
# Also feed orchestrator's risk scorer results into the server's
|
|
470
|
+
# shared risk scorer so the risk heatmap is populated.
|
|
471
|
+
for entry in orch.get_risk_report():
|
|
472
|
+
endpoint = entry.get("endpoint", "")
|
|
473
|
+
risk_score = entry.get("risk_score", 0.0)
|
|
474
|
+
if endpoint:
|
|
475
|
+
# Replay synthetic observations to seed the server scorer
|
|
476
|
+
_srv._get_risk_scorer().record(
|
|
477
|
+
endpoint,
|
|
478
|
+
_make_synthetic_result(endpoint, risk_score),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# ----------------------------------------------------------------
|
|
482
|
+
# 7. Build raw results list and update scan to COMPLETED
|
|
483
|
+
# ----------------------------------------------------------------
|
|
484
|
+
tc_by_id = {tc.id: tc for tc in suite.cases}
|
|
485
|
+
raw_results = []
|
|
486
|
+
for result in suite.results:
|
|
487
|
+
tc = tc_by_id.get(result.test_case_id)
|
|
488
|
+
target = tc.target if tc is not None else "unknown"
|
|
489
|
+
raw_results.append(
|
|
490
|
+
{
|
|
491
|
+
"test_id": result.test_case_id,
|
|
492
|
+
"target": target,
|
|
493
|
+
"passed": result.passed,
|
|
494
|
+
"execution_time_ms": result.execution_time_ms,
|
|
495
|
+
"error": result.error,
|
|
496
|
+
"actual_output": result.actual_output,
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
s = suite.summary()
|
|
501
|
+
completed_at = datetime.now(timezone.utc).isoformat()
|
|
502
|
+
_srv._scans[scan_id].update(
|
|
503
|
+
{
|
|
504
|
+
"status": _srv.ScanStatus.COMPLETED,
|
|
505
|
+
"completed_at": completed_at,
|
|
506
|
+
"summary": s,
|
|
507
|
+
"results": raw_results,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
logger.info(
|
|
512
|
+
"Demo scan %s complete: total=%d passed=%d failed=%d",
|
|
513
|
+
scan_id,
|
|
514
|
+
s["total"],
|
|
515
|
+
s["passed"],
|
|
516
|
+
s["failed"],
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Broadcast scan completion to any connected WebSocket clients
|
|
520
|
+
from mannf.product.dashboard.app import manager as _ws_mgr
|
|
521
|
+
|
|
522
|
+
await _ws_mgr.broadcast(
|
|
523
|
+
{
|
|
524
|
+
"type": "scan_update",
|
|
525
|
+
"data": {
|
|
526
|
+
"scan_id": scan_id,
|
|
527
|
+
"status": _srv.ScanStatus.COMPLETED,
|
|
528
|
+
"total": s.get("total", 0),
|
|
529
|
+
"passed": s.get("passed", 0),
|
|
530
|
+
"failed": s.get("failed", 0),
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# ---------------------------------------------------------------------------
|
|
537
|
+
# Internal helpers
|
|
538
|
+
# ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _make_synthetic_result(endpoint: str, risk_score: float) -> Any:
|
|
542
|
+
"""Create a minimal :class:`~mannf.core.testing.models.TestResult` that
|
|
543
|
+
encodes the given *risk_score* as a passed/failed outcome for seeding
|
|
544
|
+
the server's shared :class:`~mannf.core.prioritization.risk_scorer.RiskScorer`.
|
|
545
|
+
"""
|
|
546
|
+
from mannf.core.testing.models import TestResult
|
|
547
|
+
|
|
548
|
+
# risk_score ≥ 0.5 → treat as a failure signal
|
|
549
|
+
passed = risk_score < 0.5
|
|
550
|
+
return TestResult(
|
|
551
|
+
test_case_id=str(uuid.uuid4()),
|
|
552
|
+
passed=passed,
|
|
553
|
+
execution_time_ms=10.0,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ---------------------------------------------------------------------------
|
|
558
|
+
# Standalone demo runner (Issue #140)
|
|
559
|
+
# ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
#: Directory that contains pre-generated sample reports.
|
|
562
|
+
_SAMPLE_REPORTS_DIR: pathlib.Path = (
|
|
563
|
+
pathlib.Path(__file__).parent.parent.parent.parent # repo root
|
|
564
|
+
/ "docs"
|
|
565
|
+
/ "demo"
|
|
566
|
+
/ "sample-reports"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
#: HTML report template path.
|
|
570
|
+
_HTML_TEMPLATE_PATH: pathlib.Path = (
|
|
571
|
+
pathlib.Path(__file__).parent.parent.parent.parent # repo root
|
|
572
|
+
/ "docs"
|
|
573
|
+
/ "demo"
|
|
574
|
+
/ "report-template.html"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _load_sample_report(filename: str) -> Optional[Dict[str, Any]]:
|
|
579
|
+
"""Load a single JSON sample report file. Returns *None* on failure."""
|
|
580
|
+
path = _SAMPLE_REPORTS_DIR / filename
|
|
581
|
+
try:
|
|
582
|
+
with path.open(encoding="utf-8") as fh:
|
|
583
|
+
return json.load(fh)
|
|
584
|
+
except FileNotFoundError:
|
|
585
|
+
logger.warning("Sample report not found: %s", path)
|
|
586
|
+
return None
|
|
587
|
+
except json.JSONDecodeError as exc:
|
|
588
|
+
logger.warning("Failed to parse %s: %s", path, exc)
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _severity_counts(findings: List[Dict[str, Any]]) -> Dict[str, int]:
|
|
593
|
+
"""Return a dict of severity → count for a list of security findings."""
|
|
594
|
+
counts: Dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
595
|
+
for f in findings:
|
|
596
|
+
sev = str(f.get("severity", "")).lower()
|
|
597
|
+
if sev in counts:
|
|
598
|
+
counts[sev] += 1
|
|
599
|
+
return counts
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _print_section(title: str, *, width: int = 70) -> None:
|
|
603
|
+
"""Print a formatted section separator to stdout."""
|
|
604
|
+
bar = "─" * width
|
|
605
|
+
print(f"\n{bar}")
|
|
606
|
+
print(f" {title}")
|
|
607
|
+
print(bar)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _print_kv(key: str, value: str, indent: int = 4) -> None:
|
|
611
|
+
pad = " " * indent
|
|
612
|
+
print(f"{pad}{key:<30} {value}")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def run_demo(
|
|
616
|
+
mode: str = "quick",
|
|
617
|
+
output_html: Optional[str] = None,
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Load pre-generated sample report data and print a formatted demo.
|
|
620
|
+
|
|
621
|
+
Parameters
|
|
622
|
+
----------
|
|
623
|
+
mode:
|
|
624
|
+
One of:
|
|
625
|
+
|
|
626
|
+
* ``"quick"`` – Loads data and prints results instantly (default).
|
|
627
|
+
* ``"simulated"`` – Adds realistic delays between phases to mimic a
|
|
628
|
+
live scan in progress (useful for screen recordings).
|
|
629
|
+
* ``"interactive"`` – Pauses between each phase so a presenter can
|
|
630
|
+
narrate before pressing Enter to continue.
|
|
631
|
+
|
|
632
|
+
output_html:
|
|
633
|
+
Optional file path. When provided the standalone HTML report
|
|
634
|
+
template is copied to this path (ready to open in a browser or
|
|
635
|
+
send to a prospect). If *None*, no HTML file is written.
|
|
636
|
+
"""
|
|
637
|
+
delays: Dict[str, float]
|
|
638
|
+
if mode == "simulated":
|
|
639
|
+
delays = {
|
|
640
|
+
"header": 0.5,
|
|
641
|
+
"security": 2.0,
|
|
642
|
+
"functional": 1.5,
|
|
643
|
+
"visual": 1.0,
|
|
644
|
+
"accessibility": 1.2,
|
|
645
|
+
"performance": 1.0,
|
|
646
|
+
"summary": 0.8,
|
|
647
|
+
}
|
|
648
|
+
else:
|
|
649
|
+
delays = {k: 0.0 for k in ("header", "security", "functional", "visual", "accessibility", "performance", "summary")}
|
|
650
|
+
|
|
651
|
+
def _pause(phase: str) -> None:
|
|
652
|
+
delay = delays.get(phase, 0.0)
|
|
653
|
+
if mode == "interactive":
|
|
654
|
+
input(f"\n ↵ Press Enter to continue to {phase}…")
|
|
655
|
+
elif delay > 0:
|
|
656
|
+
time.sleep(delay)
|
|
657
|
+
|
|
658
|
+
# ------------------------------------------------------------------
|
|
659
|
+
# Header
|
|
660
|
+
# ------------------------------------------------------------------
|
|
661
|
+
_pause("header")
|
|
662
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
663
|
+
print("\n" + "═" * 70)
|
|
664
|
+
print(" NAT — Neural Agent Testing Platform")
|
|
665
|
+
print(" Demo Report Viewer · Issue #140")
|
|
666
|
+
print(f" {now}")
|
|
667
|
+
print("═" * 70)
|
|
668
|
+
print(" Target: https://www.demo-store.example.com")
|
|
669
|
+
print(" Suite: Full Platform Audit")
|
|
670
|
+
print(" Mode: " + mode)
|
|
671
|
+
|
|
672
|
+
# ------------------------------------------------------------------
|
|
673
|
+
# Load all reports
|
|
674
|
+
# ------------------------------------------------------------------
|
|
675
|
+
unified = _load_sample_report("unified-report.json") or {}
|
|
676
|
+
security = _load_sample_report("security-scan-report.json") or {}
|
|
677
|
+
functional = _load_sample_report("functional-test-report.json") or {}
|
|
678
|
+
visual = _load_sample_report("visual-regression-report.json") or {}
|
|
679
|
+
a11y = _load_sample_report("accessibility-report.json") or {}
|
|
680
|
+
performance = _load_sample_report("performance-report.json") or {}
|
|
681
|
+
|
|
682
|
+
# ------------------------------------------------------------------
|
|
683
|
+
# Executive summary
|
|
684
|
+
# ------------------------------------------------------------------
|
|
685
|
+
health = unified.get("overall_health_score", "N/A")
|
|
686
|
+
grade = unified.get("health_grade", "?")
|
|
687
|
+
_print_section(f"📋 EXECUTIVE SUMMARY Health Score: {health}/100 ({grade})")
|
|
688
|
+
exec_sum = unified.get("executive_summary", {})
|
|
689
|
+
headline = exec_sum.get("headline", "")
|
|
690
|
+
if headline:
|
|
691
|
+
# Word-wrap at ~64 chars
|
|
692
|
+
words = headline.split()
|
|
693
|
+
line = " "
|
|
694
|
+
for word in words:
|
|
695
|
+
if len(line) + len(word) + 1 > 68:
|
|
696
|
+
print(line)
|
|
697
|
+
line = " " + word + " "
|
|
698
|
+
else:
|
|
699
|
+
line += word + " "
|
|
700
|
+
if line.strip():
|
|
701
|
+
print(line)
|
|
702
|
+
print()
|
|
703
|
+
for action in exec_sum.get("critical_actions", []):
|
|
704
|
+
print(f" ⚠ {action}")
|
|
705
|
+
|
|
706
|
+
# ------------------------------------------------------------------
|
|
707
|
+
# Security
|
|
708
|
+
# ------------------------------------------------------------------
|
|
709
|
+
_pause("security")
|
|
710
|
+
sec_sum = security.get("summary", {})
|
|
711
|
+
_print_section(
|
|
712
|
+
"🔒 SECURITY SCAN"
|
|
713
|
+
f" {sec_sum.get('total_findings', 0)} findings · "
|
|
714
|
+
f"Risk score: {sec_sum.get('risk_score', 'N/A')}"
|
|
715
|
+
)
|
|
716
|
+
_print_kv("Endpoints scanned:", str(security.get("total_endpoints_scanned", "?")))
|
|
717
|
+
_print_kv("Scan duration:", f"{security.get('scan_duration_seconds', '?')}s")
|
|
718
|
+
_print_kv("Critical:", str(sec_sum.get("critical", 0)))
|
|
719
|
+
_print_kv("High:", str(sec_sum.get("high", 0)))
|
|
720
|
+
_print_kv("Medium:", str(sec_sum.get("medium", 0)))
|
|
721
|
+
_print_kv("Low:", str(sec_sum.get("low", 0)))
|
|
722
|
+
print()
|
|
723
|
+
for finding in security.get("findings", [])[:5]:
|
|
724
|
+
sev = finding.get("severity", "?").upper()
|
|
725
|
+
title = finding.get("title", "")
|
|
726
|
+
ep = finding.get("affected_endpoint", "")
|
|
727
|
+
cvss = finding.get("cvss_score", "")
|
|
728
|
+
print(f" [{sev:8s}] CVSS {cvss:4} {title}")
|
|
729
|
+
print(f" {ep}")
|
|
730
|
+
remaining = len(security.get("findings", [])) - 5
|
|
731
|
+
if remaining > 0:
|
|
732
|
+
print(f" … and {remaining} more findings (see security-scan-report.json)")
|
|
733
|
+
|
|
734
|
+
# ------------------------------------------------------------------
|
|
735
|
+
# Functional
|
|
736
|
+
# ------------------------------------------------------------------
|
|
737
|
+
_pause("functional")
|
|
738
|
+
func_sum = functional.get("summary", {})
|
|
739
|
+
total = func_sum.get("total", 0)
|
|
740
|
+
passed = func_sum.get("passed", 0)
|
|
741
|
+
failed = func_sum.get("failed", 0)
|
|
742
|
+
skipped = func_sum.get("skipped", 0)
|
|
743
|
+
pass_rate = func_sum.get("pass_rate", 0)
|
|
744
|
+
_print_section(
|
|
745
|
+
f"✅ FUNCTIONAL TESTS"
|
|
746
|
+
f" {passed}/{total} passed · {failed} failed · {skipped} skipped"
|
|
747
|
+
)
|
|
748
|
+
_print_kv("Pass rate:", f"{pass_rate * 100:.1f}%")
|
|
749
|
+
_print_kv("Duration:", f"{functional.get('duration_seconds', '?')}s")
|
|
750
|
+
print()
|
|
751
|
+
failed_tests = [t for t in functional.get("tests", []) if t.get("status") == "failed"]
|
|
752
|
+
for t in failed_tests:
|
|
753
|
+
print(f" [FAIL] {t['id']} {t['name']}")
|
|
754
|
+
fail = t.get("failure", {})
|
|
755
|
+
if fail.get("reason"):
|
|
756
|
+
print(f" {fail['reason']}")
|
|
757
|
+
|
|
758
|
+
# ------------------------------------------------------------------
|
|
759
|
+
# Visual regression
|
|
760
|
+
# ------------------------------------------------------------------
|
|
761
|
+
_pause("visual")
|
|
762
|
+
vis_sum = visual.get("summary", {})
|
|
763
|
+
_print_section(
|
|
764
|
+
f"🖼 VISUAL REGRESSION"
|
|
765
|
+
f" {vis_sum.get('pages_compared', 0)} pages · "
|
|
766
|
+
f"{vis_sum.get('diffs_detected', 0)} diffs detected"
|
|
767
|
+
)
|
|
768
|
+
for page in visual.get("pages", []):
|
|
769
|
+
if page.get("status") == "diff_detected":
|
|
770
|
+
pct = page.get("diff_percent", 0)
|
|
771
|
+
print(f" [DIFF] {page['name']} ({pct}% pixel diff)")
|
|
772
|
+
for region in page.get("diff_regions", []):
|
|
773
|
+
print(f" · {region['label']}: {region['description'][:60]}…")
|
|
774
|
+
|
|
775
|
+
# ------------------------------------------------------------------
|
|
776
|
+
# Accessibility
|
|
777
|
+
# ------------------------------------------------------------------
|
|
778
|
+
_pause("accessibility")
|
|
779
|
+
a11y_sum = a11y.get("summary", {})
|
|
780
|
+
_print_section(
|
|
781
|
+
f"♿ ACCESSIBILITY (WCAG {a11y.get('wcag_version', '2.1')} {a11y.get('wcag_level', 'AA')})"
|
|
782
|
+
f" Score: {a11y.get('overall_score', '?')}%"
|
|
783
|
+
)
|
|
784
|
+
_print_kv("Violations:", str(a11y_sum.get("critical", 0)))
|
|
785
|
+
_print_kv("Warnings:", str(a11y_sum.get("warnings", 0)))
|
|
786
|
+
_print_kv("Passed checks:", str(a11y_sum.get("passed_checks", 0)))
|
|
787
|
+
print()
|
|
788
|
+
for v in a11y.get("violations", []):
|
|
789
|
+
print(f" [VIOLATION] WCAG {v['wcag_criterion']} {v['issue']}")
|
|
790
|
+
print(f" {v['affected_page']}")
|
|
791
|
+
|
|
792
|
+
# ------------------------------------------------------------------
|
|
793
|
+
# Performance
|
|
794
|
+
# ------------------------------------------------------------------
|
|
795
|
+
_pause("performance")
|
|
796
|
+
perf_sum = performance.get("summary", {})
|
|
797
|
+
_print_section(
|
|
798
|
+
f"⚡ PERFORMANCE"
|
|
799
|
+
f" Overall: {performance.get('overall_performance_score', '?')}/100"
|
|
800
|
+
)
|
|
801
|
+
_print_kv("Pages tested:", str(performance.get("pages", []).__len__()))
|
|
802
|
+
_print_kv("Avg LCP:", f"{perf_sum.get('avg_lcp_ms', '?')} ms")
|
|
803
|
+
_print_kv("Avg CLS:", str(perf_sum.get("avg_cls", "?")))
|
|
804
|
+
_print_kv("Avg TTFB:", f"{perf_sum.get('avg_ttfb_ms', '?')} ms")
|
|
805
|
+
print()
|
|
806
|
+
for page in performance.get("pages", []):
|
|
807
|
+
score = page.get("performance_score", 0)
|
|
808
|
+
grade_char = "✅" if score >= 85 else ("⚠️" if score >= 70 else "❌")
|
|
809
|
+
print(f" {grade_char} {score:3d}/100 {page['page']}")
|
|
810
|
+
|
|
811
|
+
# ------------------------------------------------------------------
|
|
812
|
+
# Final summary
|
|
813
|
+
# ------------------------------------------------------------------
|
|
814
|
+
_pause("summary")
|
|
815
|
+
_print_section("🔗 CROSS-CUTTING INSIGHTS")
|
|
816
|
+
for insight in unified.get("cross_cutting_insights", []):
|
|
817
|
+
sev = insight.get("severity", "info").upper()
|
|
818
|
+
print(f" [{sev}] {insight['insight'][:72]}…")
|
|
819
|
+
|
|
820
|
+
print()
|
|
821
|
+
print("═" * 70)
|
|
822
|
+
print(" ✅ Demo complete. Full data in docs/demo/sample-reports/")
|
|
823
|
+
if output_html:
|
|
824
|
+
print(f" 📄 HTML report → {output_html}")
|
|
825
|
+
print("═" * 70 + "\n")
|
|
826
|
+
|
|
827
|
+
# ------------------------------------------------------------------
|
|
828
|
+
# HTML output
|
|
829
|
+
# ------------------------------------------------------------------
|
|
830
|
+
if output_html:
|
|
831
|
+
_write_html_report(output_html)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _write_html_report(dest: str) -> None:
|
|
835
|
+
"""Copy the standalone HTML report template to *dest*."""
|
|
836
|
+
import shutil
|
|
837
|
+
|
|
838
|
+
dest_path = pathlib.Path(dest)
|
|
839
|
+
try:
|
|
840
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
841
|
+
shutil.copy2(_HTML_TEMPLATE_PATH, dest_path)
|
|
842
|
+
logger.info("HTML report written to %s", dest_path)
|
|
843
|
+
except OSError as exc:
|
|
844
|
+
logger.warning("Could not write HTML report to %s: %s", dest_path, exc)
|