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
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { sortFindings, filterBySeverity, severitySummary } from '../utils/finding.js';
|
|
4
|
+
import { writeSARIF } from './sarif-generator.js';
|
|
5
|
+
import { DiffReporter } from './diff-reporter.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Report Generator — Generates structured output in JSON, Markdown, HTML, and SARIF formats.
|
|
9
|
+
*/
|
|
10
|
+
export class ReportGenerator {
|
|
11
|
+
constructor(config, logger) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate all reports from findings and test results.
|
|
18
|
+
*/
|
|
19
|
+
async generate({ findings, deduplicated, dedupStats, correlations, modules, testSummary, surfaceInventory, outputDir }) {
|
|
20
|
+
const reportDir = outputDir || path.join(process.cwd(), 'jaku-reports', this._timestamp());
|
|
21
|
+
if (!fs.existsSync(reportDir)) {
|
|
22
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const filteredFindings = filterBySeverity(
|
|
26
|
+
sortFindings(findings),
|
|
27
|
+
this.config.severity_threshold || 'low'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Use deduplicated findings for human-readable reports
|
|
31
|
+
const reportFindings = deduplicated
|
|
32
|
+
? filterBySeverity(sortFindings(deduplicated), this.config.severity_threshold || 'low')
|
|
33
|
+
: filteredFindings;
|
|
34
|
+
|
|
35
|
+
const summary = severitySummary(filteredFindings);
|
|
36
|
+
const dedupSummary = severitySummary(reportFindings);
|
|
37
|
+
|
|
38
|
+
const moduleList = modules || ['qa'];
|
|
39
|
+
const moduleLabel = moduleList.map(m => m.toUpperCase()).join(' + ');
|
|
40
|
+
|
|
41
|
+
const reportData = {
|
|
42
|
+
meta: {
|
|
43
|
+
agent: 'JAKU',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
modules: moduleList,
|
|
46
|
+
moduleLabel,
|
|
47
|
+
target: this.config.target_url,
|
|
48
|
+
scannedAt: new Date().toISOString(),
|
|
49
|
+
duration: testSummary?.duration || null,
|
|
50
|
+
},
|
|
51
|
+
summary,
|
|
52
|
+
dedupSummary,
|
|
53
|
+
dedupStats: dedupStats || null,
|
|
54
|
+
correlations: correlations || [],
|
|
55
|
+
testSummary: testSummary || {},
|
|
56
|
+
surfaceInventory: {
|
|
57
|
+
totalPages: surfaceInventory?.totalPages || 0,
|
|
58
|
+
totalApis: surfaceInventory?.totalApis || 0,
|
|
59
|
+
totalForms: surfaceInventory?.totalForms || 0,
|
|
60
|
+
},
|
|
61
|
+
findings: reportFindings,
|
|
62
|
+
rawFindings: filteredFindings,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Generate JSON
|
|
66
|
+
const jsonPath = path.join(reportDir, 'report.json');
|
|
67
|
+
fs.writeFileSync(jsonPath, JSON.stringify(reportData, null, 2), 'utf-8');
|
|
68
|
+
|
|
69
|
+
// Copy to latest-report.json at project root for easy access
|
|
70
|
+
const latestPath = path.join(process.cwd(), 'latest-report.json');
|
|
71
|
+
try {
|
|
72
|
+
fs.copyFileSync(jsonPath, latestPath);
|
|
73
|
+
this.logger?.info?.(`Latest report copied to ${latestPath}`);
|
|
74
|
+
} catch {
|
|
75
|
+
// Non-critical — skip if CWD is read-only (e.g. CI)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate Markdown (uses deduped findings)
|
|
79
|
+
const mdPath = path.join(reportDir, 'report.md');
|
|
80
|
+
fs.writeFileSync(mdPath, this._generateMarkdown(reportData), 'utf-8');
|
|
81
|
+
|
|
82
|
+
// Generate HTML (uses deduped findings)
|
|
83
|
+
const htmlPath = path.join(reportDir, 'report.html');
|
|
84
|
+
fs.writeFileSync(htmlPath, this._generateHTML(reportData), 'utf-8');
|
|
85
|
+
|
|
86
|
+
// Generate SARIF (uses raw findings for CI/CD accuracy)
|
|
87
|
+
const sarifPath = writeSARIF(filteredFindings, reportDir, reportData.meta);
|
|
88
|
+
this.logger?.info?.(`SARIF report generated: ${sarifPath}`);
|
|
89
|
+
|
|
90
|
+
// Generate Diff Report (regression detection)
|
|
91
|
+
const diffReporter = new DiffReporter(this.logger);
|
|
92
|
+
const diff = diffReporter.generateDiff(filteredFindings, reportDir);
|
|
93
|
+
|
|
94
|
+
this.logger?.info?.(`Reports generated at ${reportDir}`);
|
|
95
|
+
return { reportDir, jsonPath, mdPath, htmlPath, sarifPath, summary, dedupSummary, diff };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_generateMarkdown(data) {
|
|
100
|
+
const { meta, dedupSummary: summary, correlations, testSummary, surfaceInventory, findings } = data;
|
|
101
|
+
let md = '';
|
|
102
|
+
|
|
103
|
+
md += `# 呪 JAKU Security & Quality Report\n\n`;
|
|
104
|
+
md += `**Target:** ${meta.target} \n`;
|
|
105
|
+
md += `**Modules:** ${meta.moduleLabel} \n`;
|
|
106
|
+
md += `**Scanned:** ${meta.scannedAt} \n`;
|
|
107
|
+
md += `**Agent Version:** ${meta.version} \n\n`;
|
|
108
|
+
|
|
109
|
+
md += `---\n\n`;
|
|
110
|
+
md += `## Executive Summary\n\n`;
|
|
111
|
+
md += `| Metric | Value |\n`;
|
|
112
|
+
md += `|--------|-------|\n`;
|
|
113
|
+
md += `| Total Findings | ${summary.total} |\n`;
|
|
114
|
+
md += `| Critical | 🔴 ${summary.critical} |\n`;
|
|
115
|
+
md += `| High | 🟠 ${summary.high} |\n`;
|
|
116
|
+
md += `| Medium | 🟡 ${summary.medium} |\n`;
|
|
117
|
+
md += `| Low | 🔵 ${summary.low} |\n`;
|
|
118
|
+
md += `| Info | ⚪ ${summary.info} |\n\n`;
|
|
119
|
+
|
|
120
|
+
if (testSummary.total) {
|
|
121
|
+
md += `### Test Execution\n\n`;
|
|
122
|
+
md += `| Metric | Value |\n`;
|
|
123
|
+
md += `|--------|-------|\n`;
|
|
124
|
+
md += `| Total Tests | ${testSummary.total} |\n`;
|
|
125
|
+
md += `| Passed | ✅ ${testSummary.passed} |\n`;
|
|
126
|
+
md += `| Failed | ❌ ${testSummary.failed} |\n`;
|
|
127
|
+
md += `| Errors | ⚠️ ${testSummary.errors} |\n\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
md += `### Coverage\n\n`;
|
|
131
|
+
md += `| Surface | Count |\n`;
|
|
132
|
+
md += `|---------|-------|\n`;
|
|
133
|
+
md += `| Pages Crawled | ${surfaceInventory.totalPages} |\n`;
|
|
134
|
+
md += `| API Endpoints | ${surfaceInventory.totalApis} |\n`;
|
|
135
|
+
md += `| Forms Tested | ${surfaceInventory.totalForms} |\n\n`;
|
|
136
|
+
|
|
137
|
+
// ── Correlations / Attack Chains ──
|
|
138
|
+
if (correlations && correlations.length > 0) {
|
|
139
|
+
md += `---\n\n`;
|
|
140
|
+
md += `## ⚡ Attack Chain Correlations (${correlations.length})\n\n`;
|
|
141
|
+
md += `> Correlations show how individual findings combine into exploitable attack chains.\n\n`;
|
|
142
|
+
|
|
143
|
+
for (const c of correlations) {
|
|
144
|
+
const sevIcon = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵', info: '⚪' }[c.severity] || '⚪';
|
|
145
|
+
md += `### ${sevIcon} ${c.title}\n\n`;
|
|
146
|
+
md += `**Type:** ${c.type === 'attack_chain' ? 'Attack Chain' : 'Defense Gap'} \n`;
|
|
147
|
+
md += `**Severity:** ${c.severity.toUpperCase()} \n`;
|
|
148
|
+
md += `**Exploitation:** ${c.exploitation} \n\n`;
|
|
149
|
+
md += `${c.narrative}\n\n`;
|
|
150
|
+
if (c.findings && c.findings.length > 0) {
|
|
151
|
+
md += `**Linked Findings:** ${c.findings.join(', ')}\n\n`;
|
|
152
|
+
}
|
|
153
|
+
md += `---\n\n`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
md += `---\n\n`;
|
|
158
|
+
md += `## Findings\n\n`;
|
|
159
|
+
|
|
160
|
+
if (findings.length === 0) {
|
|
161
|
+
md += `✅ No findings at the configured severity threshold.\n\n`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const sev of ['critical', 'high', 'medium', 'low', 'info']) {
|
|
165
|
+
const sevFindings = findings.filter(f => f.severity === sev);
|
|
166
|
+
if (sevFindings.length === 0) continue;
|
|
167
|
+
|
|
168
|
+
const icons = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵', info: '⚪' };
|
|
169
|
+
md += `### ${icons[sev]} ${sev.toUpperCase()} (${sevFindings.length})\n\n`;
|
|
170
|
+
|
|
171
|
+
for (const f of sevFindings) {
|
|
172
|
+
md += `#### ${f.id}: ${f.title}\n\n`;
|
|
173
|
+
md += `**Affected:** ${f.affected_surface} \n`;
|
|
174
|
+
md += `**Status:** ${f.status} \n\n`;
|
|
175
|
+
md += `${f.description}\n\n`;
|
|
176
|
+
|
|
177
|
+
if (f.reproduction.length > 0) {
|
|
178
|
+
md += `**Reproduction Steps:**\n`;
|
|
179
|
+
for (const step of f.reproduction) {
|
|
180
|
+
md += `${step}\n`;
|
|
181
|
+
}
|
|
182
|
+
md += `\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (f.remediation) {
|
|
186
|
+
md += `**Remediation:** ${f.remediation}\n\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
md += `---\n\n`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
md += `\n*Report generated by JAKU 呪 v${meta.version}*\n`;
|
|
194
|
+
return md;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_generateHTML(data) {
|
|
198
|
+
const { meta, dedupSummary: summary, correlations, testSummary, surfaceInventory, findings } = data;
|
|
199
|
+
const sevColors = {
|
|
200
|
+
critical: '#ff1744',
|
|
201
|
+
high: '#ff6d00',
|
|
202
|
+
medium: '#ffd600',
|
|
203
|
+
low: '#2979ff',
|
|
204
|
+
info: '#90a4ae',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Build correlations HTML
|
|
208
|
+
const correlationsHTML = (correlations && correlations.length > 0) ? `
|
|
209
|
+
<h2>⚡ Attack Chain Correlations (${correlations.length})</h2>
|
|
210
|
+
<p style="color:var(--text-dim);font-size:0.85rem;margin-bottom:1rem">Correlations show how individual findings combine into exploitable attack chains.</p>
|
|
211
|
+
${correlations.map(c => `
|
|
212
|
+
<div class="finding-card ${c.severity}" style="border-left-width:4px;border-left-style:solid">
|
|
213
|
+
<div class="finding-header">
|
|
214
|
+
<span class="finding-title">⚡ ${this._escapeHtml(c.title)}</span>
|
|
215
|
+
<span class="sev-badge ${c.severity}">${c.severity}</span>
|
|
216
|
+
</div>
|
|
217
|
+
<div style="font-size:0.8rem;color:var(--text-dim);margin:0.25rem 0">
|
|
218
|
+
<strong>Type:</strong> ${c.type === 'attack_chain' ? 'Attack Chain' : 'Defense Gap'}
|
|
219
|
+
·
|
|
220
|
+
<strong>Exploitation:</strong> ${this._escapeHtml(c.exploitation)}
|
|
221
|
+
</div>
|
|
222
|
+
<div class="finding-desc">${this._escapeHtml(c.narrative)}</div>
|
|
223
|
+
${c.findings && c.findings.length > 0 ? `<div style="font-size:0.75rem;color:var(--accent);margin-top:0.5rem"><strong>Linked Findings:</strong> ${c.findings.join(', ')}</div>` : ''}
|
|
224
|
+
</div>`).join('')}` : '';
|
|
225
|
+
|
|
226
|
+
return `<!DOCTYPE html>
|
|
227
|
+
<html lang="en" data-theme="dark">
|
|
228
|
+
<head>
|
|
229
|
+
<meta charset="UTF-8">
|
|
230
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
231
|
+
<title>JAKU Report — ${meta.target}</title>
|
|
232
|
+
<style>
|
|
233
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
234
|
+
:root {
|
|
235
|
+
--bg: #0a0a0f; --surface: #12121a; --surface-2: #1a1a25;
|
|
236
|
+
--text: #e0e0e8; --text-dim: #8888a0; --accent: #00ff88;
|
|
237
|
+
--critical: #ff1744; --high: #ff6d00; --medium: #ffd600;
|
|
238
|
+
--low: #2979ff; --info: #90a4ae; --border: #2a2a3a;
|
|
239
|
+
}
|
|
240
|
+
body {
|
|
241
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
242
|
+
background: var(--bg); color: var(--text); line-height: 1.6;
|
|
243
|
+
padding: 2rem; max-width: 1200px; margin: 0 auto;
|
|
244
|
+
}
|
|
245
|
+
h1 { font-size: 1.8rem; color: var(--accent); margin-bottom: 0.5rem; }
|
|
246
|
+
h2 { font-size: 1.3rem; color: var(--text); margin: 2rem 0 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
|
247
|
+
h3 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
|
|
248
|
+
.meta { color: var(--text-dim); font-size: 0.85rem; margin-bottom: 2rem; }
|
|
249
|
+
.meta span { margin-right: 2rem; }
|
|
250
|
+
.summary-grid {
|
|
251
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
252
|
+
gap: 1rem; margin: 1rem 0 2rem;
|
|
253
|
+
}
|
|
254
|
+
.summary-card {
|
|
255
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
256
|
+
border-radius: 8px; padding: 1rem; text-align: center;
|
|
257
|
+
}
|
|
258
|
+
.summary-card .count { font-size: 2rem; font-weight: bold; }
|
|
259
|
+
.summary-card .label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
|
|
260
|
+
.sev-critical .count { color: var(--critical); }
|
|
261
|
+
.sev-high .count { color: var(--high); }
|
|
262
|
+
.sev-medium .count { color: var(--medium); }
|
|
263
|
+
.sev-low .count { color: var(--low); }
|
|
264
|
+
.sev-info .count { color: var(--info); }
|
|
265
|
+
.chart-bar {
|
|
266
|
+
display: flex; height: 8px; border-radius: 4px; overflow: hidden;
|
|
267
|
+
margin: 1rem 0; background: var(--surface);
|
|
268
|
+
}
|
|
269
|
+
.chart-bar div { height: 100%; transition: width 0.3s; }
|
|
270
|
+
.finding-card {
|
|
271
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
272
|
+
border-radius: 8px; padding: 1.25rem; margin: 1rem 0;
|
|
273
|
+
border-left: 4px solid var(--info);
|
|
274
|
+
}
|
|
275
|
+
.finding-card.critical { border-left-color: var(--critical); }
|
|
276
|
+
.finding-card.high { border-left-color: var(--high); }
|
|
277
|
+
.finding-card.medium { border-left-color: var(--medium); }
|
|
278
|
+
.finding-card.low { border-left-color: var(--low); }
|
|
279
|
+
.finding-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
|
|
280
|
+
.finding-id { font-size: 0.75rem; color: var(--text-dim); }
|
|
281
|
+
.finding-title { font-weight: bold; font-size: 0.95rem; }
|
|
282
|
+
.sev-badge {
|
|
283
|
+
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
|
284
|
+
font-size: 0.7rem; font-weight: bold; text-transform: uppercase;
|
|
285
|
+
}
|
|
286
|
+
.sev-badge.critical { background: var(--critical); color: #fff; }
|
|
287
|
+
.sev-badge.high { background: var(--high); color: #000; }
|
|
288
|
+
.sev-badge.medium { background: var(--medium); color: #000; }
|
|
289
|
+
.sev-badge.low { background: var(--low); color: #fff; }
|
|
290
|
+
.sev-badge.info { background: var(--info); color: #000; }
|
|
291
|
+
.finding-desc { font-size: 0.85rem; color: var(--text-dim); margin: 0.5rem 0; white-space: pre-wrap; }
|
|
292
|
+
.finding-details { margin-top: 0.75rem; }
|
|
293
|
+
.finding-details summary { cursor: pointer; font-size: 0.8rem; color: var(--accent); }
|
|
294
|
+
.finding-details pre { background: var(--surface-2); padding: 0.75rem; border-radius: 4px; margin-top: 0.5rem; font-size: 0.8rem; overflow-x: auto; }
|
|
295
|
+
.filter-bar { display: flex; gap: 0.5rem; margin: 1rem 0; flex-wrap: wrap; }
|
|
296
|
+
.filter-btn {
|
|
297
|
+
background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
|
298
|
+
padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 0.8rem; font-family: inherit;
|
|
299
|
+
}
|
|
300
|
+
.filter-btn.active { border-color: var(--accent); color: var(--accent); }
|
|
301
|
+
.filter-btn:hover { border-color: var(--accent); }
|
|
302
|
+
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--text-dim); font-size: 0.75rem; text-align: center; }
|
|
303
|
+
</style>
|
|
304
|
+
</head>
|
|
305
|
+
<body>
|
|
306
|
+
<h1>呪 JAKU</h1>
|
|
307
|
+
<div class="meta">
|
|
308
|
+
<span>Target: ${meta.target}</span>
|
|
309
|
+
<span>Modules: ${meta.moduleLabel}</span>
|
|
310
|
+
<span>Scanned: ${new Date(meta.scannedAt).toLocaleString()}</span>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<h2>Severity Breakdown</h2>
|
|
314
|
+
<div class="summary-grid">
|
|
315
|
+
<div class="summary-card sev-critical"><div class="count">${summary.critical}</div><div class="label">Critical</div></div>
|
|
316
|
+
<div class="summary-card sev-high"><div class="count">${summary.high}</div><div class="label">High</div></div>
|
|
317
|
+
<div class="summary-card sev-medium"><div class="count">${summary.medium}</div><div class="label">Medium</div></div>
|
|
318
|
+
<div class="summary-card sev-low"><div class="count">${summary.low}</div><div class="label">Low</div></div>
|
|
319
|
+
<div class="summary-card sev-info"><div class="count">${summary.info}</div><div class="label">Info</div></div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="chart-bar">
|
|
323
|
+
${summary.total > 0 ? `
|
|
324
|
+
<div style="width:${(summary.critical / summary.total) * 100}%;background:var(--critical)"></div>
|
|
325
|
+
<div style="width:${(summary.high / summary.total) * 100}%;background:var(--high)"></div>
|
|
326
|
+
<div style="width:${(summary.medium / summary.total) * 100}%;background:var(--medium)"></div>
|
|
327
|
+
<div style="width:${(summary.low / summary.total) * 100}%;background:var(--low)"></div>
|
|
328
|
+
<div style="width:${(summary.info / summary.total) * 100}%;background:var(--info)"></div>
|
|
329
|
+
` : '<div style="width:100%;background:var(--accent)"></div>'}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<h2>Coverage</h2>
|
|
333
|
+
<div class="summary-grid">
|
|
334
|
+
<div class="summary-card"><div class="count" style="color:var(--accent)">${surfaceInventory.totalPages}</div><div class="label">Pages</div></div>
|
|
335
|
+
<div class="summary-card"><div class="count" style="color:var(--accent)">${surfaceInventory.totalApis}</div><div class="label">API Endpoints</div></div>
|
|
336
|
+
<div class="summary-card"><div class="count" style="color:var(--accent)">${surfaceInventory.totalForms}</div><div class="label">Forms</div></div>
|
|
337
|
+
${testSummary?.total ? `
|
|
338
|
+
<div class="summary-card"><div class="count" style="color:var(--accent)">${testSummary.total}</div><div class="label">Tests Run</div></div>
|
|
339
|
+
<div class="summary-card"><div class="count" style="color:#00e676">${testSummary.passed}</div><div class="label">Passed</div></div>
|
|
340
|
+
<div class="summary-card"><div class="count" style="color:var(--critical)">${testSummary.failed + (testSummary.errors || 0)}</div><div class="label">Failed</div></div>
|
|
341
|
+
` : ''}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
${correlationsHTML}
|
|
345
|
+
|
|
346
|
+
<h2>Findings (${summary.total})</h2>
|
|
347
|
+
<div class="filter-bar">
|
|
348
|
+
<button class="filter-btn active" onclick="filterFindings('all')">All (${summary.total})</button>
|
|
349
|
+
${summary.critical > 0 ? `<button class="filter-btn" onclick="filterFindings('critical')">Critical (${summary.critical})</button>` : ''}
|
|
350
|
+
${summary.high > 0 ? `<button class="filter-btn" onclick="filterFindings('high')">High (${summary.high})</button>` : ''}
|
|
351
|
+
${summary.medium > 0 ? `<button class="filter-btn" onclick="filterFindings('medium')">Medium (${summary.medium})</button>` : ''}
|
|
352
|
+
${summary.low > 0 ? `<button class="filter-btn" onclick="filterFindings('low')">Low (${summary.low})</button>` : ''}
|
|
353
|
+
${summary.info > 0 ? `<button class="filter-btn" onclick="filterFindings('info')">Info (${summary.info})</button>` : ''}
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div id="findings-container">
|
|
357
|
+
${findings.map(f => `
|
|
358
|
+
<div class="finding-card ${f.severity}" data-severity="${f.severity}">
|
|
359
|
+
<div class="finding-header">
|
|
360
|
+
<span class="finding-id">${f.id}</span>
|
|
361
|
+
<span class="sev-badge ${f.severity}">${f.severity}</span>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="finding-title">${this._escapeHtml(f.title)}</div>
|
|
364
|
+
<div class="finding-desc">${this._escapeHtml(f.description)}</div>
|
|
365
|
+
<div style="font-size:0.8rem;color:var(--text-dim);margin-top:0.5rem">
|
|
366
|
+
<strong>Affected:</strong> ${this._escapeHtml(f.affected_surface)}
|
|
367
|
+
</div>
|
|
368
|
+
${f.remediation ? `<div style="font-size:0.85rem;margin-top:0.5rem;color:var(--accent)"><strong>Fix:</strong> ${this._escapeHtml(f.remediation)}</div>` : ''}
|
|
369
|
+
<details class="finding-details">
|
|
370
|
+
<summary>Evidence & Reproduction</summary>
|
|
371
|
+
<pre>${this._escapeHtml(f.reproduction?.join?.('\\n') || '')}</pre>
|
|
372
|
+
${f.evidence ? `<pre>${this._escapeHtml(typeof f.evidence === 'string' ? f.evidence : JSON.stringify(f.evidence, null, 2))}</pre>` : ''}
|
|
373
|
+
</details>
|
|
374
|
+
</div>`).join('')}
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<footer>JAKU 呪 v${meta.version} — Autonomous Security & Quality Intelligence</footer>
|
|
378
|
+
|
|
379
|
+
<script>
|
|
380
|
+
function filterFindings(severity) {
|
|
381
|
+
const cards = document.querySelectorAll('.finding-card');
|
|
382
|
+
const btns = document.querySelectorAll('.filter-btn');
|
|
383
|
+
btns.forEach(b => b.classList.remove('active'));
|
|
384
|
+
event.target.classList.add('active');
|
|
385
|
+
cards.forEach(card => {
|
|
386
|
+
card.style.display = severity === 'all' || card.dataset.severity === severity ? 'block' : 'none';
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_escapeHtml(text) {
|
|
395
|
+
if (!text) return '';
|
|
396
|
+
return String(text)
|
|
397
|
+
.replace(/&/g, '&')
|
|
398
|
+
.replace(/</g, '<')
|
|
399
|
+
.replace(/>/g, '>')
|
|
400
|
+
.replace(/"/g, '"');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
_timestamp() {
|
|
404
|
+
return new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export default ReportGenerator;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SARIF Generator — Generates Static Analysis Results Interchange Format (SARIF) v2.1.0
|
|
6
|
+
* for integration with GitHub Security Dashboard, GitLab SAST, and Azure DevOps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** CWE mapping for common finding patterns */
|
|
10
|
+
const CWE_MAP = {
|
|
11
|
+
xss: { id: 'CWE-79', name: 'Cross-site Scripting (XSS)' },
|
|
12
|
+
sqli: { id: 'CWE-89', name: 'SQL Injection' },
|
|
13
|
+
nosqli: { id: 'CWE-943', name: 'NoSQL Injection' },
|
|
14
|
+
csrf: { id: 'CWE-352', name: 'Cross-Site Request Forgery' },
|
|
15
|
+
idor: { id: 'CWE-639', name: 'Authorization Bypass Through User-Controlled Key' },
|
|
16
|
+
ssrf: { id: 'CWE-918', name: 'Server-Side Request Forgery' },
|
|
17
|
+
'open redirect': { id: 'CWE-601', name: 'URL Redirection to Untrusted Site' },
|
|
18
|
+
'missing header': { id: 'CWE-693', name: 'Protection Mechanism Failure' },
|
|
19
|
+
hsts: { id: 'CWE-319', name: 'Cleartext Transmission of Sensitive Information' },
|
|
20
|
+
csp: { id: 'CWE-1021', name: 'Improper Restriction of Rendered UI Layers' },
|
|
21
|
+
cors: { id: 'CWE-942', name: 'Overly Permissive Cross-domain Whitelist' },
|
|
22
|
+
tls: { id: 'CWE-295', name: 'Improper Certificate Validation' },
|
|
23
|
+
jwt: { id: 'CWE-347', name: 'Improper Verification of Cryptographic Signature' },
|
|
24
|
+
'api key': { id: 'CWE-312', name: 'Cleartext Storage of Sensitive Information' },
|
|
25
|
+
secret: { id: 'CWE-798', name: 'Use of Hard-coded Credentials' },
|
|
26
|
+
'prompt injection': { id: 'CWE-77', name: 'Command Injection' },
|
|
27
|
+
'race condition': { id: 'CWE-362', name: 'Concurrent Execution Using Shared Resource' },
|
|
28
|
+
'access control': { id: 'CWE-284', name: 'Improper Access Control' },
|
|
29
|
+
'file upload': { id: 'CWE-434', name: 'Unrestricted Upload of File with Dangerous Type' },
|
|
30
|
+
'path traversal': { id: 'CWE-22', name: 'Path Traversal' },
|
|
31
|
+
graphql: { id: 'CWE-200', name: 'Exposure of Sensitive Information' },
|
|
32
|
+
'rate limit': { id: 'CWE-307', name: 'Improper Restriction of Excessive Authentication Attempts' },
|
|
33
|
+
websocket: { id: 'CWE-306', name: 'Missing Authentication for Critical Function' },
|
|
34
|
+
password: { id: 'CWE-521', name: 'Weak Password Requirements' },
|
|
35
|
+
mfa: { id: 'CWE-308', name: 'Use of Single-factor Authentication' },
|
|
36
|
+
session: { id: 'CWE-614', name: 'Sensitive Cookie Without Secure Flag' },
|
|
37
|
+
oauth: { id: 'CWE-346', name: 'Origin Validation Error' },
|
|
38
|
+
'pricing': { id: 'CWE-20', name: 'Improper Input Validation' },
|
|
39
|
+
'workflow': { id: 'CWE-841', name: 'Improper Enforcement of Behavioral Workflow' },
|
|
40
|
+
'abuse': { id: 'CWE-799', name: 'Improper Control of Interaction Frequency' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SEVERITY_TO_SARIF = {
|
|
44
|
+
critical: 'error',
|
|
45
|
+
high: 'error',
|
|
46
|
+
medium: 'warning',
|
|
47
|
+
low: 'note',
|
|
48
|
+
info: 'note',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate SARIF v2.1.0 report from JAKU findings.
|
|
53
|
+
*/
|
|
54
|
+
export function generateSARIF(findings, meta = {}) {
|
|
55
|
+
const rules = [];
|
|
56
|
+
const results = [];
|
|
57
|
+
const ruleIndex = new Map();
|
|
58
|
+
|
|
59
|
+
for (const finding of findings) {
|
|
60
|
+
// Build rule ID
|
|
61
|
+
const ruleId = finding.id || `JAKU-${finding.module?.toUpperCase()}-0000`;
|
|
62
|
+
|
|
63
|
+
if (!ruleIndex.has(ruleId)) {
|
|
64
|
+
ruleIndex.set(ruleId, rules.length);
|
|
65
|
+
|
|
66
|
+
const cwe = _matchCWE(finding);
|
|
67
|
+
const rule = {
|
|
68
|
+
id: ruleId,
|
|
69
|
+
name: finding.title?.replace(/[^a-zA-Z0-9]/g, '') || 'Unknown',
|
|
70
|
+
shortDescription: { text: finding.title || 'Unknown finding' },
|
|
71
|
+
fullDescription: { text: finding.description || finding.title || '' },
|
|
72
|
+
defaultConfiguration: { level: SEVERITY_TO_SARIF[finding.severity] || 'note' },
|
|
73
|
+
helpUri: finding.references?.[0] || 'https://owasp.org/www-project-top-ten/',
|
|
74
|
+
properties: {
|
|
75
|
+
tags: [finding.module || 'security', finding.severity],
|
|
76
|
+
precision: 'medium',
|
|
77
|
+
'security-severity': _cvssFromSeverity(finding.severity),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (cwe) {
|
|
82
|
+
rule.properties.tags.push(`external/cwe/${cwe.id.replace('CWE-', '')}`);
|
|
83
|
+
rule.relationships = [{
|
|
84
|
+
target: { id: cwe.id, toolComponent: { name: 'CWE' } },
|
|
85
|
+
kinds: ['superset'],
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
rules.push(rule);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build result
|
|
93
|
+
const result = {
|
|
94
|
+
ruleId,
|
|
95
|
+
ruleIndex: ruleIndex.get(ruleId),
|
|
96
|
+
level: SEVERITY_TO_SARIF[finding.severity] || 'note',
|
|
97
|
+
message: { text: finding.description || finding.title },
|
|
98
|
+
locations: [{
|
|
99
|
+
physicalLocation: {
|
|
100
|
+
artifactLocation: {
|
|
101
|
+
uri: finding.affected_surface || meta.target || 'unknown',
|
|
102
|
+
uriBaseId: '%SRCROOT%',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}],
|
|
106
|
+
properties: {
|
|
107
|
+
severity: finding.severity,
|
|
108
|
+
module: finding.module,
|
|
109
|
+
status: finding.status || 'open',
|
|
110
|
+
timestamp: finding.timestamp || new Date().toISOString(),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (finding.remediation) {
|
|
115
|
+
result.fixes = [{ description: { text: finding.remediation } }];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (finding.evidence) {
|
|
119
|
+
result.codeFlows = [{
|
|
120
|
+
message: { text: 'Evidence' },
|
|
121
|
+
threadFlows: [{
|
|
122
|
+
locations: [{
|
|
123
|
+
location: {
|
|
124
|
+
message: { text: typeof finding.evidence === 'string' ? finding.evidence : JSON.stringify(finding.evidence) },
|
|
125
|
+
physicalLocation: {
|
|
126
|
+
artifactLocation: { uri: finding.affected_surface || 'unknown' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}],
|
|
130
|
+
}],
|
|
131
|
+
}];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
results.push(result);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sarif = {
|
|
138
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
|
|
139
|
+
version: '2.1.0',
|
|
140
|
+
runs: [{
|
|
141
|
+
tool: {
|
|
142
|
+
driver: {
|
|
143
|
+
name: 'JAKU',
|
|
144
|
+
version: meta.version || '1.0.0',
|
|
145
|
+
semanticVersion: meta.version || '1.0.0',
|
|
146
|
+
informationUri: 'https://github.com/jaku-security',
|
|
147
|
+
rules,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
results,
|
|
151
|
+
invocations: [{
|
|
152
|
+
executionSuccessful: true,
|
|
153
|
+
startTimeUtc: meta.scannedAt || new Date().toISOString(),
|
|
154
|
+
}],
|
|
155
|
+
}],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return sarif;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Write SARIF to file.
|
|
163
|
+
*/
|
|
164
|
+
export function writeSARIF(findings, outputDir, meta = {}) {
|
|
165
|
+
const sarif = generateSARIF(findings, meta);
|
|
166
|
+
const sarifPath = path.join(outputDir, 'report.sarif');
|
|
167
|
+
fs.writeFileSync(sarifPath, JSON.stringify(sarif, null, 2), 'utf-8');
|
|
168
|
+
return sarifPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Match a finding to a CWE based on title/description patterns.
|
|
173
|
+
*/
|
|
174
|
+
function _matchCWE(finding) {
|
|
175
|
+
const text = `${finding.title} ${finding.description}`.toLowerCase();
|
|
176
|
+
for (const [pattern, cwe] of Object.entries(CWE_MAP)) {
|
|
177
|
+
if (text.includes(pattern)) return cwe;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Map JAKU severity to CVSS-like numeric score.
|
|
184
|
+
*/
|
|
185
|
+
function _cvssFromSeverity(severity) {
|
|
186
|
+
const map = { critical: '9.8', high: '7.5', medium: '5.0', low: '2.5', info: '0.0' };
|
|
187
|
+
return map[severity] || '0.0';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default { generateSARIF, writeSARIF };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
target_url: null,
|
|
6
|
+
credentials: [],
|
|
7
|
+
modules_enabled: ['qa'],
|
|
8
|
+
severity_threshold: 'low',
|
|
9
|
+
halt_on_critical: false,
|
|
10
|
+
notify_webhook: null,
|
|
11
|
+
crawler: {
|
|
12
|
+
max_depth: 5,
|
|
13
|
+
max_pages: 50,
|
|
14
|
+
timeout: 30000,
|
|
15
|
+
respect_robots_txt: true,
|
|
16
|
+
},
|
|
17
|
+
viewports: {
|
|
18
|
+
mobile: { width: 375, height: 812 },
|
|
19
|
+
tablet: { width: 768, height: 1024 },
|
|
20
|
+
desktop: { width: 1440, height: 900 },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function loadConfig(cliOptions = {}) {
|
|
25
|
+
let fileConfig = {};
|
|
26
|
+
|
|
27
|
+
// Load from config file if specified or default path exists
|
|
28
|
+
const configPath = cliOptions.config || path.join(process.cwd(), 'jaku.config.json');
|
|
29
|
+
if (fs.existsSync(configPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
32
|
+
fileConfig = JSON.parse(raw);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.warn(`⚠ Warning: Could not parse config file at ${configPath}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Merge: defaults < file config < CLI options
|
|
39
|
+
const config = {
|
|
40
|
+
...DEFAULTS,
|
|
41
|
+
...fileConfig,
|
|
42
|
+
crawler: { ...DEFAULTS.crawler, ...(fileConfig.crawler || {}) },
|
|
43
|
+
viewports: { ...DEFAULTS.viewports, ...(fileConfig.viewports || {}) },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// CLI overrides
|
|
47
|
+
if (cliOptions.targetUrl) config.target_url = cliOptions.targetUrl;
|
|
48
|
+
if (cliOptions.severity) config.severity_threshold = cliOptions.severity;
|
|
49
|
+
if (cliOptions.output) config.output_dir = cliOptions.output;
|
|
50
|
+
if (cliOptions.verbose !== undefined) config.verbose = cliOptions.verbose;
|
|
51
|
+
if (cliOptions.json) config.output_json = true;
|
|
52
|
+
if (cliOptions.html) config.output_html = true;
|
|
53
|
+
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default loadConfig;
|