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,442 @@
|
|
|
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
|
+
"""Reflection and canary analysis for browser-level security scenarios.
|
|
7
|
+
|
|
8
|
+
After the :class:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator`
|
|
9
|
+
executes a security scenario, this module inspects the task results to detect:
|
|
10
|
+
|
|
11
|
+
1. **XSS reflection** — the raw payload appears in the DOM response.
|
|
12
|
+
2. **SQL/error-pattern detection** — database error strings (reusing
|
|
13
|
+
``_STACK_TRACE_MARKERS`` from ``unsafe_consumption.py``) appear in the page.
|
|
14
|
+
3. **Canary token reflection** — the unique canary embedded alongside the
|
|
15
|
+
payload appears in unexpected response locations (headers, body, error text).
|
|
16
|
+
4. **SSRF indicators** — internal service response signatures in the output.
|
|
17
|
+
|
|
18
|
+
Results are returned as :class:`~mannf.product.security.models.SecurityFinding`
|
|
19
|
+
instances so they slot directly into the existing security report pipeline.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Error / stack-trace markers (mirrors unsafe_consumption._STACK_TRACE_MARKERS)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_STACK_TRACE_MARKERS: list[str] = [
|
|
35
|
+
"Traceback",
|
|
36
|
+
"at line",
|
|
37
|
+
"Exception in thread",
|
|
38
|
+
"NullPointerException",
|
|
39
|
+
"stack trace",
|
|
40
|
+
"ORA-",
|
|
41
|
+
"SQL syntax",
|
|
42
|
+
"mysql_fetch",
|
|
43
|
+
"pg_query",
|
|
44
|
+
"SQLiteException",
|
|
45
|
+
"SQLSTATE",
|
|
46
|
+
"syntax error",
|
|
47
|
+
"unterminated quoted string",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# DOM patterns that signal successful XSS execution
|
|
51
|
+
_XSS_EXECUTION_INDICATORS: list[str] = [
|
|
52
|
+
"alert(",
|
|
53
|
+
"onerror=",
|
|
54
|
+
"onload=",
|
|
55
|
+
"<script>",
|
|
56
|
+
"javascript:",
|
|
57
|
+
"eval(",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Patterns that suggest SSRF success
|
|
61
|
+
_SSRF_RESPONSE_MARKERS: list[str] = [
|
|
62
|
+
"instance-id",
|
|
63
|
+
"ami-id",
|
|
64
|
+
"169.254.169.254",
|
|
65
|
+
"localhost responded",
|
|
66
|
+
"127.0.0.1",
|
|
67
|
+
"Connection refused",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# ReflectionAnalyzer
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ReflectionAnalyzer:
|
|
77
|
+
"""Inspect orchestrator task results for security payload execution evidence.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
security_scenarios:
|
|
82
|
+
The list of security scenario task dicts that were executed (each
|
|
83
|
+
should have a ``metadata`` key with ``canary``, ``payload_value``,
|
|
84
|
+
``payload_type``, ``target_field``, ``cwe_id``, and
|
|
85
|
+
``owasp_category``).
|
|
86
|
+
task_results:
|
|
87
|
+
The raw result items from the orchestrator report (list of dicts with
|
|
88
|
+
at least ``task_id``, ``passed``, ``error``, and optionally ``dom_snapshot``
|
|
89
|
+
and ``page_url``).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
security_scenarios: list[dict[str, Any]],
|
|
95
|
+
task_results: list[dict[str, Any]],
|
|
96
|
+
) -> None:
|
|
97
|
+
self._scenarios = security_scenarios
|
|
98
|
+
self._results = task_results
|
|
99
|
+
# Index results by task_id for fast lookup
|
|
100
|
+
self._result_map: dict[str, dict[str, Any]] = {
|
|
101
|
+
r.get("task_id", ""): r for r in task_results
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Public API
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def analyze(self) -> list[dict[str, Any]]:
|
|
109
|
+
"""Run all reflection checks and return a list of finding dicts.
|
|
110
|
+
|
|
111
|
+
Each finding dict matches the :class:`~mannf.product.security.models.SecurityFinding`
|
|
112
|
+
dataclass field layout so it can be instantiated directly.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
list[dict]
|
|
117
|
+
Zero or more security finding dicts, each with fields:
|
|
118
|
+
``check_id``, ``check_name``, ``severity``, ``endpoint``,
|
|
119
|
+
``title``, ``description``, ``evidence``, ``remediation``,
|
|
120
|
+
``cwe_id``, ``confidence``.
|
|
121
|
+
"""
|
|
122
|
+
findings: list[dict[str, Any]] = []
|
|
123
|
+
|
|
124
|
+
for scenario in self._scenarios:
|
|
125
|
+
meta = scenario.get("metadata", {})
|
|
126
|
+
if meta.get("scenario_type") != "security":
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
canary = meta.get("canary", "")
|
|
130
|
+
payload_value = meta.get("payload_value", "")
|
|
131
|
+
payload_type = meta.get("payload_type", "")
|
|
132
|
+
target_field = meta.get("target_field", "")
|
|
133
|
+
cwe_id = meta.get("cwe_id", "CWE-74")
|
|
134
|
+
owasp_category = meta.get("owasp_category", "")
|
|
135
|
+
check_id = meta.get("check_id", "BROWSER-SECURITY")
|
|
136
|
+
page_url = scenario.get("url", "")
|
|
137
|
+
|
|
138
|
+
# Find the matching result (match by url + target_field or by order)
|
|
139
|
+
result = self._find_result(scenario)
|
|
140
|
+
if result is None:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
dom = result.get("dom_snapshot", "")
|
|
144
|
+
error_text = result.get("error", "") or ""
|
|
145
|
+
combined_text = f"{dom}\n{error_text}"
|
|
146
|
+
|
|
147
|
+
finding = self._check_scenario(
|
|
148
|
+
check_id=check_id,
|
|
149
|
+
page_url=page_url,
|
|
150
|
+
target_field=target_field,
|
|
151
|
+
payload_type=payload_type,
|
|
152
|
+
payload_value=payload_value,
|
|
153
|
+
canary=canary,
|
|
154
|
+
cwe_id=cwe_id,
|
|
155
|
+
owasp_category=owasp_category,
|
|
156
|
+
combined_text=combined_text,
|
|
157
|
+
result=result,
|
|
158
|
+
)
|
|
159
|
+
if finding:
|
|
160
|
+
findings.append(finding)
|
|
161
|
+
|
|
162
|
+
return findings
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Internal checks
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _check_scenario(
|
|
169
|
+
self,
|
|
170
|
+
check_id: str,
|
|
171
|
+
page_url: str,
|
|
172
|
+
target_field: str,
|
|
173
|
+
payload_type: str,
|
|
174
|
+
payload_value: str,
|
|
175
|
+
canary: str,
|
|
176
|
+
cwe_id: str,
|
|
177
|
+
owasp_category: str,
|
|
178
|
+
combined_text: str,
|
|
179
|
+
result: dict[str, Any],
|
|
180
|
+
) -> dict[str, Any] | None:
|
|
181
|
+
"""Evaluate a single scenario result for security evidence."""
|
|
182
|
+
evidence_parts: list[str] = []
|
|
183
|
+
confidence = 0.0
|
|
184
|
+
severity = "medium"
|
|
185
|
+
|
|
186
|
+
# 1. Canary reflection check
|
|
187
|
+
if canary and canary in combined_text:
|
|
188
|
+
evidence_parts.append(f"Canary token '{canary}' reflected in DOM/error output")
|
|
189
|
+
confidence = max(confidence, 0.7)
|
|
190
|
+
|
|
191
|
+
# 2. Raw payload reflection
|
|
192
|
+
if payload_value and payload_value in combined_text:
|
|
193
|
+
evidence_parts.append(f"Payload reflected verbatim: {payload_value[:80]!r}")
|
|
194
|
+
confidence = max(confidence, 0.75)
|
|
195
|
+
|
|
196
|
+
# 3. XSS execution indicators
|
|
197
|
+
if payload_type.startswith("xss"):
|
|
198
|
+
for indicator in _XSS_EXECUTION_INDICATORS:
|
|
199
|
+
if indicator.lower() in combined_text.lower():
|
|
200
|
+
evidence_parts.append(
|
|
201
|
+
f"XSS execution indicator found: {indicator!r}"
|
|
202
|
+
)
|
|
203
|
+
confidence = max(confidence, 0.85)
|
|
204
|
+
severity = "high"
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
# 4. SQL / stack-trace error detection
|
|
208
|
+
if payload_type.startswith("sql") or payload_type.startswith("nosql"):
|
|
209
|
+
for marker in _STACK_TRACE_MARKERS:
|
|
210
|
+
if marker.lower() in combined_text.lower():
|
|
211
|
+
evidence_parts.append(f"Database error marker: {marker!r}")
|
|
212
|
+
confidence = max(confidence, 0.80)
|
|
213
|
+
severity = "high"
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# 5. SSRF indicators
|
|
217
|
+
if payload_type.startswith("ssrf"):
|
|
218
|
+
for marker in _SSRF_RESPONSE_MARKERS:
|
|
219
|
+
if marker.lower() in combined_text.lower():
|
|
220
|
+
evidence_parts.append(f"SSRF response marker: {marker!r}")
|
|
221
|
+
confidence = max(confidence, 0.90)
|
|
222
|
+
severity = "critical"
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
# 6. HTTP 500 on injection payload
|
|
226
|
+
status_code = result.get("status_code")
|
|
227
|
+
if status_code == 500 and payload_type.startswith(("sql", "nosql", "xss")):
|
|
228
|
+
evidence_parts.append("HTTP 500 returned after injection payload")
|
|
229
|
+
confidence = max(confidence, 0.60)
|
|
230
|
+
|
|
231
|
+
if not evidence_parts:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
label_hint = target_field or page_url
|
|
235
|
+
title = _make_title(payload_type, label_hint)
|
|
236
|
+
description = _make_description(payload_type, target_field, page_url, owasp_category)
|
|
237
|
+
remediation = _make_remediation(payload_type)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"check_id": check_id,
|
|
241
|
+
"check_name": _check_name(payload_type),
|
|
242
|
+
"severity": severity,
|
|
243
|
+
"endpoint": f"BROWSER {page_url}#{target_field}",
|
|
244
|
+
"title": title,
|
|
245
|
+
"description": description,
|
|
246
|
+
"evidence": "; ".join(evidence_parts),
|
|
247
|
+
"remediation": remediation,
|
|
248
|
+
"cwe_id": cwe_id,
|
|
249
|
+
"confidence": round(confidence, 2),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
# Result lookup
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
def _find_result(self, scenario: dict[str, Any]) -> dict[str, Any] | None:
|
|
257
|
+
"""Find the task result that corresponds to *scenario*."""
|
|
258
|
+
task_id = scenario.get("task_id", "")
|
|
259
|
+
if task_id and task_id in self._result_map:
|
|
260
|
+
return self._result_map[task_id]
|
|
261
|
+
|
|
262
|
+
# Fall back to matching by url + target_field metadata
|
|
263
|
+
meta = scenario.get("metadata", {})
|
|
264
|
+
target_field = meta.get("target_field", "")
|
|
265
|
+
page_url = scenario.get("url", "")
|
|
266
|
+
|
|
267
|
+
for result in self._results:
|
|
268
|
+
r_url = result.get("page_url", result.get("url", ""))
|
|
269
|
+
r_field = result.get("target_field", "")
|
|
270
|
+
if r_url == page_url and r_field == target_field:
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
# Last resort: return first result if only one scenario was run
|
|
274
|
+
if len(self._results) == 1:
|
|
275
|
+
return self._results[0]
|
|
276
|
+
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Text-generation helpers
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _check_name(payload_type: str) -> str:
|
|
286
|
+
"""Return a human-readable check name for a payload type."""
|
|
287
|
+
names: dict[str, str] = {
|
|
288
|
+
"xss": "Cross-Site Scripting (XSS)",
|
|
289
|
+
"sql": "SQL Injection",
|
|
290
|
+
"nosql": "NoSQL Injection",
|
|
291
|
+
"path_traversal": "Path Traversal",
|
|
292
|
+
"crlf": "CRLF Injection",
|
|
293
|
+
"proto_pollution": "Prototype Pollution",
|
|
294
|
+
"unicode": "Unicode Normalization Attack",
|
|
295
|
+
"ssrf": "Server-Side Request Forgery (SSRF)",
|
|
296
|
+
"llm_security": "LLM-Generated Security Test",
|
|
297
|
+
}
|
|
298
|
+
for prefix, name in names.items():
|
|
299
|
+
if payload_type.startswith(prefix):
|
|
300
|
+
return name
|
|
301
|
+
return "Security Injection Test"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _make_title(payload_type: str, label_hint: str) -> str:
|
|
305
|
+
"""Return a short finding title."""
|
|
306
|
+
name = _check_name(payload_type)
|
|
307
|
+
hint = label_hint[:60] if label_hint else "unknown field"
|
|
308
|
+
return f"Possible {name} in '{hint}'"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _make_description(
|
|
312
|
+
payload_type: str, target_field: str, page_url: str, owasp_category: str
|
|
313
|
+
) -> str:
|
|
314
|
+
"""Return a detailed finding description."""
|
|
315
|
+
descriptions: dict[str, str] = {
|
|
316
|
+
"xss": (
|
|
317
|
+
"An XSS payload injected into the form field was reflected in the page "
|
|
318
|
+
"DOM without sanitization. An attacker could use this to execute arbitrary "
|
|
319
|
+
"JavaScript in the context of other users' browsers."
|
|
320
|
+
),
|
|
321
|
+
"sql": (
|
|
322
|
+
"A SQL injection payload triggered an observable difference (reflection, "
|
|
323
|
+
"error message, or status code change). The application may be passing "
|
|
324
|
+
"user input directly to a database query without parameterization."
|
|
325
|
+
),
|
|
326
|
+
"nosql": (
|
|
327
|
+
"A NoSQL injection payload produced an observable response change, "
|
|
328
|
+
"suggesting the application may be vulnerable to operator injection attacks."
|
|
329
|
+
),
|
|
330
|
+
"path_traversal": (
|
|
331
|
+
"A path traversal sequence was accepted by the form and may have been "
|
|
332
|
+
"processed by the server, potentially allowing unauthorized file access."
|
|
333
|
+
),
|
|
334
|
+
"crlf": (
|
|
335
|
+
"A CRLF injection sequence was reflected in the response, which could "
|
|
336
|
+
"allow header injection, HTTP response splitting, or log injection."
|
|
337
|
+
),
|
|
338
|
+
"proto_pollution": (
|
|
339
|
+
"A prototype pollution payload was submitted via the form. If the server "
|
|
340
|
+
"merges user-supplied objects without sanitization, this could modify "
|
|
341
|
+
"shared object prototypes and affect application logic."
|
|
342
|
+
),
|
|
343
|
+
"unicode": (
|
|
344
|
+
"A Unicode normalization or homoglyph attack payload was reflected without "
|
|
345
|
+
"normalization. This can be used to bypass security filters or cause "
|
|
346
|
+
"unexpected behavior in string comparison operations."
|
|
347
|
+
),
|
|
348
|
+
"ssrf": (
|
|
349
|
+
"An SSRF payload submitted via a URL-accepting field returned response "
|
|
350
|
+
"content indicating the server made an outbound request to an internal "
|
|
351
|
+
"or metadata endpoint."
|
|
352
|
+
),
|
|
353
|
+
}
|
|
354
|
+
for prefix, desc in descriptions.items():
|
|
355
|
+
if payload_type.startswith(prefix):
|
|
356
|
+
return (
|
|
357
|
+
f"{desc}\n\nField: {target_field}, Page: {page_url}, "
|
|
358
|
+
f"Category: {owasp_category}"
|
|
359
|
+
)
|
|
360
|
+
return (
|
|
361
|
+
f"A security payload in '{target_field}' on {page_url} produced evidence "
|
|
362
|
+
f"of a potential vulnerability. Category: {owasp_category}"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _make_remediation(payload_type: str) -> str:
|
|
367
|
+
"""Return remediation advice for a payload type."""
|
|
368
|
+
remediations: dict[str, str] = {
|
|
369
|
+
"xss": (
|
|
370
|
+
"Encode all user-supplied data before rendering it in HTML (use "
|
|
371
|
+
"context-aware output encoding). Implement a strict Content-Security-Policy "
|
|
372
|
+
"header. Validate and sanitize input server-side."
|
|
373
|
+
),
|
|
374
|
+
"sql": (
|
|
375
|
+
"Use parameterized queries or prepared statements for all database access. "
|
|
376
|
+
"Never concatenate user input into SQL strings. Apply the principle of "
|
|
377
|
+
"least privilege to database accounts."
|
|
378
|
+
),
|
|
379
|
+
"nosql": (
|
|
380
|
+
"Validate and sanitize input before passing to database queries. "
|
|
381
|
+
"Use an allowlist for operator keys. Avoid directly deserializing user "
|
|
382
|
+
"input into query objects."
|
|
383
|
+
),
|
|
384
|
+
"path_traversal": (
|
|
385
|
+
"Canonicalize file paths before use and validate they reside within the "
|
|
386
|
+
"expected directory. Reject paths containing '..' sequences. Use an "
|
|
387
|
+
"allowlist of permitted file names."
|
|
388
|
+
),
|
|
389
|
+
"crlf": (
|
|
390
|
+
"Sanitize all user-supplied input before including it in HTTP headers or "
|
|
391
|
+
"log entries. Strip or encode CR (\\r) and LF (\\n) characters."
|
|
392
|
+
),
|
|
393
|
+
"proto_pollution": (
|
|
394
|
+
"Avoid using Object.assign or spread operators with user-supplied objects "
|
|
395
|
+
"without sanitization. Use Object.create(null) for lookup maps. Freeze "
|
|
396
|
+
"Object.prototype in security-sensitive contexts."
|
|
397
|
+
),
|
|
398
|
+
"unicode": (
|
|
399
|
+
"Normalize all user input to a canonical Unicode form (NFC/NFKC) before "
|
|
400
|
+
"comparison or storage. Apply security-aware string comparisons that are "
|
|
401
|
+
"not susceptible to homoglyph attacks."
|
|
402
|
+
),
|
|
403
|
+
"ssrf": (
|
|
404
|
+
"Validate and allowlist URLs accepted from user input. Block requests to "
|
|
405
|
+
"loopback, link-local, and metadata service addresses. Use a dedicated "
|
|
406
|
+
"egress proxy that enforces network-level controls."
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
for prefix, rem in remediations.items():
|
|
410
|
+
if payload_type.startswith(prefix):
|
|
411
|
+
return rem
|
|
412
|
+
return (
|
|
413
|
+
"Validate, sanitize, and encode all user-supplied input. Follow OWASP "
|
|
414
|
+
"Top 10 guidelines for the relevant vulnerability category."
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
# Convenience function for quick analysis
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def analyze_security_results(
|
|
424
|
+
security_scenarios: list[dict[str, Any]],
|
|
425
|
+
task_results: list[dict[str, Any]],
|
|
426
|
+
) -> list[dict[str, Any]]:
|
|
427
|
+
"""Convenience wrapper around :class:`ReflectionAnalyzer`.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
security_scenarios:
|
|
432
|
+
Security scenario task dicts (with security ``metadata``).
|
|
433
|
+
task_results:
|
|
434
|
+
Orchestrator result items.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
list[dict]
|
|
439
|
+
Security finding dicts ready for use with
|
|
440
|
+
:class:`~mannf.product.security.models.SecurityFinding`.
|
|
441
|
+
"""
|
|
442
|
+
return ReflectionAnalyzer(security_scenarios, task_results).analyze()
|