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,158 @@
1
+ /**
2
+ * BaseAgent — Abstract base class for all JAKU agents.
3
+ *
4
+ * Lifecycle: init() → run() → cleanup()
5
+ *
6
+ * Subclasses must implement:
7
+ * - name (string)
8
+ * - dependencies (string[]) — names of agents that must complete before this one
9
+ * - _execute(context) — the agent's main work
10
+ */
11
+ export class BaseAgent {
12
+ constructor() {
13
+ if (new.target === BaseAgent) {
14
+ throw new Error('BaseAgent is abstract — extend it, do not instantiate directly.');
15
+ }
16
+
17
+ this._status = 'idle'; // idle → initializing → running → done → error
18
+ this._startTime = null;
19
+ this._endTime = null;
20
+ this._findings = [];
21
+ this._eventBus = null;
22
+ this._logger = null;
23
+ this._config = null;
24
+ this._context = null;
25
+ }
26
+
27
+ /** Agent display name (e.g. "JAKU-QA"). Must be overridden. */
28
+ get name() { throw new Error('Agent must define a name'); }
29
+
30
+ /** Agent names this agent depends on. Override to add dependencies. */
31
+ get dependencies() { return []; }
32
+
33
+ /** Current agent status. */
34
+ get status() { return this._status; }
35
+
36
+ /** Duration in ms (only valid after completion). */
37
+ get duration() {
38
+ if (!this._startTime) return 0;
39
+ const end = this._endTime || Date.now();
40
+ return end - this._startTime;
41
+ }
42
+
43
+ /** All findings collected by this agent. */
44
+ get findings() { return [...this._findings]; }
45
+
46
+ /**
47
+ * Initialize the agent with shared context.
48
+ * Called by the Orchestrator before run().
49
+ */
50
+ async init(context) {
51
+ this._status = 'initializing';
52
+ this._config = context.config;
53
+ this._logger = context.logger;
54
+ this._eventBus = context.eventBus;
55
+ this._context = context;
56
+
57
+ this._eventBus.emit('agent:registered', { agentName: this.name });
58
+ this._log(`Agent initialized`);
59
+ }
60
+
61
+ /**
62
+ * Execute the agent. Handles lifecycle, timing, and error boundaries.
63
+ * Do NOT override — implement _execute() instead.
64
+ */
65
+ async run() {
66
+ this._status = 'running';
67
+ this._startTime = Date.now();
68
+
69
+ this._eventBus.emit('agent:started', {
70
+ agentName: this.name,
71
+ timestamp: new Date().toISOString(),
72
+ });
73
+ this._log(`Agent started`);
74
+
75
+ try {
76
+ await this._execute(this._context);
77
+ this._status = 'done';
78
+ this._endTime = Date.now();
79
+
80
+ this._eventBus.emit('agent:completed', {
81
+ agentName: this.name,
82
+ timestamp: new Date().toISOString(),
83
+ duration: this.duration,
84
+ findingsCount: this._findings.length,
85
+ });
86
+ this._log(`Agent completed — ${this._findings.length} findings in ${this.duration}ms`);
87
+
88
+ } catch (error) {
89
+ this._status = 'error';
90
+ this._endTime = Date.now();
91
+
92
+ this._eventBus.emit('agent:error', {
93
+ agentName: this.name,
94
+ error: error.message,
95
+ timestamp: new Date().toISOString(),
96
+ });
97
+ this._log(`Agent error: ${error.message}`, 'error');
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Cleanup resources. Override in subclass if needed.
104
+ */
105
+ async cleanup() {
106
+ // Default: no-op. Subclasses can override for resource cleanup.
107
+ }
108
+
109
+ /**
110
+ * Main execution logic. Must be implemented by subclass.
111
+ */
112
+ async _execute(_context) {
113
+ throw new Error(`${this.name} must implement _execute()`);
114
+ }
115
+
116
+ /**
117
+ * Add a finding to this agent's collection and publish it.
118
+ */
119
+ addFinding(finding) {
120
+ this._findings.push(finding);
121
+ this._eventBus.emit('finding:new', {
122
+ finding,
123
+ agentName: this.name,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Add multiple findings at once.
129
+ */
130
+ addFindings(findings) {
131
+ for (const f of findings) {
132
+ this.addFinding(f);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Emit a progress update for UI consumption.
138
+ */
139
+ progress(phase, message, percent = null) {
140
+ this._eventBus.emit('agent:progress', {
141
+ agentName: this.name,
142
+ phase,
143
+ message,
144
+ progress: percent,
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Internal logging helper.
150
+ */
151
+ _log(message, level = 'info') {
152
+ if (this._logger?.[level]) {
153
+ this._logger[level](`[${this.name}] ${message}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ export default BaseAgent;
@@ -0,0 +1,175 @@
1
+ import { BaseAgent } from './base-agent.js';
2
+ import { Crawler } from '../core/crawler.js';
3
+
4
+ /**
5
+ * JAKU-CRAWL — Discovery Agent
6
+ *
7
+ * The first agent to run. Crawls the target URL to build a complete
8
+ * surface inventory (pages, forms, API endpoints, console errors).
9
+ *
10
+ * Authentication is handled by the CLI before agents start.
11
+ * The pre-authenticated AuthManager is available via config._authManager.
12
+ *
13
+ * Crawl flow:
14
+ * 1. Crawl unauthenticated surfaces first
15
+ * 2. Re-crawl with each authenticated role to discover auth-gated pages
16
+ * 3. Merge all surfaces into a unified inventory
17
+ *
18
+ * Dependencies: none
19
+ */
20
+ export class CrawlAgent extends BaseAgent {
21
+ get name() { return 'JAKU-CRAWL'; }
22
+ get dependencies() { return []; }
23
+
24
+ async _execute(context) {
25
+ const { config, logger } = context;
26
+
27
+ // Auth manager is pre-initialized by the CLI
28
+ const authManager = config._authManager || null;
29
+ context.authManager = authManager;
30
+
31
+ // ═══ Phase 1: Unauthenticated Crawl ═══
32
+ this.progress('crawl', 'Starting unauthenticated crawl...', 10);
33
+
34
+ const unauthCrawler = new Crawler(config, logger);
35
+ const unauthInventory = await unauthCrawler.crawl(config.target_url);
36
+
37
+ // ═══ Phase 2: Authenticated Crawls (one per role) ═══
38
+ const authInventories = new Map();
39
+
40
+ if (authManager?.isAuthenticated) {
41
+ const roles = authManager.roles;
42
+ for (let i = 0; i < roles.length; i++) {
43
+ const role = roles[i];
44
+ const pct = 20 + Math.floor((i / roles.length) * 60);
45
+ this.progress('crawl', `Crawling as "${role}"...`, pct);
46
+
47
+ try {
48
+ const authState = authManager.getAuthState(role);
49
+ const postLoginUrl = authManager.getPostLoginUrl(role);
50
+ const seedLinks = authManager.getDiscoveredLinks(role);
51
+
52
+ // Start from the post-login URL if available (e.g., /dashboard),
53
+ // otherwise fall back to the target URL
54
+ const startUrl = postLoginUrl || config.target_url;
55
+
56
+ const authCrawler = new Crawler(config, logger);
57
+ const authInv = await authCrawler.crawl(startUrl, authState, seedLinks);
58
+
59
+ authInventories.set(role, authInv);
60
+ logger?.info?.(`[JAKU-CRAWL] Authenticated crawl as "${role}": ${authInv.totalPages} pages, ${authInv.totalApis} APIs (started from ${startUrl})`);
61
+ } catch (err) {
62
+ logger?.warn?.(`[JAKU-CRAWL] Authenticated crawl failed for "${role}": ${err.message}`);
63
+ }
64
+ }
65
+ }
66
+
67
+ // ═══ Phase 3: Merge Inventories ═══
68
+ const mergedInventory = this._mergeInventories(unauthInventory, authInventories);
69
+
70
+ // Store inventory in shared context for downstream agents
71
+ context.surfaceInventory = mergedInventory;
72
+
73
+ // Broadcast discovery to all listening agents
74
+ context.eventBus.emit('surface:discovered', {
75
+ inventory: mergedInventory,
76
+ agentName: this.name,
77
+ });
78
+
79
+ this.progress('crawl', `Discovery complete: ${mergedInventory.totalPages} pages, ${mergedInventory.totalApis} APIs, ${mergedInventory.totalForms} forms`, 100);
80
+
81
+ this._log(`Surface inventory: ${mergedInventory.totalPages} pages, ${mergedInventory.totalApis} APIs, ${mergedInventory.totalForms} forms`);
82
+
83
+ if (authManager?.isAuthenticated) {
84
+ this._log(`Authenticated roles: ${authManager.roles.join(', ')}`);
85
+
86
+ // Count auth-only surfaces
87
+ const unauthUrls = new Set(unauthInventory.pages.map(p => p.url));
88
+ const authOnlyPages = mergedInventory.pages.filter(p => !unauthUrls.has(p.url));
89
+ if (authOnlyPages.length > 0) {
90
+ this._log(`Auth-only pages discovered: ${authOnlyPages.length}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Merge unauthenticated and authenticated inventories into one.
97
+ * Deduplicates pages by URL, keeping the most informative version.
98
+ */
99
+ _mergeInventories(unauthInv, authInventories) {
100
+ const pageMap = new Map();
101
+ const apiMap = new Map();
102
+ const formMap = new Map();
103
+
104
+ const addPages = (pages, role = null) => {
105
+ for (const page of pages) {
106
+ const key = page.url;
107
+ if (!pageMap.has(key)) {
108
+ pageMap.set(key, { ...page, roles: role ? [role] : ['anonymous'] });
109
+ } else {
110
+ const existing = pageMap.get(key);
111
+ if (role && !existing.roles.includes(role)) {
112
+ existing.roles.push(role);
113
+ }
114
+ }
115
+ }
116
+ };
117
+
118
+ const addApis = (apis, role = null) => {
119
+ for (const api of apis) {
120
+ const key = `${api.method}::${api.url}`;
121
+ if (!apiMap.has(key)) {
122
+ apiMap.set(key, { ...api, roles: role ? [role] : ['anonymous'] });
123
+ } else {
124
+ const existing = apiMap.get(key);
125
+ if (role && !existing.roles.includes(role)) {
126
+ existing.roles.push(role);
127
+ }
128
+ }
129
+ }
130
+ };
131
+
132
+ const addForms = (forms, role = null) => {
133
+ for (const form of forms) {
134
+ const key = `${form.method}::${form.action}`;
135
+ if (!formMap.has(key)) {
136
+ formMap.set(key, { ...form, roles: role ? [role] : ['anonymous'] });
137
+ } else {
138
+ const existing = formMap.get(key);
139
+ if (role && !existing.roles.includes(role)) {
140
+ existing.roles.push(role);
141
+ }
142
+ }
143
+ }
144
+ };
145
+
146
+ addPages(unauthInv.pages);
147
+ addApis(unauthInv.apiEndpoints || []);
148
+ addForms(unauthInv.forms || []);
149
+
150
+ for (const [role, inv] of authInventories) {
151
+ addPages(inv.pages, role);
152
+ addApis(inv.apiEndpoints || [], role);
153
+ addForms(inv.forms || [], role);
154
+ }
155
+
156
+ const pages = [...pageMap.values()];
157
+ const apiEndpoints = [...apiMap.values()];
158
+ const forms = [...formMap.values()];
159
+
160
+ return {
161
+ baseUrl: unauthInv.baseUrl,
162
+ pages,
163
+ apiEndpoints,
164
+ forms,
165
+ totalPages: pages.length,
166
+ totalApis: apiEndpoints.length,
167
+ totalForms: forms.length,
168
+ crawledAt: new Date().toISOString(),
169
+ authenticated: authInventories.size > 0,
170
+ roles: ['anonymous', ...authInventories.keys()],
171
+ };
172
+ }
173
+ }
174
+
175
+ export default CrawlAgent;
@@ -0,0 +1,59 @@
1
+ import EventEmitter from 'events';
2
+
3
+ /**
4
+ * EventBus — Central message bus for inter-agent communication.
5
+ *
6
+ * Event types:
7
+ * agent:registered { agentName }
8
+ * agent:started { agentName, timestamp }
9
+ * agent:progress { agentName, phase, message, progress }
10
+ * agent:completed { agentName, timestamp, duration, findingsCount }
11
+ * agent:error { agentName, error, timestamp }
12
+ * finding:new { finding, agentName }
13
+ * surface:discovered { inventory, agentName }
14
+ * scan:started { timestamp, modules }
15
+ * scan:completed { timestamp, duration, totalFindings }
16
+ */
17
+ export class EventBus extends EventEmitter {
18
+ constructor() {
19
+ super();
20
+ this.setMaxListeners(50);
21
+ this._log = [];
22
+ }
23
+
24
+ /**
25
+ * Emit an event and record it in the audit log.
26
+ */
27
+ emit(event, data = {}) {
28
+ const entry = {
29
+ event,
30
+ data,
31
+ timestamp: new Date().toISOString(),
32
+ };
33
+ this._log.push(entry);
34
+ return super.emit(event, data);
35
+ }
36
+
37
+ /**
38
+ * Get the full event log for audit/debugging.
39
+ */
40
+ getLog() {
41
+ return [...this._log];
42
+ }
43
+
44
+ /**
45
+ * Get events filtered by type.
46
+ */
47
+ getEvents(eventType) {
48
+ return this._log.filter(e => e.event === eventType);
49
+ }
50
+
51
+ /**
52
+ * Clear the event log.
53
+ */
54
+ clearLog() {
55
+ this._log = [];
56
+ }
57
+ }
58
+
59
+ export default EventBus;