penthera 1.1.0

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 (74) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +561 -0
  4. package/SECURITY.md +31 -0
  5. package/bin/penthera.js +3 -0
  6. package/bin/scan.js +11 -0
  7. package/docs/index.md +60 -0
  8. package/docs/owasp-wstg-coverage.md +79 -0
  9. package/lib/adaptive-probes.js +334 -0
  10. package/lib/attack-surface.js +149 -0
  11. package/lib/audit-loop.js +170 -0
  12. package/lib/blackbox/adaptive-scan.js +137 -0
  13. package/lib/blackbox/client-auth.js +60 -0
  14. package/lib/blackbox/email-dns.js +135 -0
  15. package/lib/blackbox/headers.js +91 -0
  16. package/lib/blackbox/idor.js +113 -0
  17. package/lib/blackbox/jwt.js +73 -0
  18. package/lib/blackbox/oauth.js +50 -0
  19. package/lib/blackbox/openapi.js +227 -0
  20. package/lib/compliance/incident.js +169 -0
  21. package/lib/compliance/index.js +163 -0
  22. package/lib/compliance/policy-pack.js +293 -0
  23. package/lib/compliance/questionnaire.js +113 -0
  24. package/lib/compliance/readiness.js +229 -0
  25. package/lib/compliance/self-assessment.js +66 -0
  26. package/lib/crawler.js +257 -0
  27. package/lib/fuzzer.js +233 -0
  28. package/lib/injections.js +676 -0
  29. package/lib/jurisdictions/nl.json +48 -0
  30. package/lib/knowledge-graph.js +232 -0
  31. package/lib/machine.js +814 -0
  32. package/lib/nuclei-loader.js +283 -0
  33. package/lib/owasp-wstg.js +204 -0
  34. package/lib/params.js +235 -0
  35. package/lib/plugins.js +70 -0
  36. package/lib/provenance.js +82 -0
  37. package/lib/recon.js +193 -0
  38. package/lib/remediation/index.js +219 -0
  39. package/lib/retirejs.js +239 -0
  40. package/lib/supply-chain.js +131 -0
  41. package/lib/templates.js +364 -0
  42. package/lib/tls.js +145 -0
  43. package/lib/whitebox/frameworks.js +139 -0
  44. package/lib/whitebox/sbom.js +144 -0
  45. package/lib/whitebox/secrets.js +111 -0
  46. package/package.json +76 -0
  47. package/pentest.config.example.js +82 -0
  48. package/scripts/package-skill.sh +16 -0
  49. package/skills/penthera/SKILL.md +272 -0
  50. package/skills/penthera/references/authorization.md +36 -0
  51. package/skills/penthera/references/output-and-ci.md +109 -0
  52. package/skills/penthera/references/profiles-and-flags.md +74 -0
  53. package/skills/penthera/references/remediation.md +267 -0
  54. package/skills/penthera/references/reporting.md +113 -0
  55. package/skills/penthera/references/secure-defaults.md +41 -0
  56. package/skills/penthera/references/troubleshooting.md +100 -0
  57. package/skills/penthera/scripts/preflight.sh +66 -0
  58. package/skills/penthera/scripts/validate-report.mjs +53 -0
  59. package/skills/penthera/scripts/validate-report.sh +8 -0
  60. package/src/cli/ansi.js +70 -0
  61. package/src/cli/baseline.js +37 -0
  62. package/src/cli/merge-results.js +71 -0
  63. package/src/cli/onboarding.js +216 -0
  64. package/src/cli/profiles.js +53 -0
  65. package/src/cli/prompt.js +64 -0
  66. package/src/cli/run-scan.js +152 -0
  67. package/src/cli.js +536 -0
  68. package/src/report/markdown.js +182 -0
  69. package/src/reporter.js +512 -0
  70. package/src/scan-repo.js +205 -0
  71. package/src/scan-url.js +563 -0
  72. package/src/utils/auth.js +13 -0
  73. package/src/utils/http.js +38 -0
  74. package/src/utils/url.js +54 -0
