@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.
Files changed (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Directory-listing probe.
3
+
4
+ Companion to skill `detecting-directory-listing`. For each candidate
5
+ directory path, appends a trailing slash and sends a GET. If the
6
+ response is 200 and the body matches a framework-specific autoindex
7
+ fingerprint, it's a finding.
8
+
9
+ References:
10
+ OWASP WSTG-CONF-04 Review Old Backup and Unreferenced Files
11
+ CWE-548 Exposure of Information Through Directory Listing
12
+ nginx autoindex docs, Apache mod_autoindex docs
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ _PLUGIN_ROOT = Path(__file__).resolve().parents[3]
23
+ if str(_PLUGIN_ROOT) not in sys.path:
24
+ sys.path.insert(0, str(_PLUGIN_ROOT))
25
+
26
+ from lib.authz_check import require_authorization # noqa: E402
27
+ from lib.finding import Finding, Severity # noqa: E402
28
+ from lib.http_client import make_session, safe_get # noqa: E402
29
+ from lib.report import emit, exit_code # noqa: E402
30
+
31
+ SKILL_ID = "detecting-directory-listing"
32
+
33
+
34
+ # Probe set. Each entry: (path, severity, control)
35
+ PROBES = [
36
+ # CRITICAL — config + VCS directories
37
+ ("config/", Severity.CRITICAL, "NIST 800-53 SC-28"),
38
+ ("conf/", Severity.CRITICAL, "NIST 800-53 SC-28"),
39
+ (".config/", Severity.CRITICAL, "NIST 800-53 SC-28"),
40
+ (".git/", Severity.CRITICAL, "NIST 800-53 SC-28"),
41
+ (".git/objects/", Severity.CRITICAL, "NIST 800-53 SC-28"),
42
+ (".svn/", Severity.CRITICAL, "NIST 800-53 SC-28"),
43
+ (".hg/", Severity.CRITICAL, "NIST 800-53 SC-28"),
44
+ # HIGH — backup / upload / log / dump dirs
45
+ ("backup/", Severity.HIGH, "CWE-548"),
46
+ ("backups/", Severity.HIGH, "CWE-548"),
47
+ ("uploads/", Severity.HIGH, "CWE-548"),
48
+ ("upload/", Severity.HIGH, "CWE-548"),
49
+ ("logs/", Severity.HIGH, "CWE-548"),
50
+ ("log/", Severity.HIGH, "CWE-548"),
51
+ ("dump/", Severity.HIGH, "CWE-548"),
52
+ ("dumps/", Severity.HIGH, "CWE-548"),
53
+ ("tmp/", Severity.HIGH, "CWE-548"),
54
+ ("temp/", Severity.HIGH, "CWE-548"),
55
+ ("cache/", Severity.HIGH, "CWE-548"),
56
+ ("data/", Severity.HIGH, "CWE-548"),
57
+ ("private/", Severity.HIGH, "CWE-548"),
58
+ ("internal/", Severity.HIGH, "CWE-548"),
59
+ ("storage/", Severity.HIGH, "CWE-548"),
60
+ ("var/", Severity.HIGH, "CWE-548"),
61
+ ("files/", Severity.HIGH, "CWE-548"),
62
+ # MEDIUM — asset / public-ish dirs (file enumeration enabled)
63
+ ("assets/", Severity.MEDIUM, "CWE-548"),
64
+ ("static/", Severity.MEDIUM, "CWE-548"),
65
+ ("public/", Severity.MEDIUM, "CWE-548"),
66
+ ("media/", Severity.MEDIUM, "CWE-548"),
67
+ ("images/", Severity.MEDIUM, "CWE-548"),
68
+ ("img/", Severity.MEDIUM, "CWE-548"),
69
+ ("downloads/", Severity.MEDIUM, "CWE-548"),
70
+ ("docs/", Severity.MEDIUM, "CWE-548"),
71
+ ("documentation/", Severity.MEDIUM, "CWE-548"),
72
+ ("vendor/", Severity.MEDIUM, "CWE-548"),
73
+ ("node_modules/", Severity.HIGH, "CWE-548"), # higher: enables specific version-CVE lookup
74
+ ("bower_components/", Severity.HIGH, "CWE-548"),
75
+ # MEDIUM — generic root
76
+ ("", Severity.MEDIUM, "OWASP A05:2021"), # root itself
77
+ ]
78
+
79
+
80
+ # Framework-specific autoindex fingerprints applied to first 4 KiB of body
81
+ AUTOINDEX_PATTERNS = [
82
+ (r"<title>Index of /", "Apache mod_autoindex"),
83
+ (r"<h1>Index of /", "Apache mod_autoindex / nginx fancyindex"),
84
+ (r"<title>Directory listing for /", "Python http.server"),
85
+ (r"<title>Directory: /", "Caddy file_server browse"),
86
+ (r"<table[^>]*class=['\"]listing", "Caddy file_server browse"),
87
+ (r"<pre><a href=['\"]\.\.['\"]>\.\./</a>", "nginx default autoindex"),
88
+ (r"<head>\s*<title>Index of [^<]+</title>", "Generic autoindex"),
89
+ (r"^\s*<\?xml.+<ListBucketResult", "AWS S3 ListBucket XML"),
90
+ (r"<EnumerationResults", "Azure Blob list-blob XML"),
91
+ (r"<title>Objects:", "GCS bucket browse"),
92
+ (r"<h1>Listing", "Rails / Rack::Directory listing"),
93
+ (r"<title>Index - /", "Lighttpd mod_dirlisting"),
94
+ (r"^\s*<!DOCTYPE\s+html>[\s\S]+<h1>\s*Index of\s+/", "Variant Index of"),
95
+ ]
96
+
97
+
98
+ def _is_autoindex(body_text: str) -> tuple[bool, str | None]:
99
+ """Returns (matched, framework_name) — True if body looks like an autoindex page."""
100
+ sample = (body_text or "")[:4096]
101
+ for pattern, framework in AUTOINDEX_PATTERNS:
102
+ if re.search(pattern, sample, re.MULTILINE | re.IGNORECASE):
103
+ return True, framework
104
+ return False, None
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ parser = argparse.ArgumentParser(description="Directory-listing probe")
109
+ parser.add_argument("url")
110
+ parser.add_argument("--authorized", action="store_true")
111
+ parser.add_argument("--output", default=None)
112
+ parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
113
+ parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
114
+ parser.add_argument("--timeout", type=float, default=10.0)
115
+ parser.add_argument("--paths-file", default=None, help="Custom probe set (one path per line); replaces default")
116
+ args = parser.parse_args(argv)
117
+
118
+ require_authorization(args.url, args.authorized)
119
+
120
+ sess = make_session(timeout=args.timeout)
121
+ base = args.url.rstrip("/") + "/"
122
+ findings: list[Finding] = []
123
+
124
+ probe_set = PROBES
125
+ if args.paths_file:
126
+ paths = Path(args.paths_file).read_text().splitlines()
127
+ probe_set = [(p.strip().rstrip("/") + "/", Severity.MEDIUM, "custom") for p in paths if p.strip()]
128
+
129
+ for path, sev, control in probe_set:
130
+ url = base + path.lstrip("/")
131
+ resp = safe_get(sess, url, timeout=args.timeout, allow_redirects=False)
132
+ if resp is None or resp.status_code != 200:
133
+ continue
134
+ body = resp.text or ""
135
+ ctype = resp.headers.get("Content-Type", "")
136
+ # Reject application/* responses (e.g. JSON APIs returning a 200) —
137
+ # those aren't autoindex pages
138
+ if "html" not in ctype.lower() and "xml" not in ctype.lower():
139
+ continue
140
+ matched, framework = _is_autoindex(body)
141
+ if not matched:
142
+ continue
143
+ findings.append(
144
+ Finding(
145
+ skill_id=SKILL_ID,
146
+ title=f"Directory listing exposed at /{path} ({framework})",
147
+ severity=sev,
148
+ target=url,
149
+ detail=(
150
+ f"GET {url} returned 200 with HTML body matching the "
151
+ f"{framework} autoindex fingerprint. Every file in this "
152
+ "directory is enumerable to any external requestor, "
153
+ "including files the application never explicitly linked to."
154
+ ),
155
+ remediation=(
156
+ f"Disable autoindex for {path!r} at the web-server layer. "
157
+ "See references/PLAYBOOK.md for per-server snippets "
158
+ "(nginx `autoindex off`, Apache `Options -Indexes`, "
159
+ "Caddy drop the `file_server browse` directive, S3 "
160
+ "remove `s3:ListBucket` from the public bucket policy)."
161
+ ),
162
+ cwe_id="CWE-548",
163
+ affected_control=control,
164
+ evidence=(
165
+ ("framework", framework or "unknown"),
166
+ ("content_type", ctype),
167
+ ("body_sample_len", len(body)),
168
+ ),
169
+ )
170
+ )
171
+
172
+ floor = Severity(args.min_severity)
173
+ findings = [f for f in findings if f.severity.numeric >= floor.numeric]
174
+
175
+ emit(findings, args.output, args.format, args.url)
176
+ return exit_code(findings)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ sys.exit(main())
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: detecting-eval-exec-usage
3
+ description: |
4
+ Scan a source tree for dynamic-code-execution APIs that an attacker
5
+ can hijack: Python eval / exec / compile, JavaScript eval /
6
+ Function() / setTimeout(string), Ruby eval / instance_eval /
7
+ class_eval, Java ScriptEngine, PHP eval / assert($str), .NET
8
+ Activator.CreateInstance / Reflection.Emit with dynamic input.
9
+ Use when: pre-commit gate on any application that parses
10
+ user-uploaded code (rule engines, formula evaluators,
11
+ plugin systems), or post-bug-report when "we run user-supplied
12
+ expressions."
13
+ Threshold: any call to eval / exec / Function / similar where the
14
+ argument is not a string literal.
15
+ Trigger with: "scan eval", "find dynamic exec", "audit eval calls",
16
+ "code injection patterns".
17
+ allowed-tools:
18
+ - Read
19
+ - Bash(python3:*)
20
+ - Glob
21
+ - Grep
22
+ disallowed-tools:
23
+ - Bash(rm:*)
24
+ - Bash(curl:*)
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
+ - static-analysis
32
+ - code-injection
33
+ - pentest
34
+ ---
35
+
36
+ # Detecting eval / exec Usage
37
+
38
+ ## Overview
39
+
40
+ Dynamic-code-execution APIs (CWE-95 Eval Injection) let an
41
+ application interpret a string as code at runtime. If the string
42
+ contains anything user-controllable, the application has handed
43
+ the attacker arbitrary code execution.
44
+
45
+ The defensive posture: don't use these APIs. The exceptions are
46
+ narrow: rule engines, formula evaluators (spreadsheet `=` formulas),
47
+ plugin systems with explicit sandboxing. For everything else,
48
+ there's almost always a safer alternative.
49
+
50
+ ## When the skill produces findings
51
+
52
+ | Finding | Severity | Threshold | Affected control |
53
+ |---|---|---|---|
54
+ | Python `eval(...)` with non-literal | **CRITICAL** | argument contains var ref | CWE-95 |
55
+ | Python `exec(...)` with non-literal | **CRITICAL** | argument contains var ref | CWE-95 |
56
+ | Python `compile(...)` with non-literal | **HIGH** | source string contains var | CWE-95 |
57
+ | Python `__import__(var)` | **HIGH** | dynamic module loading | CWE-95 |
58
+ | JS `eval(...)` | **CRITICAL** | any | CWE-95 |
59
+ | JS `new Function(str)` | **CRITICAL** | any non-literal | CWE-95 |
60
+ | JS `setTimeout/setInterval(string)` | **HIGH** | string instead of function | CWE-95 |
61
+ | Ruby `eval(...)`/`instance_eval(...)`/`class_eval(...)` | **CRITICAL** | non-literal | CWE-95 |
62
+ | PHP `eval(...)` | **CRITICAL** | always | CWE-95 |
63
+ | PHP `assert($str)` | **CRITICAL** | (legacy code-eval form) | CWE-95 |
64
+ | PHP `create_function` | **CRITICAL** | deprecated, eval-equivalent | CWE-95 |
65
+ | Java `ScriptEngineManager` + eval | **HIGH** | dynamic script execution | CWE-95 |
66
+ | C# `Activator.CreateInstance(Type.GetType(str))` | **HIGH** | type loading from string | CWE-95 |
67
+
68
+ ## Prerequisites
69
+
70
+ - Python 3.9+
71
+ - Source tree on local filesystem
72
+
73
+ ## Instructions
74
+
75
+ ### Run
76
+
77
+ ```bash
78
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py /path/to/repo
79
+ ```
80
+
81
+ Options: `--output FILE`, `--format json|jsonl|markdown`,
82
+ `--min-severity`, `--include-tests`, `--languages LIST`.
83
+
84
+ ### Interpret
85
+
86
+ CRITICAL = direct RCE vector. Replace the dynamic execution with
87
+ explicit logic (lookup table, switch statement) or a sandboxed
88
+ expression library (Python `simpleeval`, JavaScript `expr-eval`,
89
+ Ruby `Dentaku`).
90
+
91
+ ### Remediation
92
+
93
+ See `references/PLAYBOOK.md`.
94
+
95
+ ## Examples
96
+
97
+ ### Pre-commit
98
+
99
+ ```bash
100
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
101
+ --min-severity high $(git diff --name-only main...HEAD | tr '\n' ' ')
102
+ ```
103
+
104
+ ### CI
105
+
106
+ ```yaml
107
+ - run: |
108
+ python3 plugins/security/penetration-tester/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
109
+ . --min-severity high
110
+ ```
111
+
112
+ ## Output
113
+
114
+ JSON / JSONL / Markdown. Exit codes: 0 / 1 / 2.
115
+
116
+ ## Error Handling
117
+
118
+ False positive on `eval("'literal'")` — the value is a constant
119
+ string. Verify the regex match by reading the source line.
120
+
121
+ ## Resources
122
+
123
+ - `references/THEORY.md` — Why dynamic-code execution is the
124
+ highest-impact injection class, sandbox limits, the
125
+ formula-evaluator design pattern
126
+ - `references/PLAYBOOK.md` — Per-language safe alternatives
127
+ (Python simpleeval / ast.literal_eval, JS expression-eval
128
+ libraries, Ruby Dentaku, Java scripting sandboxes)
@@ -0,0 +1,306 @@
1
+ # Eval / Exec Remediation Playbook
2
+
3
+ The universal answer: replace dynamic-code execution with explicit
4
+ logic OR a sandboxed expression library. Per-language patterns
5
+ below.
6
+
7
+ ## Python
8
+
9
+ ### Before
10
+
11
+ ```python
12
+ result = eval(user_expression)
13
+ ```
14
+
15
+ ### After (sandboxed expression eval)
16
+
17
+ ```python
18
+ from simpleeval import simple_eval
19
+ result = simple_eval(user_expression, names={"x": 10, "y": 20})
20
+ ```
21
+
22
+ ### After (literal-only eval)
23
+
24
+ ```python
25
+ import ast
26
+ # Only safe for literal values, NOT expressions
27
+ parsed = ast.literal_eval(user_input) # raises on anything non-literal
28
+ ```
29
+
30
+ ### After (lookup table for choice)
31
+
32
+ ```python
33
+ # Before: eval("function_" + name + "()")
34
+ # After: explicit dispatch
35
+ HANDLERS = {
36
+ "process_a": process_a,
37
+ "process_b": process_b,
38
+ }
39
+ handler = HANDLERS.get(name)
40
+ if handler is None:
41
+ raise ValueError(f"Unknown handler: {name}")
42
+ result = handler()
43
+ ```
44
+
45
+ ### Dynamic class instantiation
46
+
47
+ ```python
48
+ # Before: cls = eval(class_name); obj = cls()
49
+ # After:
50
+ ALLOWED_CLASSES = {
51
+ "TypeA": TypeA,
52
+ "TypeB": TypeB,
53
+ }
54
+ cls = ALLOWED_CLASSES[class_name]
55
+ obj = cls()
56
+ ```
57
+
58
+ ## JavaScript
59
+
60
+ ### Before
61
+
62
+ ```javascript
63
+ const result = eval(userInput);
64
+ ```
65
+
66
+ ### After (expression library)
67
+
68
+ ```javascript
69
+ const { Parser } = require('expr-eval');
70
+ const result = Parser.evaluate(userInput, { x: 10, y: 20 });
71
+ ```
72
+
73
+ ### After (Function constructor → don't)
74
+
75
+ ```javascript
76
+ // Before:
77
+ const fn = new Function('x', userBody);
78
+
79
+ // After: parse the function shape declaratively, never construct from string
80
+ const HANDLERS = {
81
+ 'double': (x) => x * 2,
82
+ 'square': (x) => x * x,
83
+ };
84
+ const fn = HANDLERS[userInput];
85
+ if (!fn) throw new Error('Unknown handler');
86
+ ```
87
+
88
+ ### setTimeout / setInterval — use function reference
89
+
90
+ ```javascript
91
+ // Before
92
+ setTimeout("doThing()", 1000);
93
+
94
+ // After
95
+ setTimeout(doThing, 1000);
96
+ // or
97
+ setTimeout(() => doThing(arg), 1000);
98
+ ```
99
+
100
+ ### JSON parse instead of eval
101
+
102
+ ```javascript
103
+ // Before
104
+ const data = eval('(' + jsonString + ')');
105
+
106
+ // After
107
+ const data = JSON.parse(jsonString);
108
+ ```
109
+
110
+ ## Ruby
111
+
112
+ ### Before
113
+
114
+ ```ruby
115
+ result = eval(user_expression)
116
+ ```
117
+
118
+ ### After (Dentaku for expressions)
119
+
120
+ ```ruby
121
+ require 'dentaku'
122
+ calc = Dentaku::Calculator.new
123
+ result = calc.evaluate(user_expression, x: 10, y: 20)
124
+ ```
125
+
126
+ ### After (avoid instance_eval / class_eval on user strings)
127
+
128
+ ```ruby
129
+ # Before
130
+ obj.instance_eval(user_code)
131
+
132
+ # After: define a narrow DSL, evaluate via method dispatch
133
+ ALLOWED_OPS = {
134
+ 'increment' => :increment,
135
+ 'reset' => :reset,
136
+ }
137
+ op = ALLOWED_OPS[user_input]
138
+ raise 'Unknown op' unless op
139
+ obj.send(op)
140
+ ```
141
+
142
+ ## PHP
143
+
144
+ ### Before
145
+
146
+ ```php
147
+ $result = eval($code);
148
+ ```
149
+
150
+ ### After: just don't
151
+
152
+ PHP's eval is uniquely dangerous because it injects into the
153
+ current scope. There's no sandboxed-eval alternative in the
154
+ standard library. Replace with explicit logic / dispatch table.
155
+
156
+ ```php
157
+ $handlers = [
158
+ 'process_a' => 'process_a',
159
+ 'process_b' => 'process_b',
160
+ ];
161
+ if (!isset($handlers[$name])) {
162
+ throw new InvalidArgumentException("Unknown handler: $name");
163
+ }
164
+ $fn = $handlers[$name];
165
+ $result = $fn();
166
+ ```
167
+
168
+ ### assert as eval (legacy)
169
+
170
+ ```php
171
+ // Before — yes really, this used to work as eval
172
+ assert($userString);
173
+
174
+ // After
175
+ // Remove. assert() now is a real assertion in PHP 7+, but old
176
+ // code that relied on the eval-form should be replaced with
177
+ // explicit dispatch as above.
178
+ ```
179
+
180
+ ### create_function — deprecated
181
+
182
+ ```php
183
+ // Before (deprecated since PHP 7.2, removed PHP 8.0)
184
+ $fn = create_function('$x', $userBody);
185
+
186
+ // After: anonymous functions / closures with explicit body
187
+ $multiplier = function ($x) use ($factor) {
188
+ return $x * $factor;
189
+ };
190
+ ```
191
+
192
+ ## Java — sandboxed scripting
193
+
194
+ ### Before (Nashorn / GraalJS with full access)
195
+
196
+ ```java
197
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
198
+ Object result = engine.eval(userScript);
199
+ ```
200
+
201
+ ### After (GraalJS with restricted permissions)
202
+
203
+ ```java
204
+ import org.graalvm.polyglot.*;
205
+ try (Context cx = Context.newBuilder("js")
206
+ .allowHostAccess(HostAccess.NONE)
207
+ .allowHostClassLookup(name -> false)
208
+ .allowIO(false)
209
+ .allowCreateProcess(false)
210
+ .allowCreateThread(false)
211
+ .build()) {
212
+ Value result = cx.eval("js", userScript);
213
+ }
214
+ ```
215
+
216
+ ### Or: don't use scripting at all
217
+
218
+ For most use cases where Java code shells out to a script engine,
219
+ the right answer is to define a domain-specific configuration
220
+ format (JSON / YAML) parsed by your Java code, with the
221
+ operations dispatched via a sealed-class hierarchy.
222
+
223
+ ## C# / .NET
224
+
225
+ ### Avoid Type.GetType(str) for dynamic class loading
226
+
227
+ ### Before
228
+
229
+ ```csharp
230
+ Type t = Type.GetType(userTypeName);
231
+ object instance = Activator.CreateInstance(t);
232
+ ```
233
+
234
+ ### After
235
+
236
+ ```csharp
237
+ // Allow-list of permitted types
238
+ static readonly IReadOnlyDictionary<string, Type> ALLOWED_TYPES =
239
+ new Dictionary<string, Type> {
240
+ { "TypeA", typeof(TypeA) },
241
+ { "TypeB", typeof(TypeB) },
242
+ };
243
+
244
+ if (!ALLOWED_TYPES.TryGetValue(userTypeName, out Type t)) {
245
+ throw new ArgumentException($"Unknown type: {userTypeName}");
246
+ }
247
+ object instance = Activator.CreateInstance(t);
248
+ ```
249
+
250
+ ## Plugin system patterns (safe)
251
+
252
+ If you genuinely need to run user-supplied logic:
253
+
254
+ ### Pattern 1 — WASM plugins
255
+
256
+ ```rust
257
+ // Host runtime (Rust + Wasmer)
258
+ use wasmer::{Store, Module, Instance, imports};
259
+ let module = Module::new(&store, plugin_wasm_bytes)?;
260
+ let instance = Instance::new(&store, &module, &imports! {})?;
261
+ // Call exported functions; no system access by default
262
+ let result = instance.exports.get_function("process")?.call(&[input.into()])?;
263
+ ```
264
+
265
+ ### Pattern 2 — V8 isolate (Node.js)
266
+
267
+ ```javascript
268
+ const vm = require('vm');
269
+ const context = vm.createContext({ /* explicit allow-list of globals */ });
270
+ const result = vm.runInContext(userCode, context, {
271
+ timeout: 1000, // hard timeout
272
+ breakOnSigint: true,
273
+ });
274
+ ```
275
+
276
+ Note: Node's `vm` module is NOT a true sandbox — there are escape
277
+ techniques. For true isolation, use a separate process or
278
+ `isolated-vm` library.
279
+
280
+ ### Pattern 3 — Containerized worker
281
+
282
+ Spawn a Docker container with the user's code, read-only
283
+ filesystem, no network, memory + CPU limits, timeout. The
284
+ boundary is the container runtime, not the application process.
285
+
286
+ ## Pre-commit / CI
287
+
288
+ Same pattern as previous skills:
289
+
290
+ ```yaml
291
+ - name: eval/exec scan
292
+ run: |
293
+ python3 plugins/security/penetration-tester/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
294
+ . --min-severity high
295
+ ```
296
+
297
+ ## Verification after remediation
298
+
299
+ ```bash
300
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
301
+ /path/to/repo --min-severity medium
302
+ ```
303
+
304
+ Expected: exit 0, zero MEDIUM-or-higher findings. Remaining LOW
305
+ findings (legitimate `ast.literal_eval` calls, GraalJS sandboxed
306
+ eval) are acceptable.