@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
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.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Static-analysis scan for command-injection patterns.
|
|
3
|
+
|
|
4
|
+
References:
|
|
5
|
+
CWE-78 Improper Neutralization of Special Elements used in an OS Command
|
|
6
|
+
OWASP A03:2021 Injection
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
17
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
19
|
+
|
|
20
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
21
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
22
|
+
|
|
23
|
+
SKILL_ID = "detecting-command-injection-patterns"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PY_PATTERNS = [
|
|
27
|
+
(
|
|
28
|
+
"Python subprocess with shell=True + f-string",
|
|
29
|
+
Severity.CRITICAL,
|
|
30
|
+
r"subprocess\.(?:run|call|check_output|check_call|Popen)\s*\(\s*f['\"]",
|
|
31
|
+
"python",
|
|
32
|
+
),
|
|
33
|
+
(
|
|
34
|
+
"Python subprocess with shell=True + concat",
|
|
35
|
+
Severity.CRITICAL,
|
|
36
|
+
r"subprocess\.[a-z_]+\s*\(\s*['\"][^'\"]*['\"]\s*\+\s*\w[^,)]*,[^)]*shell\s*=\s*True",
|
|
37
|
+
"python",
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"Python subprocess shell=True (any form, needs review)",
|
|
41
|
+
Severity.HIGH,
|
|
42
|
+
r"subprocess\.[a-z_]+\s*\([^)]*shell\s*=\s*True",
|
|
43
|
+
"python",
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"Python os.system with f-string / concat",
|
|
47
|
+
Severity.CRITICAL,
|
|
48
|
+
r"os\.system\s*\(\s*(?:f['\"]|['\"][^'\"]*['\"]\s*\+)",
|
|
49
|
+
"python",
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
"Python os.popen with interpolation",
|
|
53
|
+
Severity.CRITICAL,
|
|
54
|
+
r"os\.popen\s*\(\s*(?:f['\"]|['\"][^'\"]*['\"]\s*\+)",
|
|
55
|
+
"python",
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"Python commands.getoutput / getstatusoutput (legacy)",
|
|
59
|
+
Severity.HIGH,
|
|
60
|
+
r"commands\.(?:getoutput|getstatusoutput)\s*\(",
|
|
61
|
+
"python",
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
JS_PATTERNS = [
|
|
66
|
+
(
|
|
67
|
+
"Node child_process.exec with template literal",
|
|
68
|
+
Severity.CRITICAL,
|
|
69
|
+
r"(?:child_process|require\(['\"]child_process['\"]\))\.exec(?:Sync)?\s*\(\s*`[^`]*\$\{",
|
|
70
|
+
"javascript",
|
|
71
|
+
),
|
|
72
|
+
("Node exec with concat", Severity.CRITICAL, r"\.exec(?:Sync)?\s*\(\s*['\"][^'\"]*['\"]\s*\+\s*\w", "javascript"),
|
|
73
|
+
(
|
|
74
|
+
"Node execFile with shell wrapper",
|
|
75
|
+
Severity.HIGH,
|
|
76
|
+
r"\.execFile(?:Sync)?\s*\(\s*['\"](?:sh|bash|/bin/sh)['\"]",
|
|
77
|
+
"javascript",
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"Node spawn with shell:true option",
|
|
81
|
+
Severity.HIGH,
|
|
82
|
+
r"\.spawn(?:Sync)?\s*\([^)]*\{[^}]*shell\s*:\s*true",
|
|
83
|
+
"javascript",
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
RUBY_PATTERNS = [
|
|
88
|
+
("Ruby backticks with interpolation", Severity.CRITICAL, r"`[^`]*#\{[^}]+\}[^`]*`", "ruby"),
|
|
89
|
+
("Ruby Kernel#system with interpolation", Severity.CRITICAL, r"\bsystem\s*\(\s*['\"][^'\"]*#\{", "ruby"),
|
|
90
|
+
("Ruby Kernel#exec with interpolation", Severity.CRITICAL, r"\bexec\s*\(\s*['\"][^'\"]*#\{", "ruby"),
|
|
91
|
+
(
|
|
92
|
+
"Ruby Open3 with shell wrapper interpolation",
|
|
93
|
+
Severity.HIGH,
|
|
94
|
+
r"Open3\.(?:popen|capture)[a-z0-9_]*\s*\(\s*['\"][^'\"]*#\{",
|
|
95
|
+
"ruby",
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
GO_PATTERNS = [
|
|
100
|
+
(
|
|
101
|
+
"Go exec.Command with sh -c + concat",
|
|
102
|
+
Severity.HIGH,
|
|
103
|
+
r"""exec\.Command\s*\(\s*["'](?:sh|bash|/bin/sh|/bin/bash)["']\s*,\s*["']-c["']\s*,\s*[^,)]*\+""",
|
|
104
|
+
"go",
|
|
105
|
+
),
|
|
106
|
+
("Go exec.Command with fmt.Sprintf", Severity.HIGH, r"exec\.Command(?:Context)?\s*\([^)]*fmt\.Sprintf", "go"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
PHP_PATTERNS = [
|
|
110
|
+
(
|
|
111
|
+
"PHP system() / exec() with $-interpolation",
|
|
112
|
+
Severity.CRITICAL,
|
|
113
|
+
r"\b(?:system|exec|passthru|shell_exec|popen|proc_open)\s*\(\s*['\"][^'\"]*\$",
|
|
114
|
+
"php",
|
|
115
|
+
),
|
|
116
|
+
("PHP backticks with $-interpolation", Severity.CRITICAL, r"`[^`]*\$[a-z_][^`]*`", "php"),
|
|
117
|
+
(
|
|
118
|
+
"PHP escapeshellarg missing on user input",
|
|
119
|
+
Severity.MEDIUM,
|
|
120
|
+
r"\b(?:system|exec|passthru|shell_exec)\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)",
|
|
121
|
+
"php",
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
JAVA_PATTERNS = [
|
|
126
|
+
(
|
|
127
|
+
"Java Runtime.exec(String) with concat",
|
|
128
|
+
Severity.HIGH,
|
|
129
|
+
r"Runtime\.getRuntime\(\)\.exec\s*\(\s*['\"][^'\"]*['\"]\s*\+",
|
|
130
|
+
"java",
|
|
131
|
+
),
|
|
132
|
+
(
|
|
133
|
+
"Java ProcessBuilder(String) single-string form",
|
|
134
|
+
Severity.MEDIUM,
|
|
135
|
+
r"new ProcessBuilder\s*\(\s*[a-z_]+(?:\s*\+\s*[a-z_]+)+\s*\)",
|
|
136
|
+
"java",
|
|
137
|
+
),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
LANG_EXT_MAP = {
|
|
142
|
+
"python": {".py"},
|
|
143
|
+
"javascript": {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"},
|
|
144
|
+
"ruby": {".rb"},
|
|
145
|
+
"go": {".go"},
|
|
146
|
+
"java": {".java", ".kt", ".scala"},
|
|
147
|
+
"php": {".php"},
|
|
148
|
+
}
|
|
149
|
+
LANG_PATTERNS = {
|
|
150
|
+
"python": PY_PATTERNS,
|
|
151
|
+
"javascript": JS_PATTERNS,
|
|
152
|
+
"ruby": RUBY_PATTERNS,
|
|
153
|
+
"go": GO_PATTERNS,
|
|
154
|
+
"java": JAVA_PATTERNS,
|
|
155
|
+
"php": PHP_PATTERNS,
|
|
156
|
+
}
|
|
157
|
+
SKIP_DIRS = {
|
|
158
|
+
"node_modules",
|
|
159
|
+
".git",
|
|
160
|
+
"dist",
|
|
161
|
+
"build",
|
|
162
|
+
"target",
|
|
163
|
+
".cache",
|
|
164
|
+
".pnpm-store",
|
|
165
|
+
".venv",
|
|
166
|
+
"venv",
|
|
167
|
+
"__pycache__",
|
|
168
|
+
".astro",
|
|
169
|
+
".next",
|
|
170
|
+
".nuxt",
|
|
171
|
+
"vendor",
|
|
172
|
+
}
|
|
173
|
+
TEST_DIRS = {"tests", "test", "__tests__", "spec", "specs"}
|
|
174
|
+
MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def should_skip_path(path: Path, include_tests: bool) -> bool:
|
|
178
|
+
parts = set(path.parts)
|
|
179
|
+
if parts & SKIP_DIRS:
|
|
180
|
+
return True
|
|
181
|
+
if not include_tests and parts & TEST_DIRS:
|
|
182
|
+
return True
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def detect_language(path: Path, langs: set[str]) -> str | None:
|
|
187
|
+
suf = path.suffix.lower()
|
|
188
|
+
for lang in langs:
|
|
189
|
+
if suf in LANG_EXT_MAP[lang]:
|
|
190
|
+
return lang
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def scan_file(file_path: Path, repo_root: Path, langs: set[str]) -> list[Finding]:
|
|
195
|
+
findings = []
|
|
196
|
+
lang = detect_language(file_path, langs)
|
|
197
|
+
if lang is None:
|
|
198
|
+
return findings
|
|
199
|
+
try:
|
|
200
|
+
if file_path.stat().st_size > MAX_FILE_SIZE:
|
|
201
|
+
return findings
|
|
202
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
203
|
+
except (OSError, ValueError):
|
|
204
|
+
return findings
|
|
205
|
+
try:
|
|
206
|
+
rel = str(file_path.relative_to(repo_root))
|
|
207
|
+
except ValueError:
|
|
208
|
+
rel = str(file_path)
|
|
209
|
+
|
|
210
|
+
for title, sev, pattern, _lang in LANG_PATTERNS[lang]:
|
|
211
|
+
for m in re.finditer(pattern, text, re.IGNORECASE | re.MULTILINE):
|
|
212
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
213
|
+
snippet = text.splitlines()[line_no - 1].strip()[:160]
|
|
214
|
+
findings.append(
|
|
215
|
+
Finding(
|
|
216
|
+
skill_id=SKILL_ID,
|
|
217
|
+
title=f"{title} at {rel}:{line_no}",
|
|
218
|
+
severity=sev,
|
|
219
|
+
target=f"{rel}:{line_no}",
|
|
220
|
+
detail=(
|
|
221
|
+
f"File {rel} line {line_no} matches the {title} "
|
|
222
|
+
f"pattern: `{snippet}`. If the interpolated value "
|
|
223
|
+
"is user-reachable, this is a command-injection vector."
|
|
224
|
+
),
|
|
225
|
+
remediation=(
|
|
226
|
+
"Replace the shell-string call with the argument-vector "
|
|
227
|
+
"form. Python: subprocess.run([cmd, arg], shell=False). "
|
|
228
|
+
"Node: spawn(cmd, [arg], {shell: false}). Ruby: "
|
|
229
|
+
"Open3.capture3(cmd, arg). Go: exec.Command(cmd, arg). "
|
|
230
|
+
"Java: new ProcessBuilder(Arrays.asList(cmd, arg)). "
|
|
231
|
+
"See references/PLAYBOOK.md."
|
|
232
|
+
),
|
|
233
|
+
cwe_id="CWE-78",
|
|
234
|
+
affected_control="OWASP A03:2021",
|
|
235
|
+
evidence=(("file", rel), ("line", line_no), ("language", lang), ("snippet", snippet)),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return findings
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def walk_repo(root: Path, include_tests: bool, langs: set[str]) -> list[Path]:
|
|
242
|
+
out = []
|
|
243
|
+
valid_exts = set()
|
|
244
|
+
for lang in langs:
|
|
245
|
+
valid_exts |= LANG_EXT_MAP[lang]
|
|
246
|
+
for p in root.rglob("*"):
|
|
247
|
+
if not p.is_file():
|
|
248
|
+
continue
|
|
249
|
+
if should_skip_path(p, include_tests):
|
|
250
|
+
continue
|
|
251
|
+
if p.suffix.lower() not in valid_exts:
|
|
252
|
+
continue
|
|
253
|
+
out.append(p)
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(argv: list[str] | None = None) -> int:
|
|
258
|
+
parser = argparse.ArgumentParser(description="Command-injection pattern scanner")
|
|
259
|
+
parser.add_argument("path", type=Path)
|
|
260
|
+
parser.add_argument("--output", default=None)
|
|
261
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
262
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
263
|
+
parser.add_argument("--include-tests", action="store_true")
|
|
264
|
+
parser.add_argument("--languages", default="all")
|
|
265
|
+
args = parser.parse_args(argv)
|
|
266
|
+
|
|
267
|
+
if args.languages == "all":
|
|
268
|
+
langs = set(LANG_PATTERNS.keys())
|
|
269
|
+
else:
|
|
270
|
+
langs = {lang.strip() for lang in args.languages.split(",") if lang.strip() in LANG_PATTERNS}
|
|
271
|
+
|
|
272
|
+
root = args.path.resolve()
|
|
273
|
+
if not root.exists():
|
|
274
|
+
sys.stderr.write(f"ERROR: path does not exist: {root}\n")
|
|
275
|
+
return 2
|
|
276
|
+
|
|
277
|
+
files = walk_repo(root, args.include_tests, langs)
|
|
278
|
+
findings: list[Finding] = []
|
|
279
|
+
for f in files:
|
|
280
|
+
findings.extend(scan_file(f, root, langs))
|
|
281
|
+
|
|
282
|
+
floor = Severity(args.min_severity)
|
|
283
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
284
|
+
|
|
285
|
+
emit(findings, args.output, args.format, str(root))
|
|
286
|
+
return exit_code(findings)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
sys.exit(main())
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: detecting-debug-endpoints
|
|
3
|
+
description: |
|
|
4
|
+
Probe a target for accidentally-public admin / debug / introspection
|
|
5
|
+
endpoints — Spring Boot Actuator, Apache server-status, Prometheus
|
|
6
|
+
metrics, GraphQL playground, Swagger UI, phpMyAdmin, JMX-over-HTTP
|
|
7
|
+
(Jolokia), Elasticsearch _cat, Kibana / Grafana / Eureka / Consul
|
|
8
|
+
panels.
|
|
9
|
+
Use when: post-deploy verification, security audit before SOC2,
|
|
10
|
+
inheriting a system you didn't build, or a bug bounty hints at an
|
|
11
|
+
exposed introspection panel.
|
|
12
|
+
Threshold: any of the canonical 40+ admin/debug paths returns 200,
|
|
13
|
+
302 to a login, or framework-specific JSON shape (e.g., Actuator
|
|
14
|
+
returning a _links object, server-status HTML body containing
|
|
15
|
+
the Apache Server Status title).
|
|
16
|
+
Trigger with: "check debug endpoints", "actuator exposure", "admin
|
|
17
|
+
panel scan", "graphql playground check".
|
|
18
|
+
allowed-tools:
|
|
19
|
+
- Read
|
|
20
|
+
- Bash(python3:*)
|
|
21
|
+
- Bash(curl:*)
|
|
22
|
+
disallowed-tools:
|
|
23
|
+
- Bash(rm:*)
|
|
24
|
+
- Edit(/etc/*)
|
|
25
|
+
version: 3.0.0-dev
|
|
26
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
27
|
+
license: MIT
|
|
28
|
+
compatibility: Designed for Claude Code
|
|
29
|
+
tags:
|
|
30
|
+
- security
|
|
31
|
+
- information-disclosure
|
|
32
|
+
- admin-panels
|
|
33
|
+
- actuator
|
|
34
|
+
- pentest
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Detecting Debug Endpoints
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
Modern web stacks ship rich introspection by default. Spring Boot
|
|
42
|
+
Actuator exposes `/actuator/env` (every environment variable),
|
|
43
|
+
`/actuator/heapdump` (a live heap snapshot that contains credentials),
|
|
44
|
+
`/actuator/jolokia` (JMX bean invocation = pre-auth RCE in some
|
|
45
|
+
configurations). Apache `mod_status` exposes `/server-status` with
|
|
46
|
+
internal IPs, request counts, and the URL of every active request.
|
|
47
|
+
Prometheus `/metrics` exposes operational telemetry that often
|
|
48
|
+
includes connection-string-bearing labels by accident. phpMyAdmin
|
|
49
|
+
exposes the entire database if unauthenticated.
|
|
50
|
+
|
|
51
|
+
These are not bugs in the frameworks. They're features that ship
|
|
52
|
+
enabled-by-default for development convenience and stay enabled in
|
|
53
|
+
production because nobody disabled them at install time. The probe
|
|
54
|
+
set covers the canonical 40+ paths and grades each by the response
|
|
55
|
+
fingerprint specific to that framework.
|
|
56
|
+
|
|
57
|
+
## When the skill produces findings
|
|
58
|
+
|
|
59
|
+
| Finding | Severity | Threshold | Affected control |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| Spring Boot Actuator `/env` exposed | **CRITICAL** | 200 + body has `"propertySources"` | OWASP A05:2021 |
|
|
62
|
+
| Spring Boot Actuator `/heapdump` exposed | **CRITICAL** | 200 + `Content-Type: application/octet-stream` + multi-MB body | CWE-200 |
|
|
63
|
+
| Spring Boot Actuator `/jolokia` exposed | **CRITICAL** | 200 + body has `"agent":"jolokia"` | CWE-749 |
|
|
64
|
+
| phpMyAdmin reachable | **CRITICAL** | 200 + HTML body contains "phpMyAdmin" + login form | OWASP A07:2021 |
|
|
65
|
+
| Prometheus `/metrics` exposed | **HIGH** | 200 + body has `# HELP` or `# TYPE` lines | CWE-200 |
|
|
66
|
+
| Apache `mod_status` exposed | **HIGH** | 200 + body contains "Apache Server Status" | CWE-200 |
|
|
67
|
+
| Spring Boot Actuator `/actuator` index | **HIGH** | 200 + body has `"_links"` JSON | OWASP A05:2021 |
|
|
68
|
+
| Generic `/admin` returning 200 (not 401/403) | **HIGH** | 200 + HTML body with admin-shaped UI | CWE-285 |
|
|
69
|
+
| Elasticsearch `_cat` exposed | **HIGH** | 200 + body matches `health\s+status\s+index` | CWE-200 |
|
|
70
|
+
| GraphQL Playground on prod | **MEDIUM** | 200 + body contains `"GraphQLPlayground"` | CWE-200 |
|
|
71
|
+
| Swagger UI on prod | **MEDIUM** | 200 + body contains `"swagger-ui"` | CWE-200 |
|
|
72
|
+
| Spring Boot Actuator `/health` exposed | **MEDIUM** | 200 + body has `"status":"UP"` | CWE-200 |
|
|
73
|
+
| `phpinfo` page on prod | **MEDIUM** | 200 + body has `PHP Version` heading | CWE-200 |
|
|
74
|
+
| `/robots.txt` discloses admin paths | **LOW** | 200 + `Disallow:` lines mentioning `/admin` | CWE-200 |
|
|
75
|
+
|
|
76
|
+
## Prerequisites
|
|
77
|
+
|
|
78
|
+
- Python 3.9+ with `requests`
|
|
79
|
+
- Authorization for non-local targets
|
|
80
|
+
|
|
81
|
+
## Instructions
|
|
82
|
+
|
|
83
|
+
### Step 1 — Confirm Authorization
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
"Do you have authorization to perform admin / debug endpoint
|
|
87
|
+
discovery on this target? I need confirmation before proceeding."
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Step 2 — Run the scanner
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-debug-endpoints/scripts/probe_debug.py \
|
|
94
|
+
https://target.example.com \
|
|
95
|
+
--authorized
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Usage: probe_debug.py URL [OPTIONS]
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
--authorized Attest authorization (required for non-local)
|
|
105
|
+
--output FILE Write findings to FILE
|
|
106
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
107
|
+
--min-severity SEV (default: info)
|
|
108
|
+
--timeout SECS Per-probe timeout (default: 10)
|
|
109
|
+
--paths-file FILE Override the default probe set with a custom list
|
|
110
|
+
--include-redirects Treat 302/303 to /login as findings (debug panel
|
|
111
|
+
exists but auth gates it — still worth noting)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The scanner sends a GET for each path. For 200 responses, it inspects
|
|
115
|
+
the body for the framework-specific fingerprint to confirm a true
|
|
116
|
+
positive (not the app's SPA index page). For 302 responses to common
|
|
117
|
+
login paths, the panel exists but auth is in front — flagged only
|
|
118
|
+
with `--include-redirects`.
|
|
119
|
+
|
|
120
|
+
### Step 3 — Interpret findings
|
|
121
|
+
|
|
122
|
+
CRITICAL = direct compromise vector (env vars / heapdump / Jolokia /
|
|
123
|
+
phpMyAdmin). Ship same-hour fix: take the endpoint behind authn or
|
|
124
|
+
disable it. Audit for prior exploitation.
|
|
125
|
+
|
|
126
|
+
HIGH = information disclosure substantial enough to drive subsequent
|
|
127
|
+
attacks (server-status reveals request URLs including session tokens
|
|
128
|
+
in query strings; /metrics labels often contain connection strings;
|
|
129
|
+
/admin reachable means brute-force can start).
|
|
130
|
+
|
|
131
|
+
MEDIUM = posture hardening (health checks, swagger).
|
|
132
|
+
|
|
133
|
+
### Step 4 — Cross-skill chaining
|
|
134
|
+
|
|
135
|
+
After this skill, suggest:
|
|
136
|
+
|
|
137
|
+
- `detecting-exposed-secrets-files` (#6) — same deploy mistake. If
|
|
138
|
+
`/server-status` is reachable, `.git/` often is too.
|
|
139
|
+
- `auditing-cors-policy` (#3) — if a GraphQL or admin endpoint is
|
|
140
|
+
reachable AND has open CORS, the attack chain compounds.
|
|
141
|
+
|
|
142
|
+
## Examples
|
|
143
|
+
|
|
144
|
+
### Example 1 — Inheriting a system audit
|
|
145
|
+
|
|
146
|
+
User: "We just acquired example.io. Quick audit of admin surface."
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-debug-endpoints/scripts/probe_debug.py \
|
|
150
|
+
https://example.io --authorized --min-severity medium
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Commonly surfaces forgotten `/server-status` on Apache hosts,
|
|
154
|
+
`/actuator/*` left enabled from Spring Boot defaults, leftover
|
|
155
|
+
`/phpmyadmin` from initial install.
|
|
156
|
+
|
|
157
|
+
### Example 2 — Spring Boot Actuator paranoia sweep
|
|
158
|
+
|
|
159
|
+
User: "We use Spring Boot heavily. Show me everywhere Actuator is
|
|
160
|
+
reachable."
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
for ENDPOINT in $(cat spring-services.txt); do
|
|
164
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-debug-endpoints/scripts/probe_debug.py \
|
|
165
|
+
"$ENDPOINT" --authorized --format jsonl
|
|
166
|
+
done | jq 'select(.title | contains("Actuator"))'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Example 3 — CI gate against accidental re-enablement
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
- name: Debug-endpoint guard
|
|
173
|
+
run: |
|
|
174
|
+
python3 plugins/security/penetration-tester/skills/detecting-debug-endpoints/scripts/probe_debug.py \
|
|
175
|
+
"${{ secrets.STAGING_URL }}" \
|
|
176
|
+
--authorized --min-severity high
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Exit 1 fails the deploy if any HIGH or CRITICAL endpoint exposure
|
|
180
|
+
appears. Catches the regression where a debug profile gets enabled
|
|
181
|
+
in a `application-prod.yml` by accident.
|
|
182
|
+
|
|
183
|
+
## Output
|
|
184
|
+
|
|
185
|
+
JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean, 1
|
|
186
|
+
high/critical, 2 error.
|
|
187
|
+
|
|
188
|
+
## Error Handling
|
|
189
|
+
|
|
190
|
+
- **SPA catches every URL with 200** → use `--check-only` semantics
|
|
191
|
+
(default: fingerprint check filters out SPA matches).
|
|
192
|
+
- **WAF / CDN blocks the scanner** → expected for some targets.
|
|
193
|
+
Coordinate with the target's security team for an allowlist; or
|
|
194
|
+
run the scanner from inside the target's network if you have
|
|
195
|
+
authorized internal access.
|
|
196
|
+
- **Connection error** → exit 2 with underlying error.
|
|
197
|
+
|
|
198
|
+
## Resources
|
|
199
|
+
|
|
200
|
+
- `references/THEORY.md` — Per-framework reasoning: why Actuator,
|
|
201
|
+
mod_status, Prometheus, GraphQL Playground, Swagger, phpMyAdmin
|
|
202
|
+
each matter; canonical fingerprints
|
|
203
|
+
- `references/PLAYBOOK.md` — Per-framework remediation: Spring Boot
|
|
204
|
+
Actuator authn, Apache mod_status `<Location>` deny, Prometheus
|
|
205
|
+
Bearer-token, GraphQL introspection toggle, Swagger profile gate
|
|
206
|
+
- `../analyzing-tls-config/references/AUTHORIZATION.md` — Active-scan
|
|
207
|
+
authorization pattern
|