package/lib/fuzzer.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Property-Based Fuzzer (inspired by Schemathesis / fast-check / RESTler)
3
+ *
4
+ * Generates edge-case payloads for API endpoints automatically.
5
+ * No schema needed, uses heuristics based on field names.
6
+ *
7
+ * Techniques borrowed from:
8
+ * - Schemathesis, property-based testing from API schemas
9
+ * - RESTler, stateful fuzzing with dependency inference
10
+ * - fast-check, shrinkable random generators
11
+ */
12
+
13
+ // ── Payload Generators ────────────────────────────────────────────────────
14
+
15
+ /** Generate fuzz values for a given field name (heuristic-based) */
16
+ export function fuzzField(fieldName) {
17
+ const name = fieldName.toLowerCase();
18
+ const payloads = [];
19
+
20
+ // Type confusion, always test these
21
+ payloads.push(null, undefined, "", 0, false, [], {}, NaN);
22
+
23
+ // Overflow / boundary
24
+ payloads.push("x".repeat(10_000), "x".repeat(100_000));
25
+ payloads.push(Number.MAX_SAFE_INTEGER, -1, -999999, 0.1, Infinity, -Infinity);
26
+
27
+ // XSS / injection, always test
28
+ payloads.push(
29
+ '<script>alert(1)</script>',
30
+ '"><img src=x onerror=alert(1)>',
31
+ "{{7*7}}", "${7*7}", // template injection
32
+ "'; DROP TABLE users; --", // SQL injection
33
+ '{"$gt":""}', // NoSQL injection
34
+ "../../../etc/passwd", // path traversal
35
+ "file:///etc/passwd", // file URL
36
+ "javascript:alert(1)", // protocol injection
37
+ );
38
+
39
+ // Prototype pollution
40
+ payloads.push(
41
+ { "__proto__": { "isAdmin": true } },
42
+ { "constructor": { "prototype": { "isAdmin": true } } },
43
+ );
44
+
45
+ // Field-specific fuzz based on name heuristics
46
+ if (name.includes("email")) {
47
+ payloads.push(
48
+ "@", "user@", "@domain.com", "user@.com",
49
+ "a@b", "user @x.com", ".user@domain.com",
50
+ "user@domain..com", `${"a".repeat(255)}@example.com`,
51
+ "user+tag@gmail.com", "user@[127.0.0.1]",
52
+ );
53
+ }
54
+
55
+ if (name.includes("url") || name.includes("link") || name.includes("href") || name.includes("src")) {
56
+ payloads.push(
57
+ "javascript:alert(1)",
58
+ "data:text/html,<script>alert(1)</script>",
59
+ "//evil.com", "https://evil.com",
60
+ "https://localhost", "https://127.0.0.1",
61
+ "https://169.254.169.254/latest/meta-data/", // SSRF
62
+ "https://metadata.google.internal/computeMetadata/v1/",
63
+ "ftp://evil.com/malware",
64
+ `https://example.com/${"a".repeat(2048)}`, // long URL
65
+ );
66
+ }
67
+
68
+ if (name.includes("id") || name.includes("Id") || name === "uid") {
69
+ payloads.push(
70
+ "../../../etc/passwd", "..%2f..%2f..%2f",
71
+ "admin", "root", "0", "-1",
72
+ "' OR '1'='1", "1; DROP TABLE users",
73
+ "00000000-0000-0000-0000-000000000000", // nil UUID
74
+ "a".repeat(500),
75
+ );
76
+ }
77
+
78
+ if (name.includes("password") || name.includes("secret") || name.includes("token")) {
79
+ payloads.push(
80
+ "", " ", "password", "123456",
81
+ "a".repeat(10_000), // password hash DoS
82
+ "null", "undefined",
83
+ );
84
+ }
85
+
86
+ if (name.includes("name") || name.includes("title") || name.includes("text")) {
87
+ payloads.push(
88
+ '<script>alert("xss")</script>',
89
+ "Robert'); DROP TABLE students;--",
90
+ "\u0000null byte", // null byte injection
91
+ "Ṫ̈Ë̈S̈T̈", // combining diacriticals
92
+ "🎉".repeat(1000), // emoji overflow
93
+ "\r\n\r\n<html>", // CRLF injection
94
+ );
95
+ }
96
+
97
+ if (name.includes("amount") || name.includes("price") || name.includes("quantity") || name.includes("count")) {
98
+ payloads.push(
99
+ -1, -0.01, 0, 0.001,
100
+ 99999999, Number.MAX_SAFE_INTEGER,
101
+ "1e308", "NaN", "Infinity",
102
+ );
103
+ }
104
+
105
+ if (name.includes("role") || name.includes("admin") || name.includes("permission")) {
106
+ payloads.push("admin", "root", "superuser", "system", true, 1);
107
+ }
108
+
109
+ if (name.includes("redirect") || name.includes("return") || name.includes("next") || name.includes("callback")) {
110
+ payloads.push(
111
+ "https://evil.com",
112
+ "//evil.com",
113
+ "/\\evil.com",
114
+ "javascript:alert(1)",
115
+ "data:text/html,pwned",
116
+ );
117
+ }
118
+
119
+ return payloads;
120
+ }
121
+
122
+ /**
123
+ * Generate fuzzed request bodies for a route.
124
+ * For each field, generate a fuzzed variant while keeping other fields valid.
125
+ *
126
+ * @param {object} validBody - A valid request body (baseline)
127
+ * @param {object} opts - { maxPayloads: number }
128
+ * @returns {Array<{ field, payload, body }>} Fuzzed variants
129
+ */
130
+ export function generateFuzzedBodies(validBody, opts = {}) {
131
+ const maxPayloads = opts.maxPayloads || 50;
132
+ const variants = [];
133
+
134
+ for (const [field, validValue] of Object.entries(validBody)) {
135
+ const fuzzValues = fuzzField(field);
136
+
137
+ // Take a sample if too many
138
+ const sample = fuzzValues.length > maxPayloads
139
+ ? fuzzValues.sort(() => Math.random() - 0.5).slice(0, maxPayloads)
140
+ : fuzzValues;
141
+
142
+ for (const fuzzValue of sample) {
143
+ variants.push({
144
+ field,
145
+ payload: fuzzValue,
146
+ body: { ...validBody, [field]: fuzzValue },
147
+ });
148
+ }
149
+ }
150
+
151
+ return variants;
152
+ }
153
+
154
+ /**
155
+ * Run fuzz tests against an endpoint.
156
+ *
157
+ * @param {string} baseUrl
158
+ * @param {string} path
159
+ * @param {object} validBody - Known-good request body
160
+ * @param {object} opts - { method, headers, maxPayloads, timeout }
161
+ * @returns {Array<{ field, payload, status, interesting, detail }>}
162
+ */
163
+ export async function fuzzEndpoint(baseUrl, path, validBody, opts = {}) {
164
+ const method = opts.method || "POST";
165
+ const headers = opts.headers || { "Content-Type": "application/json" };
166
+ const timeout = opts.timeout || 10_000;
167
+ const findings = [];
168
+
169
+ const variants = generateFuzzedBodies(validBody, opts);
170
+
171
+ for (const variant of variants) {
172
+ let body;
173
+ try {
174
+ body = JSON.stringify(variant.body);
175
+ } catch {
176
+ continue; // can't serialize (e.g., circular)
177
+ }
178
+
179
+ const controller = new AbortController();
180
+ const timer = setTimeout(() => controller.abort(), timeout);
181
+
182
+ try {
183
+ const res = await fetch(`${baseUrl}${path}`, {
184
+ method,
185
+ headers,
186
+ body,
187
+ signal: controller.signal,
188
+ });
189
+
190
+ const text = await res.text().catch(() => "");
191
+ const result = {
192
+ field: variant.field,
193
+ payload: typeof variant.payload === "object" ? JSON.stringify(variant.payload) : String(variant.payload).slice(0, 100),
194
+ status: res.status,
195
+ interesting: false,
196
+ detail: null,
197
+ };
198
+
199
+ // Flag interesting responses.
200
+ // (No prototype-pollution flag here: a single request returning 200
201
+ // proves nothing, `payload.__proto__` is truthy for every object, so the
202
+ // old check fired on any object payload. Confirming pollution needs a
203
+ // follow-up probe that observes a polluted property; that lives in the
204
+ // adaptive engine, not the one-shot fuzzer.)
205
+ if (res.status === 500) {
206
+ result.interesting = true;
207
+ result.detail = "Server error, possible unhandled edge case";
208
+ }
209
+ if (text.includes("<script>")) {
210
+ result.interesting = true;
211
+ result.detail = "Response contains unescaped <script> tag (XSS)";
212
+ }
213
+ if (/at\s+\w+\s+\(|node_modules|\.js:\d+:\d+/.test(text)) {
214
+ result.interesting = true;
215
+ result.detail = "Response contains stack trace or internal path";
216
+ }
217
+ if (/FIREBASE_|STRIPE_|OPENAI_|DATABASE_|SECRET/.test(text)) {
218
+ result.interesting = true;
219
+ result.detail = "Response contains environment variable name";
220
+ }
221
+
222
+ if (result.interesting) {
223
+ findings.push(result);
224
+ }
225
+ } catch {
226
+ // timeout or network error, skip
227
+ } finally {
228
+ clearTimeout(timer);
229
+ }
230
+ }
231
+
232
+ return findings;
233
+ }