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,80 @@
|
|
|
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
|
+
"""API3:2023 — Broken Object Property Level Authorization (BOPLA / Mass Assignment)."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
|
|
15
|
+
from mannf.product.security.models import SecurityFinding
|
|
16
|
+
|
|
17
|
+
_SENSITIVE_FIELDS = [
|
|
18
|
+
"isAdmin", "role", "permissions", "admin", "verified", "active",
|
|
19
|
+
"is_admin", "is_verified", "is_active", "superuser",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_WRITE_METHODS = {"POST", "PUT", "PATCH"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BOPLACheck(SecurityCheck):
|
|
26
|
+
owasp_id = "API3:2023"
|
|
27
|
+
name = "Broken Object Property Level Authorization"
|
|
28
|
+
description = "Tests for mass-assignment vulnerabilities by injecting unexpected fields."
|
|
29
|
+
severity_default = "high"
|
|
30
|
+
|
|
31
|
+
async def run(
|
|
32
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
33
|
+
) -> list[SecurityFinding]:
|
|
34
|
+
if endpoint.method.upper() not in _WRITE_METHODS:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
payload = {field: True for field in _SENSITIVE_FIELDS}
|
|
38
|
+
payload["role"] = "admin"
|
|
39
|
+
|
|
40
|
+
url = endpoint.full_url
|
|
41
|
+
try:
|
|
42
|
+
resp = await client.request(
|
|
43
|
+
endpoint.method.upper(),
|
|
44
|
+
url,
|
|
45
|
+
content=json.dumps(payload),
|
|
46
|
+
headers={"Content-Type": "application/json"},
|
|
47
|
+
)
|
|
48
|
+
except httpx.RequestError:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
if resp.status_code not in range(200, 300):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
body = resp.text
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
echoed = [f for f in _SENSITIVE_FIELDS if f in body]
|
|
60
|
+
if not echoed:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
self._make_finding(
|
|
65
|
+
endpoint,
|
|
66
|
+
title="Possible mass assignment: sensitive fields echoed in response",
|
|
67
|
+
description=(
|
|
68
|
+
"The endpoint accepted and echoed back privilege-sensitive field(s) "
|
|
69
|
+
f"injected in the request body: {echoed}. "
|
|
70
|
+
"This may indicate a mass-assignment vulnerability."
|
|
71
|
+
),
|
|
72
|
+
evidence=f"Injected fields: {echoed}; Response snippet: {body[:300]}",
|
|
73
|
+
remediation=(
|
|
74
|
+
"Use an explicit allow-list (DTO/schema) for accepted request fields. "
|
|
75
|
+
"Never bind request data directly to internal models."
|
|
76
|
+
),
|
|
77
|
+
cwe_id="CWE-915",
|
|
78
|
+
confidence=0.7,
|
|
79
|
+
)
|
|
80
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
"""API2:2023 — Broken Authentication."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
|
|
13
|
+
from mannf.product.security.models import SecurityFinding
|
|
14
|
+
|
|
15
|
+
_AUTH_PATH_HINTS = (
|
|
16
|
+
"/user", "/account", "/profile", "/order", "/payment",
|
|
17
|
+
"/admin", "/me", "/session", "/private",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_INVALID_TOKENS = [
|
|
21
|
+
None, # no Authorization header
|
|
22
|
+
"Bearer invalid",
|
|
23
|
+
"Bearer ",
|
|
24
|
+
"",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _path_suggests_auth(path: str) -> bool:
|
|
29
|
+
lower = path.lower()
|
|
30
|
+
return any(hint in lower for hint in _AUTH_PATH_HINTS)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BrokenAuthCheck(SecurityCheck):
|
|
34
|
+
owasp_id = "API2:2023"
|
|
35
|
+
name = "Broken Authentication"
|
|
36
|
+
description = "Tests whether authentication is properly enforced on protected endpoints."
|
|
37
|
+
severity_default = "critical"
|
|
38
|
+
|
|
39
|
+
async def run(
|
|
40
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
41
|
+
) -> list[SecurityFinding]:
|
|
42
|
+
if not _path_suggests_auth(endpoint.path):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
findings: list[SecurityFinding] = []
|
|
46
|
+
url = endpoint.full_url
|
|
47
|
+
|
|
48
|
+
for token in _INVALID_TOKENS:
|
|
49
|
+
headers: dict[str, str] = {}
|
|
50
|
+
if token is None:
|
|
51
|
+
label = "no Authorization header"
|
|
52
|
+
elif token == "":
|
|
53
|
+
headers["Authorization"] = ""
|
|
54
|
+
label = "empty Authorization header"
|
|
55
|
+
else:
|
|
56
|
+
headers["Authorization"] = token
|
|
57
|
+
label = f"Authorization: {token}"
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
resp = await client.request(
|
|
61
|
+
endpoint.method.upper(), url, headers=headers
|
|
62
|
+
)
|
|
63
|
+
except httpx.RequestError:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if resp.status_code == 200:
|
|
67
|
+
findings.append(
|
|
68
|
+
self._make_finding(
|
|
69
|
+
endpoint,
|
|
70
|
+
title="Endpoint accessible without valid authentication",
|
|
71
|
+
description=(
|
|
72
|
+
f"The endpoint returned HTTP 200 with {label}. "
|
|
73
|
+
"Protected endpoints should reject unauthenticated requests."
|
|
74
|
+
),
|
|
75
|
+
evidence=f"{endpoint.method.upper()} {url} ({label}) → {resp.status_code}",
|
|
76
|
+
remediation=(
|
|
77
|
+
"Ensure all protected endpoints validate authentication tokens "
|
|
78
|
+
"and return 401 for missing or invalid credentials."
|
|
79
|
+
),
|
|
80
|
+
cwe_id="CWE-306",
|
|
81
|
+
confidence=0.7,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
return findings
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
"""GraphQL-specific OWASP security checks.
|
|
7
|
+
|
|
8
|
+
Each check is a read-only probe that detects a common GraphQL security
|
|
9
|
+
misconfiguration without making any destructive or state-mutating requests.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
|
|
17
|
+
from mannf.product.security.models import SecurityFinding
|
|
18
|
+
|
|
19
|
+
_INTROSPECTION_QUERY = '{ __schema { types { name } } }'
|
|
20
|
+
|
|
21
|
+
_DEPTH_QUERY = (
|
|
22
|
+
"{ a { a { a { a { a { a { a { a { a { a { id } } } } } } } } } } }"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_ALIAS_QUERY = (
|
|
26
|
+
"{ "
|
|
27
|
+
+ " ".join(f"f{i}: __typename" for i in range(20))
|
|
28
|
+
+ " }"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GraphQLIntrospectionCheck(SecurityCheck):
|
|
33
|
+
"""GRAPHQL-1: Check if introspection is enabled in production."""
|
|
34
|
+
|
|
35
|
+
owasp_id = "GRAPHQL-1"
|
|
36
|
+
name = "GraphQL Introspection Exposure"
|
|
37
|
+
description = (
|
|
38
|
+
"GraphQL introspection allows clients to query the entire schema. "
|
|
39
|
+
"Enabling it in production exposes implementation details."
|
|
40
|
+
)
|
|
41
|
+
severity_default = "medium"
|
|
42
|
+
|
|
43
|
+
async def run(
|
|
44
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
45
|
+
) -> list[SecurityFinding]:
|
|
46
|
+
try:
|
|
47
|
+
resp = await client.post(
|
|
48
|
+
endpoint.full_url,
|
|
49
|
+
json={"query": _INTROSPECTION_QUERY},
|
|
50
|
+
)
|
|
51
|
+
except httpx.RequestError:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
body = resp.json()
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
if isinstance(body, dict) and "data" in body and body["data"]:
|
|
60
|
+
schema_data = body["data"].get("__schema")
|
|
61
|
+
if schema_data:
|
|
62
|
+
return [
|
|
63
|
+
self._make_finding(
|
|
64
|
+
endpoint,
|
|
65
|
+
title="GraphQL introspection is enabled",
|
|
66
|
+
description=(
|
|
67
|
+
"The GraphQL endpoint responds to introspection queries, "
|
|
68
|
+
"exposing the full schema including all types, fields, and "
|
|
69
|
+
"queries to any client."
|
|
70
|
+
),
|
|
71
|
+
evidence=(
|
|
72
|
+
f"POST {endpoint.full_url} with introspection query "
|
|
73
|
+
f"returned schema data (HTTP {resp.status_code})"
|
|
74
|
+
),
|
|
75
|
+
remediation=(
|
|
76
|
+
"Disable GraphQL introspection in production environments. "
|
|
77
|
+
"Most GraphQL frameworks provide a configuration option to "
|
|
78
|
+
"disable introspection (e.g. `introspection: false`)."
|
|
79
|
+
),
|
|
80
|
+
cwe_id="CWE-200",
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GraphQLDepthAttackCheck(SecurityCheck):
|
|
88
|
+
"""GRAPHQL-2: Test for query depth limiting."""
|
|
89
|
+
|
|
90
|
+
owasp_id = "GRAPHQL-2"
|
|
91
|
+
name = "GraphQL Query Depth Attack"
|
|
92
|
+
description = (
|
|
93
|
+
"GraphQL servers without depth limits are vulnerable to deeply nested "
|
|
94
|
+
"queries that can exhaust server resources."
|
|
95
|
+
)
|
|
96
|
+
severity_default = "high"
|
|
97
|
+
|
|
98
|
+
async def run(
|
|
99
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
100
|
+
) -> list[SecurityFinding]:
|
|
101
|
+
try:
|
|
102
|
+
resp = await client.post(
|
|
103
|
+
endpoint.full_url,
|
|
104
|
+
json={"query": _DEPTH_QUERY},
|
|
105
|
+
)
|
|
106
|
+
except httpx.RequestError:
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
# Server should reject this with 400 or return errors; 200 without
|
|
110
|
+
# errors suggests no depth limiting is in place.
|
|
111
|
+
if resp.status_code == 200:
|
|
112
|
+
try:
|
|
113
|
+
body = resp.json()
|
|
114
|
+
except Exception:
|
|
115
|
+
body = {}
|
|
116
|
+
|
|
117
|
+
if isinstance(body, dict):
|
|
118
|
+
errors = body.get("errors") or []
|
|
119
|
+
depth_rejected = any(
|
|
120
|
+
"depth" in str(e).lower() or "complex" in str(e).lower()
|
|
121
|
+
for e in errors
|
|
122
|
+
)
|
|
123
|
+
if not depth_rejected:
|
|
124
|
+
return [
|
|
125
|
+
self._make_finding(
|
|
126
|
+
endpoint,
|
|
127
|
+
title="GraphQL query depth limiting not enforced",
|
|
128
|
+
description=(
|
|
129
|
+
"The server accepted a 10-level deeply nested query "
|
|
130
|
+
"without returning a depth-limit error. This can be "
|
|
131
|
+
"exploited to cause excessive CPU/memory usage."
|
|
132
|
+
),
|
|
133
|
+
evidence=(
|
|
134
|
+
f"10-level nested query returned HTTP {resp.status_code} "
|
|
135
|
+
f"without a depth-limit error"
|
|
136
|
+
),
|
|
137
|
+
remediation=(
|
|
138
|
+
"Implement query depth limiting (e.g. graphql-depth-limit). "
|
|
139
|
+
"A maximum depth of 5–7 is recommended for most APIs."
|
|
140
|
+
),
|
|
141
|
+
cwe_id="CWE-400",
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class GraphQLBatchQueryCheck(SecurityCheck):
|
|
149
|
+
"""GRAPHQL-3: Test for batch query limiting."""
|
|
150
|
+
|
|
151
|
+
owasp_id = "GRAPHQL-3"
|
|
152
|
+
name = "GraphQL Batch Query Attack"
|
|
153
|
+
description = (
|
|
154
|
+
"GraphQL batch queries allow multiple operations in a single request. "
|
|
155
|
+
"Without limits this can be abused to amplify attack impact."
|
|
156
|
+
)
|
|
157
|
+
severity_default = "medium"
|
|
158
|
+
|
|
159
|
+
async def run(
|
|
160
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
161
|
+
) -> list[SecurityFinding]:
|
|
162
|
+
batch = [{"query": "{ __typename }"} for _ in range(10)]
|
|
163
|
+
try:
|
|
164
|
+
resp = await client.post(endpoint.full_url, json=batch)
|
|
165
|
+
except httpx.RequestError:
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
if resp.status_code == 200:
|
|
169
|
+
try:
|
|
170
|
+
body = resp.json()
|
|
171
|
+
except Exception:
|
|
172
|
+
body = None
|
|
173
|
+
|
|
174
|
+
if isinstance(body, list) and len(body) > 1:
|
|
175
|
+
return [
|
|
176
|
+
self._make_finding(
|
|
177
|
+
endpoint,
|
|
178
|
+
title="GraphQL batch queries accepted without limits",
|
|
179
|
+
description=(
|
|
180
|
+
"The server processed a batch of 10 GraphQL operations in "
|
|
181
|
+
"a single request without rejecting or limiting the batch size."
|
|
182
|
+
),
|
|
183
|
+
evidence=(
|
|
184
|
+
f"Batch of 10 queries returned HTTP {resp.status_code} "
|
|
185
|
+
f"with {len(body)} results"
|
|
186
|
+
),
|
|
187
|
+
remediation=(
|
|
188
|
+
"Disable batching or enforce a maximum batch size. "
|
|
189
|
+
"Consider using persisted queries to prevent arbitrary batching."
|
|
190
|
+
),
|
|
191
|
+
cwe_id="CWE-400",
|
|
192
|
+
)
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class GraphQLFieldSuggestionCheck(SecurityCheck):
|
|
199
|
+
"""GRAPHQL-4: Check if field suggestions are exposed."""
|
|
200
|
+
|
|
201
|
+
owasp_id = "GRAPHQL-4"
|
|
202
|
+
name = "GraphQL Field Suggestion Exposure"
|
|
203
|
+
description = (
|
|
204
|
+
"Field suggestions in GraphQL error messages disclose valid field names, "
|
|
205
|
+
"aiding attackers in schema enumeration without introspection."
|
|
206
|
+
)
|
|
207
|
+
severity_default = "low"
|
|
208
|
+
|
|
209
|
+
async def run(
|
|
210
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
211
|
+
) -> list[SecurityFinding]:
|
|
212
|
+
# Use a deliberately misspelled field name likely to trigger a suggestion
|
|
213
|
+
typo_query = "{ ussers { id } }"
|
|
214
|
+
try:
|
|
215
|
+
resp = await client.post(endpoint.full_url, json={"query": typo_query})
|
|
216
|
+
except httpx.RequestError:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
body = resp.json()
|
|
221
|
+
except Exception:
|
|
222
|
+
body = {}
|
|
223
|
+
|
|
224
|
+
if isinstance(body, dict):
|
|
225
|
+
errors = body.get("errors") or []
|
|
226
|
+
suggestion_text = " ".join(str(e) for e in errors)
|
|
227
|
+
if "did you mean" in suggestion_text.lower():
|
|
228
|
+
return [
|
|
229
|
+
self._make_finding(
|
|
230
|
+
endpoint,
|
|
231
|
+
title="GraphQL field suggestions exposed in errors",
|
|
232
|
+
description=(
|
|
233
|
+
"The server returns 'Did you mean ...' suggestions in error "
|
|
234
|
+
"messages, which discloses valid field names and aids schema "
|
|
235
|
+
"enumeration even when introspection is disabled."
|
|
236
|
+
),
|
|
237
|
+
evidence=f"Error message contained suggestion text: {suggestion_text[:300]}",
|
|
238
|
+
remediation=(
|
|
239
|
+
"Disable field suggestions in production (e.g. set "
|
|
240
|
+
"`suggestions: false` in your GraphQL server configuration)."
|
|
241
|
+
),
|
|
242
|
+
cwe_id="CWE-209",
|
|
243
|
+
)
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class GraphQLAliasDoSCheck(SecurityCheck):
|
|
250
|
+
"""GRAPHQL-5: Test for alias-based DoS."""
|
|
251
|
+
|
|
252
|
+
owasp_id = "GRAPHQL-5"
|
|
253
|
+
name = "GraphQL Alias-Based DoS"
|
|
254
|
+
description = (
|
|
255
|
+
"Attackers can use GraphQL aliases to repeat the same expensive field "
|
|
256
|
+
"many times in a single query, multiplying server load."
|
|
257
|
+
)
|
|
258
|
+
severity_default = "medium"
|
|
259
|
+
|
|
260
|
+
async def run(
|
|
261
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
262
|
+
) -> list[SecurityFinding]:
|
|
263
|
+
try:
|
|
264
|
+
resp = await client.post(endpoint.full_url, json={"query": _ALIAS_QUERY})
|
|
265
|
+
except httpx.RequestError:
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
if resp.status_code == 200:
|
|
269
|
+
try:
|
|
270
|
+
body = resp.json()
|
|
271
|
+
except Exception:
|
|
272
|
+
body = {}
|
|
273
|
+
|
|
274
|
+
if isinstance(body, dict) and "data" in body and body["data"]:
|
|
275
|
+
# Server processed 20 aliases successfully
|
|
276
|
+
aliases_returned = len(body["data"])
|
|
277
|
+
if aliases_returned > 5:
|
|
278
|
+
return [
|
|
279
|
+
self._make_finding(
|
|
280
|
+
endpoint,
|
|
281
|
+
title="GraphQL alias-based DoS not mitigated",
|
|
282
|
+
description=(
|
|
283
|
+
"The server processed a query with 20 aliases of the same "
|
|
284
|
+
"field without rejecting it. Aliases can be used to amplify "
|
|
285
|
+
"the cost of expensive resolvers."
|
|
286
|
+
),
|
|
287
|
+
evidence=(
|
|
288
|
+
f"Query with 20 aliases returned HTTP {resp.status_code} "
|
|
289
|
+
f"with {aliases_returned} fields in data"
|
|
290
|
+
),
|
|
291
|
+
remediation=(
|
|
292
|
+
"Implement query cost analysis or field-level rate limiting. "
|
|
293
|
+
"Consider using a library such as graphql-query-complexity."
|
|
294
|
+
),
|
|
295
|
+
cwe_id="CWE-400",
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
return []
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
"""API9:2023 — Improper Inventory Management."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
|
|
13
|
+
from mannf.product.security.models import SecurityFinding
|
|
14
|
+
|
|
15
|
+
_SHADOW_PATHS = [
|
|
16
|
+
"/api/v1/docs",
|
|
17
|
+
"/swagger",
|
|
18
|
+
"/swagger-ui",
|
|
19
|
+
"/swagger-ui.html",
|
|
20
|
+
"/api-docs",
|
|
21
|
+
"/openapi.json",
|
|
22
|
+
"/.git/HEAD",
|
|
23
|
+
"/api/v2/",
|
|
24
|
+
"/health",
|
|
25
|
+
"/metrics",
|
|
26
|
+
"/actuator",
|
|
27
|
+
"/actuator/health",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InventoryCheck(SecurityCheck):
|
|
32
|
+
owasp_id = "API9:2023"
|
|
33
|
+
name = "Improper Inventory Management"
|
|
34
|
+
description = "Probes for undocumented/shadow endpoints that should not be exposed."
|
|
35
|
+
severity_default = "medium"
|
|
36
|
+
|
|
37
|
+
async def run(
|
|
38
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
39
|
+
) -> list[SecurityFinding]:
|
|
40
|
+
base = endpoint.base_url.rstrip("/")
|
|
41
|
+
findings: list[SecurityFinding] = []
|
|
42
|
+
|
|
43
|
+
for shadow_path in _SHADOW_PATHS:
|
|
44
|
+
url = f"{base}{shadow_path}"
|
|
45
|
+
try:
|
|
46
|
+
resp = await client.get(url)
|
|
47
|
+
except httpx.RequestError:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if resp.status_code == 200:
|
|
51
|
+
findings.append(
|
|
52
|
+
self._make_finding(
|
|
53
|
+
endpoint,
|
|
54
|
+
title=f"Shadow/undocumented endpoint found: {shadow_path}",
|
|
55
|
+
description=(
|
|
56
|
+
f"The path {shadow_path} returned HTTP 200, indicating an "
|
|
57
|
+
"undocumented or shadow API endpoint is publicly accessible."
|
|
58
|
+
),
|
|
59
|
+
evidence=f"GET {url} → {resp.status_code}",
|
|
60
|
+
remediation=(
|
|
61
|
+
"Remove or restrict access to undocumented endpoints, "
|
|
62
|
+
"debug paths, and outdated API versions in production. "
|
|
63
|
+
"Maintain an up-to-date API inventory."
|
|
64
|
+
),
|
|
65
|
+
cwe_id="CWE-1059",
|
|
66
|
+
confidence=0.9,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return findings
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
"""API8:2023 — Security Misconfiguration."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from mannf.product.security.checks.base import EndpointInfo, SecurityCheck
|
|
13
|
+
from mannf.product.security.models import SecurityFinding
|
|
14
|
+
|
|
15
|
+
_SECURITY_HEADERS = [
|
|
16
|
+
"X-Content-Type-Options",
|
|
17
|
+
"X-Frame-Options",
|
|
18
|
+
"X-XSS-Protection",
|
|
19
|
+
"Content-Security-Policy",
|
|
20
|
+
]
|
|
21
|
+
_HSTS_HEADER = "Strict-Transport-Security"
|
|
22
|
+
|
|
23
|
+
_DEBUG_PATHS = ["/_debug", "/debug", "/api/debug", "/.env"]
|
|
24
|
+
|
|
25
|
+
_STACK_TRACE_MARKERS = [
|
|
26
|
+
"Traceback", "at line", "Exception in thread",
|
|
27
|
+
"NullPointerException", "stack trace", "at com.", "at org.",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MisconfigCheck(SecurityCheck):
|
|
32
|
+
owasp_id = "API8:2023"
|
|
33
|
+
name = "Security Misconfiguration"
|
|
34
|
+
description = "Tests for common security misconfigurations in HTTP responses."
|
|
35
|
+
severity_default = "medium"
|
|
36
|
+
|
|
37
|
+
async def run(
|
|
38
|
+
self, endpoint: EndpointInfo, client: httpx.AsyncClient
|
|
39
|
+
) -> list[SecurityFinding]:
|
|
40
|
+
findings: list[SecurityFinding] = []
|
|
41
|
+
method = endpoint.method.upper()
|
|
42
|
+
url = endpoint.full_url
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
resp = await client.request(method, url)
|
|
46
|
+
except httpx.RequestError:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
# --- Missing security headers ---
|
|
50
|
+
resp_headers = {k.lower(): v for k, v in resp.headers.items()}
|
|
51
|
+
missing = [h for h in _SECURITY_HEADERS if h.lower() not in resp_headers]
|
|
52
|
+
if url.startswith("https://") and _HSTS_HEADER.lower() not in resp_headers:
|
|
53
|
+
missing.append(_HSTS_HEADER)
|
|
54
|
+
if missing:
|
|
55
|
+
findings.append(
|
|
56
|
+
self._make_finding(
|
|
57
|
+
endpoint,
|
|
58
|
+
title="Missing security response headers",
|
|
59
|
+
description=f"The following security headers are absent: {missing}.",
|
|
60
|
+
evidence=f"Response headers: {dict(resp.headers)}",
|
|
61
|
+
remediation=(
|
|
62
|
+
"Configure the server/framework to include all recommended "
|
|
63
|
+
"security headers on every response."
|
|
64
|
+
),
|
|
65
|
+
cwe_id="CWE-16",
|
|
66
|
+
severity="low",
|
|
67
|
+
confidence=0.9,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# --- Overly permissive CORS ---
|
|
72
|
+
evil_origin = "https://evil.example.com"
|
|
73
|
+
try:
|
|
74
|
+
cors_resp = await client.request(
|
|
75
|
+
method, url, headers={"Origin": evil_origin}
|
|
76
|
+
)
|
|
77
|
+
acao = cors_resp.headers.get("Access-Control-Allow-Origin", "")
|
|
78
|
+
if acao == evil_origin or acao == "*":
|
|
79
|
+
findings.append(
|
|
80
|
+
self._make_finding(
|
|
81
|
+
endpoint,
|
|
82
|
+
title="Overly permissive CORS policy",
|
|
83
|
+
description=(
|
|
84
|
+
"The server echoes back an arbitrary Origin or uses wildcard "
|
|
85
|
+
f"'*' in Access-Control-Allow-Origin (got: '{acao}')."
|
|
86
|
+
),
|
|
87
|
+
evidence=(
|
|
88
|
+
f"Request Origin: {evil_origin}; "
|
|
89
|
+
f"Response Access-Control-Allow-Origin: {acao}"
|
|
90
|
+
),
|
|
91
|
+
remediation=(
|
|
92
|
+
"Restrict Access-Control-Allow-Origin to a known allowlist "
|
|
93
|
+
"of trusted origins. Never reflect arbitrary Origin values."
|
|
94
|
+
),
|
|
95
|
+
cwe_id="CWE-942",
|
|
96
|
+
severity="medium",
|
|
97
|
+
confidence=0.9,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
except httpx.RequestError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# --- Verbose error / stack trace exposure ---
|
|
104
|
+
try:
|
|
105
|
+
malformed_url = url + "/%00invalid%00"
|
|
106
|
+
err_resp = await client.request(method, malformed_url)
|
|
107
|
+
body = err_resp.text
|
|
108
|
+
exposed = [m for m in _STACK_TRACE_MARKERS if m in body]
|
|
109
|
+
if exposed:
|
|
110
|
+
findings.append(
|
|
111
|
+
self._make_finding(
|
|
112
|
+
endpoint,
|
|
113
|
+
title="Verbose error / stack trace exposed in response",
|
|
114
|
+
description=(
|
|
115
|
+
"The server returned a response containing stack-trace markers "
|
|
116
|
+
f"({exposed}), which may disclose internal implementation details."
|
|
117
|
+
),
|
|
118
|
+
evidence=f"Markers found: {exposed}; snippet: {body[:300]}",
|
|
119
|
+
remediation=(
|
|
120
|
+
"Configure error handling to return generic error messages in "
|
|
121
|
+
"production. Log stack traces server-side only."
|
|
122
|
+
),
|
|
123
|
+
cwe_id="CWE-209",
|
|
124
|
+
severity="medium",
|
|
125
|
+
confidence=0.8,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
except httpx.RequestError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# --- Debug endpoints ---
|
|
132
|
+
base = endpoint.base_url.rstrip("/")
|
|
133
|
+
for debug_path in _DEBUG_PATHS:
|
|
134
|
+
try:
|
|
135
|
+
d_resp = await client.get(f"{base}{debug_path}")
|
|
136
|
+
if d_resp.status_code == 200:
|
|
137
|
+
findings.append(
|
|
138
|
+
self._make_finding(
|
|
139
|
+
endpoint,
|
|
140
|
+
title=f"Debug endpoint accessible: {debug_path}",
|
|
141
|
+
description=(
|
|
142
|
+
f"The path {debug_path} returned HTTP 200, "
|
|
143
|
+
"suggesting a debug or configuration endpoint is exposed."
|
|
144
|
+
),
|
|
145
|
+
evidence=f"GET {base}{debug_path} → {d_resp.status_code}",
|
|
146
|
+
remediation=(
|
|
147
|
+
"Disable or restrict access to debug and internal endpoints "
|
|
148
|
+
"in production environments."
|
|
149
|
+
),
|
|
150
|
+
cwe_id="CWE-215",
|
|
151
|
+
severity="high",
|
|
152
|
+
confidence=0.8,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
except httpx.RequestError:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
return findings
|