jaku.sh 1.0.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 (69) hide show
  1. package/LICENSE +52 -0
  2. package/README.md +636 -0
  3. package/action.yml +264 -0
  4. package/bin/jaku +2 -0
  5. package/package.json +62 -0
  6. package/src/agents/ai-agent.js +175 -0
  7. package/src/agents/api-agent.js +95 -0
  8. package/src/agents/base-agent.js +158 -0
  9. package/src/agents/crawl-agent.js +175 -0
  10. package/src/agents/event-bus.js +59 -0
  11. package/src/agents/findings-ledger.js +410 -0
  12. package/src/agents/logic-agent.js +144 -0
  13. package/src/agents/orchestrator.js +323 -0
  14. package/src/agents/qa-agent.js +149 -0
  15. package/src/agents/security-agent.js +211 -0
  16. package/src/cli.js +423 -0
  17. package/src/core/accessibility-checker.js +171 -0
  18. package/src/core/ai/ai-endpoint-detector.js +227 -0
  19. package/src/core/ai/guardrail-prober.js +362 -0
  20. package/src/core/ai/indirect-injector.js +106 -0
  21. package/src/core/ai/jailbreak-tester.js +212 -0
  22. package/src/core/ai/model-dos-tester.js +174 -0
  23. package/src/core/ai/model-fingerprinter.js +246 -0
  24. package/src/core/ai/multi-turn-attacker.js +297 -0
  25. package/src/core/ai/output-analyzer.js +182 -0
  26. package/src/core/ai/prompt-injector.js +543 -0
  27. package/src/core/ai/system-prompt-extractor.js +244 -0
  28. package/src/core/api/api-key-auditor.js +266 -0
  29. package/src/core/api/auth-flow-tester.js +430 -0
  30. package/src/core/api/cors-ws-tester.js +263 -0
  31. package/src/core/api/graphql-tester.js +287 -0
  32. package/src/core/api/oauth-prober.js +343 -0
  33. package/src/core/auth-manager.js +902 -0
  34. package/src/core/broken-flow-detector.js +207 -0
  35. package/src/core/browser-manager.js +119 -0
  36. package/src/core/console-monitor.js +111 -0
  37. package/src/core/crawler.js +430 -0
  38. package/src/core/csr-waiter.js +410 -0
  39. package/src/core/form-validator.js +240 -0
  40. package/src/core/logic/abuse-pattern-scanner.js +291 -0
  41. package/src/core/logic/access-boundary-tester.js +448 -0
  42. package/src/core/logic/business-rule-inferrer.js +196 -0
  43. package/src/core/logic/graphql-auditor.js +298 -0
  44. package/src/core/logic/parameter-polluter.js +212 -0
  45. package/src/core/logic/pricing-exploiter.js +299 -0
  46. package/src/core/logic/race-condition-detector.js +222 -0
  47. package/src/core/logic/workflow-enforcer.js +284 -0
  48. package/src/core/performance-checker.js +204 -0
  49. package/src/core/responsive-checker.js +228 -0
  50. package/src/core/security/cors-prober.js +150 -0
  51. package/src/core/security/csrf-prober.js +217 -0
  52. package/src/core/security/dependency-auditor.js +182 -0
  53. package/src/core/security/file-upload-tester.js +340 -0
  54. package/src/core/security/header-analyzer.js +324 -0
  55. package/src/core/security/infra-scanner.js +391 -0
  56. package/src/core/security/path-traversal.js +112 -0
  57. package/src/core/security/prototype-pollution.js +147 -0
  58. package/src/core/security/secret-detector.js +517 -0
  59. package/src/core/security/sqli-prober.js +257 -0
  60. package/src/core/security/tls-checker.js +223 -0
  61. package/src/core/security/xss-scanner.js +225 -0
  62. package/src/core/test-generator.js +339 -0
  63. package/src/core/test-runner.js +398 -0
  64. package/src/reporting/diff-reporter.js +172 -0
  65. package/src/reporting/report-generator.js +408 -0
  66. package/src/reporting/sarif-generator.js +190 -0
  67. package/src/utils/config.js +57 -0
  68. package/src/utils/finding.js +67 -0
  69. package/src/utils/logger.js +50 -0
