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,695 @@
|
|
|
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
|
+
"""Browser-level security scenario generator.
|
|
7
|
+
|
|
8
|
+
Produces task dicts compatible with
|
|
9
|
+
:meth:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator.run`
|
|
10
|
+
that exercise XSS, SQL injection, path traversal, CRLF injection, prototype
|
|
11
|
+
pollution, Unicode normalization attacks, and SSRF payloads through the
|
|
12
|
+
browser form submission path.
|
|
13
|
+
|
|
14
|
+
Reuses existing payload sets from the API-level security checks
|
|
15
|
+
(``mannf.product.security.checks``) and adds new browser-specific categories.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import uuid
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Payload catalogues
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# XSS payloads — various bypass vectors
|
|
31
|
+
_XSS_PAYLOADS: list[tuple[str, str]] = [
|
|
32
|
+
("xss_basic", "<script>alert('xss')</script>"),
|
|
33
|
+
("xss_img", "<img src=x onerror=alert(1)>"),
|
|
34
|
+
("xss_svg", "<svg onload=alert(1)>"),
|
|
35
|
+
("xss_javascript_uri", "javascript:alert(document.domain)"),
|
|
36
|
+
("xss_event", "\" onmouseover=\"alert(1)"),
|
|
37
|
+
("xss_template", "{{7*7}}"), # server-side template injection probe
|
|
38
|
+
("xss_html_entity", "<script>alert(1)</script>"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# SQL injection payloads
|
|
42
|
+
_SQL_PAYLOADS: list[tuple[str, str]] = [
|
|
43
|
+
("sql_or", "' OR '1'='1"),
|
|
44
|
+
("sql_drop", "'; DROP TABLE users; --"),
|
|
45
|
+
("sql_union", "' UNION SELECT null,null,null--"),
|
|
46
|
+
("sql_time", "' OR SLEEP(5)--"),
|
|
47
|
+
("sql_blind", "' AND 1=1--"),
|
|
48
|
+
("nosql", '{"$gt": ""}'),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Path traversal payloads
|
|
52
|
+
_PATH_TRAVERSAL_PAYLOADS: list[tuple[str, str]] = [
|
|
53
|
+
("path_traversal_unix", "../../etc/passwd"),
|
|
54
|
+
("path_traversal_win", "..\\..\\windows\\system32\\drivers\\etc\\hosts"),
|
|
55
|
+
("path_traversal_url", "%2e%2e%2f%2e%2e%2fetc%2fpasswd"),
|
|
56
|
+
("path_traversal_null", "../../etc/passwd\x00.jpg"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# CRLF injection payloads
|
|
60
|
+
_CRLF_PAYLOADS: list[tuple[str, str]] = [
|
|
61
|
+
("crlf_basic", "foo\r\nSet-Cookie: injected=1"),
|
|
62
|
+
("crlf_url_encoded", "foo%0d%0aSet-Cookie:%20injected=1"),
|
|
63
|
+
("crlf_header_split", "value\r\nX-Injected: header"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Prototype pollution payloads (relevant for JSON-consuming endpoints)
|
|
67
|
+
_PROTOTYPE_POLLUTION_PAYLOADS: list[tuple[str, str]] = [
|
|
68
|
+
("proto_pollution_proto", '__proto__[admin]=true'),
|
|
69
|
+
("proto_pollution_constructor", 'constructor[prototype][admin]=true'),
|
|
70
|
+
("proto_pollution_json", '{"__proto__":{"admin":true}}'),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Unicode normalization attack payloads
|
|
74
|
+
_UNICODE_PAYLOADS: list[tuple[str, str]] = [
|
|
75
|
+
("unicode_rtl", "\u202e\u0041\u202c"), # RTL override
|
|
76
|
+
("unicode_fullwidth", "\uff1cscript\uff1ealert(1)\uff1c/script\uff1e"), # fullwidth
|
|
77
|
+
("unicode_zero_width", "adm\u200bin"), # zero-width space inside "admin"
|
|
78
|
+
("unicode_homoglyph", "\u0430\u0440\u0433"), # Cyrillic lookalikes
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# SSRF payloads (for URL-like fields)
|
|
82
|
+
_SSRF_PAYLOADS: list[tuple[str, str]] = [
|
|
83
|
+
("ssrf_localhost", "http://localhost:80"),
|
|
84
|
+
("ssrf_127", "http://127.0.0.1"),
|
|
85
|
+
("ssrf_metadata", "http://169.254.169.254/latest/meta-data/"),
|
|
86
|
+
("ssrf_ipv6", "http://[::1]"),
|
|
87
|
+
("ssrf_0000", "http://0.0.0.0"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Mapping of OWASP categories to payload groups
|
|
91
|
+
_OWASP_CATEGORIES: dict[str, str] = {
|
|
92
|
+
"xss": "A03:2021 — Injection / CWE-79",
|
|
93
|
+
"sql": "A03:2021 — Injection / CWE-89",
|
|
94
|
+
"nosql": "A03:2021 — Injection / CWE-943",
|
|
95
|
+
"path_traversal": "A01:2021 — Path Traversal / CWE-22",
|
|
96
|
+
"crlf": "A03:2021 — CRLF Injection / CWE-93",
|
|
97
|
+
"proto_pollution": "A08:2021 — Prototype Pollution / CWE-1321",
|
|
98
|
+
"unicode": "A03:2021 — Unicode Attack / CWE-116",
|
|
99
|
+
"ssrf": "A10:2021 — SSRF / CWE-918",
|
|
100
|
+
"grpc_malformed": "A03:2021 — Injection / CWE-20 (gRPC Malformed Protobuf)",
|
|
101
|
+
"grpc_oversized": "A05:2021 — Resource Exhaustion / CWE-400 (gRPC Oversized Message)",
|
|
102
|
+
"grpc_missing": "A03:2021 — Input Validation / CWE-20 (gRPC Missing Required Fields)",
|
|
103
|
+
"grpc_type_confusion": "A03:2021 — Type Confusion / CWE-843 (gRPC Type Mismatch)",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# CWE IDs per payload category prefix
|
|
107
|
+
_CWE_MAP: dict[str, str] = {
|
|
108
|
+
"xss": "CWE-79",
|
|
109
|
+
"sql": "CWE-89",
|
|
110
|
+
"nosql": "CWE-943",
|
|
111
|
+
"path_traversal": "CWE-22",
|
|
112
|
+
"crlf": "CWE-93",
|
|
113
|
+
"proto_pollution": "CWE-1321",
|
|
114
|
+
"unicode": "CWE-116",
|
|
115
|
+
"ssrf": "CWE-918",
|
|
116
|
+
"grpc_malformed": "CWE-20",
|
|
117
|
+
"grpc_oversized": "CWE-400",
|
|
118
|
+
"grpc_missing": "CWE-20",
|
|
119
|
+
"grpc_type_confusion": "CWE-843",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# gRPC-specific fuzz payloads
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
# Malformed protobuf wire bytes — truncated, invalid wire types, etc.
|
|
127
|
+
_GRPC_MALFORMED_PAYLOADS: list[tuple[str, bytes]] = [
|
|
128
|
+
("grpc_malformed_truncated", b"\x0a\x05he"), # truncated length-delimited
|
|
129
|
+
("grpc_malformed_invalid_wire", b"\xff\xff\xff\xff"), # invalid wire type
|
|
130
|
+
("grpc_malformed_varint_overflow", b"\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"), # overlong varint
|
|
131
|
+
("grpc_malformed_empty", b""), # empty payload
|
|
132
|
+
("grpc_malformed_random", bytes(range(256))), # all byte values
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Oversized message payloads (serialised as JSON-encoded strings for the dynamic stub)
|
|
136
|
+
_GRPC_OVERSIZED_PAYLOADS: list[tuple[str, str]] = [
|
|
137
|
+
("grpc_oversized_string", "A" * 1024 * 1024), # 1 MB string field
|
|
138
|
+
("grpc_oversized_nested", '{"nested": ' + '{"a":1}' * 10000 + '}'), # deep nesting
|
|
139
|
+
("grpc_oversized_array", str(list(range(100_000)))), # large repeated field
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Missing required fields — empty dict triggers server-side validation
|
|
143
|
+
_GRPC_MISSING_FIELD_PAYLOAD = {}
|
|
144
|
+
|
|
145
|
+
# Type confusion payloads — send wrong types for fields
|
|
146
|
+
_GRPC_TYPE_CONFUSION_PAYLOADS: list[tuple[str, dict]] = [
|
|
147
|
+
("grpc_type_confusion_string_as_int", {"id": "not_an_integer"}),
|
|
148
|
+
("grpc_type_confusion_bool_as_string", {"active": "yes"}),
|
|
149
|
+
("grpc_type_confusion_null_required", {"name": None}),
|
|
150
|
+
("grpc_type_confusion_array_as_scalar", {"count": [1, 2, 3]}),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# URL-like field names that should receive SSRF payloads
|
|
155
|
+
_URL_FIELD_HINTS = {
|
|
156
|
+
"url", "uri", "href", "link", "src", "source", "redirect",
|
|
157
|
+
"callback", "webhook", "endpoint", "target", "host", "domain", "website",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Fields that should NOT receive injection payloads (hidden, file, etc.)
|
|
161
|
+
_SKIP_TYPES = {"hidden", "file", "submit", "button", "reset", "image"}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _payload_category(payload_type: str) -> str:
|
|
165
|
+
"""Return the OWASP category label for a given payload type prefix."""
|
|
166
|
+
for prefix, category in _OWASP_CATEGORIES.items():
|
|
167
|
+
if payload_type.startswith(prefix):
|
|
168
|
+
return category
|
|
169
|
+
return "A03:2021 — Injection"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _cwe_for(payload_type: str) -> str:
|
|
173
|
+
"""Return the CWE identifier for a given payload type prefix."""
|
|
174
|
+
for prefix, cwe in _CWE_MAP.items():
|
|
175
|
+
if payload_type.startswith(prefix):
|
|
176
|
+
return cwe
|
|
177
|
+
return "CWE-74"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_url_field(field: Any) -> bool:
|
|
181
|
+
"""Return True when a field appears to accept URLs (for SSRF probing)."""
|
|
182
|
+
if field.type in ("url",):
|
|
183
|
+
return True
|
|
184
|
+
# Check if any hint keyword appears as a substring in the field metadata
|
|
185
|
+
candidates = [
|
|
186
|
+
(field.name or "").lower(),
|
|
187
|
+
(field.label or "").lower(),
|
|
188
|
+
(field.placeholder or "").lower(),
|
|
189
|
+
]
|
|
190
|
+
for candidate in candidates:
|
|
191
|
+
for hint in _URL_FIELD_HINTS:
|
|
192
|
+
if hint in candidate:
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# SecurityScenarioGenerator
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class SecurityScenarioGenerator:
|
|
203
|
+
"""Generate browser-level security test scenarios from a :class:`DiscoveryModel`.
|
|
204
|
+
|
|
205
|
+
Each scenario targets a single form field with a specific attack payload.
|
|
206
|
+
After the form is submitted the :class:`~mannf.core.browser.reflection_analyzer.ReflectionAnalyzer`
|
|
207
|
+
post-processes the result to determine whether the payload executed, was
|
|
208
|
+
reflected, or triggered an error pattern.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
discovery_model:
|
|
213
|
+
The structured page graph produced by :class:`WebCrawlerAgent`.
|
|
214
|
+
llm_provider:
|
|
215
|
+
Optional LLM provider for adaptive payload generation. When set,
|
|
216
|
+
additional field-specific payloads are generated via
|
|
217
|
+
``UI_SECURITY_PAYLOAD_PROMPT``.
|
|
218
|
+
max_payloads_per_field:
|
|
219
|
+
Maximum number of attack payloads to inject into a single field.
|
|
220
|
+
include_ssrf:
|
|
221
|
+
Whether to include SSRF payloads for URL-like fields.
|
|
222
|
+
include_path_traversal:
|
|
223
|
+
Whether to include path traversal payloads.
|
|
224
|
+
include_crlf:
|
|
225
|
+
Whether to include CRLF injection payloads.
|
|
226
|
+
include_proto_pollution:
|
|
227
|
+
Whether to include prototype pollution payloads.
|
|
228
|
+
include_unicode:
|
|
229
|
+
Whether to include Unicode normalization attack payloads.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
discovery_model: Any,
|
|
235
|
+
*,
|
|
236
|
+
llm_provider: Any | None = None,
|
|
237
|
+
max_payloads_per_field: int = 5,
|
|
238
|
+
include_ssrf: bool = True,
|
|
239
|
+
include_path_traversal: bool = True,
|
|
240
|
+
include_crlf: bool = True,
|
|
241
|
+
include_proto_pollution: bool = True,
|
|
242
|
+
include_unicode: bool = True,
|
|
243
|
+
) -> None:
|
|
244
|
+
self._model = discovery_model
|
|
245
|
+
self._llm = llm_provider
|
|
246
|
+
self._max_per_field = max_payloads_per_field
|
|
247
|
+
self._include_ssrf = include_ssrf
|
|
248
|
+
self._include_path_traversal = include_path_traversal
|
|
249
|
+
self._include_crlf = include_crlf
|
|
250
|
+
self._include_proto_pollution = include_proto_pollution
|
|
251
|
+
self._include_unicode = include_unicode
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# Public API
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def generate(self) -> list[dict[str, Any]]:
|
|
258
|
+
"""Generate security test scenarios from the discovery model.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
list[dict]
|
|
263
|
+
Task dicts compatible with
|
|
264
|
+
:meth:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator.run`.
|
|
265
|
+
"""
|
|
266
|
+
scenarios: list[dict[str, Any]] = []
|
|
267
|
+
for page in self._model.pages:
|
|
268
|
+
for form in page.forms:
|
|
269
|
+
scenarios.extend(self._generate_form_security_scenarios(page, form))
|
|
270
|
+
return scenarios
|
|
271
|
+
|
|
272
|
+
async def generate_async(self) -> list[dict[str, Any]]:
|
|
273
|
+
"""Generate security scenarios, optionally augmented with LLM payloads.
|
|
274
|
+
|
|
275
|
+
LLM calls are best-effort — failures fall back to heuristic payloads.
|
|
276
|
+
"""
|
|
277
|
+
scenarios = self.generate()
|
|
278
|
+
|
|
279
|
+
if self._llm is None or not self._llm.is_available():
|
|
280
|
+
return scenarios
|
|
281
|
+
|
|
282
|
+
llm_scenarios: list[dict[str, Any]] = []
|
|
283
|
+
for page in self._model.pages:
|
|
284
|
+
for form in page.forms:
|
|
285
|
+
try:
|
|
286
|
+
extra = await self._llm_security_payloads(page, form)
|
|
287
|
+
if extra:
|
|
288
|
+
llm_scenarios.extend(
|
|
289
|
+
self._convert_llm_security_to_scenarios(page, form, extra)
|
|
290
|
+
)
|
|
291
|
+
except Exception as exc: # noqa: BLE001
|
|
292
|
+
logger.warning(
|
|
293
|
+
"LLM security-payload call failed for %s: %s", page.url, exc
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return scenarios + llm_scenarios
|
|
297
|
+
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
# Coverage helpers
|
|
300
|
+
# ------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def coverage_categories() -> list[str]:
|
|
304
|
+
"""Return all OWASP categories covered by this generator."""
|
|
305
|
+
return list(_OWASP_CATEGORIES.values())
|
|
306
|
+
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
# Internal — form-level security scenarios
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def _generate_form_security_scenarios(
|
|
312
|
+
self, page: Any, form: Any
|
|
313
|
+
) -> list[dict[str, Any]]:
|
|
314
|
+
"""Produce security scenarios for every injectable field in *form*."""
|
|
315
|
+
scenarios: list[dict[str, Any]] = []
|
|
316
|
+
fillable = [f for f in form.fields if f.type not in _SKIP_TYPES]
|
|
317
|
+
|
|
318
|
+
for field in fillable:
|
|
319
|
+
payloads = self._payloads_for_field(field)
|
|
320
|
+
for payload_type, payload_value in payloads[: self._max_per_field]:
|
|
321
|
+
canary = self._make_canary()
|
|
322
|
+
scenario = self._make_security_scenario(
|
|
323
|
+
page=page,
|
|
324
|
+
form=form,
|
|
325
|
+
fillable=fillable,
|
|
326
|
+
target_field=field,
|
|
327
|
+
payload_type=payload_type,
|
|
328
|
+
payload_value=payload_value,
|
|
329
|
+
canary=canary,
|
|
330
|
+
)
|
|
331
|
+
scenarios.append(scenario)
|
|
332
|
+
|
|
333
|
+
return scenarios
|
|
334
|
+
|
|
335
|
+
def _payloads_for_field(self, field: Any) -> list[tuple[str, str]]:
|
|
336
|
+
"""Return the list of (payload_type, payload_value) pairs for *field*."""
|
|
337
|
+
payloads: list[tuple[str, str]] = []
|
|
338
|
+
|
|
339
|
+
# XSS and SQL are broadly applicable to text-like fields
|
|
340
|
+
if field.type not in ("number", "date", "time", "month", "week", "color"):
|
|
341
|
+
payloads.extend(_XSS_PAYLOADS)
|
|
342
|
+
payloads.extend(_SQL_PAYLOADS)
|
|
343
|
+
|
|
344
|
+
# SSRF for URL-like fields
|
|
345
|
+
if self._include_ssrf and _is_url_field(field):
|
|
346
|
+
payloads.extend(_SSRF_PAYLOADS)
|
|
347
|
+
|
|
348
|
+
# Path traversal for fields that might accept file paths or names
|
|
349
|
+
if self._include_path_traversal and field.type in (
|
|
350
|
+
"text", "textarea", "search", "url"
|
|
351
|
+
):
|
|
352
|
+
payloads.extend(_PATH_TRAVERSAL_PAYLOADS)
|
|
353
|
+
|
|
354
|
+
# CRLF for all text-like fields
|
|
355
|
+
if self._include_crlf and field.type in ("text", "textarea", "search", "email"):
|
|
356
|
+
payloads.extend(_CRLF_PAYLOADS)
|
|
357
|
+
|
|
358
|
+
# Prototype pollution for text fields that might feed JSON
|
|
359
|
+
if self._include_proto_pollution and field.type in ("text", "textarea", "search"):
|
|
360
|
+
payloads.extend(_PROTOTYPE_POLLUTION_PAYLOADS)
|
|
361
|
+
|
|
362
|
+
# Unicode attacks for all text-like fields
|
|
363
|
+
if self._include_unicode and field.type in (
|
|
364
|
+
"text", "textarea", "search", "email", "password"
|
|
365
|
+
):
|
|
366
|
+
payloads.extend(_UNICODE_PAYLOADS)
|
|
367
|
+
|
|
368
|
+
return payloads
|
|
369
|
+
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
# Internal — scenario construction
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
def _make_security_scenario(
|
|
375
|
+
self,
|
|
376
|
+
page: Any,
|
|
377
|
+
form: Any,
|
|
378
|
+
fillable: list[Any],
|
|
379
|
+
target_field: Any,
|
|
380
|
+
payload_type: str,
|
|
381
|
+
payload_value: str,
|
|
382
|
+
canary: str,
|
|
383
|
+
) -> dict[str, Any]:
|
|
384
|
+
"""Build a single security test scenario task dict."""
|
|
385
|
+
steps: list[dict[str, Any]] = [
|
|
386
|
+
{"action": "navigate", "selector": page.url, "value": ""},
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
for field in fillable:
|
|
390
|
+
if field.selector == target_field.selector:
|
|
391
|
+
# Embed canary inside the payload so reflection detection is reliable
|
|
392
|
+
value = f"{payload_value}<!--{canary}-->"
|
|
393
|
+
else:
|
|
394
|
+
value = _safe_fill_value(field)
|
|
395
|
+
if value:
|
|
396
|
+
steps.append({"action": "fill", "selector": field.selector, "value": value})
|
|
397
|
+
|
|
398
|
+
steps.append({"action": "click", "selector": form.submit_selector})
|
|
399
|
+
|
|
400
|
+
label_hint = target_field.label or target_field.name or target_field.selector
|
|
401
|
+
cwe = _cwe_for(payload_type)
|
|
402
|
+
owasp = _payload_category(payload_type)
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
"url": page.url,
|
|
406
|
+
"steps": steps,
|
|
407
|
+
"metadata": {
|
|
408
|
+
"scenario_type": "security",
|
|
409
|
+
"source_page": page.url,
|
|
410
|
+
"source_form_action": form.action,
|
|
411
|
+
"description": (
|
|
412
|
+
f"Security: {payload_type} payload in '{label_hint}' "
|
|
413
|
+
f"({owasp})"
|
|
414
|
+
),
|
|
415
|
+
"generator": "security",
|
|
416
|
+
"payload_type": payload_type,
|
|
417
|
+
"payload_value": payload_value,
|
|
418
|
+
"canary": canary,
|
|
419
|
+
"target_field": target_field.selector,
|
|
420
|
+
"cwe_id": cwe,
|
|
421
|
+
"owasp_category": owasp,
|
|
422
|
+
"check_id": f"BROWSER-{payload_type.upper().split('_')[0]}",
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# ------------------------------------------------------------------
|
|
427
|
+
# Internal — canary tokens
|
|
428
|
+
# ------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _make_canary() -> str:
|
|
432
|
+
"""Generate a unique canary token for reflection detection."""
|
|
433
|
+
return f"NAT-CANARY-{uuid.uuid4().hex[:12].upper()}"
|
|
434
|
+
|
|
435
|
+
# ------------------------------------------------------------------
|
|
436
|
+
# Internal — LLM adaptive payload generation
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
async def _llm_security_payloads(
|
|
440
|
+
self, page: Any, form: Any, n: int = 3
|
|
441
|
+
) -> list[dict[str, Any]] | None:
|
|
442
|
+
"""Call the LLM for field-specific attack payloads."""
|
|
443
|
+
if self._llm is None:
|
|
444
|
+
return None
|
|
445
|
+
from mannf.product.llm.prompts import UI_SECURITY_PAYLOAD_PROMPT # noqa: PLC0415
|
|
446
|
+
|
|
447
|
+
fields_desc = _format_fields_for_llm(form.fields)
|
|
448
|
+
prompt = UI_SECURITY_PAYLOAD_PROMPT.format(
|
|
449
|
+
n=n,
|
|
450
|
+
page_url=page.url,
|
|
451
|
+
form_action=form.action,
|
|
452
|
+
form_method=form.method,
|
|
453
|
+
fields_description=fields_desc,
|
|
454
|
+
)
|
|
455
|
+
import json # noqa: PLC0415
|
|
456
|
+
try:
|
|
457
|
+
raw = await self._llm.generate(prompt)
|
|
458
|
+
return json.loads(raw)
|
|
459
|
+
except Exception as exc: # noqa: BLE001
|
|
460
|
+
logger.warning("LLM security-payload call failed: %s", exc)
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
def _convert_llm_security_to_scenarios(
|
|
464
|
+
self, page: Any, form: Any, results: list[dict[str, Any]]
|
|
465
|
+
) -> list[dict[str, Any]]:
|
|
466
|
+
"""Convert LLM-generated security payload dicts to scenario task dicts."""
|
|
467
|
+
scenarios: list[dict[str, Any]] = []
|
|
468
|
+
fillable = [f for f in form.fields if f.type not in _SKIP_TYPES]
|
|
469
|
+
field_map = {f.selector: f for f in fillable}
|
|
470
|
+
|
|
471
|
+
for item in results:
|
|
472
|
+
if not isinstance(item, dict):
|
|
473
|
+
continue
|
|
474
|
+
inputs = item.get("inputs", {})
|
|
475
|
+
payload_type = item.get("payload_type", "llm_security")
|
|
476
|
+
if not inputs:
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
steps: list[dict[str, Any]] = [
|
|
480
|
+
{"action": "navigate", "selector": page.url, "value": ""},
|
|
481
|
+
]
|
|
482
|
+
canary = self._make_canary()
|
|
483
|
+
target_selector = ""
|
|
484
|
+
|
|
485
|
+
for selector, value in inputs.items():
|
|
486
|
+
if selector not in field_map:
|
|
487
|
+
continue
|
|
488
|
+
if not target_selector:
|
|
489
|
+
target_selector = selector
|
|
490
|
+
value = f"{value}<!--{canary}-->"
|
|
491
|
+
steps.append({"action": "fill", "selector": selector, "value": str(value)})
|
|
492
|
+
|
|
493
|
+
if not target_selector:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
steps.append({"action": "click", "selector": form.submit_selector})
|
|
497
|
+
cwe = _cwe_for(payload_type)
|
|
498
|
+
owasp = _payload_category(payload_type)
|
|
499
|
+
|
|
500
|
+
scenarios.append({
|
|
501
|
+
"url": page.url,
|
|
502
|
+
"steps": steps,
|
|
503
|
+
"metadata": {
|
|
504
|
+
"scenario_type": "security",
|
|
505
|
+
"source_page": page.url,
|
|
506
|
+
"source_form_action": form.action,
|
|
507
|
+
"description": (
|
|
508
|
+
f"LLM-security: {payload_type} — "
|
|
509
|
+
f"{item.get('description', '')}"
|
|
510
|
+
),
|
|
511
|
+
"generator": "llm_security",
|
|
512
|
+
"payload_type": payload_type,
|
|
513
|
+
"canary": canary,
|
|
514
|
+
"target_field": target_selector,
|
|
515
|
+
"cwe_id": cwe,
|
|
516
|
+
"owasp_category": owasp,
|
|
517
|
+
"check_id": f"BROWSER-{payload_type.upper().split('_')[0]}",
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
return scenarios
|
|
522
|
+
|
|
523
|
+
# ------------------------------------------------------------------
|
|
524
|
+
# gRPC-specific fuzz scenario generation
|
|
525
|
+
# ------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
def generate_grpc_fuzz_scenarios(
|
|
528
|
+
self,
|
|
529
|
+
grpc_method: str,
|
|
530
|
+
rpc_type: str = "unary",
|
|
531
|
+
include_malformed: bool = True,
|
|
532
|
+
include_oversized: bool = True,
|
|
533
|
+
include_missing_fields: bool = True,
|
|
534
|
+
include_type_confusion: bool = True,
|
|
535
|
+
max_per_category: int = 3,
|
|
536
|
+
) -> list[dict[str, Any]]:
|
|
537
|
+
"""Generate gRPC-specific security fuzz scenarios for a single RPC method.
|
|
538
|
+
|
|
539
|
+
Produces test case dicts compatible with :class:`GrpcSUT.execute` that
|
|
540
|
+
exercise malformed protobuf bytes, oversized messages, missing required
|
|
541
|
+
fields, and type-confused payloads.
|
|
542
|
+
|
|
543
|
+
Parameters
|
|
544
|
+
----------
|
|
545
|
+
grpc_method:
|
|
546
|
+
Full gRPC method path (e.g. ``"/helloworld.Greeter/SayHello"``).
|
|
547
|
+
rpc_type:
|
|
548
|
+
RPC type: ``"unary"``, ``"server_streaming"``,
|
|
549
|
+
``"client_streaming"``, or ``"bidi_streaming"`` (default: unary).
|
|
550
|
+
include_malformed:
|
|
551
|
+
Include malformed protobuf wire-byte payloads.
|
|
552
|
+
include_oversized:
|
|
553
|
+
Include oversized message payloads.
|
|
554
|
+
include_missing_fields:
|
|
555
|
+
Include empty/missing-required-field payloads.
|
|
556
|
+
include_type_confusion:
|
|
557
|
+
Include type-confused field payloads.
|
|
558
|
+
max_per_category:
|
|
559
|
+
Maximum payloads per category (default: 3).
|
|
560
|
+
|
|
561
|
+
Returns
|
|
562
|
+
-------
|
|
563
|
+
list of dict
|
|
564
|
+
Scenario dicts with ``grpc_method``, ``request``, and ``metadata``.
|
|
565
|
+
"""
|
|
566
|
+
scenarios: list[dict[str, Any]] = []
|
|
567
|
+
|
|
568
|
+
if include_malformed:
|
|
569
|
+
for payload_type, raw_bytes in _GRPC_MALFORMED_PAYLOADS[:max_per_category]:
|
|
570
|
+
cwe = _cwe_for(payload_type)
|
|
571
|
+
owasp = _payload_category(payload_type)
|
|
572
|
+
scenarios.append({
|
|
573
|
+
"grpc_method": grpc_method,
|
|
574
|
+
"rpc_type": rpc_type,
|
|
575
|
+
"request_raw": list(raw_bytes), # JSON-serialisable
|
|
576
|
+
"metadata": {
|
|
577
|
+
"scenario_type": "grpc_security",
|
|
578
|
+
"generator": "security",
|
|
579
|
+
"payload_type": payload_type,
|
|
580
|
+
"description": (
|
|
581
|
+
f"gRPC security: {payload_type} — malformed protobuf bytes "
|
|
582
|
+
f"for {grpc_method}"
|
|
583
|
+
),
|
|
584
|
+
"grpc_method": grpc_method,
|
|
585
|
+
"cwe_id": cwe,
|
|
586
|
+
"owasp_category": owasp,
|
|
587
|
+
"check_id": f"GRPC-{payload_type.upper().split('_')[0]}",
|
|
588
|
+
},
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
if include_oversized:
|
|
592
|
+
for payload_type, oversized_value in _GRPC_OVERSIZED_PAYLOADS[:max_per_category]:
|
|
593
|
+
cwe = _cwe_for(payload_type)
|
|
594
|
+
owasp = _payload_category(payload_type)
|
|
595
|
+
scenarios.append({
|
|
596
|
+
"grpc_method": grpc_method,
|
|
597
|
+
"rpc_type": rpc_type,
|
|
598
|
+
"request": {"data": oversized_value},
|
|
599
|
+
"metadata": {
|
|
600
|
+
"scenario_type": "grpc_security",
|
|
601
|
+
"generator": "security",
|
|
602
|
+
"payload_type": payload_type,
|
|
603
|
+
"description": (
|
|
604
|
+
f"gRPC security: {payload_type} — oversized message "
|
|
605
|
+
f"for {grpc_method}"
|
|
606
|
+
),
|
|
607
|
+
"grpc_method": grpc_method,
|
|
608
|
+
"cwe_id": cwe,
|
|
609
|
+
"owasp_category": owasp,
|
|
610
|
+
"check_id": "GRPC-OVERSIZED",
|
|
611
|
+
},
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
if include_missing_fields:
|
|
615
|
+
cwe = _cwe_for("grpc_missing")
|
|
616
|
+
owasp = _payload_category("grpc_missing")
|
|
617
|
+
scenarios.append({
|
|
618
|
+
"grpc_method": grpc_method,
|
|
619
|
+
"rpc_type": rpc_type,
|
|
620
|
+
"request": _GRPC_MISSING_FIELD_PAYLOAD,
|
|
621
|
+
"metadata": {
|
|
622
|
+
"scenario_type": "grpc_security",
|
|
623
|
+
"generator": "security",
|
|
624
|
+
"payload_type": "grpc_missing_fields",
|
|
625
|
+
"description": (
|
|
626
|
+
f"gRPC security: missing required fields for {grpc_method}"
|
|
627
|
+
),
|
|
628
|
+
"grpc_method": grpc_method,
|
|
629
|
+
"cwe_id": cwe,
|
|
630
|
+
"owasp_category": owasp,
|
|
631
|
+
"check_id": "GRPC-MISSING-FIELDS",
|
|
632
|
+
},
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
if include_type_confusion:
|
|
636
|
+
for payload_type, request_dict in _GRPC_TYPE_CONFUSION_PAYLOADS[:max_per_category]:
|
|
637
|
+
cwe = _cwe_for(payload_type)
|
|
638
|
+
owasp = _payload_category(payload_type)
|
|
639
|
+
scenarios.append({
|
|
640
|
+
"grpc_method": grpc_method,
|
|
641
|
+
"rpc_type": rpc_type,
|
|
642
|
+
"request": request_dict,
|
|
643
|
+
"metadata": {
|
|
644
|
+
"scenario_type": "grpc_security",
|
|
645
|
+
"generator": "security",
|
|
646
|
+
"payload_type": payload_type,
|
|
647
|
+
"description": (
|
|
648
|
+
f"gRPC security: {payload_type} — type confusion "
|
|
649
|
+
f"for {grpc_method}"
|
|
650
|
+
),
|
|
651
|
+
"grpc_method": grpc_method,
|
|
652
|
+
"cwe_id": cwe,
|
|
653
|
+
"owasp_category": owasp,
|
|
654
|
+
"check_id": "GRPC-TYPE-CONFUSION",
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
return scenarios
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _safe_fill_value(field: Any) -> str:
|
|
664
|
+
"""Return a safe, benign fill value for a non-target field."""
|
|
665
|
+
type_defaults: dict[str, str] = {
|
|
666
|
+
"email": "test@example.com",
|
|
667
|
+
"password": "TestP@ss123!",
|
|
668
|
+
"tel": "555-0100",
|
|
669
|
+
"number": "1",
|
|
670
|
+
"date": "2024-01-01",
|
|
671
|
+
"url": "https://example.com",
|
|
672
|
+
"checkbox": "true",
|
|
673
|
+
"select": "",
|
|
674
|
+
"textarea": "test input",
|
|
675
|
+
}
|
|
676
|
+
return type_defaults.get(field.type, "test")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _format_fields_for_llm(fields: list[Any]) -> str:
|
|
680
|
+
"""Format field descriptors for LLM prompts."""
|
|
681
|
+
lines: list[str] = []
|
|
682
|
+
for f in fields:
|
|
683
|
+
parts = [
|
|
684
|
+
f" - selector={f.selector!r}",
|
|
685
|
+
f"type={f.type!r}",
|
|
686
|
+
f"required={f.required}",
|
|
687
|
+
]
|
|
688
|
+
if f.label:
|
|
689
|
+
parts.append(f"label={f.label!r}")
|
|
690
|
+
if f.name:
|
|
691
|
+
parts.append(f"name={f.name!r}")
|
|
692
|
+
if f.pattern:
|
|
693
|
+
parts.append(f"pattern={f.pattern!r}")
|
|
694
|
+
lines.append(", ".join(parts))
|
|
695
|
+
return "\n".join(lines)
|