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.
- package/LICENSE +52 -0
- package/README.md +636 -0
- package/action.yml +264 -0
- package/bin/jaku +2 -0
- package/package.json +62 -0
- package/src/agents/ai-agent.js +175 -0
- package/src/agents/api-agent.js +95 -0
- package/src/agents/base-agent.js +158 -0
- package/src/agents/crawl-agent.js +175 -0
- package/src/agents/event-bus.js +59 -0
- package/src/agents/findings-ledger.js +410 -0
- package/src/agents/logic-agent.js +144 -0
- package/src/agents/orchestrator.js +323 -0
- package/src/agents/qa-agent.js +149 -0
- package/src/agents/security-agent.js +211 -0
- package/src/cli.js +423 -0
- package/src/core/accessibility-checker.js +171 -0
- package/src/core/ai/ai-endpoint-detector.js +227 -0
- package/src/core/ai/guardrail-prober.js +362 -0
- package/src/core/ai/indirect-injector.js +106 -0
- package/src/core/ai/jailbreak-tester.js +212 -0
- package/src/core/ai/model-dos-tester.js +174 -0
- package/src/core/ai/model-fingerprinter.js +246 -0
- package/src/core/ai/multi-turn-attacker.js +297 -0
- package/src/core/ai/output-analyzer.js +182 -0
- package/src/core/ai/prompt-injector.js +543 -0
- package/src/core/ai/system-prompt-extractor.js +244 -0
- package/src/core/api/api-key-auditor.js +266 -0
- package/src/core/api/auth-flow-tester.js +430 -0
- package/src/core/api/cors-ws-tester.js +263 -0
- package/src/core/api/graphql-tester.js +287 -0
- package/src/core/api/oauth-prober.js +343 -0
- package/src/core/auth-manager.js +902 -0
- package/src/core/broken-flow-detector.js +207 -0
- package/src/core/browser-manager.js +119 -0
- package/src/core/console-monitor.js +111 -0
- package/src/core/crawler.js +430 -0
- package/src/core/csr-waiter.js +410 -0
- package/src/core/form-validator.js +240 -0
- package/src/core/logic/abuse-pattern-scanner.js +291 -0
- package/src/core/logic/access-boundary-tester.js +448 -0
- package/src/core/logic/business-rule-inferrer.js +196 -0
- package/src/core/logic/graphql-auditor.js +298 -0
- package/src/core/logic/parameter-polluter.js +212 -0
- package/src/core/logic/pricing-exploiter.js +299 -0
- package/src/core/logic/race-condition-detector.js +222 -0
- package/src/core/logic/workflow-enforcer.js +284 -0
- package/src/core/performance-checker.js +204 -0
- package/src/core/responsive-checker.js +228 -0
- package/src/core/security/cors-prober.js +150 -0
- package/src/core/security/csrf-prober.js +217 -0
- package/src/core/security/dependency-auditor.js +182 -0
- package/src/core/security/file-upload-tester.js +340 -0
- package/src/core/security/header-analyzer.js +324 -0
- package/src/core/security/infra-scanner.js +391 -0
- package/src/core/security/path-traversal.js +112 -0
- package/src/core/security/prototype-pollution.js +147 -0
- package/src/core/security/secret-detector.js +517 -0
- package/src/core/security/sqli-prober.js +257 -0
- package/src/core/security/tls-checker.js +223 -0
- package/src/core/security/xss-scanner.js +225 -0
- package/src/core/test-generator.js +339 -0
- package/src/core/test-runner.js +398 -0
- package/src/reporting/diff-reporter.js +172 -0
- package/src/reporting/report-generator.js +408 -0
- package/src/reporting/sarif-generator.js +190 -0
- package/src/utils/config.js +57 -0
- package/src/utils/finding.js +67 -0
- package/src/utils/logger.js +50 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { loadConfig } from './utils/config.js';
|
|
7
|
+
import { createLogger } from './utils/logger.js';
|
|
8
|
+
import { Orchestrator } from './agents/orchestrator.js';
|
|
9
|
+
import { CrawlAgent } from './agents/crawl-agent.js';
|
|
10
|
+
import { QAAgent } from './agents/qa-agent.js';
|
|
11
|
+
import { SecurityAgent } from './agents/security-agent.js';
|
|
12
|
+
import { AIAgent } from './agents/ai-agent.js';
|
|
13
|
+
import { LogicAgent } from './agents/logic-agent.js';
|
|
14
|
+
import { APIAgent } from './agents/api-agent.js';
|
|
15
|
+
import { ReportGenerator } from './reporting/report-generator.js';
|
|
16
|
+
import { AuthManager } from './core/auth-manager.js';
|
|
17
|
+
|
|
18
|
+
const BANNER = `
|
|
19
|
+
${chalk.hex('#00ff88').bold(' ╦╔═╗╦╔═╦ ╦')}
|
|
20
|
+
${chalk.hex('#00ff88').bold(' ║╠═╣╠╩╗║ ║')} ${chalk.dim('呪 Autonomous Security & Quality Intelligence')}
|
|
21
|
+
${chalk.hex('#00ff88').bold(' ╚╝╩ ╩╩ ╩╚═╝')} ${chalk.dim('v1.0.0 · Multi-Agent')}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('jaku')
|
|
28
|
+
.description('JAKU (呪) — Autonomous QA & Security scanning agent for vibe-coded apps')
|
|
29
|
+
.version('1.0.0');
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════
|
|
32
|
+
// Multi-Agent Scan Runner
|
|
33
|
+
// ═══════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
async function runScan(url, options, modulesToRun) {
|
|
36
|
+
console.log(BANNER);
|
|
37
|
+
|
|
38
|
+
const config = loadConfig({ ...options, targetUrl: url });
|
|
39
|
+
config.target_url = url;
|
|
40
|
+
config.crawler = {
|
|
41
|
+
...config.crawler,
|
|
42
|
+
max_pages: parseInt(options.maxPages) || config.crawler?.max_pages || 50,
|
|
43
|
+
max_depth: parseInt(options.maxDepth) || config.crawler?.max_depth || 5,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Propagate CLI flags to config
|
|
47
|
+
if (options.haltOnCritical) config.halt_on_critical = true;
|
|
48
|
+
if (options.webhook) config.notify_webhook = options.webhook;
|
|
49
|
+
|
|
50
|
+
// Propagate auth flags from CLI
|
|
51
|
+
if (options.username || options.password || options.loginUrl || options.authStrategy) {
|
|
52
|
+
config.auth = config.auth || {};
|
|
53
|
+
if (options.authStrategy) config.auth.strategy = options.authStrategy;
|
|
54
|
+
if (options.loginUrl) config.auth.login_url = options.loginUrl;
|
|
55
|
+
if (options.username && options.password) {
|
|
56
|
+
config.credentials = config.credentials || [];
|
|
57
|
+
// Add CLI credentials as a "cli" role if not already present
|
|
58
|
+
const hasCliRole = config.credentials.some(c => c.role === 'cli');
|
|
59
|
+
if (!hasCliRole) {
|
|
60
|
+
config.credentials.push({
|
|
61
|
+
role: 'cli',
|
|
62
|
+
username: options.username,
|
|
63
|
+
password: options.password,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── prod_safe guard ──
|
|
70
|
+
const prodIndicators = /\b(prod|production|live|www\.)\b/i;
|
|
71
|
+
const isLikelyProd = prodIndicators.test(url) && !/\b(staging|dev|test|local|sandbox)\b/i.test(url);
|
|
72
|
+
if (isLikelyProd && !options.prodSafe && !config.prod_safe) {
|
|
73
|
+
console.error(chalk.red('\n ⛔ PRODUCTION TARGET DETECTED'));
|
|
74
|
+
console.error(chalk.red(` URL "${url}" looks like a production environment.`));
|
|
75
|
+
console.error(chalk.red(' Add --prod-safe flag to confirm you have authorization to test.'));
|
|
76
|
+
console.error(chalk.dim(' Example: jaku scan https://prod.example.com --prod-safe\n'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const logger = createLogger({ verbose: options.verbose });
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
|
|
83
|
+
const runQA = modulesToRun.includes('qa');
|
|
84
|
+
const runSecurity = modulesToRun.includes('security');
|
|
85
|
+
const runAI = modulesToRun.includes('ai');
|
|
86
|
+
const runLogic = modulesToRun.includes('logic');
|
|
87
|
+
const runAPI = modulesToRun.includes('api');
|
|
88
|
+
const moduleLabel = modulesToRun.join(' + ').toUpperCase();
|
|
89
|
+
|
|
90
|
+
console.log(chalk.hex('#00ff88')(' Target: ') + chalk.white(url));
|
|
91
|
+
console.log(chalk.hex('#00ff88')(' Modules: ') + chalk.white(moduleLabel));
|
|
92
|
+
console.log(chalk.hex('#00ff88')(' Mode: ') + chalk.white('Multi-Agent Orchestration'));
|
|
93
|
+
console.log(chalk.hex('#00ff88')(' Severity:') + chalk.white(` ≥ ${config.severity_threshold}`));
|
|
94
|
+
console.log();
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════
|
|
97
|
+
// Phase 0: Authentication (before spinners)
|
|
98
|
+
// ═══════════════════════════════════════
|
|
99
|
+
const authManager = new AuthManager(config, logger);
|
|
100
|
+
const authSpinner = ora({ text: chalk.dim('Detecting login forms...'), color: 'yellow' }).start();
|
|
101
|
+
|
|
102
|
+
// Pause spinner before prompting (so readline works cleanly)
|
|
103
|
+
authManager._onBeforePrompt = () => authSpinner.stop();
|
|
104
|
+
authManager._onAfterPrompt = () => { }; // don't restart — prompt handles its own output
|
|
105
|
+
|
|
106
|
+
await authManager.authenticate();
|
|
107
|
+
|
|
108
|
+
if (authManager.isAuthenticated) {
|
|
109
|
+
authSpinner.succeed(chalk.dim('Authenticated: ') + authManager.roles.map(r => chalk.hex('#00ff88')(r)).join(', '));
|
|
110
|
+
} else {
|
|
111
|
+
authSpinner.info(chalk.dim('No credentials — scanning unauthenticated'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Inject auth manager into config for agents to access
|
|
115
|
+
config._authManager = authManager;
|
|
116
|
+
|
|
117
|
+
// ═══════════════════════════════════════
|
|
118
|
+
// Build the agent constellation
|
|
119
|
+
// ═══════════════════════════════════════
|
|
120
|
+
const orchestrator = new Orchestrator(config, logger);
|
|
121
|
+
|
|
122
|
+
// JAKU-CRAWL always runs (all modules depend on it)
|
|
123
|
+
orchestrator.register(new CrawlAgent());
|
|
124
|
+
|
|
125
|
+
// Register module agents
|
|
126
|
+
let qaAgent = null;
|
|
127
|
+
let secAgent = null;
|
|
128
|
+
|
|
129
|
+
if (runQA) {
|
|
130
|
+
qaAgent = new QAAgent();
|
|
131
|
+
orchestrator.register(qaAgent);
|
|
132
|
+
}
|
|
133
|
+
if (runSecurity) {
|
|
134
|
+
secAgent = new SecurityAgent();
|
|
135
|
+
orchestrator.register(secAgent);
|
|
136
|
+
}
|
|
137
|
+
if (runAI) {
|
|
138
|
+
orchestrator.register(new AIAgent());
|
|
139
|
+
}
|
|
140
|
+
if (runLogic) {
|
|
141
|
+
orchestrator.register(new LogicAgent());
|
|
142
|
+
}
|
|
143
|
+
if (runAPI) {
|
|
144
|
+
orchestrator.register(new APIAgent());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ═══════════════════════════════════════
|
|
148
|
+
// Wire up CLI progress display
|
|
149
|
+
// ═══════════════════════════════════════
|
|
150
|
+
const spinners = {};
|
|
151
|
+
let activeAgentCount = 0;
|
|
152
|
+
const parallelIndicator = () => activeAgentCount > 1 ? chalk.cyan(' ⚡parallel') : '';
|
|
153
|
+
|
|
154
|
+
orchestrator.on('agent:started', ({ agentName }) => {
|
|
155
|
+
activeAgentCount++;
|
|
156
|
+
const color = agentName === 'JAKU-SEC' ? 'yellow' : agentName === 'JAKU-AI' ? 'magenta' : agentName === 'JAKU-LOGIC' ? 'cyan' : agentName === 'JAKU-API' ? 'red' : 'green';
|
|
157
|
+
spinners[agentName] = ora({
|
|
158
|
+
text: chalk.dim(`[${agentName}] `) + 'Starting...' + parallelIndicator(),
|
|
159
|
+
color,
|
|
160
|
+
}).start();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
orchestrator.on('agent:progress', ({ agentName, phase, message }) => {
|
|
164
|
+
if (spinners[agentName]) {
|
|
165
|
+
spinners[agentName].text = chalk.dim(`[${agentName}] `) + message + parallelIndicator();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
orchestrator.on('agent:completed', ({ agentName, duration, findingsCount }) => {
|
|
170
|
+
activeAgentCount--;
|
|
171
|
+
if (spinners[agentName]) {
|
|
172
|
+
spinners[agentName].succeed(
|
|
173
|
+
chalk.dim(`[${agentName}] `) +
|
|
174
|
+
`Complete — ${chalk.hex('#00ff88').bold(findingsCount)} findings in ${(duration / 1000).toFixed(1)}s`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
orchestrator.on('agent:error', ({ agentName, error }) => {
|
|
180
|
+
activeAgentCount--;
|
|
181
|
+
if (spinners[agentName]) {
|
|
182
|
+
spinners[agentName].fail(chalk.dim(`[${agentName}] `) + chalk.red(`Error: ${error}`));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ═══════════════════════════════════════
|
|
187
|
+
// Execute the multi-agent pipeline
|
|
188
|
+
// ═══════════════════════════════════════
|
|
189
|
+
let results;
|
|
190
|
+
try {
|
|
191
|
+
results = await orchestrator.run();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(chalk.red(`\n Orchestrator failed: ${err.message}`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ═══════════════════════════════════════
|
|
198
|
+
// Report Generation
|
|
199
|
+
// ═══════════════════════════════════════
|
|
200
|
+
const reportSpinner = ora({
|
|
201
|
+
text: 'Generating reports...',
|
|
202
|
+
color: 'green',
|
|
203
|
+
}).start();
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const duration = Date.now() - startTime;
|
|
207
|
+
const reporter = new ReportGenerator(config, logger);
|
|
208
|
+
|
|
209
|
+
const testSummary = qaAgent?.testSummary || {};
|
|
210
|
+
|
|
211
|
+
const { reportDir, summary, dedupSummary } = await reporter.generate({
|
|
212
|
+
findings: results.findings,
|
|
213
|
+
deduplicated: results.deduplicated,
|
|
214
|
+
dedupStats: results.dedupStats,
|
|
215
|
+
correlations: results.correlations || [],
|
|
216
|
+
modules: modulesToRun,
|
|
217
|
+
testSummary: { ...testSummary, duration },
|
|
218
|
+
surfaceInventory: results.surfaceInventory,
|
|
219
|
+
outputDir: config.output_dir,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
reportSpinner.succeed(`Reports saved to ${chalk.underline(reportDir)}`);
|
|
223
|
+
|
|
224
|
+
// ═══════════════════════════════════════
|
|
225
|
+
// Final Summary
|
|
226
|
+
// ═══════════════════════════════════════
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(chalk.hex('#00ff88').bold(' ═══ SCAN COMPLETE ═══'));
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(` ${chalk.dim('Duration:')} ${(duration / 1000).toFixed(1)}s`);
|
|
231
|
+
console.log(` ${chalk.dim('Modules:')} ${moduleLabel}`);
|
|
232
|
+
console.log(` ${chalk.dim('Agents:')} ${Object.keys(results.agents).length} agents executed`);
|
|
233
|
+
|
|
234
|
+
// Agent breakdown
|
|
235
|
+
for (const [name, agent] of Object.entries(results.agents)) {
|
|
236
|
+
const statusIcon = agent.status === 'done' ? chalk.hex('#00ff88')('✔') : chalk.red('✘');
|
|
237
|
+
console.log(` ${chalk.dim(' ' + name + ':')} ${statusIcon} ${agent.findingsCount} findings (${(agent.duration / 1000).toFixed(1)}s)`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log();
|
|
241
|
+
const displaySummary = dedupSummary || summary;
|
|
242
|
+
const dedupStats = results.dedupStats;
|
|
243
|
+
if (dedupStats && dedupStats.duplicatesRemoved > 0) {
|
|
244
|
+
console.log(` ${chalk.dim('Findings:')} ${displaySummary.total} unique ${chalk.dim(`(from ${dedupStats.rawCount} raw, ${dedupStats.reductionPercent}% deduped)`)}`);
|
|
245
|
+
} else {
|
|
246
|
+
console.log(` ${chalk.dim('Findings:')} ${summary.total}`);
|
|
247
|
+
}
|
|
248
|
+
if (displaySummary.critical > 0) console.log(` ${chalk.red(' Critical:')} ${displaySummary.critical}`);
|
|
249
|
+
if (displaySummary.high > 0) console.log(` ${chalk.hex('#ff6d00')(' High:')} ${displaySummary.high}`);
|
|
250
|
+
if (displaySummary.medium > 0) console.log(` ${chalk.yellow(' Medium:')} ${displaySummary.medium}`);
|
|
251
|
+
if (displaySummary.low > 0) console.log(` ${chalk.blue(' Low:')} ${displaySummary.low}`);
|
|
252
|
+
if (displaySummary.info > 0) console.log(` ${chalk.gray(' Info:')} ${displaySummary.info}`);
|
|
253
|
+
|
|
254
|
+
// Correlations
|
|
255
|
+
if (results.correlations?.length > 0) {
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(chalk.hex('#ff6d00').bold(' ═══ CORRELATIONS ═══'));
|
|
258
|
+
for (const c of results.correlations) {
|
|
259
|
+
console.log(` ${chalk.hex('#ff6d00')('⚡')} ${c.title}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log();
|
|
264
|
+
|
|
265
|
+
if (summary.critical > 0) {
|
|
266
|
+
console.log(chalk.red.bold(' ⚠ CRITICAL findings detected — immediate action required!'));
|
|
267
|
+
if (config.halt_on_critical) process.exit(1);
|
|
268
|
+
} else if (summary.high > 0) {
|
|
269
|
+
console.log(chalk.hex('#ff6d00')(' ⚠ HIGH severity findings detected — review recommended.'));
|
|
270
|
+
} else if (summary.total === 0) {
|
|
271
|
+
console.log(chalk.hex('#00ff88')(' ✔ No findings at the configured severity threshold. Clean scan!'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
reportSpinner.fail('Report generation failed: ' + err.message);
|
|
277
|
+
logger.error('Report generation failed', err);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ═══════════════════════════════════════════════
|
|
283
|
+
// Fix 7: Multi-target URL resolver
|
|
284
|
+
// ═══════════════════════════════════════════════
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve target URLs from either a primary URL, a comma-separated list (--targets),
|
|
288
|
+
* or a file path (one URL per line).
|
|
289
|
+
*/
|
|
290
|
+
function _resolveTargets(primaryUrl, targetsOption) {
|
|
291
|
+
if (!targetsOption) return [primaryUrl];
|
|
292
|
+
|
|
293
|
+
// Check if it looks like a file path
|
|
294
|
+
if (fs.existsSync(targetsOption)) {
|
|
295
|
+
const lines = fs.readFileSync(targetsOption, 'utf-8')
|
|
296
|
+
.split('\n')
|
|
297
|
+
.map(l => l.trim())
|
|
298
|
+
.filter(l => l && l.startsWith('http'));
|
|
299
|
+
return lines.length > 0 ? lines : [primaryUrl];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Comma-separated list
|
|
303
|
+
const targets = targetsOption.split(',').map(t => t.trim()).filter(t => t.startsWith('http'));
|
|
304
|
+
return targets.length > 0 ? targets : [primaryUrl];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ═══════════════════════════════════════════════
|
|
308
|
+
// Commands
|
|
309
|
+
// ═══════════════════════════════════════════════
|
|
310
|
+
|
|
311
|
+
program
|
|
312
|
+
.command('scan')
|
|
313
|
+
.description('Run JAKU scan with selected modules (default: qa + security)')
|
|
314
|
+
.argument('<url>', 'Target URL to scan')
|
|
315
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
316
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
317
|
+
.option('-m, --modules <list>', 'Comma-separated modules to run (qa,security,ai,logic,api)', 'qa,security,ai,logic,api')
|
|
318
|
+
.option('-s, --severity <level>', 'Minimum severity threshold (critical|high|medium|low)', 'low')
|
|
319
|
+
.option('--targets <urls-or-file>', 'Comma-separated target URLs or path to a file with one URL per line (multi-target mode)')
|
|
320
|
+
.option('--json', 'Output JSON report')
|
|
321
|
+
.option('--html', 'Output HTML report')
|
|
322
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
323
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
324
|
+
.option('--halt-on-critical', 'Abort scan immediately on critical finding')
|
|
325
|
+
.option('--webhook <url>', 'POST findings to webhook URL on completion')
|
|
326
|
+
.option('--prod-safe', 'Confirm authorization to scan production targets')
|
|
327
|
+
.option('--auth-strategy <type>', 'Auth strategy: auto|form|api|cookie (default: auto)')
|
|
328
|
+
.option('--login-url <url>', 'Login page URL for form-based auth')
|
|
329
|
+
.option('--username <user>', 'Username/email for authenticated scanning')
|
|
330
|
+
.option('--password <pass>', 'Password for authenticated scanning')
|
|
331
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
332
|
+
.action(async (url, options) => {
|
|
333
|
+
const modules = options.modules.split(',').map(m => m.trim().toLowerCase());
|
|
334
|
+
|
|
335
|
+
// Fix 7: Multi-target support — parse --targets flag
|
|
336
|
+
const targets = _resolveTargets(url, options.targets);
|
|
337
|
+
|
|
338
|
+
if (targets.length === 1) {
|
|
339
|
+
await runScan(targets[0], options, modules);
|
|
340
|
+
} else {
|
|
341
|
+
console.log(chalk.hex('#00ff88').bold(`\n ═══ MULTI-TARGET SCAN: ${targets.length} targets ═══\n`));
|
|
342
|
+
for (let i = 0; i < targets.length; i++) {
|
|
343
|
+
const target = targets[i];
|
|
344
|
+
console.log(chalk.hex('#00ff88').bold(`\n ── Target ${i + 1}/${targets.length}: ${target} ──\n`));
|
|
345
|
+
await runScan(target, options, modules).catch(err => {
|
|
346
|
+
console.error(chalk.red(` ✘ Scan failed for ${target}: ${err.message}`));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
console.log(chalk.hex('#00ff88').bold(`\n ═══ MULTI-TARGET SCAN COMPLETE ═══\n`));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
program
|
|
354
|
+
.command('qa')
|
|
355
|
+
.description('Run Module 01 only: Quality Assurance & Functional Testing')
|
|
356
|
+
.argument('<url>', 'Target URL to scan')
|
|
357
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
358
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
359
|
+
.option('-s, --severity <level>', 'Severity threshold', 'low')
|
|
360
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
361
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
362
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
363
|
+
.action(async (url, options) => {
|
|
364
|
+
await runScan(url, options, ['qa']);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
program
|
|
368
|
+
.command('security')
|
|
369
|
+
.description('Run Module 02 only: Security Vulnerability Scanning')
|
|
370
|
+
.argument('<url>', 'Target URL to scan')
|
|
371
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
372
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
373
|
+
.option('-s, --severity <level>', 'Severity threshold', 'low')
|
|
374
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
375
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
376
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
377
|
+
.action(async (url, options) => {
|
|
378
|
+
await runScan(url, options, ['security']);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
program
|
|
382
|
+
.command('ai')
|
|
383
|
+
.description('Run Module 04 only: Prompt Injection & AI Abuse Detection')
|
|
384
|
+
.argument('<url>', 'Target URL to scan')
|
|
385
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
386
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
387
|
+
.option('-s, --severity <level>', 'Severity threshold', 'low')
|
|
388
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
389
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
390
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
391
|
+
.action(async (url, options) => {
|
|
392
|
+
await runScan(url, options, ['ai']);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
program
|
|
396
|
+
.command('logic')
|
|
397
|
+
.description('Run Module 03 only: Business Logic Validation')
|
|
398
|
+
.argument('<url>', 'Target URL to scan')
|
|
399
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
400
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
401
|
+
.option('-s, --severity <level>', 'Severity threshold', 'low')
|
|
402
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
403
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
404
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
405
|
+
.action(async (url, options) => {
|
|
406
|
+
await runScan(url, options, ['logic']);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
program
|
|
410
|
+
.command('api')
|
|
411
|
+
.description('Run Module 05 only: API & Auth Flow Verification')
|
|
412
|
+
.argument('<url>', 'Target URL to scan')
|
|
413
|
+
.option('-c, --config <path>', 'Path to jaku.config.json')
|
|
414
|
+
.option('-o, --output <dir>', 'Output directory for reports')
|
|
415
|
+
.option('-s, --severity <level>', 'Severity threshold', 'low')
|
|
416
|
+
.option('--max-pages <n>', 'Maximum pages to crawl', '50')
|
|
417
|
+
.option('--max-depth <n>', 'Maximum crawl depth', '5')
|
|
418
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
419
|
+
.action(async (url, options) => {
|
|
420
|
+
await runScan(url, options, ['api']);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
program.parse();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { createFinding } from '../utils/finding.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AccessibilityChecker — Checks WCAG 2.2 compliance using axe-core.
|
|
6
|
+
*
|
|
7
|
+
* Categories:
|
|
8
|
+
* - Critical: keyboard trap, missing form labels, missing alt text on interactive elements
|
|
9
|
+
* - Serious: color contrast, focus visible, duplicate IDs
|
|
10
|
+
* - Moderate: language attribute, skip navigation, heading order
|
|
11
|
+
*
|
|
12
|
+
* Uses axe-core injected via Playwright for accurate real-browser analysis.
|
|
13
|
+
*/
|
|
14
|
+
export class AccessibilityChecker {
|
|
15
|
+
// axe-core CDN version to inject
|
|
16
|
+
static AXE_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js';
|
|
17
|
+
|
|
18
|
+
// Severity mapping from axe-core impact levels
|
|
19
|
+
static SEVERITY_MAP = {
|
|
20
|
+
critical: 'critical',
|
|
21
|
+
serious: 'high',
|
|
22
|
+
moderate: 'medium',
|
|
23
|
+
minor: 'low',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Categories to report (filter noise)
|
|
27
|
+
static INCLUDE_RULES = new Set([
|
|
28
|
+
'image-alt', 'label', 'label-content-name-mismatch', 'input-button-name',
|
|
29
|
+
'button-name', 'link-name', 'aria-required-attr', 'aria-valid-attr',
|
|
30
|
+
'color-contrast', 'color-contrast-enhanced',
|
|
31
|
+
'keyboard', 'focus-trap', 'focusable-disabled', 'focus-order-semantics',
|
|
32
|
+
'duplicate-id', 'duplicate-id-active', 'duplicate-id-aria',
|
|
33
|
+
'html-has-lang', 'html-lang-valid', 'document-title',
|
|
34
|
+
'heading-order', 'bypass', 'landmark-one-main',
|
|
35
|
+
'form-field-multiple-labels', 'autocomplete-valid',
|
|
36
|
+
'scrollable-region-focusable', 'select-name', 'textarea-label',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
constructor(config, logger) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.logger = logger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async check(surfaceInventory) {
|
|
45
|
+
const findings = [];
|
|
46
|
+
const pages = surfaceInventory.pages.filter(p => p.status < 400).slice(0, 15);
|
|
47
|
+
|
|
48
|
+
if (pages.length === 0) return findings;
|
|
49
|
+
|
|
50
|
+
const browser = await chromium.launch({ headless: true });
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
for (const pageData of pages) {
|
|
54
|
+
const results = await this._runAxe(browser, pageData.url);
|
|
55
|
+
if (!results) continue;
|
|
56
|
+
|
|
57
|
+
for (const violation of results) {
|
|
58
|
+
// Skip rules not in our inclusion list (reduces noise)
|
|
59
|
+
if (!AccessibilityChecker.INCLUDE_RULES.has(violation.id)) continue;
|
|
60
|
+
|
|
61
|
+
const severity = AccessibilityChecker.SEVERITY_MAP[violation.impact] || 'low';
|
|
62
|
+
const affectedCount = violation.nodes?.length || 1;
|
|
63
|
+
|
|
64
|
+
findings.push(createFinding({
|
|
65
|
+
module: 'qa',
|
|
66
|
+
title: `Accessibility (WCAG 2.2): ${violation.help} on ${new URL(pageData.url).pathname}`,
|
|
67
|
+
severity,
|
|
68
|
+
affected_surface: pageData.url,
|
|
69
|
+
description: `${violation.description} This violates WCAG 2.2 success criterion ${violation.helpUrl ? `(see reference)` : violation.id}. ${affectedCount} element${affectedCount > 1 ? 's are' : ' is'} affected on this page.\n\n${violation.help}.`,
|
|
70
|
+
reproduction: [
|
|
71
|
+
`1. Open ${pageData.url}`,
|
|
72
|
+
'2. Run axe-core in DevTools: await axe.run()',
|
|
73
|
+
`3. Look for violation: "${violation.id}"`,
|
|
74
|
+
`4. Affected selectors: ${(violation.nodes || []).slice(0, 3).map(n => n.target?.[0] || 'unknown').join(', ')}`,
|
|
75
|
+
],
|
|
76
|
+
evidence: JSON.stringify({
|
|
77
|
+
rule: violation.id,
|
|
78
|
+
impact: violation.impact,
|
|
79
|
+
affectedCount,
|
|
80
|
+
sampleNodes: (violation.nodes || []).slice(0, 2).map(n => ({
|
|
81
|
+
target: n.target,
|
|
82
|
+
html: n.html?.substring(0, 150),
|
|
83
|
+
failureSummary: n.failureSummary,
|
|
84
|
+
})),
|
|
85
|
+
}, null, 2).substring(0, 800),
|
|
86
|
+
remediation: violation.helpUrl
|
|
87
|
+
? `See axe-core guidance: ${violation.helpUrl}`
|
|
88
|
+
: this._getGenericRemediation(violation.id),
|
|
89
|
+
references: [
|
|
90
|
+
'https://www.w3.org/WAI/WCAG22/quickref/',
|
|
91
|
+
violation.helpUrl || 'https://dequeuniversity.com/rules/axe/',
|
|
92
|
+
],
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.logger?.debug?.(`Accessibility: ${pageData.url} — ${results.length} violations`);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await browser.close();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.logger?.info?.(`Accessibility Checker: found ${findings.length} issues`);
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async _runAxe(browser, url) {
|
|
107
|
+
const page = await browser.newPage({
|
|
108
|
+
viewport: { width: 1440, height: 900 },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
113
|
+
await page.waitForTimeout(1500);
|
|
114
|
+
|
|
115
|
+
// Inject axe-core
|
|
116
|
+
await page.addScriptTag({ url: AccessibilityChecker.AXE_CDN }).catch(async () => {
|
|
117
|
+
// Fallback: try local CDN or skip
|
|
118
|
+
const axeSource = await this._fetchAxeCore().catch(() => null);
|
|
119
|
+
if (axeSource) await page.addScriptTag({ content: axeSource });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await page.waitForTimeout(500);
|
|
123
|
+
|
|
124
|
+
// Run axe
|
|
125
|
+
const results = await page.evaluate(async () => {
|
|
126
|
+
if (typeof axe === 'undefined') return null;
|
|
127
|
+
const result = await axe.run(document, {
|
|
128
|
+
runOnly: {
|
|
129
|
+
type: 'tag',
|
|
130
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return result.violations;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.logger?.debug?.(`Axe run failed for ${url}: ${err.message}`);
|
|
139
|
+
return null;
|
|
140
|
+
} finally {
|
|
141
|
+
await page.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async _fetchAxeCore() {
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(AccessibilityChecker.AXE_CDN);
|
|
148
|
+
return await res.text();
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_getGenericRemediation(ruleId) {
|
|
155
|
+
const remediations = {
|
|
156
|
+
'image-alt': 'Add descriptive alt attributes to all <img> elements. Use alt="" for decorative images.',
|
|
157
|
+
'label': 'Associate every form input with a visible <label> element using for/id pairing or aria-label.',
|
|
158
|
+
'color-contrast': 'Ensure text has a contrast ratio of at least 4.5:1 (3:1 for large text) against its background. Use a contrast checker tool.',
|
|
159
|
+
'keyboard': 'All interactive elements must be operable via keyboard alone. Test Tab, Enter, Space, Arrow keys.',
|
|
160
|
+
'focus-trap': 'Never trap keyboard focus permanently in a component. Modal dialogs should trap focus but provide an escape path (Escape key, close button).',
|
|
161
|
+
'duplicate-id': 'Each id attribute must be unique within the document. Duplicate IDs break ARIA relationships and cause accessibility failures.',
|
|
162
|
+
'html-has-lang': 'Add a lang attribute to the <html> element to identify the page language (e.g., <html lang="en">).',
|
|
163
|
+
'document-title': 'Every page must have a descriptive, unique <title> element.',
|
|
164
|
+
'heading-order': 'Heading levels must not be skipped (e.g., h1 → h3 without h2). Maintain proper hierarchy.',
|
|
165
|
+
'bypass': 'Provide a "Skip to main content" link as the first focusable element to allow keyboard users to bypass navigation.',
|
|
166
|
+
};
|
|
167
|
+
return remediations[ruleId] || 'Follow the WCAG 2.2 guidelines at https://www.w3.org/WAI/WCAG22/quickref/';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default AccessibilityChecker;
|