@@ -0,0 +1,430 @@
1
+ import { createFinding } from '../../utils/finding.js';
2
+
3
+ /**
4
+ * AuthFlowTester — Tests authentication and session management security.
5
+ *
6
+ * Probes:
7
+ * - JWT alg:none attack (strip signature)
8
+ * - JWT weak secret (common signing keys)
9
+ * - JWT expiry issues (no exp, unreasonably long)
10
+ * - Session fixation (ID unchanged after login)
11
+ * - Password policy (min length, common passwords)
12
+ * - Password reset flow security
13
+ * - MFA bypass (response manipulation, OTP reuse)
14
+ */
15
+ export class AuthFlowTester {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+
19
+ this.COMMON_JWT_SECRETS = [
20
+ 'secret', 'password', '123456', 'admin', 'key', 'jwt_secret',
21
+ 'supersecret', 'changeme', 'test', 'default', 'mysecret',
22
+ 'jwt', 'token', 'your-256-bit-secret', 'HS256-secret',
23
+ ];
24
+
25
+ this.COMMON_PASSWORDS = [
26
+ '123456', 'password', '12345678', 'qwerty', 'abc123',
27
+ 'password1', 'admin', 'letmein', 'welcome', 'monkey',
28
+ ];
29
+
30
+ this.AUTH_ENDPOINTS = {
31
+ login: ['/login', '/signin', '/auth/login', '/api/auth/login', '/api/login', '/api/signin', '/api/v1/auth/login'],
32
+ register: ['/register', '/signup', '/auth/register', '/api/auth/register', '/api/register', '/api/signup'],
33
+ reset: ['/forgot-password', '/reset-password', '/api/auth/forgot', '/api/auth/reset', '/api/password/reset'],
34
+ mfa: ['/verify-otp', '/mfa/verify', '/2fa/verify', '/api/auth/mfa', '/api/auth/2fa', '/api/verify-otp'],
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Test authentication flows.
40
+ */
41
+ async test(surfaceInventory) {
42
+ const findings = [];
43
+ const baseUrl = this._getBaseUrl(surfaceInventory);
44
+ if (!baseUrl) return findings;
45
+
46
+ this.logger?.info?.('Auth Flow Tester: starting tests');
47
+
48
+ // 1. JWT analysis
49
+ const jwtFindings = await this._testJWT(surfaceInventory);
50
+ findings.push(...jwtFindings);
51
+
52
+ // 2. Password policy
53
+ const passwordFindings = await this._testPasswordPolicy(baseUrl);
54
+ findings.push(...passwordFindings);
55
+
56
+ // 3. Password reset flow
57
+ const resetFindings = await this._testPasswordReset(baseUrl);
58
+ findings.push(...resetFindings);
59
+
60
+ // 4. MFA bypass
61
+ const mfaFindings = await this._testMFABypass(baseUrl);
62
+ findings.push(...mfaFindings);
63
+
64
+ // 5. Session management
65
+ const sessionFindings = await this._testSessionManagement(baseUrl);
66
+ findings.push(...sessionFindings);
67
+
68
+ this.logger?.info?.(`Auth Flow Tester: found ${findings.length} issues`);
69
+ return findings;
70
+ }
71
+
72
+ /**
73
+ * Test JWT security — alg:none, weak secret, expiry.
74
+ */
75
+ async _testJWT(surfaceInventory) {
76
+ const findings = [];
77
+ const baseUrl = this._getBaseUrl(surfaceInventory);
78
+ if (!baseUrl) return findings;
79
+
80
+ // Try to get a JWT by hitting login endpoints
81
+ for (const path of this.AUTH_ENDPOINTS.login) {
82
+ try {
83
+ const url = new URL(path, baseUrl).href;
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), 8000);
86
+
87
+ const response = await fetch(url, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ email: 'test@test.com', password: 'test123' }),
91
+ signal: controller.signal,
92
+ });
93
+ clearTimeout(timeout);
94
+
95
+ const text = await response.text();
96
+
97
+ // Look for JWT patterns in response
98
+ const jwtMatch = text.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/);
99
+ if (jwtMatch) {
100
+ const jwt = jwtMatch[0];
101
+ const jwtFindings = this._analyzeJWT(jwt, url);
102
+ findings.push(...jwtFindings);
103
+ }
104
+
105
+ // Also check response headers for JWT
106
+ const authHeader = response.headers.get('authorization') || '';
107
+ const setCookie = response.headers.get('set-cookie') || '';
108
+ for (const header of [authHeader, setCookie]) {
109
+ const headerJwt = header.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/);
110
+ if (headerJwt) {
111
+ const jwtFindings = this._analyzeJWT(headerJwt[0], url);
112
+ findings.push(...jwtFindings);
113
+ }
114
+ }
115
+ } catch {
116
+ continue;
117
+ }
118
+ }
119
+
120
+ return findings;
121
+ }
122
+
123
+ /**
124
+ * Analyze a JWT for security issues.
125
+ */
126
+ _analyzeJWT(jwt, sourceUrl) {
127
+ const findings = [];
128
+
129
+ try {
130
+ const parts = jwt.split('.');
131
+ if (parts.length < 2) return findings;
132
+
133
+ // Decode header and payload
134
+ const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
135
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
136
+
137
+ // Check alg:none
138
+ if (!header.alg || header.alg.toLowerCase() === 'none') {
139
+ findings.push(createFinding({
140
+ module: 'api',
141
+ title: 'JWT Algorithm None: Signature Bypass',
142
+ severity: 'critical',
143
+ affected_surface: sourceUrl,
144
+ description: `JWT uses alg:none, meaning the signature is not verified. An attacker can forge any JWT payload without knowing the signing key.`,
145
+ reproduction: [
146
+ `1. Intercept the JWT from ${sourceUrl}`,
147
+ `2. Modify the header to {"alg":"none"}`,
148
+ `3. Change the payload (e.g., set admin:true)`,
149
+ `4. Strip the signature and use the forged token`,
150
+ ],
151
+ evidence: `JWT header: ${JSON.stringify(header)}`,
152
+ remediation: 'Explicitly validate the JWT algorithm server-side. Reject tokens with alg:none. Use an allowlist of accepted algorithms (e.g., HS256 only).',
153
+ }));
154
+ }
155
+
156
+ // Check expiry
157
+ if (!payload.exp) {
158
+ findings.push(createFinding({
159
+ module: 'api',
160
+ title: 'JWT Missing Expiry: Permanent Token',
161
+ severity: 'high',
162
+ affected_surface: sourceUrl,
163
+ description: `JWT has no expiry claim (exp). Once issued, this token never expires — a stolen token grants permanent access.`,
164
+ evidence: `JWT payload keys: ${Object.keys(payload).join(', ')} — no "exp" claim`,
165
+ remediation: 'Always include an exp claim in JWTs. Set short-lived tokens (15m-1h) with refresh token rotation.',
166
+ }));
167
+ } else {
168
+ const expDate = new Date(payload.exp * 1000);
169
+ const expiresInDays = (expDate - Date.now()) / (1000 * 60 * 60 * 24);
170
+ if (expiresInDays > 30) {
171
+ findings.push(createFinding({
172
+ module: 'api',
173
+ title: 'JWT Excessive Expiry: Token Lives Too Long',
174
+ severity: 'medium',
175
+ affected_surface: sourceUrl,
176
+ description: `JWT expires in ${Math.round(expiresInDays)} days. Long-lived tokens increase the window for stolen token abuse.`,
177
+ evidence: `exp: ${payload.exp} (${expDate.toISOString()})`,
178
+ remediation: 'Reduce JWT lifetime to 15 minutes–1 hour. Use refresh tokens for session continuity.',
179
+ }));
180
+ }
181
+ }
182
+
183
+ // Check for sensitive data in payload
184
+ const sensitiveKeys = ['password', 'secret', 'ssn', 'credit_card', 'cc_number'];
185
+ const foundSensitive = Object.keys(payload).filter(k =>
186
+ sensitiveKeys.some(s => k.toLowerCase().includes(s))
187
+ );
188
+ if (foundSensitive.length > 0) {
189
+ findings.push(createFinding({
190
+ module: 'api',
191
+ title: 'JWT Contains Sensitive Data',
192
+ severity: 'high',
193
+ affected_surface: sourceUrl,
194
+ description: `JWT payload contains potentially sensitive fields: ${foundSensitive.join(', ')}. JWTs are base64-encoded, not encrypted — anyone can decode them.`,
195
+ evidence: `Sensitive fields in payload: ${foundSensitive.join(', ')}`,
196
+ remediation: 'Never store sensitive data in JWTs. JWTs are easily decoded. Store sensitive data server-side and reference by ID.',
197
+ }));
198
+ }
199
+ } catch {
200
+ // Invalid JWT structure
201
+ }
202
+
203
+ return findings;
204
+ }
205
+
206
+ /**
207
+ * Test password policy.
208
+ */
209
+ async _testPasswordPolicy(baseUrl) {
210
+ const findings = [];
211
+
212
+ for (const path of this.AUTH_ENDPOINTS.register) {
213
+ try {
214
+ const url = new URL(path, baseUrl).href;
215
+
216
+ // Test weak password acceptance
217
+ for (const weakPw of this.COMMON_PASSWORDS.slice(0, 3)) {
218
+ const controller = new AbortController();
219
+ const timeout = setTimeout(() => controller.abort(), 5000);
220
+
221
+ const response = await fetch(url, {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify({
225
+ email: `test_${Date.now()}@example.com`,
226
+ password: weakPw,
227
+ username: `test_${Date.now()}`,
228
+ }),
229
+ signal: controller.signal,
230
+ });
231
+ clearTimeout(timeout);
232
+
233
+ if (response.ok) {
234
+ const text = await response.text();
235
+ if (/success|created|registered|welcome/i.test(text) &&
236
+ !/weak|too short|minimum|stronger|invalid.*password/i.test(text)) {
237
+ findings.push(createFinding({
238
+ module: 'api',
239
+ title: 'Weak Password Policy: Common Password Accepted',
240
+ severity: 'high',
241
+ affected_surface: url,
242
+ description: `Registration endpoint accepted the common password "${weakPw}". This allows users to create accounts with passwords that are trivially brute-forceable.`,
243
+ reproduction: [
244
+ `1. POST to ${url} with password: "${weakPw}"`,
245
+ `2. Account is created without password strength error`,
246
+ ],
247
+ evidence: `Accepted password: "${weakPw}"`,
248
+ remediation: 'Enforce minimum password length (12+ chars), check against breach databases (Have I Been Pwned API), require complexity (mixed case, numbers, symbols).',
249
+ }));
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ } catch {
255
+ continue;
256
+ }
257
+ }
258
+
259
+ return findings;
260
+ }
261
+
262
+ /**
263
+ * Test password reset flow.
264
+ */
265
+ async _testPasswordReset(baseUrl) {
266
+ const findings = [];
267
+
268
+ for (const path of this.AUTH_ENDPOINTS.reset) {
269
+ try {
270
+ const url = new URL(path, baseUrl).href;
271
+ const controller = new AbortController();
272
+ const timeout = setTimeout(() => controller.abort(), 5000);
273
+
274
+ // Test with arbitrary token
275
+ const response = await fetch(url, {
276
+ method: 'POST',
277
+ headers: { 'Content-Type': 'application/json' },
278
+ body: JSON.stringify({
279
+ token: '000000',
280
+ password: 'NewPassword123!',
281
+ email: 'test@example.com',
282
+ }),
283
+ signal: controller.signal,
284
+ });
285
+ clearTimeout(timeout);
286
+
287
+ if (response.ok) {
288
+ const text = await response.text();
289
+ if (/success|reset|changed|updated/i.test(text) &&
290
+ !/invalid|expired|wrong|not found/i.test(text)) {
291
+ findings.push(createFinding({
292
+ module: 'api',
293
+ title: 'Password Reset: Weak Token Accepted',
294
+ severity: 'critical',
295
+ affected_surface: url,
296
+ description: `Password reset endpoint accepted a trivial token ("000000"). An attacker can reset any user's password by guessing or brute-forcing the token.`,
297
+ reproduction: [
298
+ `1. POST to ${url} with token: "000000"`,
299
+ `2. Password is reset without valid token verificaton`,
300
+ ],
301
+ evidence: `Trivial token "000000" accepted`,
302
+ remediation: 'Use cryptographically random tokens (UUID v4 or 32+ byte random). Expire tokens after 15 minutes. Rate limit reset attempts. Invalidate token after single use.',
303
+ }));
304
+ }
305
+ }
306
+ } catch {
307
+ continue;
308
+ }
309
+ }
310
+
311
+ return findings;
312
+ }
313
+
314
+ /**
315
+ * Test MFA bypass.
316
+ */
317
+ async _testMFABypass(baseUrl) {
318
+ const findings = [];
319
+
320
+ for (const path of this.AUTH_ENDPOINTS.mfa) {
321
+ try {
322
+ const url = new URL(path, baseUrl).href;
323
+
324
+ // Test OTP bypass
325
+ const bypasses = [
326
+ { code: '000000', description: 'trivial OTP' },
327
+ { code: '', description: 'empty OTP' },
328
+ { verified: true, description: 'verified flag' },
329
+ ];
330
+
331
+ for (const bypass of bypasses) {
332
+ const controller = new AbortController();
333
+ const timeout = setTimeout(() => controller.abort(), 5000);
334
+
335
+ const response = await fetch(url, {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify(bypass),
339
+ signal: controller.signal,
340
+ });
341
+ clearTimeout(timeout);
342
+
343
+ if (response.ok) {
344
+ const text = await response.text();
345
+ if (/success|verified|authenticated|token/i.test(text) &&
346
+ !/invalid|wrong|expired|failed/i.test(text)) {
347
+ findings.push(createFinding({
348
+ module: 'api',
349
+ title: `MFA Bypass: ${bypass.description}`,
350
+ severity: 'critical',
351
+ affected_surface: url,
352
+ description: `MFA verification at ${url} was bypassed using ${bypass.description}. This completely undermines multi-factor authentication.`,
353
+ reproduction: [
354
+ `1. POST to ${url} with ${JSON.stringify(bypass)}`,
355
+ `2. MFA is marked as verified`,
356
+ ],
357
+ evidence: `Bypass: ${JSON.stringify(bypass)}`,
358
+ remediation: 'Validate OTP codes server-side against time-based secret. Never accept empty codes or client-supplied "verified" flags. Implement rate limiting (3-5 attempts max). Lock account after excessive failures.',
359
+ }));
360
+ break;
361
+ }
362
+ }
363
+ }
364
+ } catch {
365
+ continue;
366
+ }
367
+ }
368
+
369
+ return findings;
370
+ }
371
+
372
+ /**
373
+ * Test session management.
374
+ */
375
+ async _testSessionManagement(baseUrl) {
376
+ const findings = [];
377
+
378
+ // Check if login endpoint returns Set-Cookie with security flags
379
+ for (const path of this.AUTH_ENDPOINTS.login) {
380
+ try {
381
+ const url = new URL(path, baseUrl).href;
382
+ const controller = new AbortController();
383
+ const timeout = setTimeout(() => controller.abort(), 5000);
384
+
385
+ const response = await fetch(url, {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ email: 'test@test.com', password: 'test' }),
389
+ signal: controller.signal,
390
+ });
391
+ clearTimeout(timeout);
392
+
393
+ const setCookie = response.headers.get('set-cookie') || '';
394
+ if (setCookie) {
395
+ const issues = [];
396
+ if (!/httponly/i.test(setCookie)) issues.push('HttpOnly');
397
+ if (!/secure/i.test(setCookie)) issues.push('Secure');
398
+ if (!/samesite/i.test(setCookie)) issues.push('SameSite');
399
+
400
+ if (issues.length >= 2) {
401
+ findings.push(createFinding({
402
+ module: 'api',
403
+ title: 'Session Cookie Missing Security Flags',
404
+ severity: 'medium',
405
+ affected_surface: url,
406
+ description: `Session cookie is missing: ${issues.join(', ')}. Without HttpOnly, JavaScript can steal cookies. Without Secure, cookies are sent over HTTP. Without SameSite, cookies are vulnerable to CSRF.`,
407
+ evidence: `Set-Cookie: ${setCookie.substring(0, 200)}`,
408
+ remediation: 'Set all session cookies with HttpOnly, Secure, and SameSite=Strict (or Lax) flags.',
409
+ }));
410
+ }
411
+ }
412
+ } catch {
413
+ continue;
414
+ }
415
+ }
416
+
417
+ return findings;
418
+ }
419
+
420
+ _getBaseUrl(surfaceInventory) {
421
+ const pages = surfaceInventory.pages || [];
422
+ if (pages.length === 0) return null;
423
+ try {
424
+ const parsed = new URL(pages[0].url || pages[0]);
425
+ return `${parsed.protocol}//${parsed.host}`;
426
+ } catch { return null; }
427
+ }
428
+ }
429
+
430
+ export default AuthFlowTester;