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,343 @@
1
+ import { createFinding } from '../../utils/finding.js';
2
+
3
+ /**
4
+ * OAuthProber — Tests OAuth/SSO flow security.
5
+ *
6
+ * Probes:
7
+ * - Missing state parameter (CSRF via OAuth)
8
+ * - Open redirect via redirect_uri manipulation
9
+ * - Token leakage in URL fragments / referrer
10
+ * - Scope escalation
11
+ * - Missing PKCE
12
+ * - Deprecated implicit flow detection
13
+ */
14
+ export class OAuthProber {
15
+ constructor(logger) {
16
+ this.logger = logger;
17
+
18
+ this.OAUTH_ENDPOINTS = [
19
+ '/oauth/authorize', '/auth/authorize', '/oauth2/authorize',
20
+ '/api/oauth/authorize', '/connect/authorize', '/oauth/login',
21
+ '/.well-known/openid-configuration', '/oauth/.well-known/openid-configuration',
22
+ ];
23
+
24
+ this.CALLBACK_ENDPOINTS = [
25
+ '/callback', '/oauth/callback', '/auth/callback',
26
+ '/api/auth/callback', '/login/callback', '/oauth2/callback',
27
+ ];
28
+ }
29
+
30
+ /**
31
+ * Test OAuth flow security.
32
+ */
33
+ async probe(surfaceInventory) {
34
+ const findings = [];
35
+ const baseUrl = this._getBaseUrl(surfaceInventory);
36
+ if (!baseUrl) return findings;
37
+
38
+ this.logger?.info?.('OAuth Prober: starting tests');
39
+
40
+ // 1. Discover OAuth endpoints
41
+ const oauthEndpoints = await this._discoverOAuth(baseUrl);
42
+
43
+ // 2. Test state parameter
44
+ const stateFindings = await this._testMissingState(baseUrl, oauthEndpoints);
45
+ findings.push(...stateFindings);
46
+
47
+ // 3. Test redirect_uri manipulation
48
+ const redirectFindings = await this._testOpenRedirect(baseUrl, oauthEndpoints);
49
+ findings.push(...redirectFindings);
50
+
51
+ // 4. Test callback endpoints for token leakage
52
+ const callbackFindings = await this._testCallbackSecurity(baseUrl);
53
+ findings.push(...callbackFindings);
54
+
55
+ // 5. Check OpenID Configuration
56
+ const oidcFindings = await this._checkOIDCConfig(baseUrl);
57
+ findings.push(...oidcFindings);
58
+
59
+ this.logger?.info?.(`OAuth Prober: found ${findings.length} issues`);
60
+ return findings;
61
+ }
62
+
63
+ async _discoverOAuth(baseUrl) {
64
+ const discovered = [];
65
+
66
+ // Step 1: Fingerprint the homepage to detect SPA catch-all
67
+ let homeFingerprint = null;
68
+ try {
69
+ const homeResp = await fetch(baseUrl, {
70
+ redirect: 'follow',
71
+ signal: AbortSignal.timeout(5000),
72
+ });
73
+ if (homeResp.ok) {
74
+ homeFingerprint = {
75
+ length: (await homeResp.text()).length,
76
+ status: homeResp.status,
77
+ };
78
+ }
79
+ } catch { /* ignore */ }
80
+
81
+ for (const path of this.OAUTH_ENDPOINTS) {
82
+ try {
83
+ const url = new URL(path, baseUrl).href;
84
+ const response = await fetch(url, {
85
+ method: 'GET',
86
+ redirect: 'manual',
87
+ signal: AbortSignal.timeout(5000),
88
+ });
89
+
90
+ // Must be a redirect (302/301) or return OAuth-specific responses
91
+ if (response.status >= 300 && response.status < 400) {
92
+ const location = response.headers.get('location') || '';
93
+ // Real OAuth endpoints redirect to login pages, consent screens, or error pages
94
+ // Skip if it's just a www redirect (SPA routing)
95
+ const locUrl = new URL(location, baseUrl);
96
+ const reqUrl = new URL(url);
97
+ // If it's just a www prefix redirect, it's not OAuth
98
+ if (locUrl.pathname === reqUrl.pathname && locUrl.hostname !== reqUrl.hostname) continue;
99
+ discovered.push({ url, path, status: response.status });
100
+ } else if (response.status === 200) {
101
+ // 200 could be an OAuth discovery endpoint or SPA catch-all
102
+ const body = await response.text();
103
+ // Check for OAuth-specific JSON responses (.well-known/openid-configuration)
104
+ if (body.includes('authorization_endpoint') || body.includes('token_endpoint')) {
105
+ discovered.push({ url, path, status: response.status });
106
+ }
107
+ // Check for OAuth error page (not SPA catch-all)
108
+ else if (body.includes('oauth') && body.includes('error') && !body.includes('id="root"') && !body.includes('id="app"')) {
109
+ discovered.push({ url, path, status: response.status });
110
+ }
111
+ // If body length matches homepage, it's SPA catch-all — skip
112
+ else if (homeFingerprint && Math.abs(body.length - homeFingerprint.length) < 10) {
113
+ continue;
114
+ }
115
+ } else if (response.status === 401 || response.status === 400) {
116
+ // OAuth endpoints often return 401/400 for missing params
117
+ discovered.push({ url, path, status: response.status });
118
+ }
119
+ // Skip 404, 403, 500+ — not OAuth endpoints
120
+ } catch {
121
+ continue;
122
+ }
123
+ }
124
+
125
+ return discovered;
126
+ }
127
+
128
+ async _testMissingState(baseUrl, endpoints) {
129
+ const findings = [];
130
+
131
+ for (const endpoint of endpoints) {
132
+ try {
133
+ // Request OAuth authorize without state parameter
134
+ const url = new URL(endpoint.url);
135
+ url.searchParams.set('client_id', 'test');
136
+ url.searchParams.set('redirect_uri', `${baseUrl}/callback`);
137
+ url.searchParams.set('response_type', 'code');
138
+ // Deliberately omit 'state' parameter
139
+
140
+ const response = await fetch(url.href, {
141
+ redirect: 'manual',
142
+ signal: AbortSignal.timeout(5000),
143
+ });
144
+
145
+ // If it redirects without requiring state, that's CSRF-vulnerable
146
+ if (response.status >= 300 && response.status < 400) {
147
+ const location = response.headers.get('location') || '';
148
+
149
+ // Skip simple www-prefix or trailing-slash redirects (SPA routing)
150
+ const locUrl = new URL(location, baseUrl);
151
+ const reqUrl = new URL(url.href);
152
+ if (locUrl.pathname === reqUrl.pathname && locUrl.hostname !== reqUrl.hostname) continue;
153
+
154
+ // Must redirect to something OAuth-related (code, token, login, consent)
155
+ const isOAuthRedirect = /code=|token=|login|consent|authorize|error=|oauth/i.test(location);
156
+ if (!isOAuthRedirect && !location.includes('state=')) {
157
+ // If it redirects to the same path with params appended, it's just URL normalization
158
+ if (locUrl.searchParams.toString() === reqUrl.searchParams.toString()) continue;
159
+ }
160
+
161
+ if (!location.includes('state=') && !location.includes('error')) {
162
+ findings.push(createFinding({
163
+ module: 'api',
164
+ title: 'OAuth CSRF: Missing State Parameter',
165
+ severity: 'high',
166
+ affected_surface: endpoint.url,
167
+ description: `OAuth authorize endpoint at ${endpoint.url} processes requests without a state parameter. This enables CSRF attacks where an attacker forces a victim to log in with the attacker's account (login CSRF).`,
168
+ reproduction: [
169
+ `1. Request ${url.href} (no state parameter)`,
170
+ `2. Server redirects without requiring state`,
171
+ `3. Attacker crafts URL and sends to victim`,
172
+ ],
173
+ evidence: `Redirect location: ${location.substring(0, 200)}`,
174
+ remediation: 'Require and validate a cryptographically random state parameter on all OAuth flows. Reject authorization requests without state.',
175
+ }));
176
+ }
177
+ }
178
+ } catch {
179
+ continue;
180
+ }
181
+ }
182
+
183
+ return findings;
184
+ }
185
+
186
+ async _testOpenRedirect(baseUrl, endpoints) {
187
+ const findings = [];
188
+ const evilRedirects = [
189
+ 'https://evil.com/steal',
190
+ 'https://evil.com@legitimate.com',
191
+ '//evil.com',
192
+ 'https://legitimate.com.evil.com/steal',
193
+ ];
194
+
195
+ for (const endpoint of endpoints) {
196
+ for (const evilUri of evilRedirects) {
197
+ try {
198
+ const url = new URL(endpoint.url);
199
+ url.searchParams.set('redirect_uri', evilUri);
200
+ url.searchParams.set('client_id', 'test');
201
+ url.searchParams.set('response_type', 'code');
202
+
203
+ const response = await fetch(url.href, {
204
+ redirect: 'manual',
205
+ signal: AbortSignal.timeout(5000),
206
+ });
207
+
208
+ if (response.status >= 300 && response.status < 400) {
209
+ const location = response.headers.get('location') || '';
210
+
211
+ // Skip www-prefix redirects (SPA routing)
212
+ // A real open redirect would redirect to evil.com domain, not just append params to the same path
213
+ const locUrl = new URL(location, baseUrl);
214
+ const reqUrl = new URL(url.href);
215
+ if (locUrl.pathname === reqUrl.pathname && locUrl.hostname !== reqUrl.hostname) {
216
+ // www redirect — check if evil.com is in the actual redirect target, not just the query string
217
+ if (!locUrl.hostname.includes('evil.com')) continue;
218
+ }
219
+
220
+ if (location.includes('evil.com')) {
221
+ // Verify evil.com is in the redirect HOST, not just echoed in query params
222
+ const isHostRedirect = locUrl.hostname.includes('evil.com');
223
+ const isQueryEcho = location.includes('redirect_uri=') && !isHostRedirect;
224
+ if (isQueryEcho) continue; // Just echoed our param back, not a real redirect
225
+
226
+ findings.push(createFinding({
227
+ module: 'api',
228
+ title: 'OAuth Open Redirect: Arbitrary redirect_uri',
229
+ severity: 'critical',
230
+ affected_surface: endpoint.url,
231
+ description: `OAuth endpoint accepts arbitrary redirect_uri values. An attacker can steal authorization codes by redirecting the OAuth flow to their server: ${evilUri}`,
232
+ reproduction: [
233
+ `1. Set redirect_uri to "${evilUri}"`,
234
+ `2. Server redirects the authorization code to attacker's domain`,
235
+ ],
236
+ evidence: `redirect_uri: ${evilUri}\nServer redirected to: ${location.substring(0, 200)}`,
237
+ remediation: 'Strictly validate redirect_uri against a pre-registered allowlist. Use exact string matching, not prefix matching. Reject any URI not in the allowlist.',
238
+ }));
239
+ break;
240
+ }
241
+ }
242
+ } catch {
243
+ continue;
244
+ }
245
+ }
246
+ }
247
+
248
+ return findings;
249
+ }
250
+
251
+ async _testCallbackSecurity(baseUrl) {
252
+ const findings = [];
253
+
254
+ for (const path of this.CALLBACK_ENDPOINTS) {
255
+ try {
256
+ const url = new URL(path, baseUrl).href;
257
+ // Test if callback accepts token in query string (should be fragment)
258
+ const testUrl = `${url}?code=test_auth_code&state=test_state`;
259
+
260
+ const response = await fetch(testUrl, {
261
+ redirect: 'manual',
262
+ signal: AbortSignal.timeout(5000),
263
+ });
264
+
265
+ if (response.status === 200) {
266
+ const text = await response.text();
267
+ // Check if the page handles the code parameter
268
+ if (text.includes('test_auth_code') || text.includes('code=')) {
269
+ findings.push(createFinding({
270
+ module: 'api',
271
+ title: 'OAuth Token Leakage: Code in URL',
272
+ severity: 'medium',
273
+ affected_surface: url,
274
+ description: `OAuth callback at ${url} processes authorization codes from query parameters. Codes in URLs can leak via browser history, referrer headers, and server logs.`,
275
+ evidence: `Callback echoed the authorization code`,
276
+ remediation: 'Use POST-based callback handling. If using query parameters, ensure codes are single-use and short-lived (30 seconds). Implement PKCE to prevent code interception.',
277
+ }));
278
+ }
279
+ }
280
+ } catch {
281
+ continue;
282
+ }
283
+ }
284
+
285
+ return findings;
286
+ }
287
+
288
+ async _checkOIDCConfig(baseUrl) {
289
+ const findings = [];
290
+
291
+ try {
292
+ const url = new URL('/.well-known/openid-configuration', baseUrl).href;
293
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
294
+
295
+ if (response.ok) {
296
+ const config = await response.json();
297
+
298
+ // Check for implicit flow support
299
+ const grantTypes = config.grant_types_supported || [];
300
+ if (grantTypes.includes('implicit')) {
301
+ findings.push(createFinding({
302
+ module: 'api',
303
+ title: 'OAuth Implicit Flow Supported (Deprecated)',
304
+ severity: 'medium',
305
+ affected_surface: url,
306
+ description: `OpenID Configuration advertises support for the implicit grant type. Implicit flow is deprecated because tokens are exposed in URL fragments and browser history.`,
307
+ evidence: `grant_types_supported: ${JSON.stringify(grantTypes)}`,
308
+ remediation: 'Disable implicit flow. Use authorization code flow with PKCE for all clients, including SPAs.',
309
+ }));
310
+ }
311
+
312
+ // Check for missing PKCE support
313
+ const codeMethods = config.code_challenge_methods_supported || [];
314
+ if (grantTypes.includes('authorization_code') && codeMethods.length === 0) {
315
+ findings.push(createFinding({
316
+ module: 'api',
317
+ title: 'OAuth Missing PKCE Support',
318
+ severity: 'high',
319
+ affected_surface: url,
320
+ description: `Authorization code flow is supported but PKCE (Proof Key for Code Exchange) is not. Without PKCE, authorization codes can be intercepted by malicious apps.`,
321
+ evidence: `code_challenge_methods_supported: not present`,
322
+ remediation: 'Implement PKCE support (RFC 7636). Require code_challenge on all authorization requests. Support S256 code_challenge_method.',
323
+ }));
324
+ }
325
+ }
326
+ } catch {
327
+ // No OIDC config
328
+ }
329
+
330
+ return findings;
331
+ }
332
+
333
+ _getBaseUrl(surfaceInventory) {
334
+ const pages = surfaceInventory.pages || [];
335
+ if (pages.length === 0) return null;
336
+ try {
337
+ const parsed = new URL(pages[0].url || pages[0]);
338
+ return `${parsed.protocol}//${parsed.host}`;
339
+ } catch { return null; }
340
+ }
341
+ }
342
+
343
+ export default OAuthProber;