@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,159 @@
|
|
|
1
|
+
# Eval / Exec Theory
|
|
2
|
+
|
|
3
|
+
## Why this is the highest-impact injection class
|
|
4
|
+
|
|
5
|
+
SQL injection (skill #11) gives the attacker control over a database
|
|
6
|
+
query. Command injection (skill #12) gives the attacker control over
|
|
7
|
+
a shell process. Eval injection gives the attacker control over the
|
|
8
|
+
application's own interpreter, in the application's own process,
|
|
9
|
+
with the application's own permissions.
|
|
10
|
+
|
|
11
|
+
There's no privilege boundary between "user-supplied string" and
|
|
12
|
+
"arbitrary code in the application." `eval()` collapses them. Any
|
|
13
|
+
filtering, allow-list, or input validation that the application
|
|
14
|
+
does happens BEFORE the eval; the eval's interpreter sees the
|
|
15
|
+
filtered string and executes it. If the attacker's payload uses
|
|
16
|
+
language constructs the filter didn't anticipate, the filter is
|
|
17
|
+
bypassed by definition.
|
|
18
|
+
|
|
19
|
+
This is why "don't eval user input" is the only safe rule. There's
|
|
20
|
+
no "but with these escapes it's safe" version.
|
|
21
|
+
|
|
22
|
+
## When eval seems necessary
|
|
23
|
+
|
|
24
|
+
Three legitimate use cases drive the temptation:
|
|
25
|
+
|
|
26
|
+
1. **Formula evaluators.** Spreadsheet-like `=SUM(A1:A5)` cells,
|
|
27
|
+
custom alerting expressions ("alert when latency > 100ms"),
|
|
28
|
+
pricing-rule engines.
|
|
29
|
+
|
|
30
|
+
2. **Plugin systems.** Users supply small scripts that the
|
|
31
|
+
application runs on their behalf. Examples: Lambda@Edge, Cloudflare
|
|
32
|
+
Workers, custom log processors.
|
|
33
|
+
|
|
34
|
+
3. **Configuration as code.** Rare but real: a config file that's
|
|
35
|
+
actually a Python / JS source file evaluated at startup.
|
|
36
|
+
|
|
37
|
+
For case 1: use a sandboxed expression library. `simpleeval` for
|
|
38
|
+
Python, `expr-eval` or `mathjs` for JS, `Dentaku` for Ruby. These
|
|
39
|
+
libraries parse a restricted grammar (math + comparison + a curated
|
|
40
|
+
function set) and execute it on a value model that doesn't have
|
|
41
|
+
filesystem / network / process access.
|
|
42
|
+
|
|
43
|
+
For case 2: run user scripts in a real sandbox: WASM
|
|
44
|
+
(WebAssembly with no system imports), V8 isolate (Node's vm
|
|
45
|
+
module with strict no-globals setup), Lua with stripped-down libs,
|
|
46
|
+
or a containerized worker. Don't run untrusted code in the same
|
|
47
|
+
process as the application.
|
|
48
|
+
|
|
49
|
+
For case 3: don't. Use JSON/YAML/TOML config. The "config as code"
|
|
50
|
+
flexibility argument is rarely worth the eval-injection surface.
|
|
51
|
+
|
|
52
|
+
## Why even allow-list filtering fails
|
|
53
|
+
|
|
54
|
+
A common attempt: "allow only alphanumeric + math operators in the
|
|
55
|
+
evaluated string."
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import re
|
|
59
|
+
if re.match(r"^[\d\+\-\*/\(\)\s\.]+$", user_input):
|
|
60
|
+
result = eval(user_input)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This looks safe. It's not. Python's eval can:
|
|
64
|
+
|
|
65
|
+
- Use `__import__` and `__builtins__` accessors via attribute
|
|
66
|
+
lookup (`().__class__.__bases__[0].__subclasses__()[X]`)
|
|
67
|
+
- Trigger arbitrary code through `__getattr__` on numeric types
|
|
68
|
+
- Call `compile()` on a sub-expression and execute that
|
|
69
|
+
|
|
70
|
+
Attackers have published "polyglot" payloads that look like pure
|
|
71
|
+
math but reach arbitrary functions via Python's metaprogramming.
|
|
72
|
+
The character-class filter is necessary-but-not-sufficient. The
|
|
73
|
+
ONLY safe approach is a separate interpreter that doesn't have
|
|
74
|
+
access to the language's full surface.
|
|
75
|
+
|
|
76
|
+
`ast.literal_eval` is safe — it ONLY parses literals (numbers,
|
|
77
|
+
strings, lists, dicts, tuples, booleans, None). No function calls,
|
|
78
|
+
no name references. Use it when you need to evaluate user-supplied
|
|
79
|
+
literal values; don't use it (or anything like it) for
|
|
80
|
+
expression evaluation more generally.
|
|
81
|
+
|
|
82
|
+
## Per-language safe patterns
|
|
83
|
+
|
|
84
|
+
### Python — simpleeval for expressions
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from simpleeval import simple_eval
|
|
88
|
+
|
|
89
|
+
# Safe: only basic math + curated function set
|
|
90
|
+
result = simple_eval("latency * 1.5 + 10", names={"latency": 80})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or `asteval`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from asteval import Interpreter
|
|
97
|
+
aeval = Interpreter()
|
|
98
|
+
result = aeval("a + b * 2", symtable={"a": 1, "b": 2})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### JavaScript — expr-eval
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const { Parser } = require('expr-eval');
|
|
105
|
+
const parser = new Parser();
|
|
106
|
+
const expr = parser.parse('latency * 1.5 + 10');
|
|
107
|
+
const result = expr.evaluate({ latency: 80 });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Ruby — Dentaku
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
require 'dentaku'
|
|
114
|
+
calc = Dentaku::Calculator.new
|
|
115
|
+
calc.evaluate('latency * 1.5 + 10', latency: 80)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Java — sandboxed Nashorn / GraalJS
|
|
119
|
+
|
|
120
|
+
```java
|
|
121
|
+
// GraalJS with restricted permissions
|
|
122
|
+
Context cx = Context.newBuilder("js")
|
|
123
|
+
.allowHostAccess(HostAccess.NONE)
|
|
124
|
+
.allowHostClassLookup(name -> false)
|
|
125
|
+
.build();
|
|
126
|
+
Value result = cx.eval("js", "1 + 2");
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Avoid plugin-system eval
|
|
130
|
+
|
|
131
|
+
If users need to extend the application with custom logic, the
|
|
132
|
+
right model is:
|
|
133
|
+
|
|
134
|
+
1. Define a narrow API the plugin can call (e.g., "transform this
|
|
135
|
+
payload, return a transformed version").
|
|
136
|
+
2. Run the plugin in an isolated sandbox: WASM with no system
|
|
137
|
+
imports, V8 isolate, separate container, separate language
|
|
138
|
+
runtime.
|
|
139
|
+
3. Apply timeouts, memory limits, syscall whitelists.
|
|
140
|
+
|
|
141
|
+
Don't `eval()` the plugin string in the application process. Even
|
|
142
|
+
"trusted" plugin scripts shouldn't have arbitrary access to the
|
|
143
|
+
host application's memory and modules.
|
|
144
|
+
|
|
145
|
+
## The pickle / serialization overlap
|
|
146
|
+
|
|
147
|
+
Python `pickle.loads()` is effectively eval-equivalent — pickle can
|
|
148
|
+
execute arbitrary code during deserialization. This is covered in
|
|
149
|
+
depth by skill #14 (`detecting-insecure-deserialization`); this
|
|
150
|
+
skill flags pickle usage as a cross-reference but the remediation
|
|
151
|
+
guidance lives in #14.
|
|
152
|
+
|
|
153
|
+
## Primary sources
|
|
154
|
+
|
|
155
|
+
- [CWE-95 Eval Injection](https://cwe.mitre.org/data/definitions/95.html)
|
|
156
|
+
- [Python ast.literal_eval docs](https://docs.python.org/3/library/ast.html#ast.literal_eval)
|
|
157
|
+
- [simpleeval — safe Python expression evaluation](https://github.com/danthedeckie/simpleeval)
|
|
158
|
+
- [Bandit B102 (exec_used) / B307 (eval)](https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html)
|
|
159
|
+
- [OWASP Code Review Guide — Dynamic code execution](https://owasp.org/www-project-code-review-guide/)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Static-analysis scan for eval / exec / dynamic-code-execution APIs.
|
|
3
|
+
|
|
4
|
+
References:
|
|
5
|
+
CWE-95 Improper Neutralization of Directives in Dynamically Evaluated Code
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
16
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
18
|
+
|
|
19
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
20
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
21
|
+
|
|
22
|
+
SKILL_ID = "detecting-eval-exec-usage"
|
|
23
|
+
|
|
24
|
+
PY_PATTERNS = [
|
|
25
|
+
("Python eval()", Severity.CRITICAL, r"\beval\s*\(\s*(?!['\"])", "python"),
|
|
26
|
+
("Python exec()", Severity.CRITICAL, r"\bexec\s*\(\s*(?!['\"])", "python"),
|
|
27
|
+
("Python compile() with non-literal", Severity.HIGH, r"\bcompile\s*\(\s*(?!['\"])", "python"),
|
|
28
|
+
("Python __import__ with variable", Severity.HIGH, r"__import__\s*\(\s*(?!['\"])", "python"),
|
|
29
|
+
(
|
|
30
|
+
"Python pickle.loads (eval-class deserialization)",
|
|
31
|
+
Severity.HIGH,
|
|
32
|
+
r"\bpickle\.loads?\s*\(",
|
|
33
|
+
"python",
|
|
34
|
+
), # cross-listed; #14 covers in depth
|
|
35
|
+
]
|
|
36
|
+
JS_PATTERNS = [
|
|
37
|
+
("JavaScript eval()", Severity.CRITICAL, r"\beval\s*\(", "javascript"),
|
|
38
|
+
(
|
|
39
|
+
"JavaScript new Function() with non-literal",
|
|
40
|
+
Severity.CRITICAL,
|
|
41
|
+
r"new\s+Function\s*\(\s*(?!['\"`])",
|
|
42
|
+
"javascript",
|
|
43
|
+
),
|
|
44
|
+
("JavaScript setTimeout with string arg", Severity.HIGH, r"setTimeout\s*\(\s*['\"`]", "javascript"),
|
|
45
|
+
("JavaScript setInterval with string arg", Severity.HIGH, r"setInterval\s*\(\s*['\"`]", "javascript"),
|
|
46
|
+
]
|
|
47
|
+
RUBY_PATTERNS = [
|
|
48
|
+
("Ruby eval()", Severity.CRITICAL, r"\beval\s*\(", "ruby"),
|
|
49
|
+
(
|
|
50
|
+
"Ruby instance_eval / class_eval with non-block",
|
|
51
|
+
Severity.HIGH,
|
|
52
|
+
r"\b(?:instance_eval|class_eval|module_eval)\s*\(\s*['\"]",
|
|
53
|
+
"ruby",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
PHP_PATTERNS = [
|
|
57
|
+
("PHP eval()", Severity.CRITICAL, r"\beval\s*\(", "php"),
|
|
58
|
+
("PHP assert() with string (legacy eval form)", Severity.CRITICAL, r"\bassert\s*\(\s*['\"$]", "php"),
|
|
59
|
+
("PHP create_function (deprecated, eval-equivalent)", Severity.CRITICAL, r"\bcreate_function\s*\(", "php"),
|
|
60
|
+
]
|
|
61
|
+
JAVA_PATTERNS = [
|
|
62
|
+
("Java ScriptEngine.eval", Severity.HIGH, r"\bScriptEngine[A-Za-z]*\b.*\.eval\s*\(", "java"),
|
|
63
|
+
("Java GroovyShell.evaluate", Severity.HIGH, r"\bGroovyShell\b.*\.evaluate\s*\(", "java"),
|
|
64
|
+
]
|
|
65
|
+
CSHARP_PATTERNS = [
|
|
66
|
+
(
|
|
67
|
+
"C# Activator.CreateInstance(Type.GetType(str))",
|
|
68
|
+
Severity.HIGH,
|
|
69
|
+
r"Activator\.CreateInstance\s*\(\s*Type\.GetType\s*\(",
|
|
70
|
+
"csharp",
|
|
71
|
+
),
|
|
72
|
+
("C# Reflection.Emit", Severity.MEDIUM, r"\bReflection\.Emit\b", "csharp"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
LANG_EXT_MAP = {
|
|
76
|
+
"python": {".py"},
|
|
77
|
+
"javascript": {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"},
|
|
78
|
+
"ruby": {".rb"},
|
|
79
|
+
"php": {".php"},
|
|
80
|
+
"java": {".java", ".kt", ".scala"},
|
|
81
|
+
"csharp": {".cs"},
|
|
82
|
+
}
|
|
83
|
+
LANG_PATTERNS = {
|
|
84
|
+
"python": PY_PATTERNS,
|
|
85
|
+
"javascript": JS_PATTERNS,
|
|
86
|
+
"ruby": RUBY_PATTERNS,
|
|
87
|
+
"php": PHP_PATTERNS,
|
|
88
|
+
"java": JAVA_PATTERNS,
|
|
89
|
+
"csharp": CSHARP_PATTERNS,
|
|
90
|
+
}
|
|
91
|
+
SKIP_DIRS = {
|
|
92
|
+
"node_modules",
|
|
93
|
+
".git",
|
|
94
|
+
"dist",
|
|
95
|
+
"build",
|
|
96
|
+
"target",
|
|
97
|
+
".cache",
|
|
98
|
+
".pnpm-store",
|
|
99
|
+
".venv",
|
|
100
|
+
"venv",
|
|
101
|
+
"__pycache__",
|
|
102
|
+
".astro",
|
|
103
|
+
".next",
|
|
104
|
+
".nuxt",
|
|
105
|
+
"vendor",
|
|
106
|
+
}
|
|
107
|
+
TEST_DIRS = {"tests", "test", "__tests__", "spec", "specs"}
|
|
108
|
+
MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def should_skip_path(path: Path, include_tests: bool) -> bool:
|
|
112
|
+
parts = set(path.parts)
|
|
113
|
+
if parts & SKIP_DIRS:
|
|
114
|
+
return True
|
|
115
|
+
if not include_tests and parts & TEST_DIRS:
|
|
116
|
+
return True
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def detect_language(path: Path, langs: set[str]) -> str | None:
|
|
121
|
+
suf = path.suffix.lower()
|
|
122
|
+
for lang in langs:
|
|
123
|
+
if suf in LANG_EXT_MAP[lang]:
|
|
124
|
+
return lang
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def scan_file(file_path: Path, repo_root: Path, langs: set[str]) -> list[Finding]:
|
|
129
|
+
findings = []
|
|
130
|
+
lang = detect_language(file_path, langs)
|
|
131
|
+
if lang is None:
|
|
132
|
+
return findings
|
|
133
|
+
try:
|
|
134
|
+
if file_path.stat().st_size > MAX_FILE_SIZE:
|
|
135
|
+
return findings
|
|
136
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
137
|
+
except (OSError, ValueError):
|
|
138
|
+
return findings
|
|
139
|
+
try:
|
|
140
|
+
rel = str(file_path.relative_to(repo_root))
|
|
141
|
+
except ValueError:
|
|
142
|
+
rel = str(file_path)
|
|
143
|
+
|
|
144
|
+
for title, sev, pattern, _lang in LANG_PATTERNS[lang]:
|
|
145
|
+
for m in re.finditer(pattern, text, re.MULTILINE):
|
|
146
|
+
line_no = text[: m.start()].count("\n") + 1
|
|
147
|
+
snippet = text.splitlines()[line_no - 1].strip()[:160]
|
|
148
|
+
findings.append(
|
|
149
|
+
Finding(
|
|
150
|
+
skill_id=SKILL_ID,
|
|
151
|
+
title=f"{title} at {rel}:{line_no}",
|
|
152
|
+
severity=sev,
|
|
153
|
+
target=f"{rel}:{line_no}",
|
|
154
|
+
detail=(
|
|
155
|
+
f"File {rel} line {line_no} uses {title}: `{snippet}`. "
|
|
156
|
+
"If the evaluated string is user-reachable, this is "
|
|
157
|
+
"an arbitrary-code-execution vector."
|
|
158
|
+
),
|
|
159
|
+
remediation=(
|
|
160
|
+
"Replace dynamic code execution with explicit logic "
|
|
161
|
+
"(lookup table, switch statement) or a sandboxed "
|
|
162
|
+
"expression library. Python: simpleeval / ast.literal_eval. "
|
|
163
|
+
"JS: expr-eval / mathjs. Ruby: Dentaku. See "
|
|
164
|
+
"references/PLAYBOOK.md."
|
|
165
|
+
),
|
|
166
|
+
cwe_id="CWE-95",
|
|
167
|
+
affected_control="OWASP A03:2021",
|
|
168
|
+
evidence=(("file", rel), ("line", line_no), ("language", lang), ("snippet", snippet)),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return findings
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def walk_repo(root: Path, include_tests: bool, langs: set[str]) -> list[Path]:
|
|
175
|
+
out = []
|
|
176
|
+
valid_exts = set()
|
|
177
|
+
for lang in langs:
|
|
178
|
+
valid_exts |= LANG_EXT_MAP[lang]
|
|
179
|
+
for p in root.rglob("*"):
|
|
180
|
+
if not p.is_file():
|
|
181
|
+
continue
|
|
182
|
+
if should_skip_path(p, include_tests):
|
|
183
|
+
continue
|
|
184
|
+
if p.suffix.lower() not in valid_exts:
|
|
185
|
+
continue
|
|
186
|
+
out.append(p)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main(argv: list[str] | None = None) -> int:
|
|
191
|
+
parser = argparse.ArgumentParser(description="eval / exec usage scanner")
|
|
192
|
+
parser.add_argument("path", type=Path)
|
|
193
|
+
parser.add_argument("--output", default=None)
|
|
194
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
195
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
196
|
+
parser.add_argument("--include-tests", action="store_true")
|
|
197
|
+
parser.add_argument("--languages", default="all")
|
|
198
|
+
args = parser.parse_args(argv)
|
|
199
|
+
|
|
200
|
+
if args.languages == "all":
|
|
201
|
+
langs = set(LANG_PATTERNS.keys())
|
|
202
|
+
else:
|
|
203
|
+
langs = {lang.strip() for lang in args.languages.split(",") if lang.strip() in LANG_PATTERNS}
|
|
204
|
+
|
|
205
|
+
root = args.path.resolve()
|
|
206
|
+
if not root.exists():
|
|
207
|
+
sys.stderr.write(f"ERROR: path does not exist: {root}\n")
|
|
208
|
+
return 2
|
|
209
|
+
|
|
210
|
+
files = walk_repo(root, args.include_tests, langs)
|
|
211
|
+
findings: list[Finding] = []
|
|
212
|
+
for f in files:
|
|
213
|
+
findings.extend(scan_file(f, root, langs))
|
|
214
|
+
|
|
215
|
+
floor = Severity(args.min_severity)
|
|
216
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
217
|
+
|
|
218
|
+
emit(findings, args.output, args.format, str(root))
|
|
219
|
+
return exit_code(findings)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
sys.exit(main())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: detecting-exposed-secrets-files
|
|
3
|
+
description: |
|
|
4
|
+
Probe a target for accidentally-served secret-bearing files in the web root
|
|
5
|
+
— `.git/`, `.env`, `.DS_Store`, backup files, database dumps, key files,
|
|
6
|
+
CI configs, IDE configs.
|
|
7
|
+
Use when: post-deploy verification on a new release, or SOC2 auditor asked
|
|
8
|
+
"what's reachable in the web root that shouldn't be," or a bug-bounty
|
|
9
|
+
report hints at a leaked file.
|
|
10
|
+
Threshold: any of the canonical 40+ paths returns 200 OR returns a body
|
|
11
|
+
matching the expected fingerprint of the file type (e.g., `.git/HEAD`
|
|
12
|
+
returns content starting with `ref:` or a 40-char hex SHA).
|
|
13
|
+
Trigger with: "check exposed files", "git directory exposure",
|
|
14
|
+
"env file leak", "backup file scan".
|
|
15
|
+
allowed-tools:
|
|
16
|
+
- Read
|
|
17
|
+
- Bash(python3:*)
|
|
18
|
+
- Bash(curl:*)
|
|
19
|
+
disallowed-tools:
|
|
20
|
+
- Bash(rm:*)
|
|
21
|
+
- Edit(/etc/*)
|
|
22
|
+
version: 3.0.0-dev
|
|
23
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
24
|
+
license: MIT
|
|
25
|
+
compatibility: Designed for Claude Code
|
|
26
|
+
tags:
|
|
27
|
+
- security
|
|
28
|
+
- information-disclosure
|
|
29
|
+
- secrets
|
|
30
|
+
- pentest
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# Detecting Exposed Secrets Files
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
The single highest-value pentest probe per HTTP request. A `.git/config`
|
|
38
|
+
disclosure leaks repo URL + credentials embedded in remote URLs. A `.env`
|
|
39
|
+
disclosure leaks every API key the app has. A `backup.sql` disclosure
|
|
40
|
+
leaks the entire database. These are not "weak crypto" findings that need
|
|
41
|
+
a chained exploit. They are direct, immediate compromise.
|
|
42
|
+
|
|
43
|
+
The probe set is the canonical 40+ paths web servers commonly expose by
|
|
44
|
+
accident: VCS directories (`.git/`, `.svn/`, `.hg/`), dotenv files,
|
|
45
|
+
OS metadata (`.DS_Store`), database dumps, archive files, IDE configs,
|
|
46
|
+
CI configs, and key files. Each is fingerprinted to distinguish a true
|
|
47
|
+
positive (server returns the file's expected content) from a 200 OK
|
|
48
|
+
that's actually the application's SPA index page catching the route.
|
|
49
|
+
|
|
50
|
+
## When the skill produces findings
|
|
51
|
+
|
|
52
|
+
| Finding | Severity | Threshold | Affected control |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `.git/HEAD` reachable + valid content | **CRITICAL** | 200 + body matches `ref:` or 40-char SHA | NIST 800-53 SC-28 |
|
|
55
|
+
| `.git/config` reachable + repo URL leaked | **CRITICAL** | 200 + body matches `[remote` | NIST 800-53 SC-28 |
|
|
56
|
+
| `.env` reachable + dotenv format | **CRITICAL** | 200 + body matches `KEY=VALUE` lines | OWASP A05:2021 |
|
|
57
|
+
| `*.sql` / `*.dump` / `backup.*` reachable | **CRITICAL** | 200 + body looks like SQL or binary dump | CWE-538 |
|
|
58
|
+
| `.aws/credentials` reachable | **CRITICAL** | 200 + body matches `[default]\naws_access_key_id` | CWE-200 |
|
|
59
|
+
| `id_rsa` / `*.pem` / `*.key` reachable | **CRITICAL** | 200 + body matches `BEGIN PRIVATE KEY` or `BEGIN RSA` | CWE-321 |
|
|
60
|
+
| `.svn/entries` / `.hg/store/` reachable | **HIGH** | 200 + body matches VCS format | NIST 800-53 SC-28 |
|
|
61
|
+
| `.DS_Store` reachable | **MEDIUM** | 200 + binary blob with `Bud1` magic | CWE-538 |
|
|
62
|
+
| IDE configs (`.idea/`, `.vscode/`) reachable | **LOW** | 200 + JSON/XML | CWE-200 |
|
|
63
|
+
| `composer.json` / `package.json` reachable on prod | **LOW** | 200 + valid JSON in non-API root | CWE-200 |
|
|
64
|
+
|
|
65
|
+
## Prerequisites
|
|
66
|
+
|
|
67
|
+
- Python 3.9+ with `requests` library
|
|
68
|
+
- Authorization for non-local targets
|
|
69
|
+
|
|
70
|
+
## Instructions
|
|
71
|
+
|
|
72
|
+
### Step 1 — Confirm Authorization
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
"Do you have authorization to perform secret-file discovery on this
|
|
76
|
+
target? I need confirmation before proceeding."
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Step 2 — Run the scanner
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
83
|
+
https://target.example.com \
|
|
84
|
+
--authorized
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Usage: probe_secrets.py URL [OPTIONS]
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
--authorized Attest authorization (required for non-local)
|
|
94
|
+
--output FILE Write findings to FILE
|
|
95
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
96
|
+
--min-severity SEV (default: info)
|
|
97
|
+
--timeout SECS Per-probe timeout (default: 10)
|
|
98
|
+
--paths-file FILE Override the canonical probe set with a custom list
|
|
99
|
+
--check-only Skip body-fingerprint verification (faster, more
|
|
100
|
+
false positives — useful when target serves SPA
|
|
101
|
+
index for everything)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The scanner sends a GET for each path in the canonical probe set. For
|
|
105
|
+
every 200 response, it inspects the body to confirm the response really
|
|
106
|
+
is the expected file type (not the app's SPA index catching the route).
|
|
107
|
+
|
|
108
|
+
### Step 3 — Interpret findings
|
|
109
|
+
|
|
110
|
+
CRITICAL = direct credential / source code / database exposure.
|
|
111
|
+
Ship same-hour fix (configure web server to deny + audit logs for
|
|
112
|
+
anyone who already exfiltrated).
|
|
113
|
+
|
|
114
|
+
### Step 4 — Cross-skill chaining
|
|
115
|
+
|
|
116
|
+
After this skill, suggest `detecting-debug-endpoints` (#7) — the same
|
|
117
|
+
deploy mistake that exposes `.git/` often exposes `/admin/` and
|
|
118
|
+
`/server-status/`. And `detecting-directory-listing` (#9) — if any of
|
|
119
|
+
the secret-file paths returned a directory listing instead of the file
|
|
120
|
+
itself, autoindex is enabled.
|
|
121
|
+
|
|
122
|
+
## Examples
|
|
123
|
+
|
|
124
|
+
### Example 1 — Post-deploy verification
|
|
125
|
+
|
|
126
|
+
User: "We just rolled out v4.2. Make sure we didn't deploy `.env` or
|
|
127
|
+
the `.git/` dir by accident."
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
131
|
+
https://app.example.com --authorized --min-severity high
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Example 2 — Bug bounty triage
|
|
135
|
+
|
|
136
|
+
User: "Submission claims our .git/ is exposed."
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
140
|
+
https://app.example.com --authorized --format json --output exposure.json
|
|
141
|
+
jq '.[] | select(.title | contains(".git"))' exposure.json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The fingerprint check distinguishes real `.git/HEAD` (returns
|
|
145
|
+
`ref: refs/heads/main` or a 40-char hex SHA) from false-positive SPA
|
|
146
|
+
index pages that 200 on any path.
|
|
147
|
+
|
|
148
|
+
### Example 3 — CI gate against future deploys
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
- name: Exposed-files gate
|
|
152
|
+
run: |
|
|
153
|
+
python3 plugins/security/penetration-tester/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
154
|
+
"${{ secrets.STAGING_URL }}" \
|
|
155
|
+
--authorized --min-severity critical
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Exit 1 fails the deploy if any CRITICAL finding lands.
|
|
159
|
+
|
|
160
|
+
## Output
|
|
161
|
+
|
|
162
|
+
JSON / JSONL / Markdown. Exit codes: 0 clean, 1 high/critical, 2 error.
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
- **Target SPA-catches every URL with 200** → use `--check-only` to skip
|
|
167
|
+
body-fingerprint; expect more false positives but get any real exposure.
|
|
168
|
+
- **All probes timeout** → likely DDoS protection blocking the scanner;
|
|
169
|
+
contact the target's security team for an allowlist.
|
|
170
|
+
- **Connection error** → exit 2.
|
|
171
|
+
|
|
172
|
+
## Resources
|
|
173
|
+
|
|
174
|
+
- `references/THEORY.md` — Why each path matters, fingerprint patterns,
|
|
175
|
+
RFC / OWASP / NIST anchors
|
|
176
|
+
- `references/PLAYBOOK.md` — Per-server config snippets to block each
|
|
177
|
+
category of path (nginx, Apache, Caddy, ALB, GCP LB)
|
|
178
|
+
- `../analyzing-tls-config/references/AUTHORIZATION.md` — Active-scan
|
|
179
|
+
authorization
|