@mseep/anything-analyzer 3.6.50
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/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { BrowserWindow, nativeImage } from "electron";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { TabManager } from "./tab-manager";
|
|
4
|
+
|
|
5
|
+
/** Custom titlebar height in renderer (px) */
|
|
6
|
+
const TITLEBAR_HEIGHT = 40;
|
|
7
|
+
/** Tab bar height in renderer (px) */
|
|
8
|
+
const TAB_BAR_HEIGHT = 33; // 32px height + 1px border-bottom
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WindowManager — Creates and manages the main BrowserWindow
|
|
12
|
+
* and delegates embedded browser tabs to TabManager.
|
|
13
|
+
*/
|
|
14
|
+
export class WindowManager {
|
|
15
|
+
private mainWindow: BrowserWindow | null = null;
|
|
16
|
+
private tabManager: TabManager | null = null;
|
|
17
|
+
/** Browser area height ratio (0.0 ~ 1.0), default 70% */
|
|
18
|
+
private browserRatio = 0.7;
|
|
19
|
+
/** Whether the browser view should be visible */
|
|
20
|
+
private targetViewVisible = true;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create the main application window.
|
|
24
|
+
*/
|
|
25
|
+
createMainWindow(): BrowserWindow {
|
|
26
|
+
this.mainWindow = new BrowserWindow({
|
|
27
|
+
width: 1400,
|
|
28
|
+
height: 900,
|
|
29
|
+
minWidth: 1024,
|
|
30
|
+
minHeight: 700,
|
|
31
|
+
title: "Anything Analyzer",
|
|
32
|
+
icon: nativeImage.createFromPath(join(__dirname, "../../resources/icon.png")),
|
|
33
|
+
frame: false,
|
|
34
|
+
autoHideMenuBar: true,
|
|
35
|
+
webPreferences: {
|
|
36
|
+
preload: join(__dirname, "../preload/index.js"),
|
|
37
|
+
sandbox: false,
|
|
38
|
+
contextIsolation: true,
|
|
39
|
+
nodeIntegration: false,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (process.env["ELECTRON_RENDERER_URL"]) {
|
|
44
|
+
this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]).catch(() => {});
|
|
45
|
+
} else {
|
|
46
|
+
this.mainWindow.loadFile(join(__dirname, "../renderer/index.html")).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.mainWindow;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the tab manager and create the first (default) tab.
|
|
54
|
+
*/
|
|
55
|
+
initTabs(): TabManager {
|
|
56
|
+
if (!this.mainWindow) throw new Error("Main window not created");
|
|
57
|
+
|
|
58
|
+
this.tabManager = new TabManager();
|
|
59
|
+
this.tabManager.init(
|
|
60
|
+
this.mainWindow,
|
|
61
|
+
() => this.calculateTargetBounds(),
|
|
62
|
+
() => this.targetViewVisible,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Create the first tab
|
|
66
|
+
this.tabManager.createTab();
|
|
67
|
+
|
|
68
|
+
// Update bounds when window resizes
|
|
69
|
+
this.mainWindow.on("resize", () => {
|
|
70
|
+
this.tabManager?.updateBounds();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return this.tabManager;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Navigate the active tab to a URL.
|
|
78
|
+
*/
|
|
79
|
+
async navigateTo(url: string): Promise<void> {
|
|
80
|
+
const wc = this.tabManager?.getActiveWebContents();
|
|
81
|
+
if (!wc || wc.isDestroyed()) return;
|
|
82
|
+
let normalizedUrl = url;
|
|
83
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
84
|
+
normalizedUrl = `https://${url}`;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await wc.loadURL(normalizedUrl);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.warn('[WindowManager] navigateTo failed:', (err as Error).message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Go back in the active tab.
|
|
95
|
+
*/
|
|
96
|
+
goBack(): void {
|
|
97
|
+
const wc = this.tabManager?.getActiveWebContents();
|
|
98
|
+
if (wc && !wc.isDestroyed() && wc.canGoBack()) wc.goBack();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Go forward in the active tab.
|
|
103
|
+
*/
|
|
104
|
+
goForward(): void {
|
|
105
|
+
const wc = this.tabManager?.getActiveWebContents();
|
|
106
|
+
if (wc && !wc.isDestroyed() && wc.canGoForward()) wc.goForward();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Reload the active tab.
|
|
111
|
+
*/
|
|
112
|
+
reload(): void {
|
|
113
|
+
const wc = this.tabManager?.getActiveWebContents();
|
|
114
|
+
if (wc && !wc.isDestroyed()) wc.reload();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the main window instance.
|
|
119
|
+
*/
|
|
120
|
+
getMainWindow(): BrowserWindow | null {
|
|
121
|
+
return this.mainWindow;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the TabManager instance.
|
|
126
|
+
*/
|
|
127
|
+
getTabManager(): TabManager | null {
|
|
128
|
+
return this.tabManager;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Propagate app shutdown state to tab manager. */
|
|
132
|
+
setShuttingDown(shuttingDown: boolean): void {
|
|
133
|
+
this.tabManager?.setShuttingDown(shuttingDown);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the active tab's WebContents (for backward compatibility).
|
|
138
|
+
*/
|
|
139
|
+
getTargetWebContents() {
|
|
140
|
+
return this.tabManager?.getActiveWebContents() || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Show or hide the active tab's browser view using bounds (not add/remove).
|
|
145
|
+
*/
|
|
146
|
+
setTargetViewVisible(visible: boolean): void {
|
|
147
|
+
this.targetViewVisible = visible;
|
|
148
|
+
if (!this.mainWindow || !this.tabManager) return;
|
|
149
|
+
const activeTab = this.tabManager.getActiveTab();
|
|
150
|
+
if (!activeTab) return;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
if (activeTab.view.webContents.isDestroyed()) return;
|
|
154
|
+
if (visible) {
|
|
155
|
+
this.tabManager.updateBounds();
|
|
156
|
+
} else {
|
|
157
|
+
activeTab.view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
/* View may have been destroyed during operation — safe to ignore */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether the browser view is currently meant to be visible.
|
|
166
|
+
*/
|
|
167
|
+
isTargetViewVisible(): boolean {
|
|
168
|
+
return this.targetViewVisible;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate bounds for the target browser view area.
|
|
173
|
+
* Browser view fills all remaining space below the tab bar + browser panel.
|
|
174
|
+
* Sidebar (221px) is on the left.
|
|
175
|
+
*/
|
|
176
|
+
private calculateTargetBounds(): Electron.Rectangle {
|
|
177
|
+
if (!this.mainWindow) return { x: 0, y: 0, width: 0, height: 0 };
|
|
178
|
+
|
|
179
|
+
const contentBounds = this.mainWindow.getContentBounds();
|
|
180
|
+
const width = contentBounds.width;
|
|
181
|
+
const height = contentBounds.height;
|
|
182
|
+
const sidebarWidth = 221; // 220px sidebar + 1px border-right
|
|
183
|
+
const browserPanelHeight = 49; // padding 8+8 + Input 32 + borderBottom 1
|
|
184
|
+
const statusBarHeight = 26;
|
|
185
|
+
const topOffset = TITLEBAR_HEIGHT + TAB_BAR_HEIGHT + browserPanelHeight;
|
|
186
|
+
const browserHeight = Math.max(0, height - topOffset - statusBarHeight);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
x: sidebarWidth,
|
|
190
|
+
y: topOffset,
|
|
191
|
+
width: width - sidebarWidth,
|
|
192
|
+
height: browserHeight,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Set the browser area height ratio and update bounds.
|
|
198
|
+
* @param ratio 0.0 ~ 1.0
|
|
199
|
+
*/
|
|
200
|
+
setBrowserRatio(ratio: number): void {
|
|
201
|
+
this.browserRatio = Math.max(0.15, Math.min(0.85, ratio));
|
|
202
|
+
// Don't call updateBounds here — the renderer will report exact bounds
|
|
203
|
+
// via syncBrowserBounds after its layout updates.
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Set exact bounds for the active browser tab view.
|
|
208
|
+
* Called by the renderer which measures the actual placeholder position.
|
|
209
|
+
*/
|
|
210
|
+
syncBrowserBounds(bounds: Electron.Rectangle): void {
|
|
211
|
+
const tab = this.tabManager?.getActiveTab();
|
|
212
|
+
if (tab) {
|
|
213
|
+
try {
|
|
214
|
+
if (!tab.view.webContents.isDestroyed()) {
|
|
215
|
+
tab.view.setBounds(bounds);
|
|
216
|
+
}
|
|
217
|
+
} catch { /* view destroyed */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get current browser area height ratio.
|
|
223
|
+
*/
|
|
224
|
+
getBrowserRatio(): number {
|
|
225
|
+
return this.browserRatio;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Destroy all tabs and clean up.
|
|
230
|
+
*/
|
|
231
|
+
destroyTargetView(): void {
|
|
232
|
+
this.tabManager?.destroyEverything();
|
|
233
|
+
this.tabManager = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook script injected into the target browser page context.
|
|
3
|
+
* Intercepts fetch, XMLHttpRequest, crypto.subtle, and document.cookie.
|
|
4
|
+
*/
|
|
5
|
+
;(function () {
|
|
6
|
+
const HOOK_MSG_TYPE = 'ar-hook'
|
|
7
|
+
|
|
8
|
+
function sendHookData(hookType: string, functionName: string, args: unknown, result: unknown, callStack: string | null): void {
|
|
9
|
+
try {
|
|
10
|
+
window.postMessage({ type: HOOK_MSG_TYPE, hookType, functionName, arguments: JSON.stringify(args), result: result != null ? JSON.stringify(result) : null, callStack, timestamp: Date.now() }, '*')
|
|
11
|
+
} catch { /* ignore serialization errors */ }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getCallStack(): string {
|
|
15
|
+
return new Error().stack?.split('\n').slice(2).join('\n') || ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function arrayBufferToHex(buffer: ArrayBuffer): string {
|
|
19
|
+
return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Hook: window.fetch
|
|
23
|
+
const originalFetch = window.fetch
|
|
24
|
+
const hookedFetch = function(this: typeof globalThis, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
25
|
+
const stack = getCallStack()
|
|
26
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url
|
|
27
|
+
const method = init?.method || (input instanceof Request ? input.method : 'GET')
|
|
28
|
+
sendHookData('fetch', 'window.fetch', { url, method, body: init?.body?.toString() }, null, stack)
|
|
29
|
+
return originalFetch.call(this, input, init).then(response => {
|
|
30
|
+
sendHookData('fetch', 'window.fetch.response', { url, method }, { status: response.status, statusText: response.statusText }, null)
|
|
31
|
+
return response
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
try { Object.defineProperty(window, 'fetch', { value: hookedFetch, writable: false, configurable: false }) } catch { (window as any).fetch = hookedFetch }
|
|
35
|
+
|
|
36
|
+
// Hook: XMLHttpRequest
|
|
37
|
+
const XHRProto = XMLHttpRequest.prototype
|
|
38
|
+
const originalOpen = XHRProto.open
|
|
39
|
+
const originalSend = XHRProto.send
|
|
40
|
+
const originalSetHeader = XHRProto.setRequestHeader
|
|
41
|
+
|
|
42
|
+
XHRProto.open = function(method: string, url: string | URL, ...args: any[]) {
|
|
43
|
+
(this as any)._arMethod = method;
|
|
44
|
+
(this as any)._arUrl = typeof url === 'string' ? url : url.href;
|
|
45
|
+
(this as any)._arHeaders = {}
|
|
46
|
+
return (originalOpen as Function).call(this, method, url, ...args)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
XHRProto.setRequestHeader = function(name: string, value: string) {
|
|
50
|
+
if ((this as any)._arHeaders) (this as any)._arHeaders[name] = value
|
|
51
|
+
return originalSetHeader.call(this, name, value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
XHRProto.send = function(body?: Document | XMLHttpRequestBodyInit | null) {
|
|
55
|
+
const xhr = this as any
|
|
56
|
+
const stack = getCallStack()
|
|
57
|
+
sendHookData('xhr', 'XMLHttpRequest.send', { method: xhr._arMethod, url: xhr._arUrl, headers: xhr._arHeaders, body: body?.toString() || null }, null, stack)
|
|
58
|
+
this.addEventListener('load', function() {
|
|
59
|
+
sendHookData('xhr', 'XMLHttpRequest.response', { method: xhr._arMethod, url: xhr._arUrl }, { status: this.status, statusText: this.statusText }, null)
|
|
60
|
+
})
|
|
61
|
+
return originalSend.call(this, body)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Hook: crypto.subtle
|
|
65
|
+
if (window.crypto?.subtle) {
|
|
66
|
+
const subtle = window.crypto.subtle
|
|
67
|
+
for (const methodName of ['sign', 'digest', 'encrypt', 'decrypt'] as const) {
|
|
68
|
+
const original = subtle[methodName].bind(subtle)
|
|
69
|
+
;(subtle as any)[methodName] = async function(...args: any[]) {
|
|
70
|
+
const stack = getCallStack()
|
|
71
|
+
const serializedArgs = args.map(arg => {
|
|
72
|
+
if (arg instanceof ArrayBuffer) return arrayBufferToHex(arg)
|
|
73
|
+
if (ArrayBuffer.isView(arg)) return arrayBufferToHex(arg.buffer)
|
|
74
|
+
return arg
|
|
75
|
+
})
|
|
76
|
+
sendHookData('crypto', `crypto.subtle.${methodName}`, serializedArgs, null, stack)
|
|
77
|
+
const result = await (original as Function)(...args)
|
|
78
|
+
sendHookData('crypto', `crypto.subtle.${methodName}.result`, serializedArgs, result instanceof ArrayBuffer ? arrayBufferToHex(result) : result, null)
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- Third-party crypto library hooks ----
|
|
85
|
+
|
|
86
|
+
function truncateArg(val: unknown): string {
|
|
87
|
+
const s = typeof val === 'string' ? val : JSON.stringify(val)
|
|
88
|
+
return s && s.length > 500 ? s.substring(0, 500) + '...' : (s || '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function wrapMethod(obj: any, methodName: string, libLabel: string): void {
|
|
92
|
+
if (typeof obj[methodName] !== 'function') return
|
|
93
|
+
const original = obj[methodName]
|
|
94
|
+
obj[methodName] = function(this: any, ...args: any[]) {
|
|
95
|
+
const stack = getCallStack()
|
|
96
|
+
const truncatedArgs = args.map(a => truncateArg(a))
|
|
97
|
+
sendHookData('crypto_lib', `${libLabel}.${methodName}`, truncatedArgs, null, stack)
|
|
98
|
+
try {
|
|
99
|
+
const result = original.apply(this, args)
|
|
100
|
+
if (result && typeof result === 'object' && typeof result.toString === 'function') {
|
|
101
|
+
sendHookData('crypto_lib', `${libLabel}.${methodName}.result`, truncatedArgs, truncateArg(result.toString()), null)
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
} catch (e) {
|
|
105
|
+
throw e
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hookCryptoJS(CryptoJS: any): void {
|
|
111
|
+
if (!CryptoJS || CryptoJS._arHooked) return
|
|
112
|
+
CryptoJS._arHooked = true
|
|
113
|
+
|
|
114
|
+
// AES / DES / TripleDES / Rabbit / RC4
|
|
115
|
+
for (const cipher of ['AES', 'DES', 'TripleDES', 'Rabbit', 'RC4']) {
|
|
116
|
+
if (CryptoJS[cipher]) {
|
|
117
|
+
wrapMethod(CryptoJS[cipher], 'encrypt', `CryptoJS.${cipher}`)
|
|
118
|
+
wrapMethod(CryptoJS[cipher], 'decrypt', `CryptoJS.${cipher}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Hash functions
|
|
123
|
+
for (const hash of ['MD5', 'SHA1', 'SHA256', 'SHA512', 'SHA3', 'RIPEMD160']) {
|
|
124
|
+
if (typeof CryptoJS[hash] === 'function') {
|
|
125
|
+
const original = CryptoJS[hash]
|
|
126
|
+
CryptoJS[hash] = function(...args: any[]) {
|
|
127
|
+
const stack = getCallStack()
|
|
128
|
+
sendHookData('crypto_lib', `CryptoJS.${hash}`, args.map(a => truncateArg(a)), null, stack)
|
|
129
|
+
const result = original.apply(this, args)
|
|
130
|
+
sendHookData('crypto_lib', `CryptoJS.${hash}.result`, [], truncateArg(result?.toString()), null)
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// HMAC functions
|
|
137
|
+
for (const hmac of ['HmacSHA1', 'HmacSHA256', 'HmacSHA512', 'HmacMD5']) {
|
|
138
|
+
if (typeof CryptoJS[hmac] === 'function') {
|
|
139
|
+
const original = CryptoJS[hmac]
|
|
140
|
+
CryptoJS[hmac] = function(...args: any[]) {
|
|
141
|
+
const stack = getCallStack()
|
|
142
|
+
sendHookData('crypto_lib', `CryptoJS.${hmac}`, args.map(a => truncateArg(a)), null, stack)
|
|
143
|
+
const result = original.apply(this, args)
|
|
144
|
+
sendHookData('crypto_lib', `CryptoJS.${hmac}.result`, [], truncateArg(result?.toString()), null)
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PBKDF2
|
|
151
|
+
if (typeof CryptoJS.PBKDF2 === 'function') {
|
|
152
|
+
const original = CryptoJS.PBKDF2
|
|
153
|
+
CryptoJS.PBKDF2 = function(...args: any[]) {
|
|
154
|
+
const stack = getCallStack()
|
|
155
|
+
sendHookData('crypto_lib', 'CryptoJS.PBKDF2', args.map(a => truncateArg(a)), null, stack)
|
|
156
|
+
const result = original.apply(this, args)
|
|
157
|
+
sendHookData('crypto_lib', 'CryptoJS.PBKDF2.result', [], truncateArg(result?.toString()), null)
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// enc.Base64 / enc.Hex
|
|
163
|
+
if (CryptoJS.enc) {
|
|
164
|
+
for (const enc of ['Base64', 'Hex', 'Utf8', 'Latin1']) {
|
|
165
|
+
if (CryptoJS.enc[enc]) {
|
|
166
|
+
wrapMethod(CryptoJS.enc[enc], 'stringify', `CryptoJS.enc.${enc}`)
|
|
167
|
+
wrapMethod(CryptoJS.enc[enc], 'parse', `CryptoJS.enc.${enc}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hookJSEncrypt(JSEncryptClass: any): void {
|
|
174
|
+
if (!JSEncryptClass || JSEncryptClass._arHooked) return
|
|
175
|
+
JSEncryptClass._arHooked = true
|
|
176
|
+
const proto = JSEncryptClass.prototype
|
|
177
|
+
if (proto) {
|
|
178
|
+
for (const method of ['encrypt', 'decrypt', 'sign', 'verify', 'setPublicKey', 'setPrivateKey']) {
|
|
179
|
+
wrapMethod(proto, method, 'JSEncrypt')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hookForge(forge: any): void {
|
|
185
|
+
if (!forge || forge._arHooked) return
|
|
186
|
+
forge._arHooked = true
|
|
187
|
+
if (forge.pki) {
|
|
188
|
+
wrapMethod(forge.pki, 'publicKeyFromPem', 'forge.pki')
|
|
189
|
+
wrapMethod(forge.pki, 'privateKeyFromPem', 'forge.pki')
|
|
190
|
+
wrapMethod(forge.pki, 'certificateFromPem', 'forge.pki')
|
|
191
|
+
}
|
|
192
|
+
if (forge.cipher) {
|
|
193
|
+
wrapMethod(forge.cipher, 'createCipher', 'forge.cipher')
|
|
194
|
+
wrapMethod(forge.cipher, 'createDecipher', 'forge.cipher')
|
|
195
|
+
}
|
|
196
|
+
if (forge.md) {
|
|
197
|
+
for (const alg of ['sha256', 'sha1', 'sha512', 'md5']) {
|
|
198
|
+
if (forge.md[alg]) wrapMethod(forge.md[alg], 'create', `forge.md.${alg}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (forge.util) {
|
|
202
|
+
wrapMethod(forge.util, 'encode64', 'forge.util')
|
|
203
|
+
wrapMethod(forge.util, 'decode64', 'forge.util')
|
|
204
|
+
}
|
|
205
|
+
if (forge.hmac) {
|
|
206
|
+
wrapMethod(forge.hmac, 'create', 'forge.hmac')
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hookSmCrypto(name: string, obj: any): void {
|
|
211
|
+
if (!obj || obj._arHooked) return
|
|
212
|
+
obj._arHooked = true
|
|
213
|
+
for (const method of ['doEncrypt', 'doDecrypt', 'doSignature', 'doVerifySignature', 'encrypt', 'decrypt']) {
|
|
214
|
+
wrapMethod(obj, method, name)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Trap library globals: fires when library is assigned to window
|
|
219
|
+
function trapGlobal(name: string, hookFn: (lib: any) => void): void {
|
|
220
|
+
// If already present, hook immediately
|
|
221
|
+
if ((window as any)[name]) {
|
|
222
|
+
try { hookFn((window as any)[name]) } catch { /* ignore */ }
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
// Set a defineProperty trap for lazy loading
|
|
226
|
+
let _val: any = undefined
|
|
227
|
+
try {
|
|
228
|
+
Object.defineProperty(window, name, {
|
|
229
|
+
get() { return _val },
|
|
230
|
+
set(v) {
|
|
231
|
+
_val = v
|
|
232
|
+
if (v) { try { hookFn(v) } catch { /* ignore */ } }
|
|
233
|
+
},
|
|
234
|
+
configurable: true,
|
|
235
|
+
enumerable: true,
|
|
236
|
+
})
|
|
237
|
+
} catch { /* CSP or frozen global */ }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
trapGlobal('CryptoJS', hookCryptoJS)
|
|
241
|
+
trapGlobal('JSEncrypt', hookJSEncrypt)
|
|
242
|
+
trapGlobal('forge', hookForge)
|
|
243
|
+
trapGlobal('sm2', (obj) => hookSmCrypto('sm2', obj))
|
|
244
|
+
trapGlobal('sm3', (obj) => hookSmCrypto('sm3', obj))
|
|
245
|
+
trapGlobal('sm4', (obj) => hookSmCrypto('sm4', obj))
|
|
246
|
+
|
|
247
|
+
// Hook native btoa/atob
|
|
248
|
+
const originalBtoa = window.btoa
|
|
249
|
+
const originalAtob = window.atob
|
|
250
|
+
window.btoa = function(data: string): string {
|
|
251
|
+
sendHookData('crypto_lib', 'btoa', { data: truncateArg(data) }, null, getCallStack())
|
|
252
|
+
return originalBtoa.call(window, data)
|
|
253
|
+
}
|
|
254
|
+
window.atob = function(data: string): string {
|
|
255
|
+
sendHookData('crypto_lib', 'atob', { data: truncateArg(data) }, null, getCallStack())
|
|
256
|
+
return originalAtob.call(window, data)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Hook: document.cookie setter
|
|
260
|
+
const cookieDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
|
|
261
|
+
if (cookieDesc) {
|
|
262
|
+
try {
|
|
263
|
+
Object.defineProperty(document, 'cookie', {
|
|
264
|
+
get: function() { return cookieDesc.get?.call(this) },
|
|
265
|
+
set: function(value: string) { sendHookData('cookie_set', 'document.cookie.set', { value }, null, getCallStack()); return cookieDesc.set?.call(this, value) },
|
|
266
|
+
configurable: false
|
|
267
|
+
})
|
|
268
|
+
} catch { /* CSP or already locked */ }
|
|
269
|
+
}
|
|
270
|
+
})()
|