@lievclouds/dsh-cortex 0.5.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 +434 -412
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -110
- 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/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 +291 -137
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- 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 +144 -2
- 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 -17
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/host/cortex/index.js +42 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/tools.js +2 -0
- 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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { ArrowRight, Braces, CornerDownRight, Loader2, Play } from 'lucide-react'
|
|
3
|
+
import type { Vulnerability } from '@shared/types'
|
|
4
|
+
import type { RepeaterSendResult } from '@shared/repeater'
|
|
5
|
+
import { Button } from '@renderer/components/ui/button'
|
|
6
|
+
import { Checkbox } from '@renderer/components/ui/checkbox'
|
|
7
|
+
import { Skeleton } from '@renderer/components/ui/skeleton'
|
|
8
|
+
import { useI18n } from '@renderer/i18n'
|
|
9
|
+
import { getApi } from '@renderer/lib/api'
|
|
10
|
+
import {
|
|
11
|
+
formatRequestBody,
|
|
12
|
+
parseRawRequest,
|
|
13
|
+
stripDerivedHeaders
|
|
14
|
+
} from '@renderer/lib/repeaterRaw'
|
|
15
|
+
import {
|
|
16
|
+
defaultRequestIndex,
|
|
17
|
+
extractRepeaterRequests,
|
|
18
|
+
resolveRepeaterUrl
|
|
19
|
+
} from '@renderer/lib/retestRequest'
|
|
20
|
+
import {
|
|
21
|
+
buildHtmlPreview,
|
|
22
|
+
headerValue,
|
|
23
|
+
isHtmlResponse,
|
|
24
|
+
isRedirectResponse,
|
|
25
|
+
prettyJsonBody,
|
|
26
|
+
statusTone
|
|
27
|
+
} from '@renderer/lib/retestResponse'
|
|
28
|
+
import { cn } from '@renderer/lib/utils'
|
|
29
|
+
|
|
30
|
+
interface YakitSendPanelProps {
|
|
31
|
+
vuln: Vulnerability | null
|
|
32
|
+
loading: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function describeSendError(cause: unknown, connectionRefused: string): string {
|
|
36
|
+
const raw = cause instanceof Error ? cause.message : String(cause)
|
|
37
|
+
const cleaned = raw
|
|
38
|
+
.replace(/^Error invoking remote method '[^']+':\s*/, '')
|
|
39
|
+
.replace(/^Error:\s*/, '')
|
|
40
|
+
.trim()
|
|
41
|
+
if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i.test(cleaned)) return connectionRefused
|
|
42
|
+
return cleaned === '' ? connectionRefused : cleaned
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Yakit-style manual HTTP sender: the request editor fills the left half with
|
|
47
|
+
* the send button at its top-left, the response viewer fills the right half, and
|
|
48
|
+
* both panes use the full available height. This is the plain Web Fuzzer module
|
|
49
|
+
* (no payload markers, attack modes, batch jobs or result table); every request
|
|
50
|
+
* still goes through the retest proxy and the finding origin allowlist.
|
|
51
|
+
*/
|
|
52
|
+
export function YakitSendPanel({ vuln, loading }: YakitSendPanelProps) {
|
|
53
|
+
const { t } = useI18n()
|
|
54
|
+
const [raw, setRaw] = useState('')
|
|
55
|
+
const [step, setStep] = useState(0)
|
|
56
|
+
const [sending, setSending] = useState(false)
|
|
57
|
+
const [followRedirects, setFollowRedirects] = useState(false)
|
|
58
|
+
const [bodyView, setBodyView] = useState<'raw' | 'pretty' | 'html'>('raw')
|
|
59
|
+
const [error, setError] = useState<string | null>(null)
|
|
60
|
+
const [result, setResult] = useState<RepeaterSendResult | null>(null)
|
|
61
|
+
|
|
62
|
+
const requests = useMemo(() => (vuln === null ? [] : extractRepeaterRequests(vuln)), [vuln])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setResult(null)
|
|
66
|
+
setError(null)
|
|
67
|
+
if (vuln === null) {
|
|
68
|
+
setRaw('')
|
|
69
|
+
setStep(0)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const nextStep = defaultRequestIndex(vuln, requests)
|
|
73
|
+
setStep(nextStep)
|
|
74
|
+
const initial = requests[nextStep]?.raw ?? ''
|
|
75
|
+
setRaw(formatRequestBody(initial) ?? initial)
|
|
76
|
+
// Reset only when the selected vulnerability changes; user edits must survive.
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [vuln?.id])
|
|
79
|
+
|
|
80
|
+
const selectStep = (nextStep: number): void => {
|
|
81
|
+
const next = requests[nextStep]
|
|
82
|
+
if (next === undefined) return
|
|
83
|
+
setStep(nextStep)
|
|
84
|
+
setRaw(formatRequestBody(next.raw) ?? next.raw)
|
|
85
|
+
setResult(null)
|
|
86
|
+
setError(null)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Pretty-print the JSON request body while keeping the request line/headers. */
|
|
90
|
+
const formatRequest = (): void => {
|
|
91
|
+
const formatted = formatRequestBody(raw)
|
|
92
|
+
if (formatted === null) {
|
|
93
|
+
setError(t('retest.repeater.formatRequestFailed'))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
setRaw(formatted)
|
|
97
|
+
setError(null)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rawBytes = useMemo(() => new TextEncoder().encode(raw).byteLength, [raw])
|
|
101
|
+
const responseIsRedirect =
|
|
102
|
+
result !== null && isRedirectResponse(result.response.status, result.response.headers)
|
|
103
|
+
const responseIsHtml =
|
|
104
|
+
result !== null && isHtmlResponse(result.response.body, result.response.headers)
|
|
105
|
+
const formattedJson = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
result === null
|
|
108
|
+
? null
|
|
109
|
+
: prettyJsonBody(result.response.body, headerValue(result.response.headers, 'content-type')),
|
|
110
|
+
[result]
|
|
111
|
+
)
|
|
112
|
+
const htmlPreview = useMemo(
|
|
113
|
+
() =>
|
|
114
|
+
result === null || vuln === null
|
|
115
|
+
? ''
|
|
116
|
+
: buildHtmlPreview(result.response.body, vuln.target_url),
|
|
117
|
+
[result, vuln]
|
|
118
|
+
)
|
|
119
|
+
/** Status line + response headers, shared by the raw and formatted views. */
|
|
120
|
+
const responseHead = useMemo(() => {
|
|
121
|
+
if (result === null) return ''
|
|
122
|
+
const lines = [`HTTP/1.1 ${result.response.status}`]
|
|
123
|
+
for (const [name, value] of Object.entries(result.response.headers)) {
|
|
124
|
+
lines.push(`${name}: ${value}`)
|
|
125
|
+
}
|
|
126
|
+
return lines.join('\n')
|
|
127
|
+
}, [result])
|
|
128
|
+
/** Raw response as one block: status + headers + blank line + body. */
|
|
129
|
+
const rawResponse = useMemo(
|
|
130
|
+
() => (result === null ? '' : `${responseHead}\n\n${result.response.body}`),
|
|
131
|
+
[result, responseHead]
|
|
132
|
+
)
|
|
133
|
+
/** JSON view keeps the status/header block and only formats the body. */
|
|
134
|
+
const jsonResponse = useMemo(
|
|
135
|
+
() => (formattedJson === null ? '' : `${responseHead}\n\n${formattedJson}`),
|
|
136
|
+
[formattedJson, responseHead]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const send = async (): Promise<void> => {
|
|
140
|
+
if (vuln === null || sending) return
|
|
141
|
+
const api = getApi()
|
|
142
|
+
if (!api) {
|
|
143
|
+
setError(t('common.apiUnavailable'))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
setSending(true)
|
|
147
|
+
setError(null)
|
|
148
|
+
try {
|
|
149
|
+
const parsed = parseRawRequest(raw)
|
|
150
|
+
const response = await api.sendRepeaterRequest({
|
|
151
|
+
vulnId: vuln.id,
|
|
152
|
+
method: parsed.method,
|
|
153
|
+
url: resolveRepeaterUrl(parsed.url, vuln.target_url),
|
|
154
|
+
headers: stripDerivedHeaders(parsed.headers),
|
|
155
|
+
...(parsed.body === '' ? {} : { body: parsed.body }),
|
|
156
|
+
followRedirects
|
|
157
|
+
})
|
|
158
|
+
setResult(response)
|
|
159
|
+
// Formatted JSON is the default response view; raw stays one click away.
|
|
160
|
+
setBodyView(
|
|
161
|
+
prettyJsonBody(response.response.body, headerValue(response.response.headers, 'content-type')) !== null
|
|
162
|
+
? 'pretty'
|
|
163
|
+
: 'raw'
|
|
164
|
+
)
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
setError(describeSendError(cause, t('retest.repeater.connectionFailed')))
|
|
167
|
+
setResult(null)
|
|
168
|
+
} finally {
|
|
169
|
+
setSending(false)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (loading && vuln === null) {
|
|
174
|
+
return (
|
|
175
|
+
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-2">
|
|
176
|
+
<Skeleton className="h-full min-h-[360px] w-full" />
|
|
177
|
+
<Skeleton className="h-full min-h-[360px] w-full" />
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (vuln === null) {
|
|
183
|
+
return (
|
|
184
|
+
<div className="flex min-h-0 flex-1 items-center justify-center rounded-lg border border-dashed border-border bg-background/60 text-[12px] text-muted-foreground">
|
|
185
|
+
{t('retest.repeater.empty')}
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
data-testid="fuzzer-panel"
|
|
193
|
+
className="grid min-h-0 flex-1 items-stretch gap-3 lg:grid-cols-2"
|
|
194
|
+
>
|
|
195
|
+
{/* ------------------------------------------------------------- request */}
|
|
196
|
+
<section
|
|
197
|
+
data-testid="fuzzer-request-pane"
|
|
198
|
+
aria-label={t('fuzzer.request.paneAria')}
|
|
199
|
+
className="flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-background"
|
|
200
|
+
>
|
|
201
|
+
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
|
|
202
|
+
<Button
|
|
203
|
+
size="sm"
|
|
204
|
+
data-testid="fuzzer-send"
|
|
205
|
+
onClick={() => void send()}
|
|
206
|
+
disabled={sending || raw.trim() === ''}
|
|
207
|
+
className="gap-1.5"
|
|
208
|
+
>
|
|
209
|
+
{sending ? (
|
|
210
|
+
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin poc-spinner" />
|
|
211
|
+
) : (
|
|
212
|
+
<Play aria-hidden="true" className="h-3.5 w-3.5" />
|
|
213
|
+
)}
|
|
214
|
+
{sending ? t('retest.repeater.sending') : t('retest.repeater.send')}
|
|
215
|
+
</Button>
|
|
216
|
+
<label className="flex shrink-0 cursor-pointer items-center gap-1.5 text-[10.5px] text-muted-foreground">
|
|
217
|
+
<Checkbox
|
|
218
|
+
data-testid="fuzzer-follow-redirects"
|
|
219
|
+
checked={followRedirects}
|
|
220
|
+
onChange={(event) => setFollowRedirects(event.target.checked)}
|
|
221
|
+
/>
|
|
222
|
+
{t('retest.repeater.followRedirects')}
|
|
223
|
+
</label>
|
|
224
|
+
<Button
|
|
225
|
+
variant="outline"
|
|
226
|
+
size="xs"
|
|
227
|
+
data-testid="fuzzer-format-request"
|
|
228
|
+
title={t('retest.repeater.formatRequestTitle')}
|
|
229
|
+
onClick={formatRequest}
|
|
230
|
+
className="shrink-0 gap-1"
|
|
231
|
+
>
|
|
232
|
+
<Braces aria-hidden="true" className="h-3 w-3" />
|
|
233
|
+
{t('retest.repeater.formatRequest')}
|
|
234
|
+
</Button>
|
|
235
|
+
<span className="shrink-0 rounded border border-border bg-sidebar px-1.5 py-0.5 font-mono text-[9.5px] text-muted-foreground">
|
|
236
|
+
{vuln.id}
|
|
237
|
+
</span>
|
|
238
|
+
{requests.length > 0 ? (
|
|
239
|
+
<div
|
|
240
|
+
className="ml-auto flex min-w-0 items-center gap-1 overflow-x-auto"
|
|
241
|
+
role="tablist"
|
|
242
|
+
aria-label={t('retest.repeater.stepsAria')}
|
|
243
|
+
>
|
|
244
|
+
{requests.map((request, index) => (
|
|
245
|
+
<button
|
|
246
|
+
key={`${index}-${request.label}`}
|
|
247
|
+
type="button"
|
|
248
|
+
role="tab"
|
|
249
|
+
aria-selected={index === step}
|
|
250
|
+
title={request.label}
|
|
251
|
+
data-testid={`fuzzer-request-step-${index}`}
|
|
252
|
+
onClick={() => selectStep(index)}
|
|
253
|
+
className={cn(
|
|
254
|
+
'h-6 shrink-0 rounded border px-2 font-mono text-[10px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
255
|
+
index === step
|
|
256
|
+
? 'border-accent/50 bg-accent-soft font-medium text-accent'
|
|
257
|
+
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
{t('retest.repeater.stepN', { n: index + 1 })}
|
|
261
|
+
</button>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
) : null}
|
|
265
|
+
<span className={cn('shrink-0 font-mono text-[9.5px] tabular-nums text-muted-foreground', requests.length === 0 && 'ml-auto')}>
|
|
266
|
+
{t('retest.repeater.bytes', { count: rawBytes })}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div className="flex min-h-0 flex-1 flex-col p-3">
|
|
271
|
+
<textarea
|
|
272
|
+
data-testid="fuzzer-request"
|
|
273
|
+
aria-label={t('retest.repeater.requestAria')}
|
|
274
|
+
value={raw}
|
|
275
|
+
spellCheck={false}
|
|
276
|
+
onChange={(event) => setRaw(event.target.value)}
|
|
277
|
+
className="min-h-0 w-full flex-1 resize-none rounded-lg border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground caret-accent placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
278
|
+
/>
|
|
279
|
+
{error !== null ? (
|
|
280
|
+
<p
|
|
281
|
+
role="alert"
|
|
282
|
+
data-testid="fuzzer-send-error"
|
|
283
|
+
className="mt-2 rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-[10.5px] leading-relaxed text-red-700"
|
|
284
|
+
>
|
|
285
|
+
{error}
|
|
286
|
+
</p>
|
|
287
|
+
) : null}
|
|
288
|
+
</div>
|
|
289
|
+
</section>
|
|
290
|
+
|
|
291
|
+
{/* ------------------------------------------------------------ response */}
|
|
292
|
+
<section
|
|
293
|
+
data-testid="fuzzer-response-pane"
|
|
294
|
+
aria-label={t('fuzzer.response.aria')}
|
|
295
|
+
className="flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-background"
|
|
296
|
+
>
|
|
297
|
+
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
|
|
298
|
+
<ArrowRight aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
299
|
+
<span className="shrink-0 text-[12px] font-medium text-foreground">{t('retest.repeater.responseTitle')}</span>
|
|
300
|
+
{result !== null ? (
|
|
301
|
+
<>
|
|
302
|
+
<span
|
|
303
|
+
data-testid="fuzzer-response-status"
|
|
304
|
+
className={`rounded border px-1.5 py-0.5 font-mono text-[10px] tabular-nums ${statusTone(result.response.status)}`}
|
|
305
|
+
>
|
|
306
|
+
{result.response.status}
|
|
307
|
+
</span>
|
|
308
|
+
<span className="font-mono text-[9.5px] tabular-nums text-muted-foreground">
|
|
309
|
+
{result.response.durationMs} ms · {result.response.body.length} B
|
|
310
|
+
</span>
|
|
311
|
+
{result.response.truncated ? (
|
|
312
|
+
<span className="text-[9.5px] text-amber-700">{t('retest.repeater.truncated')}</span>
|
|
313
|
+
) : null}
|
|
314
|
+
{responseIsRedirect ? (
|
|
315
|
+
<Button
|
|
316
|
+
variant="outline"
|
|
317
|
+
size="xs"
|
|
318
|
+
onClick={() => void send()}
|
|
319
|
+
disabled={sending}
|
|
320
|
+
className="gap-1 text-[10.5px]"
|
|
321
|
+
>
|
|
322
|
+
<CornerDownRight aria-hidden="true" className="h-3 w-3" />
|
|
323
|
+
{t('retest.repeater.followRedirect')}
|
|
324
|
+
</Button>
|
|
325
|
+
) : null}
|
|
326
|
+
{formattedJson !== null || responseIsHtml ? (
|
|
327
|
+
<div className="ml-auto flex shrink-0 items-center gap-0.5 rounded-md border border-border bg-muted p-0.5">
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
data-testid="fuzzer-view-raw"
|
|
331
|
+
onClick={() => setBodyView('raw')}
|
|
332
|
+
className={cn(
|
|
333
|
+
'rounded px-1.5 py-0.5 text-[10px] transition-colors',
|
|
334
|
+
bodyView === 'raw'
|
|
335
|
+
? 'bg-background font-medium text-foreground shadow-sm'
|
|
336
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
339
|
+
{t('retest.repeater.viewRaw')}
|
|
340
|
+
</button>
|
|
341
|
+
{formattedJson !== null ? (
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
data-testid="fuzzer-view-json"
|
|
345
|
+
onClick={() => setBodyView('pretty')}
|
|
346
|
+
className={cn(
|
|
347
|
+
'rounded px-1.5 py-0.5 text-[10px] transition-colors',
|
|
348
|
+
bodyView === 'pretty'
|
|
349
|
+
? 'bg-background font-medium text-foreground shadow-sm'
|
|
350
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
{t('retest.repeater.formatJson')}
|
|
354
|
+
</button>
|
|
355
|
+
) : null}
|
|
356
|
+
{responseIsHtml ? (
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
data-testid="fuzzer-view-html"
|
|
360
|
+
onClick={() => setBodyView('html')}
|
|
361
|
+
className={cn(
|
|
362
|
+
'rounded px-1.5 py-0.5 text-[10px] transition-colors',
|
|
363
|
+
bodyView === 'html'
|
|
364
|
+
? 'bg-background font-medium text-foreground shadow-sm'
|
|
365
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
366
|
+
)}
|
|
367
|
+
>
|
|
368
|
+
{t('retest.repeater.viewHtml')}
|
|
369
|
+
</button>
|
|
370
|
+
) : null}
|
|
371
|
+
</div>
|
|
372
|
+
) : null}
|
|
373
|
+
</>
|
|
374
|
+
) : null}
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{result === null ? (
|
|
378
|
+
<p className="flex flex-1 items-center justify-center px-3 text-[11.5px] text-muted-foreground">
|
|
379
|
+
{t('retest.repeater.emptyResponse')}
|
|
380
|
+
</p>
|
|
381
|
+
) : (
|
|
382
|
+
<div className="flex min-h-0 flex-1 flex-col p-3">
|
|
383
|
+
{responseIsHtml && bodyView === 'html' ? (
|
|
384
|
+
<iframe
|
|
385
|
+
title={t('retest.repeater.htmlPreview')}
|
|
386
|
+
sandbox=""
|
|
387
|
+
referrerPolicy="no-referrer"
|
|
388
|
+
srcDoc={htmlPreview}
|
|
389
|
+
className="min-h-0 w-full flex-1 rounded-md border border-border bg-white"
|
|
390
|
+
/>
|
|
391
|
+
) : formattedJson !== null && bodyView === 'pretty' ? (
|
|
392
|
+
<pre
|
|
393
|
+
data-testid="fuzzer-response-json"
|
|
394
|
+
className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground"
|
|
395
|
+
>
|
|
396
|
+
{jsonResponse}
|
|
397
|
+
</pre>
|
|
398
|
+
) : (
|
|
399
|
+
<pre
|
|
400
|
+
data-testid="fuzzer-response-body"
|
|
401
|
+
className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground"
|
|
402
|
+
>
|
|
403
|
+
{rawResponse.slice(0, 200_000)}
|
|
404
|
+
</pre>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</section>
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
@@ -21,6 +21,7 @@ export const commonMessages: MessageBundle = {
|
|
|
21
21
|
'nav.snapshots': '认知快照',
|
|
22
22
|
'nav.mcp': 'MCP 接入',
|
|
23
23
|
'nav.retest': '漏洞复测',
|
|
24
|
+
'nav.fuzzer': 'Web Fuzzer',
|
|
24
25
|
'sidebar.databaseConnected': '数据库已连接',
|
|
25
26
|
'sidebar.databaseButton': '数据库',
|
|
26
27
|
'mcp.connect.button': '接入 Agent',
|
|
@@ -109,6 +110,7 @@ export const commonMessages: MessageBundle = {
|
|
|
109
110
|
'nav.snapshots': 'Snapshots',
|
|
110
111
|
'nav.mcp': 'MCP Access',
|
|
111
112
|
'nav.retest': 'Vulnerability Retest',
|
|
113
|
+
'nav.fuzzer': 'Web Fuzzer',
|
|
112
114
|
'sidebar.databaseConnected': 'Database connected',
|
|
113
115
|
'sidebar.databaseButton': 'Database',
|
|
114
116
|
'mcp.connect.button': 'Connect agents',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MessageBundle } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Web Fuzzer page chrome (manual Yakit-style sender).
|
|
5
|
+
*
|
|
6
|
+
* The request/response controls reuse the existing `retest.repeater.*` keys, so
|
|
7
|
+
* only the page-level target selector needs its own bundle.
|
|
8
|
+
*/
|
|
9
|
+
export const fuzzerMessages: MessageBundle = {
|
|
10
|
+
zh: {
|
|
11
|
+
'fuzzer.page.description':
|
|
12
|
+
'Yakit 风格的手动发包页面:左侧编辑请求、右侧查看响应;请求仍受目标漏洞 origin 白名单约束。',
|
|
13
|
+
'fuzzer.target.title': '目标漏洞',
|
|
14
|
+
'fuzzer.target.select': '选择目标漏洞',
|
|
15
|
+
'fuzzer.target.empty': '没有可用的 Web 漏洞',
|
|
16
|
+
'fuzzer.target.loadFailed': '无法加载 Web 漏洞:{error}',
|
|
17
|
+
'fuzzer.target.detailFailed': '无法加载漏洞详情',
|
|
18
|
+
'fuzzer.request.paneAria': '请求编辑与发送',
|
|
19
|
+
'fuzzer.response.aria': '响应查看面板'
|
|
20
|
+
},
|
|
21
|
+
en: {
|
|
22
|
+
'fuzzer.page.description':
|
|
23
|
+
'Yakit-style manual HTTP sender: edit the request on the left and inspect the response on the right. Every request is still constrained by the target finding origin allowlist.',
|
|
24
|
+
'fuzzer.target.title': 'Target finding',
|
|
25
|
+
'fuzzer.target.select': 'Select target finding',
|
|
26
|
+
'fuzzer.target.empty': 'No Web findings available',
|
|
27
|
+
'fuzzer.target.loadFailed': 'Cannot load Web findings: {error}',
|
|
28
|
+
'fuzzer.target.detailFailed': 'Cannot load the finding details',
|
|
29
|
+
'fuzzer.request.paneAria': 'Request editor and send controls',
|
|
30
|
+
'fuzzer.response.aria': 'Response viewer'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -2,8 +2,10 @@ import type { MessageBundle } from './types'
|
|
|
2
2
|
import { assetMessages } from './asset'
|
|
3
3
|
import { commonMessages } from './common'
|
|
4
4
|
import { dashboardMessages } from './dashboard'
|
|
5
|
+
import { fuzzerMessages } from './fuzzer'
|
|
5
6
|
import { internalMessages } from './internal'
|
|
6
7
|
import { mcpMessages } from './mcp'
|
|
8
|
+
import { miniappMessages } from './miniapp'
|
|
7
9
|
import { nebulaMessages } from './nebula'
|
|
8
10
|
import { retestMessages } from './retest'
|
|
9
11
|
import { snapshotMessages } from './snapshot'
|
|
@@ -22,7 +24,9 @@ const bundles: MessageBundle[] = [
|
|
|
22
24
|
snapshotMessages,
|
|
23
25
|
nebulaMessages,
|
|
24
26
|
mcpMessages,
|
|
25
|
-
retestMessages
|
|
27
|
+
retestMessages,
|
|
28
|
+
miniappMessages,
|
|
29
|
+
fuzzerMessages
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
function mergeBundles(lang: 'zh' | 'en'): Record<string, string> {
|