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,207 @@
|
|
|
1
|
+
import { createFinding } from '../utils/finding.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Broken Flow Detector — Detects common broken patterns in web applications.
|
|
5
|
+
* Analyzes crawl data for dead links, empty states, unresponsive elements, etc.
|
|
6
|
+
*/
|
|
7
|
+
export class BrokenFlowDetector {
|
|
8
|
+
constructor(logger) {
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
this.findings = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Analyze crawled surfaces for broken flows.
|
|
15
|
+
*/
|
|
16
|
+
analyze(surfaceInventory) {
|
|
17
|
+
this._detectDeadLinks(surfaceInventory);
|
|
18
|
+
this._detectServerErrors(surfaceInventory);
|
|
19
|
+
this._detectSlowPages(surfaceInventory);
|
|
20
|
+
this._detectPagesWithErrors(surfaceInventory);
|
|
21
|
+
this._detectMissingTitles(surfaceInventory);
|
|
22
|
+
this._detectFormsWithoutSubmit(surfaceInventory);
|
|
23
|
+
|
|
24
|
+
this.logger?.info?.(`Broken flow detector found ${this.findings.length} issues`);
|
|
25
|
+
return this.findings;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect pages that returned 404 or other client errors.
|
|
30
|
+
*/
|
|
31
|
+
_detectDeadLinks(inventory) {
|
|
32
|
+
for (const page of inventory.pages) {
|
|
33
|
+
if (page.status >= 400 && page.status < 500) {
|
|
34
|
+
this.findings.push(
|
|
35
|
+
createFinding({
|
|
36
|
+
module: 'qa',
|
|
37
|
+
title: `Dead Link: ${page.status} at ${this._shortUrl(page.url)}`,
|
|
38
|
+
severity: page.status === 404 ? 'medium' : 'low',
|
|
39
|
+
affected_surface: page.url,
|
|
40
|
+
description: `The page returned HTTP ${page.status}. This indicates a broken link or missing resource that users may encounter during navigation.`,
|
|
41
|
+
reproduction: [
|
|
42
|
+
`1. Navigate to ${page.url}`,
|
|
43
|
+
`2. Observe HTTP ${page.status} response`,
|
|
44
|
+
],
|
|
45
|
+
remediation: page.status === 404
|
|
46
|
+
? 'Either fix the broken link pointing here or create the missing page/resource. Consider adding a custom 404 page with navigation back to working pages.'
|
|
47
|
+
: `Investigate why the page returns ${page.status} and fix the underlying issue.`,
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect pages that returned 5xx server errors.
|
|
56
|
+
*/
|
|
57
|
+
_detectServerErrors(inventory) {
|
|
58
|
+
for (const page of inventory.pages) {
|
|
59
|
+
if (typeof page.status === 'number' && page.status >= 500) {
|
|
60
|
+
this.findings.push(
|
|
61
|
+
createFinding({
|
|
62
|
+
module: 'qa',
|
|
63
|
+
title: `Server Error: ${page.status} at ${this._shortUrl(page.url)}`,
|
|
64
|
+
severity: 'high',
|
|
65
|
+
affected_surface: page.url,
|
|
66
|
+
description: `The page returned HTTP ${page.status} (server error). This indicates a server-side issue that completely blocks the user experience.`,
|
|
67
|
+
reproduction: [
|
|
68
|
+
`1. Navigate to ${page.url}`,
|
|
69
|
+
`2. Observe HTTP ${page.status} server error`,
|
|
70
|
+
],
|
|
71
|
+
remediation: 'Check server logs for the root cause. This is a server-side error that needs immediate attention.',
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
} else if (page.status === 'error') {
|
|
75
|
+
this.findings.push(
|
|
76
|
+
createFinding({
|
|
77
|
+
module: 'qa',
|
|
78
|
+
title: `Page Load Failure: ${this._shortUrl(page.url)}`,
|
|
79
|
+
severity: 'high',
|
|
80
|
+
affected_surface: page.url,
|
|
81
|
+
description: `The page failed to load entirely: ${page.error || 'Unknown error'}. This may indicate a timeout, DNS failure, or crash.`,
|
|
82
|
+
reproduction: [
|
|
83
|
+
`1. Attempt to navigate to ${page.url}`,
|
|
84
|
+
`2. Observe page load failure`,
|
|
85
|
+
],
|
|
86
|
+
remediation: 'Investigate the page load failure. Check for timeouts, infinite redirects, or server availability.',
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect pages with slow load times.
|
|
95
|
+
*/
|
|
96
|
+
_detectSlowPages(inventory) {
|
|
97
|
+
const SLOW_THRESHOLD = 5000; // 5 seconds
|
|
98
|
+
|
|
99
|
+
for (const page of inventory.pages) {
|
|
100
|
+
if (page.loadTime > SLOW_THRESHOLD) {
|
|
101
|
+
this.findings.push(
|
|
102
|
+
createFinding({
|
|
103
|
+
module: 'qa',
|
|
104
|
+
title: `Slow Page Load: ${(page.loadTime / 1000).toFixed(1)}s at ${this._shortUrl(page.url)}`,
|
|
105
|
+
severity: 'low',
|
|
106
|
+
affected_surface: page.url,
|
|
107
|
+
description: `This page took ${(page.loadTime / 1000).toFixed(1)} seconds to reach network idle. Pages loading over ${SLOW_THRESHOLD / 1000}s significantly hurt user experience and SEO.`,
|
|
108
|
+
reproduction: [
|
|
109
|
+
`1. Navigate to ${page.url}`,
|
|
110
|
+
`2. Measure the time to load (observed: ${page.loadTime}ms)`,
|
|
111
|
+
],
|
|
112
|
+
remediation: 'Optimize page load performance. Consider lazy loading, code splitting, image optimization, and caching.',
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Detect pages that had console errors during load.
|
|
121
|
+
*/
|
|
122
|
+
_detectPagesWithErrors(inventory) {
|
|
123
|
+
for (const page of inventory.pages) {
|
|
124
|
+
const errorCount = (page.consoleErrors || []).length;
|
|
125
|
+
const failedCount = (page.failedRequests || []).length;
|
|
126
|
+
|
|
127
|
+
if (errorCount + failedCount > 5) {
|
|
128
|
+
this.findings.push(
|
|
129
|
+
createFinding({
|
|
130
|
+
module: 'qa',
|
|
131
|
+
title: `High Error Rate: ${errorCount + failedCount} issues on ${this._shortUrl(page.url)}`,
|
|
132
|
+
severity: 'medium',
|
|
133
|
+
affected_surface: page.url,
|
|
134
|
+
description: `This page generated ${errorCount} console errors and ${failedCount} failed network requests during load. A high error rate indicates significant quality issues.`,
|
|
135
|
+
reproduction: [
|
|
136
|
+
`1. Navigate to ${page.url}`,
|
|
137
|
+
`2. Open DevTools console`,
|
|
138
|
+
`3. Observe ${errorCount} errors and ${failedCount} failed requests`,
|
|
139
|
+
],
|
|
140
|
+
remediation: 'Systematically address console errors and failed requests. Prioritize JavaScript exceptions and critical API failures.',
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Detect pages with missing or empty titles.
|
|
149
|
+
*/
|
|
150
|
+
_detectMissingTitles(inventory) {
|
|
151
|
+
for (const page of inventory.pages) {
|
|
152
|
+
if (typeof page.status !== 'number' || page.status >= 400) continue;
|
|
153
|
+
if (!page.title || page.title.trim() === '') {
|
|
154
|
+
this.findings.push(
|
|
155
|
+
createFinding({
|
|
156
|
+
module: 'qa',
|
|
157
|
+
title: `Missing Page Title: ${this._shortUrl(page.url)}`,
|
|
158
|
+
severity: 'low',
|
|
159
|
+
affected_surface: page.url,
|
|
160
|
+
description: 'This page has no <title> tag or it is empty. Page titles are critical for SEO, accessibility, and user orientation.',
|
|
161
|
+
reproduction: [
|
|
162
|
+
`1. Navigate to ${page.url}`,
|
|
163
|
+
`2. Check the browser tab — no title is displayed`,
|
|
164
|
+
],
|
|
165
|
+
remediation: 'Add a descriptive <title> tag to the page\'s <head>. Each page should have a unique, descriptive title.',
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Detect forms that have no submit button.
|
|
174
|
+
*/
|
|
175
|
+
_detectFormsWithoutSubmit(inventory) {
|
|
176
|
+
for (const form of inventory.forms) {
|
|
177
|
+
if (!form.hasSubmitButton) {
|
|
178
|
+
this.findings.push(
|
|
179
|
+
createFinding({
|
|
180
|
+
module: 'qa',
|
|
181
|
+
title: `Form Without Submit Button: ${form.id} on ${this._shortUrl(form.page)}`,
|
|
182
|
+
severity: 'low',
|
|
183
|
+
affected_surface: form.page,
|
|
184
|
+
description: `The form "${form.id}" (action: ${form.action}) has no visible submit button (no <button type="submit"> or <input type="submit">). Users may not know how to submit the form.`,
|
|
185
|
+
reproduction: [
|
|
186
|
+
`1. Navigate to ${form.page}`,
|
|
187
|
+
`2. Locate form "${form.id}"`,
|
|
188
|
+
`3. Observe no submit button is present`,
|
|
189
|
+
],
|
|
190
|
+
remediation: 'Add a visible submit button to the form. If submission is handled via JavaScript, ensure the interaction is clear to users.',
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_shortUrl(url) {
|
|
198
|
+
try {
|
|
199
|
+
const u = new URL(url);
|
|
200
|
+
return u.pathname === '/' ? u.hostname : u.pathname;
|
|
201
|
+
} catch {
|
|
202
|
+
return url;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default BrokenFlowDetector;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BrowserManager — Global Playwright browser lifecycle manager.
|
|
5
|
+
*
|
|
6
|
+
* Fixes:
|
|
7
|
+
* - Fix 2: Ensures browsers are always closed on process exit, even if agents
|
|
8
|
+
* crash mid-execution, preventing zombie Chromium processes on CI runners.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const browser = await BrowserManager.launch({ headless: true });
|
|
12
|
+
* // browser is tracked globally and closed on process.exit
|
|
13
|
+
*/
|
|
14
|
+
export class BrowserManager {
|
|
15
|
+
static _instances = new Set();
|
|
16
|
+
static _exitHandlerRegistered = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Launch a new browser, registering it for automatic cleanup on exit.
|
|
20
|
+
*/
|
|
21
|
+
static async launch(options = {}) {
|
|
22
|
+
if (!BrowserManager._exitHandlerRegistered) {
|
|
23
|
+
BrowserManager._registerExitHandlers();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const browser = await chromium.launch({ headless: true, ...options });
|
|
27
|
+
BrowserManager._instances.add(browser);
|
|
28
|
+
|
|
29
|
+
// Remove from registry when browser closes normally
|
|
30
|
+
browser.on('disconnected', () => {
|
|
31
|
+
BrowserManager._instances.delete(browser);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return browser;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a function that uses a browser. Guarantees cleanup.
|
|
39
|
+
* @param {object} options - Playwright launch options
|
|
40
|
+
* @param {Function} fn - async (browser) => result
|
|
41
|
+
*/
|
|
42
|
+
static async withBrowser(options, fn) {
|
|
43
|
+
const browser = await BrowserManager.launch(options);
|
|
44
|
+
try {
|
|
45
|
+
return await fn(browser);
|
|
46
|
+
} finally {
|
|
47
|
+
await BrowserManager._closeSafely(browser);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Close all tracked browsers — called automatically on process exit.
|
|
53
|
+
*/
|
|
54
|
+
static async closeAll() {
|
|
55
|
+
const closePromises = [...BrowserManager._instances].map(b =>
|
|
56
|
+
BrowserManager._closeSafely(b)
|
|
57
|
+
);
|
|
58
|
+
await Promise.allSettled(closePromises);
|
|
59
|
+
BrowserManager._instances.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async _closeSafely(browser) {
|
|
63
|
+
try {
|
|
64
|
+
if (browser.isConnected()) {
|
|
65
|
+
await browser.close();
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Best effort
|
|
69
|
+
} finally {
|
|
70
|
+
BrowserManager._instances.delete(browser);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register exit handlers to clean up zombie browsers.
|
|
76
|
+
*/
|
|
77
|
+
static _registerExitHandlers() {
|
|
78
|
+
BrowserManager._exitHandlerRegistered = true;
|
|
79
|
+
|
|
80
|
+
const cleanup = async (signal) => {
|
|
81
|
+
if (BrowserManager._instances.size > 0) {
|
|
82
|
+
await BrowserManager.closeAll();
|
|
83
|
+
}
|
|
84
|
+
// Re-emit after cleanup so the process actually exits
|
|
85
|
+
if (signal) process.kill(process.pid, signal);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Synchronous exit — use sync-compatible cleanup
|
|
89
|
+
process.on('exit', () => {
|
|
90
|
+
// Can't await in 'exit' handler — fire and forget
|
|
91
|
+
for (const browser of BrowserManager._instances) {
|
|
92
|
+
try { browser.close(); } catch { /* ignore */ }
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Async-capable signals
|
|
97
|
+
process.once('SIGINT', async () => {
|
|
98
|
+
await cleanup('SIGINT');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
process.once('SIGTERM', async () => {
|
|
102
|
+
await cleanup('SIGTERM');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
process.on('uncaughtException', async (err) => {
|
|
106
|
+
console.error('[JAKU] Uncaught exception — cleaning up browsers:', err.message);
|
|
107
|
+
await BrowserManager.closeAll();
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
process.on('unhandledRejection', async (reason) => {
|
|
112
|
+
console.error('[JAKU] Unhandled rejection — cleaning up browsers:', reason);
|
|
113
|
+
await BrowserManager.closeAll();
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default BrowserManager;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createFinding } from '../utils/finding.js';
|
|
2
|
+
import { CSRWaiter } from './csr-waiter.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Console Monitor — Captures and classifies browser console output.
|
|
6
|
+
* Hooks into Playwright page events to capture errors, warnings, and failed requests.
|
|
7
|
+
*/
|
|
8
|
+
export class ConsoleMonitor {
|
|
9
|
+
constructor(logger) {
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.entries = [];
|
|
12
|
+
this.findings = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Analyze console errors and failed requests from crawled surfaces.
|
|
17
|
+
*/
|
|
18
|
+
analyze(surfaceInventory) {
|
|
19
|
+
const errorMap = new Map(); // Deduplication
|
|
20
|
+
|
|
21
|
+
for (const page of surfaceInventory.pages) {
|
|
22
|
+
// Process console errors — filter out known CSR/Supabase loading-state noise
|
|
23
|
+
for (const error of (page.consoleErrors || [])) {
|
|
24
|
+
// Skip errors that are just loading-state artifacts from Supabase/Clerk/etc.
|
|
25
|
+
if (!CSRWaiter.isRealError(error.text || error.message || '')) continue;
|
|
26
|
+
|
|
27
|
+
const key = `${error.type}:${error.text}`;
|
|
28
|
+
if (!errorMap.has(key)) {
|
|
29
|
+
errorMap.set(key, {
|
|
30
|
+
...error,
|
|
31
|
+
pages: [error.url],
|
|
32
|
+
count: 1,
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
const existing = errorMap.get(key);
|
|
36
|
+
if (!existing.pages.includes(error.url)) {
|
|
37
|
+
existing.pages.push(error.url);
|
|
38
|
+
}
|
|
39
|
+
existing.count++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Process failed network requests
|
|
44
|
+
for (const req of (page.failedRequests || [])) {
|
|
45
|
+
this.findings.push(
|
|
46
|
+
createFinding({
|
|
47
|
+
module: 'qa',
|
|
48
|
+
title: `Failed Network Request: ${req.method} ${this._truncateUrl(req.url)}`,
|
|
49
|
+
severity: 'medium',
|
|
50
|
+
affected_surface: req.page,
|
|
51
|
+
description: `A network request to ${req.url} failed with error: ${req.failure}. This indicates a broken resource, missing API endpoint, or connectivity issue.`,
|
|
52
|
+
reproduction: [
|
|
53
|
+
`1. Navigate to ${req.page}`,
|
|
54
|
+
`2. Observe the network panel for failed ${req.method} request to ${req.url}`,
|
|
55
|
+
`3. Error: ${req.failure}`,
|
|
56
|
+
],
|
|
57
|
+
evidence: JSON.stringify(req, null, 2),
|
|
58
|
+
remediation: 'Verify the endpoint exists and is accessible. Check for CORS issues, missing routes, or server errors.',
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert deduplicated errors to findings
|
|
65
|
+
for (const [, error] of errorMap) {
|
|
66
|
+
const severity = error.type === 'exception' ? 'high' : 'medium';
|
|
67
|
+
this.findings.push(
|
|
68
|
+
createFinding({
|
|
69
|
+
module: 'qa',
|
|
70
|
+
title: `Console ${error.type === 'exception' ? 'Exception' : 'Error'}: ${this._truncateText(error.text, 80)}`,
|
|
71
|
+
severity,
|
|
72
|
+
affected_surface: error.pages.join(', '),
|
|
73
|
+
description: `A JavaScript ${error.type} was detected across ${error.count} occurrence(s) on ${error.pages.length} page(s):\n\n${error.text}`,
|
|
74
|
+
reproduction: [
|
|
75
|
+
`1. Navigate to ${error.pages[0]}`,
|
|
76
|
+
`2. Open browser DevTools console`,
|
|
77
|
+
`3. Observe the error: ${error.text}`,
|
|
78
|
+
],
|
|
79
|
+
evidence: JSON.stringify({
|
|
80
|
+
type: error.type,
|
|
81
|
+
message: error.text,
|
|
82
|
+
pages: error.pages,
|
|
83
|
+
occurrences: error.count,
|
|
84
|
+
}, null, 2),
|
|
85
|
+
remediation: error.type === 'exception'
|
|
86
|
+
? 'This is an uncaught JavaScript exception. Add try/catch blocks or fix the root cause to prevent runtime crashes.'
|
|
87
|
+
: 'Investigate and resolve the console error. Even non-critical errors can indicate underlying issues.',
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.logger?.info?.(`Console monitor found ${this.findings.length} issues`);
|
|
93
|
+
return this.findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_truncateText(text, maxLen = 100) {
|
|
97
|
+
if (!text) return '';
|
|
98
|
+
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_truncateUrl(url) {
|
|
102
|
+
try {
|
|
103
|
+
const u = new URL(url);
|
|
104
|
+
return u.pathname.length > 50 ? u.pathname.substring(0, 50) + '...' : u.pathname;
|
|
105
|
+
} catch {
|
|
106
|
+
return url;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default ConsoleMonitor;
|