octocode-cli 1.3.1 → 1.5.1
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/README.md +193 -34
- package/out/chunks/chunk-7476PETK.js +309 -0
- package/out/chunks/chunk-CVNNNSMQ.js +26 -0
- package/out/chunks/chunk-OQBJTZWK.js +60 -0
- package/out/chunks/chunk-UCZCF3BQ.js +9 -0
- package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
- package/out/chunks/commands-XBFPLHSQ.js +8 -0
- package/out/chunks/help-P7TCOYAJ.js +10 -0
- package/out/chunks/main-help-ULF5PAQY.js +10 -0
- package/out/chunks/prompts-5E6VKRX5.js +8 -0
- package/out/chunks/spinner-URV2OX6O.js +8 -0
- package/out/chunks/tool-command-M6VA7P2F.js +8 -0
- package/out/octocode-cli.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +13 -1
- package/skills/agentic-flow-best-practices/SKILL.md +280 -0
- package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
- package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
- package/skills/agentic-flow-best-practices/references/resources.md +112 -0
- package/skills/octocode-chrome-devtools/README.md +541 -0
- package/skills/octocode-chrome-devtools/SKILL.md +197 -0
- package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
- package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
- package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
- package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
- package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
- package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
- package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
- package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
- package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
- package/skills/octocode-design/README.md +2 -2
- package/skills/octocode-documentation-writer/README.md +1 -1
- package/skills/octocode-engineer/README.md +1 -1
- package/skills/octocode-engineer/SKILL.md +4 -2
- package/skills/octocode-engineer/references/output-files.md +3 -3
- package/skills/octocode-engineer/scripts/run.js +136 -136
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
- package/skills/octocode-install/SKILL.md +1 -1
- package/skills/octocode-pull-request-reviewer/README.md +5 -5
- package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
- package/skills/octocode-research/AGENTS.md +1 -1
- package/skills/octocode-research/README.md +2 -2
- package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
- package/skills/octocode-research/scripts/server.js +191 -246
- package/skills/octocode-research/src/routes/github.ts +4 -21
- package/skills/octocode-research/src/routes/local.ts +4 -21
- package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
- package/skills/octocode-search-skill/SKILL.md +245 -229
- package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
- package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
- package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
- package/skills/octocode-search-skill/references/install-reference.md +130 -0
- package/skills/octocode-search-skill/references/references-template.md +27 -0
- package/skills/octocode-search-skill/references/references.md +62 -0
- package/skills/octocode-slides/README.md +307 -0
- package/skills/octocode-slides/SKILL.md +410 -0
- package/skills/octocode-slides/references/01-brief.md +156 -0
- package/skills/octocode-slides/references/02-research.md +149 -0
- package/skills/octocode-slides/references/03-outline.md +172 -0
- package/skills/octocode-slides/references/04-design.md +301 -0
- package/skills/octocode-slides/references/05-implementation.md +213 -0
- package/skills/octocode-slides/references/06-review.md +258 -0
- package/skills/octocode-slides/references/animation.md +281 -0
- package/skills/octocode-slides/references/design-system.md +316 -0
- package/skills/octocode-slides/references/html-templates.md +673 -0
- package/skills/octocode-slides/references/image-generation.md +448 -0
- package/skills/octocode-slides/references/resources.md +840 -0
- package/skills/octocode-slides/references/slide-rules.md +541 -0
- package/skills/octocode-slides/references/wireframes.md +727 -0
- package/skills/octocode-slides/scripts/animation.js +182 -0
- package/skills/octocode-slides/scripts/base.css +353 -0
- package/skills/octocode-slides/scripts/base.html +655 -0
- package/skills/octocode-slides/scripts/generate_image.py +221 -0
- package/skills/octocode-slides/scripts/navbridge.js +79 -0
- package/skills/octocode-slides/scripts/presenter.js +316 -0
- package/skills/octocode-slides/scripts/slide.html +248 -0
- package/skills/octocode-stats/SKILL.md +73 -0
- package/skills/octocode-stats/assets/template.html +1332 -0
- package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
- package/assets/example.png +0 -0
- package/out/chunks/chunk-QCY7Q7YW.js +0 -389
- package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
- package/out/chunks/commands-OCTZP2TO.js +0 -51
- package/out/chunks/help-XPXP46ZT.js +0 -10
- package/out/chunks/main-help-35HX2UDQ.js +0 -10
- package/out/chunks/tool-command-HOSMVLNK.js +0 -8
- package/skills/octocode-search-skill/INSTALL_REFERENCE.md +0 -112
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# CDP Browser Surface Pattern Details
|
|
2
|
+
|
|
3
|
+
## WebSocket Surveillance
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
export async function run(cdp) {
|
|
7
|
+
await cdp.send('Network.enable', {});
|
|
8
|
+
|
|
9
|
+
const sockets = new Map();
|
|
10
|
+
let frameCount = 0;
|
|
11
|
+
let socketCount = 0;
|
|
12
|
+
const pageHost = (() => { try { return new URL(cdp.targetInfo.url).hostname; } catch { return ''; } })();
|
|
13
|
+
|
|
14
|
+
cdp.on('Network.webSocketCreated', ({ requestId, url }) => {
|
|
15
|
+
sockets.set(requestId, url);
|
|
16
|
+
socketCount++;
|
|
17
|
+
console.log(`[NETWORK] WS opened: ${url}`);
|
|
18
|
+
try {
|
|
19
|
+
const host = new URL(url).hostname;
|
|
20
|
+
if (pageHost && host !== pageHost) console.log(`[FINDING] WS_UNKNOWN_HOST: ${url}`);
|
|
21
|
+
} catch {}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
cdp.on('Network.webSocketFrameSent', ({ requestId, response }) => {
|
|
25
|
+
const url = sockets.get(requestId) ?? 'unknown';
|
|
26
|
+
const payload = response.payloadData ?? '';
|
|
27
|
+
frameCount++;
|
|
28
|
+
console.log(`[NETWORK] WS SENT → ${url} (${payload.length} chars)`);
|
|
29
|
+
if (/token|password|secret|key|auth/i.test(payload))
|
|
30
|
+
console.log(`[FINDING] SENSITIVE_IN_WS_FRAME: sent to ${url}`);
|
|
31
|
+
if (/^[A-Za-z0-9+/]{40,}={0,2}$/.test(payload.trim()))
|
|
32
|
+
console.log(`[FINDING] WS_BASE64_FRAME: possible encoded data sent to ${url}`);
|
|
33
|
+
if (payload.length > 100000)
|
|
34
|
+
console.log(`[FINDING] LARGE_WS_FRAME: ${(payload.length / 1024).toFixed(1)}KB sent to ${url}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
cdp.on('Network.webSocketFrameReceived', ({ requestId, response }) => {
|
|
38
|
+
const url = sockets.get(requestId) ?? 'unknown';
|
|
39
|
+
const payload = response.payloadData ?? '';
|
|
40
|
+
frameCount++;
|
|
41
|
+
console.log(`[NETWORK] WS RECV ← ${url} (${payload.length} chars)`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cdp.on('Network.webSocketClosed', ({ requestId }) => {
|
|
45
|
+
const url = sockets.get(requestId) ?? 'unknown';
|
|
46
|
+
console.log(`[NETWORK] WS closed: ${url}`);
|
|
47
|
+
sockets.delete(requestId);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log('[FINDING] WebSocket monitoring active — collecting for 15s...');
|
|
51
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
52
|
+
console.log(`[METRIC] WS sockets seen: ${socketCount} Total frames: ${frameCount}`);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Search Text Across All Resources
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// Search for a string in all loaded JS, CSS, and network response bodies
|
|
60
|
+
// Requires Debugger.enable + Debugger.setSkipAllPauses (prevents breakpoint hangs)
|
|
61
|
+
export async function run(cdp) {
|
|
62
|
+
await cdp.send('Network.enable', {});
|
|
63
|
+
await cdp.send('Runtime.enable', {});
|
|
64
|
+
await cdp.send('Page.enable', {});
|
|
65
|
+
await cdp.send('DOM.enable', {});
|
|
66
|
+
await cdp.send('CSS.enable', {});
|
|
67
|
+
await cdp.send('Debugger.enable', {});
|
|
68
|
+
await cdp.send('Debugger.setSkipAllPauses', { skip: true }); // CRITICAL: prevents Runtime.evaluate hangs
|
|
69
|
+
|
|
70
|
+
cdp.on('Page.javascriptDialogOpening', () =>
|
|
71
|
+
cdp.send('Page.handleJavaScriptDialog', { accept: true }));
|
|
72
|
+
|
|
73
|
+
const SEARCH_TERM = 'YOUR_TERM_HERE';
|
|
74
|
+
const scripts = {};
|
|
75
|
+
const styleSheets = {};
|
|
76
|
+
const responseIds = [];
|
|
77
|
+
|
|
78
|
+
cdp.on('Debugger.scriptParsed', ({ scriptId, url }) => { scripts[scriptId] = url || '(inline)'; });
|
|
79
|
+
cdp.on('CSS.styleSheetAdded', ({ header }) => { styleSheets[header.styleSheetId] = header.sourceURL || '(inline)'; });
|
|
80
|
+
cdp.on('Network.responseReceived', ({ requestId, response }) => {
|
|
81
|
+
const ct = (response.mimeType ?? '').toLowerCase();
|
|
82
|
+
if (['json','text','html','javascript','xml','css'].some(t => ct.includes(t)))
|
|
83
|
+
responseIds.push([requestId, response.url]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await cdp.send('Page.navigate', { url: 'https://TARGET_URL/' });
|
|
87
|
+
await new Promise(r => setTimeout(r, 4000));
|
|
88
|
+
|
|
89
|
+
// Search in JS
|
|
90
|
+
for (const [scriptId, url] of Object.entries(scripts)) {
|
|
91
|
+
let result;
|
|
92
|
+
try { ({ result } = await cdp.send('Debugger.searchInContent', {
|
|
93
|
+
scriptId, query: SEARCH_TERM, caseSensitive: false, isRegex: false
|
|
94
|
+
})); } catch { continue; }
|
|
95
|
+
if (result?.length)
|
|
96
|
+
result.forEach(r => console.log(`[SEARCH] JS L${r.lineNumber}: ${r.lineContent.trim().slice(0,120)}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Search in CSS
|
|
100
|
+
for (const [styleSheetId] of Object.entries(styleSheets)) {
|
|
101
|
+
let text;
|
|
102
|
+
try { ({ text } = await cdp.send('CSS.getStyleSheetText', { styleSheetId })); } catch { continue; }
|
|
103
|
+
if (text?.toLowerCase().includes(SEARCH_TERM.toLowerCase()))
|
|
104
|
+
console.log(`[SEARCH] CSS hit found`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Search in network bodies
|
|
108
|
+
for (const [requestId, url] of responseIds) {
|
|
109
|
+
try {
|
|
110
|
+
const { body, base64Encoded } = await cdp.send('Network.getResponseBody', { requestId });
|
|
111
|
+
const text = base64Encoded ? Buffer.from(body, 'base64').toString() : (body ?? '');
|
|
112
|
+
if (text.toLowerCase().includes(SEARCH_TERM.toLowerCase()))
|
|
113
|
+
console.log(`[SEARCH] BODY hit in ${url.split('/').pop() || url}`);
|
|
114
|
+
} catch { continue; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Search in DOM text
|
|
118
|
+
const { result: domRes } = await cdp.send('Runtime.evaluate', {
|
|
119
|
+
expression: `(function() {
|
|
120
|
+
const term = ${JSON.stringify(SEARCH_TERM.toLowerCase())};
|
|
121
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
122
|
+
const hits = []; let node;
|
|
123
|
+
while ((node = walker.nextNode()))
|
|
124
|
+
if (node.textContent.toLowerCase().includes(term))
|
|
125
|
+
hits.push('<' + node.parentElement?.tagName + '> ' + node.textContent.trim().slice(0,80));
|
|
126
|
+
return hits;
|
|
127
|
+
})()`, returnByValue: true
|
|
128
|
+
});
|
|
129
|
+
(domRes.value ?? []).forEach(h => console.log(`[SEARCH] DOM: ${h}`));
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## File Upload
|
|
134
|
+
|
|
135
|
+
Upload a file via a native `input[type="file"]` element using `DOM.setFileInputFiles`. Files must be absolute paths on the machine running Chrome.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
export async function run(cdp) {
|
|
139
|
+
await cdp.send('DOM.enable', {});
|
|
140
|
+
await cdp.send('Runtime.enable', {});
|
|
141
|
+
await cdp.send('Page.enable', {});
|
|
142
|
+
|
|
143
|
+
cdp.on('Page.javascriptDialogOpening', () =>
|
|
144
|
+
cdp.send('Page.handleJavaScriptDialog', { accept: true }));
|
|
145
|
+
|
|
146
|
+
await cdp.send('Page.navigate', { url: 'https://example.com/upload' });
|
|
147
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
148
|
+
|
|
149
|
+
// Find the file input
|
|
150
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: 0 });
|
|
151
|
+
const { nodeId } = await cdp.send('DOM.querySelector', {
|
|
152
|
+
nodeId: root.nodeId,
|
|
153
|
+
selector: 'input[type="file"]',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (nodeId === 0) {
|
|
157
|
+
console.log('[FINDING] NO_FILE_INPUT: no input[type="file"] found on page');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set files — absolute paths only
|
|
162
|
+
await cdp.send('DOM.setFileInputFiles', {
|
|
163
|
+
nodeId,
|
|
164
|
+
files: ['/absolute/path/to/your-file.txt'],
|
|
165
|
+
// For multiple files: files: ['/path/a.txt', '/path/b.png']
|
|
166
|
+
});
|
|
167
|
+
console.log('[AUTOMATE] file set on input[type="file"]');
|
|
168
|
+
|
|
169
|
+
// Dispatch change + input events so React/Vue/Angular frameworks detect the selection
|
|
170
|
+
await cdp.send('Runtime.evaluate', {
|
|
171
|
+
expression: `
|
|
172
|
+
const el = document.querySelector('input[type="file"]');
|
|
173
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
174
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
175
|
+
`,
|
|
176
|
+
});
|
|
177
|
+
console.log('[AUTOMATE] change/input events dispatched');
|
|
178
|
+
|
|
179
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
180
|
+
console.log('[METRIC] File upload step complete');
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Gotchas:**
|
|
185
|
+
- `files` must be **absolute paths** — relative paths and URLs are rejected
|
|
186
|
+
- `multiple` inputs: pass all files in one array `files: ['/a', '/b']`
|
|
187
|
+
- Hidden file inputs triggered by a button: click the button first via `Runtime.evaluate` click, then call `DOM.setFileInputFiles` on the now-visible (or still-hidden) `nodeId`
|
|
188
|
+
- Always dispatch `change` and `input` events after setting files — CDP sets the value silently, frameworks won't react otherwise
|
|
189
|
+
- If the nodeId is 0 and you know the input exists, the page may still be loading — add a `waitForSelector` call before `DOM.querySelector`
|
|
190
|
+
|
|
191
|
+
## Save Files Screenshots PDFs and Metadata
|
|
192
|
+
|
|
193
|
+
Always use `cdp.outputDir` — it is the only writable location in sandbox mode and works on Windows, macOS, and Linux.
|
|
194
|
+
Output lands in `<TMPDIR>/.octocode-chrome-devtools/<timestamp>/` — the agent reads the `[CDP_RUNNER] Output dir:` line in stderr to find it.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
export async function run(cdp) {
|
|
198
|
+
await cdp.send('Page.enable', {});
|
|
199
|
+
|
|
200
|
+
const { writeFileSync } = await import('fs');
|
|
201
|
+
const { join } = await import('path');
|
|
202
|
+
|
|
203
|
+
// ── Screenshot ────────────────────────────────────────────────────────────
|
|
204
|
+
await cdp.send('Page.navigate', { url: 'https://example.com' });
|
|
205
|
+
await new Promise(r => setTimeout(r, 2000)); // wait for render
|
|
206
|
+
|
|
207
|
+
const { data: pngData } = await cdp.send('Page.captureScreenshot', { format: 'png' });
|
|
208
|
+
const screenshotPath = join(cdp.outputDir, 'screenshot.png');
|
|
209
|
+
writeFileSync(screenshotPath, Buffer.from(pngData, 'base64'));
|
|
210
|
+
console.log(`[SCREENSHOT] ${screenshotPath}`);
|
|
211
|
+
|
|
212
|
+
// ── PDF ───────────────────────────────────────────────────────────────────
|
|
213
|
+
const { data: pdfData } = await cdp.send('Page.printToPDF', { printBackground: true });
|
|
214
|
+
const pdfPath = join(cdp.outputDir, 'page.pdf');
|
|
215
|
+
writeFileSync(pdfPath, Buffer.from(pdfData, 'base64'));
|
|
216
|
+
console.log(`[FINDING] PDF saved → ${pdfPath}`);
|
|
217
|
+
|
|
218
|
+
// ── Metadata / findings JSON ──────────────────────────────────────────────
|
|
219
|
+
const metadata = {
|
|
220
|
+
url: cdp.targetInfo.url,
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
findings: [], // push [FINDING] items here to get a machine-readable report
|
|
223
|
+
};
|
|
224
|
+
// metadata.findings.push({ type: 'HTTP_ERROR', status: 404, url: '...' });
|
|
225
|
+
const metaPath = join(cdp.outputDir, 'metadata.json');
|
|
226
|
+
writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
227
|
+
console.log(`[METRIC] metadata saved → ${metaPath}`);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Key rules:**
|
|
232
|
+
- Use `cdp.outputDir` — never `os.tmpdir()` directly in sandbox mode
|
|
233
|
+
- Write pattern: `const { writeFileSync } = await import('fs'); const { join } = await import('path');`
|
|
234
|
+
- The runner logs `[CDP_RUNNER] Output dir: <path>` to stderr — the agent reads this to locate all output files
|
|
235
|
+
|
|
236
|
+
## Shadow DOM Querying Inside Shadow Roots
|
|
237
|
+
|
|
238
|
+
`DOM.querySelector / querySelectorAll` do **not** pierce shadow boundaries. Use `Runtime.evaluate` with a recursive traversal, or `DOM.getDocument({ pierce: true })` to inspect the full tree.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
// Requires: DOM.enable, Runtime.enable
|
|
242
|
+
|
|
243
|
+
// Returns a remote objectId — use with Runtime.callFunctionOn or DOM.resolveNode
|
|
244
|
+
async function queryShadowDOM(cdp, selector) {
|
|
245
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
246
|
+
expression: `(function pierce(root, sel) {
|
|
247
|
+
const el = root.querySelector(sel);
|
|
248
|
+
if (el) return el;
|
|
249
|
+
for (const host of root.querySelectorAll('*')) {
|
|
250
|
+
if (host.shadowRoot) {
|
|
251
|
+
const found = pierce(host.shadowRoot, sel);
|
|
252
|
+
if (found) return found;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
})(document, ${JSON.stringify(selector)})`,
|
|
257
|
+
returnByValue: false, // keep remote objectId for further CDP calls
|
|
258
|
+
});
|
|
259
|
+
return result.objectId ?? null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function run(cdp) {
|
|
263
|
+
await cdp.send('DOM.enable', {});
|
|
264
|
+
await cdp.send('Runtime.enable', {});
|
|
265
|
+
|
|
266
|
+
// Get document tree including shadow roots (pierce: true)
|
|
267
|
+
// Shadow roots appear as DOCUMENT_FRAGMENT nodes (nodeType 11) in the children array
|
|
268
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: 3, pierce: true });
|
|
269
|
+
console.log(`[DOM] Root: ${root.nodeName}, childCount: ${root.childNodeCount}`);
|
|
270
|
+
|
|
271
|
+
// Extract all text from elements inside shadow roots via Runtime.evaluate
|
|
272
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
273
|
+
expression: `(function() {
|
|
274
|
+
const hits = [];
|
|
275
|
+
function walk(root) {
|
|
276
|
+
for (const el of root.querySelectorAll('*')) {
|
|
277
|
+
if (el.shadowRoot) walk(el.shadowRoot);
|
|
278
|
+
}
|
|
279
|
+
// change 'my-component button' to your target selector
|
|
280
|
+
for (const el of root.querySelectorAll('my-component button')) {
|
|
281
|
+
hits.push(el.textContent.trim());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
walk(document);
|
|
285
|
+
return JSON.stringify(hits);
|
|
286
|
+
})()`,
|
|
287
|
+
returnByValue: true,
|
|
288
|
+
});
|
|
289
|
+
const items = JSON.parse(result.value ?? '[]');
|
|
290
|
+
items.forEach(t => console.log(`[SCRAPE] shadow-DOM item: "${t}"`));
|
|
291
|
+
console.log(`[METRIC] Shadow DOM items found: ${items.length}`);
|
|
292
|
+
if (items.length === 0)
|
|
293
|
+
console.log('[FINDING] SHADOW_DOM_EMPTY: selector found nothing in shadow roots — check host element and inner selector');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function clickInShadowDOM(cdp, hostSelector, innerSelector) {
|
|
297
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
298
|
+
expression: `(function() {
|
|
299
|
+
const host = document.querySelector(${JSON.stringify(hostSelector)});
|
|
300
|
+
const el = host?.shadowRoot?.querySelector(${JSON.stringify(innerSelector)});
|
|
301
|
+
if (!el) return { found: false };
|
|
302
|
+
const r = el.getBoundingClientRect();
|
|
303
|
+
return { found: true, x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
304
|
+
})()`,
|
|
305
|
+
returnByValue: true,
|
|
306
|
+
});
|
|
307
|
+
if (!result.value?.found) {
|
|
308
|
+
console.log(`[FINDING] SHADOW_DOM_NOT_FOUND: "${innerSelector}" not found in "${hostSelector}" shadow root`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const { x, y } = result.value;
|
|
312
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
313
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
314
|
+
console.log(`[AUTOMATE] clicked "${innerSelector}" inside "${hostSelector}" shadow root`);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Key facts:**
|
|
319
|
+
- `DOM.getDocument({ pierce: true })` includes shadow roots in the returned tree — shadow roots are `nodeType: 11` (DOCUMENT_FRAGMENT_NODE)
|
|
320
|
+
- `DOM.querySelector / querySelectorAll` do **NOT** cross shadow boundaries even with `pierce: true` on the document
|
|
321
|
+
- `Runtime.evaluate` with recursive traversal is the most reliable approach for querying
|
|
322
|
+
- **Closed shadow roots** (`attachShadow({ mode: 'closed' })`) — JavaScript cannot access `.shadowRoot`; CDP has no bypass
|
|
323
|
+
- **Nested shadows** — the `pierce()` helper above handles arbitrary nesting depth
|
|
324
|
+
|
|
325
|
+
## Source Map Resolution
|
|
326
|
+
|
|
327
|
+
Resolves minified compiled positions back to original source names and files.
|
|
328
|
+
Requires `sourcemap-resolver.mjs` to be present in the same directory as the script.
|
|
329
|
+
The sandbox runner stages `sourcemap-resolver.mjs` in `$TMPDIR` automatically for generated scripts.
|
|
330
|
+
Works gracefully when maps are absent — always returns `null` instead of throwing.
|
|
331
|
+
|
|
332
|
+
**When to add this pattern:**
|
|
333
|
+
- `js-coverage` intent — show readable function names in DEAD_CODE findings
|
|
334
|
+
- `debug` intent — enrich stack frames with original file + line
|
|
335
|
+
- Any intent where you want to understand *what* a minified script does
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
// Import the resolver BEFORE enabling other domains (must register scriptParsed ASAP)
|
|
339
|
+
const { createSourceMapResolver } = await import(
|
|
340
|
+
new URL('./sourcemap-resolver.mjs', import.meta.url).href
|
|
341
|
+
);
|
|
342
|
+
const resolver = await createSourceMapResolver(cdp);
|
|
343
|
+
// Debugger.enable and Debugger.setSkipAllPauses are called internally by createSourceMapResolver
|
|
344
|
+
|
|
345
|
+
// ...enable Network, Profiler, etc. and navigate the page...
|
|
346
|
+
|
|
347
|
+
// After page is fully loaded, wait for all map loads to settle:
|
|
348
|
+
await resolver.settle();
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
// Option A: resolve a single generated position
|
|
352
|
+
const orig = resolver.resolve(scriptId, lineNumber, columnNumber); // all 0-indexed
|
|
353
|
+
if (orig) {
|
|
354
|
+
const fnName = orig.name ?? '(anonymous)';
|
|
355
|
+
const src = orig.source?.split('/').slice(-2).join('/') ?? 'unknown'; // last 2 path parts
|
|
356
|
+
console.log(`[SOURCEMAP] ${fnName} → ${src}:${orig.line}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Option B: enrich Profiler coverage results with source map data
|
|
360
|
+
for (const script of coverageResult) {
|
|
361
|
+
const url = script.url;
|
|
362
|
+
if (!url || url.startsWith('chrome-extension')) continue;
|
|
363
|
+
|
|
364
|
+
for (const fn of script.functions) {
|
|
365
|
+
const isUsed = fn.ranges.some(r => r.count > 0);
|
|
366
|
+
if (isUsed) continue; // only report dead code
|
|
367
|
+
|
|
368
|
+
// Try to resolve the first byte of the function
|
|
369
|
+
const [startLine, startCol] = offsetToLineCol(fn.ranges[0]?.startOffset ?? 0, compiledText);
|
|
370
|
+
const orig = resolver.resolve(script.scriptId, startLine, startCol);
|
|
371
|
+
|
|
372
|
+
const displayName = orig?.name ?? (fn.functionName?.length > 2 ? fn.functionName : null);
|
|
373
|
+
if (!displayName) continue; // skip mangled single/double chars
|
|
374
|
+
|
|
375
|
+
const loc = orig
|
|
376
|
+
? `${orig.source?.split('/').pop() ?? 'unknown'}:${orig.line}`
|
|
377
|
+
: url.split('/').pop();
|
|
378
|
+
console.log(`[FINDING] DEAD_CODE: ${displayName} in ${loc}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Helper: convert character offset to {line, col} (0-indexed) — only needed for Profiler
|
|
383
|
+
function offsetToLineCol(offset, source) {
|
|
384
|
+
let line = 0, col = 0;
|
|
385
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
386
|
+
if (source[i] === '\n') { line++; col = 0; } else col++;
|
|
387
|
+
}
|
|
388
|
+
return [line, col];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
resolver.printSummary();
|
|
392
|
+
// Emits: [SOURCEMAP] 42 scripts: 12 maps loaded, 3 failed, 27 had no map
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Key facts:**
|
|
396
|
+
- `Debugger.scriptParsed` fires for every script during page load — resolver must be created before navigation
|
|
397
|
+
- `resolver.settle()` must be called after page is loaded to ensure all async map fetches complete
|
|
398
|
+
- Inline `data:application/json;base64,...` maps are decoded instantly with no network call
|
|
399
|
+
- External `.map` URLs are fetched with a 4s timeout; failures are silently counted
|
|
400
|
+
- `sourcesContent` (full original source code) is **always stripped** — never stored or emitted
|
|
401
|
+
- `resolver.resolve()` returns `null` when script has no map or position is outside all segments
|
|
402
|
+
- The Profiler gives `startOffset` (byte offset) not `{line,col}` — use `offsetToLineCol()` helper
|
|
403
|
+
- Short function names (`length <= 2`) after mangling are meaningless — skip unless `orig.name` resolves them
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# CDP Observation Pattern Details
|
|
2
|
+
|
|
3
|
+
## Network Console (most common)
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
export async function run(cdp) {
|
|
7
|
+
await cdp.send('Network.enable', {});
|
|
8
|
+
await cdp.send('Runtime.enable', {});
|
|
9
|
+
await cdp.send('Log.enable', {});
|
|
10
|
+
|
|
11
|
+
const requests = new Map();
|
|
12
|
+
|
|
13
|
+
cdp.on('Network.requestWillBeSent', ({ requestId, request }) => {
|
|
14
|
+
requests.set(requestId, { url: request.url, method: request.method });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
cdp.on('Network.responseReceived', ({ requestId, response }) => {
|
|
18
|
+
const r = requests.get(requestId);
|
|
19
|
+
if (!r) return;
|
|
20
|
+
console.log(`[NETWORK] ${response.status} ${r.method} ${r.url}`);
|
|
21
|
+
if (response.status >= 400)
|
|
22
|
+
console.log(`[NETWORK_ERROR] HTTP ${response.status} → ${r.url}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
cdp.on('Network.loadingFailed', ({ requestId, errorText }) => {
|
|
26
|
+
const r = requests.get(requestId);
|
|
27
|
+
console.log(`[NETWORK_FAILED] ${r?.url ?? 'unknown'}: ${errorText}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cdp.on('Runtime.consoleAPICalled', ({ type, args }) => {
|
|
31
|
+
const msg = args.map(a => a.value ?? a.description ?? '[object]').join(' ');
|
|
32
|
+
console.log(`[CONSOLE:${type.toUpperCase()}] ${msg}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
cdp.on('Runtime.exceptionThrown', ({ exceptionDetails }) => {
|
|
36
|
+
const desc = exceptionDetails.exception?.description ?? exceptionDetails.text;
|
|
37
|
+
console.log(`[EXCEPTION] ${desc}`);
|
|
38
|
+
if (exceptionDetails.stackTrace?.callFrames?.[0]) {
|
|
39
|
+
const f = exceptionDetails.stackTrace.callFrames[0];
|
|
40
|
+
console.log(`[EXCEPTION_LOCATION] ${f.url}:${f.lineNumber}:${f.columnNumber} in ${f.functionName}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cdp.on('Log.entryAdded', ({ entry }) => {
|
|
45
|
+
if (entry.level === 'error' || entry.level === 'warning')
|
|
46
|
+
console.log(`[LOG:${entry.level.toUpperCase()}] [${entry.source}] ${entry.text}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Navigate inside run() when monitoring load events; use --new-tab about:blank for that path
|
|
50
|
+
await cdp.send('Page.enable', {});
|
|
51
|
+
cdp.on('Page.javascriptDialogOpening', () =>
|
|
52
|
+
cdp.send('Page.handleJavaScriptDialog', { accept: true }));
|
|
53
|
+
await cdp.send('Page.navigate', { url: 'https://example.com/' });
|
|
54
|
+
|
|
55
|
+
console.log('[FINDING] Monitoring active — collecting events for 10s...');
|
|
56
|
+
await new Promise(r => setTimeout(r, 10000));
|
|
57
|
+
console.log(`[METRIC] Total requests captured: ${requests.size}`);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Performance Audit
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
export async function run(cdp) {
|
|
65
|
+
await cdp.send('Performance.enable', {});
|
|
66
|
+
await cdp.send('Runtime.enable', {});
|
|
67
|
+
|
|
68
|
+
const { metrics } = await cdp.send('Performance.getMetrics', {});
|
|
69
|
+
const m = Object.fromEntries(metrics.map(x => [x.name, x.value]));
|
|
70
|
+
|
|
71
|
+
console.log('[PERFORMANCE] JSHeapUsedSize:', (m.JSHeapUsedSize / 1024 / 1024).toFixed(2), 'MB');
|
|
72
|
+
console.log('[PERFORMANCE] TaskDuration:', m.TaskDuration?.toFixed(3), 's');
|
|
73
|
+
console.log('[PERFORMANCE] LayoutCount:', m.LayoutCount);
|
|
74
|
+
console.log('[PERFORMANCE] RecalcStyleCount:', m.RecalcStyleCount);
|
|
75
|
+
console.log('[PERFORMANCE] ScriptDuration:', m.ScriptDuration?.toFixed(3), 's');
|
|
76
|
+
console.log('[PERFORMANCE] Full metrics:', JSON.stringify(m, null, 2));
|
|
77
|
+
|
|
78
|
+
if (m.JSHeapUsedSize > 50_000_000)
|
|
79
|
+
console.log('[FINDING] HIGH_MEMORY: JS heap > 50MB — possible memory leak');
|
|
80
|
+
if (m.LayoutCount > 20)
|
|
81
|
+
console.log('[FINDING] LAYOUT_THRASHING: >20 forced layouts — check for read/write interleaving');
|
|
82
|
+
if (m.ScriptDuration > 2)
|
|
83
|
+
console.log('[FINDING] SLOW_SCRIPTS: script execution > 2s — profile for long tasks');
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Core Web Vitals (inject before navigate)
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// Add BEFORE Page.navigate — uses Page.addScriptToEvaluateOnNewDocument
|
|
91
|
+
await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
92
|
+
source: `
|
|
93
|
+
window.__CWV__ = {};
|
|
94
|
+
new PerformanceObserver(list => {
|
|
95
|
+
for (const e of list.getEntries())
|
|
96
|
+
if (e.entryType === 'largest-contentful-paint') window.__CWV__.LCP = e.startTime;
|
|
97
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
98
|
+
new PerformanceObserver(list => {
|
|
99
|
+
for (const e of list.getEntries())
|
|
100
|
+
if (e.entryType === 'layout-shift' && !e.hadRecentInput)
|
|
101
|
+
window.__CWV__.CLS = (window.__CWV__.CLS || 0) + e.value;
|
|
102
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
103
|
+
new PerformanceObserver(list => {
|
|
104
|
+
for (const e of list.getEntries())
|
|
105
|
+
if (e.name === 'first-contentful-paint') window.__CWV__.FCP = e.startTime;
|
|
106
|
+
}).observe({ type: 'paint', buffered: true });
|
|
107
|
+
`
|
|
108
|
+
});
|
|
109
|
+
// After page settles, read back:
|
|
110
|
+
const { result } = await cdp.send('Runtime.evaluate', { expression: `window.__CWV__`, returnByValue: true });
|
|
111
|
+
const cwv = result.value ?? {};
|
|
112
|
+
if (cwv.FCP) console.log(`[PERFORMANCE] FCP: ${cwv.FCP.toFixed(0)} ms [${cwv.FCP < 1800 ? 'GOOD' : 'POOR'}]`);
|
|
113
|
+
if (cwv.LCP) console.log(`[PERFORMANCE] LCP: ${cwv.LCP.toFixed(0)} ms [${cwv.LCP < 2500 ? 'GOOD' : 'POOR'}]`);
|
|
114
|
+
if (cwv.CLS != null) console.log(`[PERFORMANCE] CLS: ${cwv.CLS.toFixed(4)} [${cwv.CLS < 0.1 ? 'GOOD' : 'POOR'}]`);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## DOM Accessibility Audit
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
export async function run(cdp) {
|
|
121
|
+
await cdp.send('DOM.enable', {});
|
|
122
|
+
await cdp.send('Runtime.enable', {});
|
|
123
|
+
|
|
124
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: 2 });
|
|
125
|
+
console.log(`[DOM] Root: ${root.nodeName}, children: ${root.childNodeCount}`);
|
|
126
|
+
|
|
127
|
+
const checks = [
|
|
128
|
+
['Total elements', `document.querySelectorAll('*').length`],
|
|
129
|
+
['Images missing alt', `document.querySelectorAll('img:not([alt])').length`],
|
|
130
|
+
['Inputs missing label',`document.querySelectorAll('input:not([aria-label]):not([id])').length`],
|
|
131
|
+
['Empty buttons', `document.querySelectorAll('button:empty').length`],
|
|
132
|
+
['Inline scripts', `document.querySelectorAll('script:not([src])').length`],
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const [label, expr] of checks) {
|
|
136
|
+
const { result } = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true });
|
|
137
|
+
console.log(`[DOM] ${label}: ${result.value}`);
|
|
138
|
+
if (label === 'Total elements' && result.value > 1500)
|
|
139
|
+
console.log('[FINDING] LARGE_DOM: >1500 elements — may hurt rendering performance');
|
|
140
|
+
if (label === 'Images missing alt' && result.value > 0)
|
|
141
|
+
console.log(`[FINDING] ACCESSIBILITY: ${result.value} images missing alt text`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { result: title } = await cdp.send('Runtime.evaluate', {
|
|
145
|
+
expression: 'document.title', returnByValue: true,
|
|
146
|
+
});
|
|
147
|
+
console.log(`[DOM] Page title: "${title.value}"`);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Heap Memory Audit (leak detection)
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
export async function run(cdp) {
|
|
155
|
+
await cdp.send('HeapProfiler.enable', {});
|
|
156
|
+
|
|
157
|
+
const chunks = [];
|
|
158
|
+
cdp.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => chunks.push(chunk));
|
|
159
|
+
await cdp.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false });
|
|
160
|
+
|
|
161
|
+
const snapshot = JSON.parse(chunks.join(''));
|
|
162
|
+
const { node_count, edge_count } = snapshot.snapshot.meta;
|
|
163
|
+
console.log(`[METRIC] Heap nodes: ${node_count}, edges: ${edge_count}`);
|
|
164
|
+
|
|
165
|
+
const strings = snapshot.strings;
|
|
166
|
+
const nodeFields = snapshot.snapshot.meta.node_fields;
|
|
167
|
+
const nodeSize = nodeFields.length;
|
|
168
|
+
const nodes = snapshot.nodes;
|
|
169
|
+
|
|
170
|
+
const typeCounts = {};
|
|
171
|
+
for (let i = 0; i < nodes.length; i += nodeSize) {
|
|
172
|
+
const name = strings[nodes[i + 1]];
|
|
173
|
+
typeCounts[name] = (typeCounts[name] ?? 0) + nodes[i + 3];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const top = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
177
|
+
console.log('[PERFORMANCE] Top retained types by self_size:');
|
|
178
|
+
for (const [name, size] of top) {
|
|
179
|
+
console.log(`[PERFORMANCE] ${name}: ${(size / 1024).toFixed(1)} KB`);
|
|
180
|
+
if (size > 5_000_000)
|
|
181
|
+
console.log(`[FINDING] HIGH_RETENTION: "${name}" retains ${(size / 1024 / 1024).toFixed(1)} MB — possible leak`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const detachedIdx = nodeFields.indexOf('detachedness');
|
|
185
|
+
if (detachedIdx !== -1) {
|
|
186
|
+
let detached = 0;
|
|
187
|
+
for (let i = 0; i < nodes.length; i += nodeSize)
|
|
188
|
+
if (nodes[i + detachedIdx] === 1) detached++;
|
|
189
|
+
console.log(`[METRIC] Detached DOM nodes: ${detached}`);
|
|
190
|
+
if (detached > 50)
|
|
191
|
+
console.log(`[FINDING] DETACHED_NODES: ${detached} detached DOM nodes — likely memory leak`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Security Audit
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
export async function run(cdp) {
|
|
200
|
+
const TARGET_URL = 'https://example.com'; // ← set target URL
|
|
201
|
+
|
|
202
|
+
await cdp.send('Network.enable', {});
|
|
203
|
+
await cdp.send('Runtime.enable', {});
|
|
204
|
+
await cdp.send('DOM.enable', {});
|
|
205
|
+
await cdp.send('Page.enable', {});
|
|
206
|
+
|
|
207
|
+
const requests = new Map();
|
|
208
|
+
|
|
209
|
+
cdp.on('Network.requestWillBeSent', ({ requestId, request }) => {
|
|
210
|
+
requests.set(requestId, { url: request.url, method: request.method });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
cdp.on('Network.responseReceived', async ({ requestId, response }) => {
|
|
214
|
+
const r = requests.get(requestId);
|
|
215
|
+
if (!r) return;
|
|
216
|
+
const headers = response.headers ?? {};
|
|
217
|
+
if (!headers['content-security-policy'])
|
|
218
|
+
console.log(`[FINDING] MISSING_CSP: ${r.url}`);
|
|
219
|
+
if (!headers['strict-transport-security'])
|
|
220
|
+
console.log(`[FINDING] MISSING_HSTS: ${r.url}`);
|
|
221
|
+
if (!headers['x-frame-options'] && !headers['content-security-policy']?.includes('frame-ancestors'))
|
|
222
|
+
console.log(`[FINDING] MISSING_XFRAME: ${r.url}`);
|
|
223
|
+
const csp = headers['content-security-policy'] ?? '';
|
|
224
|
+
if (csp.includes('unsafe-eval')) console.log(`[FINDING] WEAK_CSP: unsafe-eval in ${r.url}`);
|
|
225
|
+
if (csp.includes('unsafe-inline')) console.log(`[FINDING] WEAK_CSP: unsafe-inline in ${r.url}`);
|
|
226
|
+
if (r.method === 'POST') {
|
|
227
|
+
try {
|
|
228
|
+
const { body } = await cdp.send('Network.getRequestPostData', { requestId });
|
|
229
|
+
if (/token|password|secret|apikey|jwt|auth/.test(body.toLowerCase()))
|
|
230
|
+
console.log(`[FINDING] SENSITIVE_IN_POST: ${r.url} — body contains sensitive key`);
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Use getCookies scoped to TARGET_URL — getAllCookies returns the entire browser jar
|
|
236
|
+
// and floods output with third-party ad/tracker cookies irrelevant to the audited site
|
|
237
|
+
const { cookies } = await cdp.send('Network.getCookies', { urls: [TARGET_URL] });
|
|
238
|
+
for (const c of cookies) {
|
|
239
|
+
if (!c.httpOnly) console.log(`[FINDING] COOKIE_NO_HTTPONLY: ${c.name}`);
|
|
240
|
+
if (!c.secure) console.log(`[FINDING] COOKIE_NO_SECURE: ${c.name}`);
|
|
241
|
+
if (c.sameSite === 'None' && !c.secure)
|
|
242
|
+
console.log(`[FINDING] COOKIE_SAMESITE_NONE_INSECURE: ${c.name}`);
|
|
243
|
+
}
|
|
244
|
+
console.log(`[SECURITY] Cookies audited: ${cookies.length}`);
|
|
245
|
+
|
|
246
|
+
const { result: ls } = await cdp.send('Runtime.evaluate', {
|
|
247
|
+
expression: `JSON.stringify(Object.entries(localStorage).map(([k,v]) => ({ k, size: new Blob([k+v]).size })))`, returnByValue: true,
|
|
248
|
+
});
|
|
249
|
+
const lsEntries = JSON.parse(ls.value ?? '[]');
|
|
250
|
+
for (const { k } of lsEntries)
|
|
251
|
+
if (/token|auth|jwt|secret|key|password/i.test(k))
|
|
252
|
+
console.log(`[FINDING] SENSITIVE_IN_STORAGE: localStorage key "${k}"`);
|
|
253
|
+
console.log(`[SECURITY] localStorage keys: ${lsEntries.length}`);
|
|
254
|
+
|
|
255
|
+
const { result: proto } = await cdp.send('Runtime.evaluate', {
|
|
256
|
+
expression: `JSON.stringify(Object.keys(Object.getOwnPropertyDescriptors(Object.prototype)).filter(k => !['constructor','__defineGetter__','__defineSetter__','hasOwnProperty','__lookupGetter__','__lookupSetter__','isPrototypeOf','propertyIsEnumerable','toString','valueOf','__proto__','toLocaleString'].includes(k)))`,
|
|
257
|
+
returnByValue: true,
|
|
258
|
+
});
|
|
259
|
+
const polluted = JSON.parse(proto.value ?? '[]');
|
|
260
|
+
if (polluted.length > 0)
|
|
261
|
+
console.log(`[FINDING] PROTOTYPE_POLLUTION: unexpected keys on Object.prototype: ${polluted.join(', ')}`);
|
|
262
|
+
|
|
263
|
+
const { result: docObj } = await cdp.send('Runtime.evaluate', { expression: 'document' });
|
|
264
|
+
const { listeners } = await cdp.send('DOMDebugger.getEventListeners', { objectId: docObj.objectId });
|
|
265
|
+
for (const l of listeners) {
|
|
266
|
+
if (['keydown', 'keyup'].includes(l.type) && l.scriptId)
|
|
267
|
+
console.log(`[FINDING] POSSIBLE_KEYLOGGER: document ${l.type} listener at ${l.scriptId}:${l.lineNumber}`);
|
|
268
|
+
if (['copy', 'paste'].includes(l.type))
|
|
269
|
+
console.log(`[FINDING] CLIPBOARD_LISTENER: document ${l.type} listener — possible hijack`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
273
|
+
console.log(`[METRIC] Security audit complete — requests: ${requests.size}, cookies: ${cookies.length}`);
|
|
274
|
+
}
|
|
275
|
+
```
|