Add LLM key-hunter toolkit, vault, and skill
- tools/scripts/llm-key-hunter: GitHub leak hunting pipeline (hunt_*, pivot miner, two-layer verify/content caches, per-provider verification) - usable_keys: verified key vault across 12 providers (deepseek, minimax, volcanoark, longcat, codingplan, zhipu free-tier, mimo, siliconflow, etc.) - .grok/skills/llm-key-hunter: operator skill for the hunt/verify/vault flow - NewAPI channel import scripts and CDP capture helpers - Result verdict buckets (excluding multi-GB blob caches and dedup dumps)
This commit is contained in:
1 parent
a3f698806b
commit
5d215e1649
684 files changed
+133838
No files matched your search
Executable
+185
@@ -0,0 +1,185 @@
|
||||
# CDP Capture — Browser Session Recorder
|
||||
|
||||
> Connects to your **real Chrome browser** via Chrome DevTools Protocol and records everything you do.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Launch Chrome with debugging enabled
|
||||
|
||||
```powershell
|
||||
# Fresh profile (no logins)
|
||||
.\tools\scripts\launch-chrome.ps1
|
||||
|
||||
# Your real profile (with all your logins/cookies)
|
||||
.\tools\scripts\launch-chrome.ps1 -UseRealProfile
|
||||
|
||||
# Custom port and starting URL
|
||||
.\tools\scripts\launch-chrome.ps1 -Port 9223 -Url "https://example.com"
|
||||
```
|
||||
|
||||
### 2. Start the capture tool
|
||||
|
||||
```powershell
|
||||
cd tools\scripts\cdp-capture
|
||||
npm install # first time only
|
||||
node capture.js # start capturing
|
||||
```
|
||||
|
||||
### 3. Browse normally
|
||||
|
||||
Open tabs, log in, click around, fill forms. Everything is recorded in real-time.
|
||||
|
||||
### 4. Press `Ctrl+C` to stop
|
||||
|
||||
Cookies, localStorage, and session summary are saved on exit.
|
||||
|
||||
---
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
node capture.js [options]
|
||||
|
||||
--port=<port> CDP port (default: 9222)
|
||||
--host=<host> CDP host (default: localhost)
|
||||
--tab=<index> Tab to attach to (default: 0, use -1 for all tabs)
|
||||
--screenshot Take screenshot on each navigation
|
||||
--dom Save DOM snapshot on each navigation
|
||||
--bodies Capture response bodies (heavy!)
|
||||
--filter=<regex> Only capture network requests matching URL pattern
|
||||
--outdir=<path> Base output directory (default: project root)
|
||||
--no-interactions Disable interaction capture
|
||||
--no-network Disable network capture
|
||||
--no-console Disable console capture
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```powershell
|
||||
# Capture everything from all tabs, with screenshots and DOM
|
||||
node capture.js --tab=-1 --screenshot --dom
|
||||
|
||||
# Only capture API calls, ignore static resources
|
||||
node capture.js --filter="/api/|/graphql|/auth"
|
||||
|
||||
# Capture response bodies too (for analyzing API responses)
|
||||
node capture.js --bodies --filter="/api/"
|
||||
|
||||
# Capture from a specific tab
|
||||
node capture.js --tab=2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Gets Captured
|
||||
|
||||
| Data | File | Description |
|
||||
|---|---|---|
|
||||
| **Network requests** | `logs/<session>/network.jsonl` | Every HTTP request: URL, method, headers, POST data |
|
||||
| **Network responses** | `logs/<session>/network.jsonl` | Status, headers, MIME type, remote IP |
|
||||
| **Response bodies** | `logs/<session>/network_bodies.jsonl` | Response body content (with `--bodies`) |
|
||||
| **Console messages** | `logs/<session>/console.jsonl` | console.log/warn/error + exceptions |
|
||||
| **User interactions** | `logs/<session>/interactions.jsonl` | Clicks, inputs, form submits, keydowns |
|
||||
| **Navigations** | `logs/<session>/navigations.jsonl` | URL changes, page loads |
|
||||
| **Screenshots** | `evidence/screenshots/<session>/` | PNG screenshots (with `--screenshot`) |
|
||||
| **DOM snapshots** | `scans/host/<session>/` | Full HTML of page (with `--dom`) |
|
||||
| **Cookies** | `loot/credentials/<session>/cookies_tab*.json` | All cookies (on exit) |
|
||||
| **localStorage** | `loot/credentials/<session>/localStorage_tab*.json` | All localStorage (on exit) |
|
||||
| **sessionStorage** | `loot/credentials/<session>/sessionStorage_tab*.json` | All sessionStorage (on exit) |
|
||||
| **Summary** | `logs/<session>/summary.json` | Session stats and metadata |
|
||||
|
||||
---
|
||||
|
||||
## Interaction Data Format
|
||||
|
||||
Each interaction is a JSON line in `interactions.jsonl`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "click",
|
||||
"target": {
|
||||
"tag": "button",
|
||||
"id": "submit-btn",
|
||||
"class": "btn btn-primary",
|
||||
"text": "Login",
|
||||
"xpath": "/html[1]/body[1]/div[1]/form[1]/button[1]"
|
||||
},
|
||||
"timestamp": 1722000000000,
|
||||
"url": "https://example.com/login"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "input",
|
||||
"target": {
|
||||
"tag": "input",
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"value": "admin"
|
||||
},
|
||||
"timestamp": 1722000001000,
|
||||
"url": "https://example.com/login"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "submit",
|
||||
"target": {
|
||||
"tag": "form",
|
||||
"action": "https://example.com/api/login",
|
||||
"method": "post"
|
||||
},
|
||||
"formData": {
|
||||
"username": "admin",
|
||||
"password": "secret123"
|
||||
},
|
||||
"timestamp": 1722000002000,
|
||||
"url": "https://example.com/login"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Chrome (--remote-debugging-port=9222)
|
||||
│
|
||||
├── CDP WebSocket connection
|
||||
│
|
||||
├── Network domain → all HTTP requests/responses
|
||||
├── Runtime domain → console messages + JS evaluation
|
||||
├── Page domain → navigation events + screenshots
|
||||
├── DOM domain → DOM snapshots
|
||||
│
|
||||
└── Injected JS listeners → clicks, inputs, form submits
|
||||
│
|
||||
└── __capture() binding → CDP event → JSONL file
|
||||
```
|
||||
|
||||
The script injects JavaScript event listeners into every page via `Runtime.addBinding`. These listeners capture user interactions and send them back through CDP's binding mechanism — no console.log pollution, no polling, real-time capture.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Login flow capture** — Record yourself logging into a site, then replay/automate it
|
||||
2. **API reverse engineering** — See all API calls, headers, and payloads as you browse
|
||||
3. **Session extraction** — Grab cookies and tokens for use in scripts
|
||||
4. **Evidence collection** — Screenshots and DOM snapshots for reports
|
||||
5. **User behavior analysis** — See exactly what was clicked and typed
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
tools/scripts/
|
||||
├── cdp-capture/
|
||||
│ ├── package.json # Dependencies
|
||||
│ ├── capture.js # Main capture script
|
||||
│ └── README.md # This file
|
||||
└── launch-chrome.ps1 # Chrome launcher with CDP enabled
|
||||
```
|
||||
Executable
+525
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* CDP Capture — Chrome DevTools Protocol session recorder
|
||||
*
|
||||
* Connects to a real Chrome instance via CDP and captures:
|
||||
* - All network requests (URL, method, headers, body, response)
|
||||
* - All console messages
|
||||
* - All user interactions (clicks, inputs, form submits)
|
||||
* - All page navigations
|
||||
* - Cookies & localStorage (on exit or on demand)
|
||||
* - DOM snapshots & screenshots (on demand or per-navigation)
|
||||
*
|
||||
* Usage:
|
||||
* node capture.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --port=<port> CDP port (default: 9222)
|
||||
* --host=<host> CDP host (default: localhost)
|
||||
* --tab=<index> Tab index to attach to (default: 0, use -1 for all tabs)
|
||||
* --screenshot Take screenshot on each navigation
|
||||
* --dom Save DOM snapshot on each navigation
|
||||
* --bodies Capture response bodies (default: off, can be heavy)
|
||||
* --filter=<regex> Only capture network requests matching this URL pattern
|
||||
* --outdir=<path> Base output directory (default: ../../.. i.e. project root)
|
||||
* --no-interactions Disable interaction capture
|
||||
* --no-network Disable network capture
|
||||
* --no-console Disable console capture
|
||||
*/
|
||||
|
||||
const CDP = require('chrome-remote-interface');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ─── CLI Args ───────────────────────────────────────────────
|
||||
function parseArgs() {
|
||||
const args = { port: 9222, host: 'localhost', tab: 0, outdir: null,
|
||||
screenshot: false, dom: false, bodies: false, filter: null,
|
||||
interactions: true, network: true, console: true };
|
||||
|
||||
for (const arg of process.argv.slice(2)) {
|
||||
const [key, val] = arg.startsWith('--') ? arg.slice(2).split('=') : [arg, true];
|
||||
switch (key) {
|
||||
case 'port': args.port = parseInt(val); break;
|
||||
case 'host': args.host = val; break;
|
||||
case 'tab': args.tab = parseInt(val); break;
|
||||
case 'screenshot': args.screenshot = true; break;
|
||||
case 'dom': args.dom = true; break;
|
||||
case 'bodies': args.bodies = true; break;
|
||||
case 'filter': args.filter = val; break;
|
||||
case 'outdir': args.outdir = val; break;
|
||||
case 'no-interactions': args.interactions = false; break;
|
||||
case 'no-network': args.network = false; break;
|
||||
case 'no-console': args.console = false; break;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
// ─── Session Setup ──────────────────────────────────────────
|
||||
const args = parseArgs();
|
||||
const now = new Date();
|
||||
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const sessionName = `session_${ts}`;
|
||||
|
||||
const projectRoot = args.outdir
|
||||
? path.resolve(args.outdir)
|
||||
: path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
const dirs = {
|
||||
logs: path.join(projectRoot, 'logs', sessionName),
|
||||
scans: path.join(projectRoot, 'scans', 'host', sessionName),
|
||||
screenshots: path.join(projectRoot, 'evidence', 'screenshots', sessionName),
|
||||
loot: path.join(projectRoot, 'loot', 'credentials', sessionName),
|
||||
};
|
||||
|
||||
for (const d of Object.values(dirs)) fs.mkdirSync(d, { recursive: true });
|
||||
|
||||
// ─── Output Writers ─────────────────────────────────────────
|
||||
const writers = {};
|
||||
function getWriter(name) {
|
||||
if (!writers[name]) {
|
||||
writers[name] = fs.createWriteStream(path.join(dirs.logs, `${name}.jsonl`), { flags: 'a' });
|
||||
}
|
||||
return writers[name];
|
||||
}
|
||||
|
||||
function writeLine(name, obj) {
|
||||
const w = getWriter(name);
|
||||
w.write(JSON.stringify(obj) + '\n');
|
||||
}
|
||||
|
||||
// ─── Interaction Injection Script ───────────────────────────
|
||||
const injectScript = `
|
||||
(function() {
|
||||
if (window.__cdpCaptureInjected) return;
|
||||
window.__cdpCaptureInjected = true;
|
||||
|
||||
function serialize(el) {
|
||||
if (!el) return null;
|
||||
return {
|
||||
tag: el.tagName?.toLowerCase(),
|
||||
id: el.id || undefined,
|
||||
class: el.className?.toString()?.substring(0, 300) || undefined,
|
||||
text: el.innerText?.substring(0, 300) || undefined,
|
||||
href: el.href || undefined,
|
||||
name: el.name || undefined,
|
||||
type: el.type || undefined,
|
||||
value: el.value?.substring(0, 1000) || undefined,
|
||||
action: el.action || undefined,
|
||||
method: el.method || undefined,
|
||||
xpath: (function() {
|
||||
try {
|
||||
const s = [];
|
||||
let n = el;
|
||||
while (n && n.nodeType === 1) {
|
||||
let idx = 1, sib = n.previousElementSibling;
|
||||
while (sib) { if (sib.tagName === n.tagName) idx++; sib = sib.previousElementSibling; }
|
||||
s.unshift(n.tagName.toLowerCase() + '[' + idx + ']');
|
||||
n = n.parentElement;
|
||||
}
|
||||
return '/' + s.join('/');
|
||||
} catch(e) { return undefined; }
|
||||
})()
|
||||
};
|
||||
}
|
||||
|
||||
// Capture clicks
|
||||
document.addEventListener('click', function(e) {
|
||||
__capture(JSON.stringify({
|
||||
type: 'click',
|
||||
target: serialize(e.target),
|
||||
timestamp: Date.now(),
|
||||
url: location.href
|
||||
}));
|
||||
}, true);
|
||||
|
||||
// Capture input changes
|
||||
document.addEventListener('input', function(e) {
|
||||
__capture(JSON.stringify({
|
||||
type: 'input',
|
||||
target: serialize(e.target),
|
||||
timestamp: Date.now(),
|
||||
url: location.href
|
||||
}));
|
||||
}, true);
|
||||
|
||||
// Capture form submissions
|
||||
document.addEventListener('submit', function(e) {
|
||||
var formData = {};
|
||||
try {
|
||||
var fd = new FormData(e.target);
|
||||
fd.forEach(function(v, k) { formData[k] = (typeof v === 'string') ? v.substring(0, 1000) : '[file]'; });
|
||||
} catch(err) {}
|
||||
__capture(JSON.stringify({
|
||||
type: 'submit',
|
||||
target: serialize(e.target),
|
||||
formData: formData,
|
||||
timestamp: Date.now(),
|
||||
url: location.href
|
||||
}));
|
||||
}, true);
|
||||
|
||||
// Capture keydown (for Enter, etc.)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
__capture(JSON.stringify({
|
||||
type: 'keydown',
|
||||
key: e.key,
|
||||
target: serialize(e.target),
|
||||
timestamp: Date.now(),
|
||||
url: location.href
|
||||
}));
|
||||
}
|
||||
}, true);
|
||||
})();
|
||||
`;
|
||||
|
||||
// ─── Tab Capture ────────────────────────────────────────────
|
||||
const stats = { requests: 0, responses: 0, console: 0, interactions: 0, navigations: 0, screenshots: 0, domSnapshots: 0 };
|
||||
const pendingRequests = new Map();
|
||||
const filterRegex = args.filter ? new RegExp(args.filter) : null;
|
||||
|
||||
async function attachToTarget(target, index) {
|
||||
const label = `[Tab ${index}: ${target.url?.substring(0, 80)}]`;
|
||||
console.log(`\n${label} Attaching...`);
|
||||
|
||||
const client = await CDP({ target: target.id, host: args.host, port: args.port });
|
||||
const { Network, Page, Runtime, Console, DOM, Storage } = client;
|
||||
|
||||
// ── Enable domains ──
|
||||
await Network.enable();
|
||||
await Page.enable();
|
||||
await Runtime.enable();
|
||||
await Console.enable();
|
||||
if (args.dom) await DOM.enable();
|
||||
|
||||
// ── Add binding for interaction capture ──
|
||||
if (args.interactions) {
|
||||
await Runtime.addBinding({ name: '__capture' });
|
||||
}
|
||||
|
||||
// ── Network Events ──
|
||||
if (args.network) {
|
||||
Network.requestWillBeSent((params) => {
|
||||
const url = params.request.url;
|
||||
if (filterRegex && !filterRegex.test(url)) return;
|
||||
|
||||
const entry = {
|
||||
requestId: params.requestId,
|
||||
url: url,
|
||||
method: params.request.method,
|
||||
headers: params.request.headers,
|
||||
postData: params.request.postData?.substring(0, 10000),
|
||||
type: params.type,
|
||||
initiator: params.initiator?.url,
|
||||
timestamp: params.timestamp,
|
||||
wallTime: params.wallTime,
|
||||
};
|
||||
pendingRequests.set(params.requestId, entry);
|
||||
writeLine('network', { event: 'request', ...entry });
|
||||
stats.requests++;
|
||||
});
|
||||
|
||||
Network.responseReceived((params) => {
|
||||
const url = params.response.url;
|
||||
if (filterRegex && !filterRegex.test(url)) return;
|
||||
|
||||
const entry = {
|
||||
requestId: params.requestId,
|
||||
url: url,
|
||||
status: params.response.status,
|
||||
statusText: params.response.statusText,
|
||||
mimeType: params.response.mimeType,
|
||||
headers: params.response.headers,
|
||||
remoteIP: params.response.remoteIPAddress,
|
||||
remotePort: params.response.remotePort,
|
||||
protocol: params.response.protocol,
|
||||
timestamp: params.timestamp,
|
||||
};
|
||||
writeLine('network', { event: 'response', ...entry });
|
||||
stats.responses++;
|
||||
});
|
||||
|
||||
if (args.bodies) {
|
||||
Network.loadingFinished(async (params) => {
|
||||
try {
|
||||
const { body, base64Encoded } = await Network.getResponseBody({
|
||||
requestId: params.requestId,
|
||||
});
|
||||
const req = pendingRequests.get(params.requestId);
|
||||
if (req) {
|
||||
writeLine('network_bodies', {
|
||||
requestId: params.requestId,
|
||||
url: req.url,
|
||||
body: base64Encoded ? `[base64 ${body.length} chars]` : body.substring(0, 50000),
|
||||
base64Encoded,
|
||||
});
|
||||
}
|
||||
} catch (e) { /* body not available */ }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Console Events ──
|
||||
if (args.console) {
|
||||
Runtime.consoleAPICalled((params) => {
|
||||
const args = params.args.map(a => a.value ?? a.description ?? a.unserializableValue ?? '');
|
||||
writeLine('console', {
|
||||
type: params.type,
|
||||
args: args,
|
||||
stackTrace: params.stackTrace?.callFrames?.[0]?.functionName,
|
||||
timestamp: params.timestamp,
|
||||
url: target.url,
|
||||
});
|
||||
stats.console++;
|
||||
});
|
||||
|
||||
Runtime.exceptionThrown((params) => {
|
||||
writeLine('console', {
|
||||
type: 'exception',
|
||||
text: params.exceptionDetails.text,
|
||||
exception: params.exceptionDetails.exception?.description?.substring(0, 2000),
|
||||
url: params.exceptionDetails.url,
|
||||
lineNumber: params.exceptionDetails.lineNumber,
|
||||
timestamp: Date.now() / 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Interaction Events (via binding) ──
|
||||
if (args.interactions) {
|
||||
Runtime.bindingCalled((params) => {
|
||||
if (params.name === '__capture') {
|
||||
try {
|
||||
const data = JSON.parse(params.payload);
|
||||
writeLine('interactions', data);
|
||||
stats.interactions++;
|
||||
} catch (e) { /* malformed */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Navigation Events ──
|
||||
Page.frameNavigated(async (params) => {
|
||||
if (params.frame.parentId) return; // only top-level
|
||||
const url = params.frame.url;
|
||||
writeLine('navigations', {
|
||||
url: url,
|
||||
name: params.frame.name,
|
||||
timestamp: Date.now() / 1000,
|
||||
});
|
||||
stats.navigations++;
|
||||
console.log(`${label} Navigation → ${url.substring(0, 100)}`);
|
||||
|
||||
// Re-inject interaction listeners after navigation
|
||||
if (args.interactions) {
|
||||
try {
|
||||
await Runtime.evaluate({ expression: injectScript, returnByValue: false });
|
||||
} catch (e) { /* page not ready */ }
|
||||
}
|
||||
|
||||
// Screenshot on navigation
|
||||
if (args.screenshot) {
|
||||
try {
|
||||
await takeScreenshot(client, index, url);
|
||||
} catch (e) { /* timing */ }
|
||||
}
|
||||
|
||||
// DOM snapshot on navigation
|
||||
if (args.dom) {
|
||||
try {
|
||||
await saveDOMSnapshot(client, index, url);
|
||||
} catch (e) { /* timing */ }
|
||||
}
|
||||
});
|
||||
|
||||
// ── Inject listeners on load ──
|
||||
Page.loadEventFired(async () => {
|
||||
if (args.interactions) {
|
||||
try {
|
||||
await Runtime.evaluate({ expression: injectScript, returnByValue: false });
|
||||
console.log(`${label} Interaction listeners injected.`);
|
||||
} catch (e) { /* page not ready */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Initial injection (in case page is already loaded)
|
||||
if (args.interactions) {
|
||||
try {
|
||||
await Runtime.evaluate({ expression: injectScript, returnByValue: false });
|
||||
} catch (e) { /* page not ready */ }
|
||||
}
|
||||
|
||||
console.log(`${label} Capturing. (network=${args.network}, console=${args.console}, interactions=${args.interactions})`);
|
||||
return client;
|
||||
}
|
||||
|
||||
// ─── Screenshot ──────────────────────────────────────────────
|
||||
async function takeScreenshot(client, tabIndex, url) {
|
||||
const { Page } = client;
|
||||
const { data } = await Page.captureScreenshot({ format: 'png' });
|
||||
const fname = `tab${tabIndex}_${Date.now()}.png`;
|
||||
fs.writeFileSync(path.join(dirs.screenshots, fname), Buffer.from(data, 'base64'));
|
||||
writeLine('screenshots', { file: fname, url, timestamp: Date.now() / 1000 });
|
||||
stats.screenshots++;
|
||||
console.log(` 📸 Screenshot: ${fname}`);
|
||||
}
|
||||
|
||||
// ─── DOM Snapshot ────────────────────────────────────────────
|
||||
async function saveDOMSnapshot(client, tabIndex, url) {
|
||||
const { DOM, Page } = client;
|
||||
const { root } = await DOM.getDocument({ depth: -1 });
|
||||
const html = await DOM.getOuterHTML({ nodeId: root.nodeId });
|
||||
const fname = `tab${tabIndex}_${Date.now()}.html`;
|
||||
fs.writeFileSync(path.join(dirs.scans, fname), html.outerHTML || String(html));
|
||||
stats.domSnapshots++;
|
||||
console.log(` 📄 DOM snapshot: ${fname}`);
|
||||
}
|
||||
|
||||
// ─── Cookie & Storage Extraction ─────────────────────────────
|
||||
async function extractCookies(clients) {
|
||||
console.log('\nExtracting cookies & storage...');
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const client = clients[i];
|
||||
if (!client) continue;
|
||||
try {
|
||||
const { Network, Runtime, Storage } = client;
|
||||
|
||||
// Cookies via Network domain
|
||||
const { cookies } = await Network.getCookies({});
|
||||
fs.writeFileSync(
|
||||
path.join(dirs.loot, `cookies_tab${i}.json`),
|
||||
JSON.stringify(cookies, null, 2)
|
||||
);
|
||||
|
||||
// localStorage
|
||||
const lsResult = await Runtime.evaluate({
|
||||
expression: `JSON.stringify(Object.fromEntries(Object.entries(localStorage)))`,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (lsResult.result?.value) {
|
||||
fs.writeFileSync(
|
||||
path.join(dirs.loot, `localStorage_tab${i}.json`),
|
||||
lsResult.result.value
|
||||
);
|
||||
}
|
||||
|
||||
// sessionStorage
|
||||
const ssResult = await Runtime.evaluate({
|
||||
expression: `JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))`,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (ssResult.result?.value) {
|
||||
fs.writeFileSync(
|
||||
path.join(dirs.loot, `sessionStorage_tab${i}.json`),
|
||||
ssResult.result.value
|
||||
);
|
||||
}
|
||||
|
||||
console.log(` Tab ${i}: ${cookies.length} cookies, localStorage + sessionStorage saved`);
|
||||
} catch (e) {
|
||||
console.log(` Tab ${i}: extraction failed (${e.message})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────
|
||||
async function main() {
|
||||
console.log('╔══════════════════════════════════════════════════╗');
|
||||
console.log('║ CDP Capture — Session Recorder ║');
|
||||
console.log('╚══════════════════════════════════════════════════╝');
|
||||
console.log(`\nSession: ${sessionName}`);
|
||||
console.log(`Output: ${projectRoot}`);
|
||||
console.log(`CDP: ${args.host}:${args.port}`);
|
||||
|
||||
// Get browser targets
|
||||
let targets;
|
||||
try {
|
||||
targets = await CDP.List({ host: args.host, port: args.port });
|
||||
} catch (e) {
|
||||
console.error(`\n❌ Cannot connect to CDP at ${args.host}:${args.port}`);
|
||||
console.error(` Make sure Chrome is running with --remote-debugging-port=${args.port}`);
|
||||
console.error(` Use: tools/scripts/launch-chrome.ps1`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pageTargets = targets.filter(t => t.type === 'page');
|
||||
if (pageTargets.length === 0) {
|
||||
console.error('\n❌ No browser tabs found. Open a tab in Chrome first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nFound ${pageTargets.length} tab(s):`);
|
||||
pageTargets.forEach((t, i) => {
|
||||
console.log(` [${i}] ${t.url?.substring(0, 100)}`);
|
||||
});
|
||||
|
||||
// Select tabs to attach
|
||||
let tabsToAttach;
|
||||
if (args.tab === -1) {
|
||||
tabsToAttach = pageTargets.map((t, i) => ({ target: t, index: i }));
|
||||
} else if (args.tab >= 0 && args.tab < pageTargets.length) {
|
||||
tabsToAttach = [{ target: pageTargets[args.tab], index: args.tab }];
|
||||
} else {
|
||||
console.error(`\n❌ Invalid tab index: ${args.tab}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Attach to targets
|
||||
const clients = [];
|
||||
for (const { target, index } of tabsToAttach) {
|
||||
try {
|
||||
const client = await attachToTarget(target, index);
|
||||
clients.push(client);
|
||||
} catch (e) {
|
||||
console.error(`Tab ${index}: attach failed — ${e.message}`);
|
||||
clients.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Capturing ${clients.filter(Boolean).length} tab(s). Press Ctrl+C to stop.\n`);
|
||||
|
||||
// ── Graceful Shutdown ──
|
||||
let shuttingDown = false;
|
||||
process.on('SIGINT', async () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log('\n\nShutting down...');
|
||||
|
||||
// Extract cookies & storage
|
||||
await extractCookies(clients);
|
||||
|
||||
// Write summary
|
||||
const summary = {
|
||||
session: sessionName,
|
||||
startedAt: now.toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
args: args,
|
||||
stats: stats,
|
||||
outputDirs: dirs,
|
||||
};
|
||||
fs.writeFileSync(path.join(dirs.logs, 'summary.json'), JSON.stringify(summary, null, 2));
|
||||
console.log(`\n📊 Session Summary:`);
|
||||
console.log(` Network requests: ${stats.requests}`);
|
||||
console.log(` Network responses: ${stats.responses}`);
|
||||
console.log(` Console messages: ${stats.console}`);
|
||||
console.log(` Interactions: ${stats.interactions}`);
|
||||
console.log(` Navigations: ${stats.navigations}`);
|
||||
console.log(` Screenshots: ${stats.screenshots}`);
|
||||
console.log(` DOM snapshots: ${stats.domSnapshots}`);
|
||||
console.log(`\n📁 Output: ${dirs.logs}`);
|
||||
|
||||
// Close writers
|
||||
for (const w of Object.values(writers)) w.end();
|
||||
|
||||
// Close clients
|
||||
for (const c of clients) { if (c) try { await c.close(); } catch (e) {} }
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error('Fatal error:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "cdp-capture",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cdp-capture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"chrome-remote-interface": "^0.33.3"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-remote-interface": {
|
||||
"version": "0.33.3",
|
||||
"resolved": "https://npm.qxy1828.com/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz",
|
||||
"integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "2.11.x",
|
||||
"ws": "^7.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"chrome-remote-interface": "bin/client.js"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://npm.qxy1828.com/commander/-/commander-2.11.0.tgz",
|
||||
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.13",
|
||||
"resolved": "https://npm.qxy1828.com/ws/-/ws-7.5.13.tgz",
|
||||
"integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "cdp-capture",
|
||||
"version": "1.0.0",
|
||||
"description": "Chrome DevTools Protocol capture tool — records all browser activity from a real Chrome session",
|
||||
"main": "capture.js",
|
||||
"scripts": {
|
||||
"start": "node capture.js",
|
||||
"capture": "node capture.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chrome-remote-interface": "^0.33.3"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user