@lievclouds/dsh-cortex 0.4.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.
- package/README.md +1 -1
- package/client.js +428 -406
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -106
- package/src/cortex-ui/dsh/api.ts +5 -2
- package/src/cortex-ui/dsh/css-overrides.css +8 -0
- package/src/cortex-ui/renderer/App.tsx +39 -0
- package/src/cortex-ui/renderer/components/asset/AssetDetail.tsx +9 -1
- package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
- package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +333 -148
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- package/src/cortex-ui/renderer/components/vuln/DirectoryPickerDialog.tsx +187 -0
- package/src/cortex-ui/renderer/components/vuln/ExportReportDialog.tsx +173 -59
- package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +16 -4
- package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
- package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
- package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
- package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
- package/src/cortex-ui/renderer/i18n/messages/retest.ts +146 -2
- package/src/cortex-ui/renderer/i18n/messages/vuln.ts +26 -6
- package/src/cortex-ui/renderer/lib/curl.ts +635 -0
- package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
- package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
- package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
- package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
- package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -0
- package/src/cortex-ui/renderer/lib/transport/httpClient.ts +1 -1
- package/src/cortex-ui/renderer/pages/DashboardPage.tsx +3 -3
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/cortex-ui/shared/ipc.ts +3 -4
- package/src/cortex-ui/shared/types.ts +9 -0
- package/src/host/cortex/fs.js +72 -0
- package/src/host/cortex/index.js +49 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/settings.js +54 -0
- package/src/host/cortex/tools.js +2 -0
- package/src/host/cortex/vulns.js +36 -13
- package/src/host/miniapp/analyze.js +1078 -0
- package/src/host/miniapp/assets-import.js +294 -0
- package/src/host/miniapp/detect.js +426 -0
- package/src/host/miniapp/format.js +263 -0
- package/src/host/miniapp/roots.js +360 -0
- package/src/host/miniapp/routes.js +304 -0
- package/src/host/miniapp/service.js +559 -0
- package/src/host/miniapp/store.js +756 -0
- package/src/host/miniapp/tools.js +335 -0
- package/src/host/miniapp/unpack.js +1010 -0
- package/src/host/miniapp/wxapkg.js +416 -0
- package/src/host/reports-office.js +341 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed client for the mini-program reverse-engineering routes.
|
|
3
|
+
*
|
|
4
|
+
* Contract: `.tmp/miniapp-contract.md` §3.7 — every route lives under
|
|
5
|
+
* `/cortex-api/cortex`, talks JSON and uses the host `{error, code}` envelope
|
|
6
|
+
* for non-2xx responses. The page calls these wrappers directly (instead of
|
|
7
|
+
* `CortexApi`) because the miniapp feature is plugin-host-only and does not
|
|
8
|
+
* exist in the upstream Cortex IPC surface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const BASE = '/cortex-api/cortex'
|
|
12
|
+
|
|
13
|
+
export class MiniappApiError extends Error {
|
|
14
|
+
code: string
|
|
15
|
+
|
|
16
|
+
constructor(message: string, code: string) {
|
|
17
|
+
super(message)
|
|
18
|
+
this.name = 'MiniappApiError'
|
|
19
|
+
this.code = code
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type MiniappStatus = 'detected' | 'decrypting' | 'unpacked' | 'unsupported' | 'failed'
|
|
24
|
+
|
|
25
|
+
export const MINIAPP_STATUSES: readonly MiniappStatus[] = [
|
|
26
|
+
'detected',
|
|
27
|
+
'decrypting',
|
|
28
|
+
'unpacked',
|
|
29
|
+
'unsupported',
|
|
30
|
+
'failed'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export type MiniappFormat = 'plain' | 'v1mmwx' | 'unknown'
|
|
34
|
+
|
|
35
|
+
export type MiniappKind = 'app' | 'plugin' | 'library' | 'unknown'
|
|
36
|
+
|
|
37
|
+
/** One detected package group (all packages of one appid+version). */
|
|
38
|
+
export interface MiniappRow {
|
|
39
|
+
id: string
|
|
40
|
+
appid: string
|
|
41
|
+
app_name: string
|
|
42
|
+
kind?: MiniappKind
|
|
43
|
+
version: string
|
|
44
|
+
root_kind: string
|
|
45
|
+
package_dir: string
|
|
46
|
+
size_bytes: number
|
|
47
|
+
file_count: number
|
|
48
|
+
format: MiniappFormat | string
|
|
49
|
+
status: MiniappStatus
|
|
50
|
+
unpacked_path: string
|
|
51
|
+
error: string
|
|
52
|
+
detected_at: string
|
|
53
|
+
updated_at: string
|
|
54
|
+
version_count?: number
|
|
55
|
+
is_latest?: boolean
|
|
56
|
+
has_analysis?: boolean
|
|
57
|
+
/** Number of WeChat user-cache copies collapsed into this row. */
|
|
58
|
+
source_count?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MiniappListResponse {
|
|
62
|
+
miniapps: MiniappRow[]
|
|
63
|
+
total: number
|
|
64
|
+
appCount: number
|
|
65
|
+
customRoots: string[]
|
|
66
|
+
workspaceRoot: string
|
|
67
|
+
lastScanAt: string | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MiniappScanError {
|
|
71
|
+
root?: string
|
|
72
|
+
path?: string
|
|
73
|
+
message: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface MiniappScanRoot {
|
|
77
|
+
kind: string
|
|
78
|
+
path: string
|
|
79
|
+
exists: boolean
|
|
80
|
+
filesFound?: number
|
|
81
|
+
errors?: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface MiniappScanResponse {
|
|
85
|
+
ok: boolean
|
|
86
|
+
scannedAt: string
|
|
87
|
+
totalSources: number
|
|
88
|
+
totalPackages: number
|
|
89
|
+
added: number
|
|
90
|
+
updated: number
|
|
91
|
+
unchanged: number
|
|
92
|
+
errors: MiniappScanError[]
|
|
93
|
+
roots?: MiniappScanRoot[]
|
|
94
|
+
appCount?: number
|
|
95
|
+
miniapps: MiniappRow[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MiniappRoot {
|
|
99
|
+
kind: string
|
|
100
|
+
path: string
|
|
101
|
+
label: string
|
|
102
|
+
exists: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface MiniappRootsResponse {
|
|
106
|
+
roots: MiniappRoot[]
|
|
107
|
+
customRoots: string[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MiniappVersion {
|
|
111
|
+
id: string
|
|
112
|
+
version: string
|
|
113
|
+
status: MiniappStatus
|
|
114
|
+
format: string
|
|
115
|
+
size_bytes: number
|
|
116
|
+
file_count: number
|
|
117
|
+
updated_at: string
|
|
118
|
+
unpacked_path: string
|
|
119
|
+
source_count?: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface MiniappManifestTotals {
|
|
123
|
+
packages?: number
|
|
124
|
+
files?: number
|
|
125
|
+
bytes?: number
|
|
126
|
+
skipped?: number
|
|
127
|
+
conflicts?: number
|
|
128
|
+
unsupportedPackages?: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MiniappManifest {
|
|
132
|
+
appid?: string
|
|
133
|
+
version?: string
|
|
134
|
+
generatedAt?: string
|
|
135
|
+
packages?: Array<{ file: string; size: number; format: string; entries: number; bytes: number }>
|
|
136
|
+
totals?: MiniappManifestTotals
|
|
137
|
+
files?: Array<{ path: string; size: number }>
|
|
138
|
+
conflicts?: Array<{ path: string; keptFrom: string; ignoredFrom: string }>
|
|
139
|
+
error?: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface MiniappFileStats {
|
|
143
|
+
total: number
|
|
144
|
+
text: number
|
|
145
|
+
binary: number
|
|
146
|
+
bytes: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface MiniappAnalysis {
|
|
150
|
+
appid: string
|
|
151
|
+
version: string
|
|
152
|
+
generatedAt: string
|
|
153
|
+
files: MiniappFileStats
|
|
154
|
+
byExtension: Array<{ ext: string; count: number; bytes: number }>
|
|
155
|
+
pages: string[]
|
|
156
|
+
subPackages: Array<{ root: string; pages: string[] }>
|
|
157
|
+
domains: Array<{ host: string; count: number; examples: string[]; files: string[] }>
|
|
158
|
+
endpoints: Array<{ path: string; method?: string; count: number; files: string[] }>
|
|
159
|
+
secrets: Array<{
|
|
160
|
+
info_type: string
|
|
161
|
+
name: string
|
|
162
|
+
value_masked: string
|
|
163
|
+
value?: string
|
|
164
|
+
source: string
|
|
165
|
+
line: number
|
|
166
|
+
potential_impact?: string
|
|
167
|
+
}>
|
|
168
|
+
parameters: Array<{
|
|
169
|
+
name: string
|
|
170
|
+
endpoint: string
|
|
171
|
+
location: string
|
|
172
|
+
values: string[]
|
|
173
|
+
pattern?: string
|
|
174
|
+
source: string
|
|
175
|
+
}>
|
|
176
|
+
largestFiles: Array<{ path: string; size: number }>
|
|
177
|
+
warnings: string[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface MiniappExtractedCounts {
|
|
181
|
+
domains: number
|
|
182
|
+
endpoints: number
|
|
183
|
+
secrets: number
|
|
184
|
+
parameters: number
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MiniappDetailResponse {
|
|
188
|
+
miniapp: MiniappRow & { versions: MiniappVersion[] }
|
|
189
|
+
manifest: MiniappManifest | null
|
|
190
|
+
analysis: MiniappAnalysis | null
|
|
191
|
+
overviewText: string
|
|
192
|
+
extractedCounts: MiniappExtractedCounts
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface MiniappUnpackTotals {
|
|
196
|
+
packages?: number
|
|
197
|
+
files?: number
|
|
198
|
+
bytes?: number
|
|
199
|
+
skipped?: number
|
|
200
|
+
conflicts?: number
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface MiniappUnpackResponse {
|
|
204
|
+
ok: boolean
|
|
205
|
+
status: MiniappStatus
|
|
206
|
+
miniapp: MiniappRow
|
|
207
|
+
manifest: MiniappManifest | null
|
|
208
|
+
totals: MiniappUnpackTotals
|
|
209
|
+
error: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface MiniappAnalyzeResponse {
|
|
213
|
+
ok: boolean
|
|
214
|
+
analysis: MiniappAnalysis | null
|
|
215
|
+
overviewText: string
|
|
216
|
+
counts: MiniappExtractedCounts
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface MiniappTreeEntry {
|
|
220
|
+
name: string
|
|
221
|
+
path: string
|
|
222
|
+
type: 'file' | 'dir'
|
|
223
|
+
size: number
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface MiniappTreeResponse {
|
|
227
|
+
path: string
|
|
228
|
+
entries: MiniappTreeEntry[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface MiniappFileMatch {
|
|
232
|
+
line: number
|
|
233
|
+
text: string
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface MiniappFileResponse {
|
|
237
|
+
path: string
|
|
238
|
+
content: string
|
|
239
|
+
offset: number
|
|
240
|
+
limit: number
|
|
241
|
+
loadedLines?: number
|
|
242
|
+
totalLines: number
|
|
243
|
+
hasMore?: boolean
|
|
244
|
+
truncated: boolean
|
|
245
|
+
sourceTruncated?: boolean
|
|
246
|
+
matches?: MiniappFileMatch[]
|
|
247
|
+
matchCount?: number
|
|
248
|
+
matchTruncated?: boolean
|
|
249
|
+
size: number
|
|
250
|
+
binary: boolean
|
|
251
|
+
formatted?: boolean
|
|
252
|
+
language?: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface MiniappSearchMatch {
|
|
256
|
+
file: string
|
|
257
|
+
line: number
|
|
258
|
+
text: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface MiniappSearchResponse {
|
|
262
|
+
matches: MiniappSearchMatch[]
|
|
263
|
+
scanned: number
|
|
264
|
+
truncated: boolean
|
|
265
|
+
query: string
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface MiniappImportAsset {
|
|
269
|
+
host_port: string
|
|
270
|
+
created: boolean
|
|
271
|
+
interfaces: number
|
|
272
|
+
leaks: number
|
|
273
|
+
parameters: number
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface MiniappImportResponse {
|
|
277
|
+
ok: boolean
|
|
278
|
+
project: string
|
|
279
|
+
assets: MiniappImportAsset[]
|
|
280
|
+
totals: { assets: number; interfaces: number; leaks: number; parameters: number }
|
|
281
|
+
warnings: string[]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface MiniappOpenResponse {
|
|
285
|
+
ok: boolean
|
|
286
|
+
path: string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface MiniappListParams {
|
|
290
|
+
search?: string
|
|
291
|
+
status?: MiniappStatus | ''
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function toQuery(query: Record<string, unknown> | undefined): string {
|
|
295
|
+
if (!query) return ''
|
|
296
|
+
const params = new URLSearchParams()
|
|
297
|
+
for (const key of Object.keys(query)) {
|
|
298
|
+
const value = query[key]
|
|
299
|
+
if (value === undefined || value === null || value === '') continue
|
|
300
|
+
params.set(key, String(value))
|
|
301
|
+
}
|
|
302
|
+
const text = params.toString()
|
|
303
|
+
return text ? `?${text}` : ''
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function request<T>(path: string, init?: { method?: string; json?: unknown }): Promise<T> {
|
|
307
|
+
const method = (init && init.method) || 'GET'
|
|
308
|
+
const options: RequestInit = {
|
|
309
|
+
method,
|
|
310
|
+
credentials: 'same-origin',
|
|
311
|
+
cache: 'no-store',
|
|
312
|
+
headers: { Accept: 'application/json' }
|
|
313
|
+
}
|
|
314
|
+
if (init && init.json !== undefined) {
|
|
315
|
+
options.body = JSON.stringify(init.json)
|
|
316
|
+
;(options.headers as Record<string, string>)['Content-Type'] = 'application/json'
|
|
317
|
+
}
|
|
318
|
+
const response = await fetch(BASE + path, options)
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
let payload: {
|
|
321
|
+
error?: string
|
|
322
|
+
message?: string
|
|
323
|
+
code?: string
|
|
324
|
+
details?: { code?: string }
|
|
325
|
+
} | null = null
|
|
326
|
+
try {
|
|
327
|
+
payload = await response.json()
|
|
328
|
+
} catch {
|
|
329
|
+
/* non-JSON error */
|
|
330
|
+
}
|
|
331
|
+
// The host envelope uses a generic `BAD_REQUEST` code and puts the specific
|
|
332
|
+
// reason (e.g. `BINARY_FILE`) in `details.code`; prefer the specific one so
|
|
333
|
+
// callers can branch on it without parsing messages.
|
|
334
|
+
const specific = payload?.details?.code
|
|
335
|
+
throw new MiniappApiError(
|
|
336
|
+
String(payload?.error || payload?.message || `HTTP ${response.status}`),
|
|
337
|
+
String(specific || payload?.code || `HTTP_${response.status}`)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
if (response.status === 204) return undefined as T
|
|
341
|
+
const text = await response.text()
|
|
342
|
+
if (!text) return undefined as T
|
|
343
|
+
return JSON.parse(text) as T
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const encodeId = (id: string): string => encodeURIComponent(id)
|
|
347
|
+
|
|
348
|
+
export function listMiniapps(params: MiniappListParams = {}): Promise<MiniappListResponse> {
|
|
349
|
+
return request<MiniappListResponse>(`/miniapps${toQuery({ search: params.search, status: params.status })}`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function scanMiniapps(customRoots: string[] = []): Promise<MiniappScanResponse> {
|
|
353
|
+
return request<MiniappScanResponse>('/miniapps/scan', { method: 'POST', json: { customRoots } })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function listMiniappRoots(): Promise<MiniappRootsResponse> {
|
|
357
|
+
return request<MiniappRootsResponse>('/miniapps/roots')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function setMiniappRoots(
|
|
361
|
+
roots: string[],
|
|
362
|
+
mode: 'replace' | 'append' = 'append'
|
|
363
|
+
): Promise<MiniappRootsResponse> {
|
|
364
|
+
return request<MiniappRootsResponse>('/miniapps/roots', { method: 'POST', json: { roots, mode } })
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getMiniapp(id: string): Promise<MiniappDetailResponse> {
|
|
368
|
+
return request<MiniappDetailResponse>(`/miniapps/${encodeId(id)}`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function unpackMiniapp(id: string): Promise<MiniappUnpackResponse> {
|
|
372
|
+
return request<MiniappUnpackResponse>(`/miniapps/${encodeId(id)}/unpack`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
json: {}
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function analyzeMiniapp(id: string): Promise<MiniappAnalyzeResponse> {
|
|
379
|
+
return request<MiniappAnalyzeResponse>(`/miniapps/${encodeId(id)}/analyze`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
json: {}
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function getMiniappTree(id: string, path = ''): Promise<MiniappTreeResponse> {
|
|
386
|
+
return request<MiniappTreeResponse>(`/miniapps/${encodeId(id)}/tree${toQuery({ path })}`)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function readMiniappFile(
|
|
390
|
+
id: string,
|
|
391
|
+
path: string,
|
|
392
|
+
offset = 0,
|
|
393
|
+
limit = 2000,
|
|
394
|
+
maxBytes = 1024 * 1024,
|
|
395
|
+
find = '',
|
|
396
|
+
findRegex = false
|
|
397
|
+
): Promise<MiniappFileResponse> {
|
|
398
|
+
return request<MiniappFileResponse>(
|
|
399
|
+
`/miniapps/${encodeId(id)}/file${toQuery({
|
|
400
|
+
path,
|
|
401
|
+
offset,
|
|
402
|
+
limit,
|
|
403
|
+
maxBytes,
|
|
404
|
+
find,
|
|
405
|
+
findRegex: findRegex ? 1 : 0
|
|
406
|
+
})}`
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface MiniappSearchParams {
|
|
411
|
+
query: string
|
|
412
|
+
regex?: boolean
|
|
413
|
+
glob?: string
|
|
414
|
+
maxResults?: number
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function searchMiniappFiles(
|
|
418
|
+
id: string,
|
|
419
|
+
params: MiniappSearchParams
|
|
420
|
+
): Promise<MiniappSearchResponse> {
|
|
421
|
+
return request<MiniappSearchResponse>(
|
|
422
|
+
`/miniapps/${encodeId(id)}/search${toQuery({
|
|
423
|
+
query: params.query,
|
|
424
|
+
regex: params.regex ? 1 : 0,
|
|
425
|
+
glob: params.glob,
|
|
426
|
+
maxResults: params.maxResults
|
|
427
|
+
})}`
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function importMiniappAssets(
|
|
432
|
+
id: string,
|
|
433
|
+
input: { project_name?: string; host_port?: string } = {}
|
|
434
|
+
): Promise<MiniappImportResponse> {
|
|
435
|
+
return request<MiniappImportResponse>(`/miniapps/${encodeId(id)}/import-assets`, {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
json: input
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function openMiniappDir(id: string): Promise<MiniappOpenResponse> {
|
|
442
|
+
return request<MiniappOpenResponse>(`/miniapps/${encodeId(id)}/open`, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
json: {}
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function renameMiniapp(id: string, appName: string): Promise<{ ok: boolean; miniapp: MiniappRow }> {
|
|
449
|
+
return request<{ ok: boolean; miniapp: MiniappRow }>(`/miniapps/${encodeId(id)}/name`, {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
json: { app_name: appName }
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function toMiniappErrorMessage(error: unknown): string {
|
|
456
|
+
if (error instanceof Error) return error.message
|
|
457
|
+
if (typeof error === 'string') return error
|
|
458
|
+
return String(error)
|
|
459
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bot, Boxes, Brain, FlaskConical, Network, Orbit, ShieldAlert, type LucideIcon } from 'lucide-react'
|
|
1
|
+
import { Bot, Boxes, Brain, FlaskConical, Network, Orbit, PackageSearch, ShieldAlert, Zap, type LucideIcon } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
export type AppPage =
|
|
4
4
|
| 'dashboard'
|
|
@@ -8,6 +8,8 @@ export type AppPage =
|
|
|
8
8
|
| 'snapshots'
|
|
9
9
|
| 'mcp'
|
|
10
10
|
| 'retest'
|
|
11
|
+
| 'fuzzer'
|
|
12
|
+
| 'miniapps'
|
|
11
13
|
| 'ai-config'
|
|
12
14
|
|
|
13
15
|
/** Cross-page deep link used by the nebula inspector ("view full details"). */
|
|
@@ -37,6 +39,8 @@ export const NAV_ITEMS: readonly NavItem[] = [
|
|
|
37
39
|
{ page: 'assets', labelKey: 'nav.assets', icon: Boxes },
|
|
38
40
|
{ page: 'snapshots', labelKey: 'nav.snapshots', icon: Brain },
|
|
39
41
|
{ page: 'retest', labelKey: 'nav.retest', icon: FlaskConical },
|
|
42
|
+
{ page: 'fuzzer', labelKey: 'nav.fuzzer', icon: Zap },
|
|
43
|
+
{ page: 'miniapps', labelKey: 'nav.miniapps', icon: PackageSearch },
|
|
40
44
|
{ page: 'ai-config', labelKey: 'retest.aiConfig.nav', icon: Bot }
|
|
41
45
|
]
|
|
42
46
|
|
|
@@ -65,6 +69,14 @@ export const PAGE_META: Record<AppPage, { titleKey: string; descriptionKey: stri
|
|
|
65
69
|
titleKey: 'nav.retest',
|
|
66
70
|
descriptionKey: 'retest.page.repeaterSubtitle'
|
|
67
71
|
},
|
|
72
|
+
fuzzer: {
|
|
73
|
+
titleKey: 'nav.fuzzer',
|
|
74
|
+
descriptionKey: 'fuzzer.page.description'
|
|
75
|
+
},
|
|
76
|
+
miniapps: {
|
|
77
|
+
titleKey: 'nav.miniapps',
|
|
78
|
+
descriptionKey: 'miniapp.page.description'
|
|
79
|
+
},
|
|
68
80
|
'ai-config': {
|
|
69
81
|
titleKey: 'retest.aiConfig.nav',
|
|
70
82
|
descriptionKey: 'retest.aiConfig.description'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw HTTP request helpers shared by the single Repeater and the Fuzzer.
|
|
3
|
+
*
|
|
4
|
+
* The request editor uses the Burp-style raw form:
|
|
5
|
+
*
|
|
6
|
+
* POST /api/login HTTP/1.1
|
|
7
|
+
* Host: target
|
|
8
|
+
* Content-Type: application/json
|
|
9
|
+
*
|
|
10
|
+
* {"user":"demo"}
|
|
11
|
+
*
|
|
12
|
+
* Fuzzer injection points are marked inline with paired `§` characters, e.g.
|
|
13
|
+
* `id=§1§`. Nothing in this module talks to the network.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ParsedRawRequest {
|
|
17
|
+
method: string
|
|
18
|
+
url: string
|
|
19
|
+
headers: Record<string, string>
|
|
20
|
+
body: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Protocol-level headers copied from a report are re-derived by the outbound
|
|
25
|
+
* request (Host from the URL, length/connection by the HTTP client). Dropping
|
|
26
|
+
* them here keeps raw requests sendable without a validation error.
|
|
27
|
+
*/
|
|
28
|
+
export const DERIVED_HEADERS = new Set([
|
|
29
|
+
'host',
|
|
30
|
+
'content-length',
|
|
31
|
+
'connection',
|
|
32
|
+
'transfer-encoding',
|
|
33
|
+
'upgrade',
|
|
34
|
+
'keep-alive',
|
|
35
|
+
'proxy-connection'
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/** Parse a Burp-like raw request into the typed repeater payload. */
|
|
39
|
+
export function parseRawRequest(text: string): ParsedRawRequest {
|
|
40
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
41
|
+
const requestLine = (lines.shift() ?? '').trim()
|
|
42
|
+
const match = /^([A-Za-z]+)\s+(\S+)\s+HTTP\/\d(?:\.\d)?$/.exec(requestLine)
|
|
43
|
+
if (match === null) throw new Error('请求行必须形如:GET /path HTTP/1.1')
|
|
44
|
+
const headers: Record<string, string> = {}
|
|
45
|
+
let index = 0
|
|
46
|
+
for (; index < lines.length; index += 1) {
|
|
47
|
+
const line = lines[index]
|
|
48
|
+
if (line === undefined) break
|
|
49
|
+
if (line.trim() === '') {
|
|
50
|
+
index += 1
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
const colon = line.indexOf(':')
|
|
54
|
+
if (colon <= 0) throw new Error(`请求头不合法:${line}`)
|
|
55
|
+
const name = line.slice(0, colon).trim()
|
|
56
|
+
const value = line.slice(colon + 1).trim()
|
|
57
|
+
if (name !== '') headers[name] = value
|
|
58
|
+
}
|
|
59
|
+
const body = lines.slice(index).join('\n')
|
|
60
|
+
return { method: match[1].toUpperCase(), url: match[2], headers, body }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Serialize the typed payload back into the raw editor form. */
|
|
64
|
+
export function serializeRawRequest(request: {
|
|
65
|
+
method: string
|
|
66
|
+
url: string
|
|
67
|
+
headers: Record<string, string>
|
|
68
|
+
body?: string
|
|
69
|
+
}): string {
|
|
70
|
+
const lines = [`${request.method.toUpperCase()} ${request.url} HTTP/1.1`]
|
|
71
|
+
for (const [name, value] of Object.entries(request.headers)) lines.push(`${name}: ${value}`)
|
|
72
|
+
const body = request.body ?? ''
|
|
73
|
+
if (body !== '') lines.push('', body)
|
|
74
|
+
return lines.join('\n')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Copy a header map without the protocol-level headers the Host re-derives. */
|
|
78
|
+
export function stripDerivedHeaders(headers: Record<string, string>): Record<string, string> {
|
|
79
|
+
return Object.fromEntries(
|
|
80
|
+
Object.entries(headers).filter(([name]) => !DERIVED_HEADERS.has(name.toLowerCase()))
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pretty-print a JSON request body while keeping the request line and headers
|
|
86
|
+
* untouched. Returns null when the request has no body or the body is not JSON.
|
|
87
|
+
*/
|
|
88
|
+
export function formatRequestBody(raw: string): string | null {
|
|
89
|
+
const text = raw.replace(/\r\n/g, '\n')
|
|
90
|
+
const separator = text.indexOf('\n\n')
|
|
91
|
+
if (separator < 0) return null
|
|
92
|
+
const head = text.slice(0, separator)
|
|
93
|
+
const body = text.slice(separator + 2).trim()
|
|
94
|
+
if (body === '') return null
|
|
95
|
+
let parsed: unknown
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(body)
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
return `${head}\n\n${JSON.stringify(parsed, null, 2)}`
|
|
102
|
+
}
|