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.
- package/CHANGELOG.md +168 -0
- package/LICENSE +21 -0
- package/README.md +561 -0
- package/SECURITY.md +31 -0
- package/bin/penthera.js +3 -0
- package/bin/scan.js +11 -0
- package/docs/index.md +60 -0
- package/docs/owasp-wstg-coverage.md +79 -0
- package/lib/adaptive-probes.js +334 -0
- package/lib/attack-surface.js +149 -0
- package/lib/audit-loop.js +170 -0
- package/lib/blackbox/adaptive-scan.js +137 -0
- package/lib/blackbox/client-auth.js +60 -0
- package/lib/blackbox/email-dns.js +135 -0
- package/lib/blackbox/headers.js +91 -0
- package/lib/blackbox/idor.js +113 -0
- package/lib/blackbox/jwt.js +73 -0
- package/lib/blackbox/oauth.js +50 -0
- package/lib/blackbox/openapi.js +227 -0
- package/lib/compliance/incident.js +169 -0
- package/lib/compliance/index.js +163 -0
- package/lib/compliance/policy-pack.js +293 -0
- package/lib/compliance/questionnaire.js +113 -0
- package/lib/compliance/readiness.js +229 -0
- package/lib/compliance/self-assessment.js +66 -0
- package/lib/crawler.js +257 -0
- package/lib/fuzzer.js +233 -0
- package/lib/injections.js +676 -0
- package/lib/jurisdictions/nl.json +48 -0
- package/lib/knowledge-graph.js +232 -0
- package/lib/machine.js +814 -0
- package/lib/nuclei-loader.js +283 -0
- package/lib/owasp-wstg.js +204 -0
- package/lib/params.js +235 -0
- package/lib/plugins.js +70 -0
- package/lib/provenance.js +82 -0
- package/lib/recon.js +193 -0
- package/lib/remediation/index.js +219 -0
- package/lib/retirejs.js +239 -0
- package/lib/supply-chain.js +131 -0
- package/lib/templates.js +364 -0
- package/lib/tls.js +145 -0
- package/lib/whitebox/frameworks.js +139 -0
- package/lib/whitebox/sbom.js +144 -0
- package/lib/whitebox/secrets.js +111 -0
- package/package.json +76 -0
- package/pentest.config.example.js +82 -0
- package/scripts/package-skill.sh +16 -0
- package/skills/penthera/SKILL.md +272 -0
- package/skills/penthera/references/authorization.md +36 -0
- package/skills/penthera/references/output-and-ci.md +109 -0
- package/skills/penthera/references/profiles-and-flags.md +74 -0
- package/skills/penthera/references/remediation.md +267 -0
- package/skills/penthera/references/reporting.md +113 -0
- package/skills/penthera/references/secure-defaults.md +41 -0
- package/skills/penthera/references/troubleshooting.md +100 -0
- package/skills/penthera/scripts/preflight.sh +66 -0
- package/skills/penthera/scripts/validate-report.mjs +53 -0
- package/skills/penthera/scripts/validate-report.sh +8 -0
- package/src/cli/ansi.js +70 -0
- package/src/cli/baseline.js +37 -0
- package/src/cli/merge-results.js +71 -0
- package/src/cli/onboarding.js +216 -0
- package/src/cli/profiles.js +53 -0
- package/src/cli/prompt.js +64 -0
- package/src/cli/run-scan.js +152 -0
- package/src/cli.js +536 -0
- package/src/report/markdown.js +182 -0
- package/src/reporter.js +512 -0
- package/src/scan-repo.js +205 -0
- package/src/scan-url.js +563 -0
- package/src/utils/auth.js +13 -0
- package/src/utils/http.js +38 -0
- 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
|
+
}
|