@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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the single-Repeater history / favorites Host routes.
|
|
3
|
+
*
|
|
4
|
+
* The routes live under the same `/cortex-api/cortex` surface but are not part
|
|
5
|
+
* of `CortexApi`; the dialogs use these plain same-origin fetches.
|
|
6
|
+
*/
|
|
7
|
+
import type { RepeaterSendResult } from '@shared/repeater'
|
|
8
|
+
|
|
9
|
+
const API_BASE = '/cortex-api/cortex'
|
|
10
|
+
|
|
11
|
+
export type RepeaterStoredRequest = RepeaterSendResult['request']
|
|
12
|
+
export type RepeaterStoredResponse = RepeaterSendResult['response']
|
|
13
|
+
|
|
14
|
+
export interface RepeaterHistoryEntry {
|
|
15
|
+
id: string
|
|
16
|
+
createdAt: string
|
|
17
|
+
vulnId: string
|
|
18
|
+
raw: string
|
|
19
|
+
request: RepeaterStoredRequest
|
|
20
|
+
response: RepeaterStoredResponse | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RepeaterCollectionEntry {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
createdAt: string
|
|
27
|
+
vulnId: string
|
|
28
|
+
raw: string
|
|
29
|
+
request: RepeaterStoredRequest
|
|
30
|
+
response: RepeaterStoredResponse | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SaveRepeaterCollectionInput {
|
|
34
|
+
name: string
|
|
35
|
+
vulnId: string
|
|
36
|
+
raw: string
|
|
37
|
+
request: RepeaterStoredRequest
|
|
38
|
+
response?: RepeaterStoredResponse | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ApiErrorPayload {
|
|
42
|
+
error?: unknown
|
|
43
|
+
message?: unknown
|
|
44
|
+
code?: unknown
|
|
45
|
+
details?: unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Stable client error: `code` mirrors the Host error envelope when present. */
|
|
49
|
+
export class RepeaterStoreApiError extends Error {
|
|
50
|
+
readonly code: string
|
|
51
|
+
readonly status: number
|
|
52
|
+
readonly details: unknown
|
|
53
|
+
|
|
54
|
+
constructor(code: string, message: string, status: number, details?: unknown) {
|
|
55
|
+
super(message)
|
|
56
|
+
this.name = 'RepeaterStoreApiError'
|
|
57
|
+
this.code = code
|
|
58
|
+
this.status = status
|
|
59
|
+
this.details = details
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function errorMessage(error: unknown): string {
|
|
64
|
+
if (error instanceof RepeaterStoreApiError) return error.message
|
|
65
|
+
if (error instanceof Error && error.message !== '') return error.message
|
|
66
|
+
return typeof error === 'string' && error !== '' ? error : 'Unknown error'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { errorMessage as repeaterStoreErrorMessage }
|
|
70
|
+
|
|
71
|
+
interface RequestOptions {
|
|
72
|
+
method?: 'GET' | 'POST' | 'DELETE'
|
|
73
|
+
body?: unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function requestJson<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
77
|
+
const init: RequestInit = { method: options.method ?? 'GET' }
|
|
78
|
+
if (options.body !== undefined) {
|
|
79
|
+
init.headers = { 'Content-Type': 'application/json' }
|
|
80
|
+
init.body = JSON.stringify(options.body)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let response: Response
|
|
84
|
+
try {
|
|
85
|
+
response = await fetch(`${API_BASE}${path}`, init)
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
88
|
+
throw new RepeaterStoreApiError('NETWORK_ERROR', detail, 0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const text = await response.text().catch(() => '')
|
|
92
|
+
let payload: unknown = null
|
|
93
|
+
if (text !== '') {
|
|
94
|
+
try {
|
|
95
|
+
payload = JSON.parse(text) as unknown
|
|
96
|
+
} catch {
|
|
97
|
+
payload = null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const envelope = (payload ?? {}) as ApiErrorPayload
|
|
103
|
+
const message =
|
|
104
|
+
typeof envelope.error === 'string' && envelope.error !== ''
|
|
105
|
+
? envelope.error
|
|
106
|
+
: typeof envelope.message === 'string' && envelope.message !== ''
|
|
107
|
+
? envelope.message
|
|
108
|
+
: `HTTP ${response.status}`
|
|
109
|
+
const code =
|
|
110
|
+
typeof envelope.code === 'string' && envelope.code !== ''
|
|
111
|
+
? envelope.code
|
|
112
|
+
: `HTTP_${response.status}`
|
|
113
|
+
throw new RepeaterStoreApiError(code, message, response.status, envelope.details)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return payload as T
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const encodeId = (value: string): string => encodeURIComponent(value)
|
|
120
|
+
|
|
121
|
+
function unwrapItems<T>(payload: unknown): T[] {
|
|
122
|
+
if (Array.isArray(payload)) return payload as T[]
|
|
123
|
+
if (payload !== null && typeof payload === 'object') {
|
|
124
|
+
const items = (payload as { items?: unknown }).items
|
|
125
|
+
if (Array.isArray(items)) return items as T[]
|
|
126
|
+
}
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function listRepeaterHistory(limit = 20): Promise<RepeaterHistoryEntry[]> {
|
|
131
|
+
const safeLimit = Math.max(1, Math.min(50, Math.trunc(limit)))
|
|
132
|
+
return requestJson<unknown>(`/repeater/history?limit=${safeLimit}`).then((payload) =>
|
|
133
|
+
unwrapItems<RepeaterHistoryEntry>(payload)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function clearRepeaterHistory(): Promise<boolean> {
|
|
138
|
+
return requestJson<{ cleared?: boolean }>('/repeater/history', { method: 'DELETE' }).then(
|
|
139
|
+
(payload) => payload.cleared === true
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function listRepeaterCollections(): Promise<RepeaterCollectionEntry[]> {
|
|
144
|
+
return requestJson<unknown>('/repeater/collections').then((payload) =>
|
|
145
|
+
unwrapItems<RepeaterCollectionEntry>(payload)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function saveRepeaterCollection(
|
|
150
|
+
input: SaveRepeaterCollectionInput
|
|
151
|
+
): Promise<RepeaterCollectionEntry> {
|
|
152
|
+
return requestJson<{ item: RepeaterCollectionEntry }>('/repeater/collections', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: input
|
|
155
|
+
}).then((payload) => payload.item)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function deleteRepeaterCollection(id: string): Promise<boolean> {
|
|
159
|
+
return requestJson<{ deleted?: boolean }>(
|
|
160
|
+
`/repeater/collections/${encodeId(id)}`,
|
|
161
|
+
{ method: 'DELETE' }
|
|
162
|
+
).then((payload) => payload.deleted === true)
|
|
163
|
+
}
|
|
@@ -1,17 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Response formatting helpers for the retest repeater (Yakit/Burp-style JSON
|
|
3
|
-
* view). Kept away from the TSX
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Response formatting helpers for the retest repeater (Yakit/Burp-style JSON
|
|
3
|
+
* view) and the Fuzzer result detail. Kept away from the TSX components so
|
|
4
|
+
* they are unit-testable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Pretty-print JSON bodies; null when the body is not JSON. */
|
|
8
|
+
export function prettyJsonBody(body: string, contentType: string): string | null {
|
|
9
|
+
const trimmed = body.trim()
|
|
10
|
+
if (trimmed.length === 0) return null
|
|
11
|
+
if (!/json/i.test(contentType) && !/^[{[]/.test(trimmed)) return null
|
|
12
|
+
if (trimmed.length > 2_000_000) return null
|
|
13
|
+
try {
|
|
14
|
+
return JSON.stringify(JSON.parse(body), null, 2)
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Case-insensitive response/request header lookup. */
|
|
21
|
+
export function headerValue(headers: Record<string, string>, name: string): string {
|
|
22
|
+
const lower = name.toLowerCase()
|
|
23
|
+
const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === lower)
|
|
24
|
+
return entry?.[1] ?? ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when a response declares (or visibly contains) HTML. */
|
|
28
|
+
export function isHtmlResponse(body: string, headers: Record<string, string>): boolean {
|
|
29
|
+
const contentType = headerValue(headers, 'content-type')
|
|
30
|
+
if (/text\/html|application\/xhtml\+xml/i.test(contentType)) return true
|
|
31
|
+
return /^\s*(?:<!doctype\s+html|<html[\s>])/i.test(body.slice(0, 300))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Rendered HTML needs a base URL so relative CSS/images resolve against the target. */
|
|
35
|
+
export function buildHtmlPreview(body: string, targetUrl: string): string {
|
|
36
|
+
let base = ''
|
|
37
|
+
try {
|
|
38
|
+
base = `${new URL(targetUrl).origin}/`
|
|
39
|
+
} catch {
|
|
40
|
+
base = ''
|
|
41
|
+
}
|
|
42
|
+
if (base === '') return body
|
|
43
|
+
const tag = `<base href="${base.replace(/"/g, '"')}">`
|
|
44
|
+
if (/<head(?:\s[^>]*)?>/i.test(body)) {
|
|
45
|
+
return body.replace(/<head(?:\s[^>]*)?>/i, (match) => `${match}${tag}`)
|
|
46
|
+
}
|
|
47
|
+
return `${tag}${body}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Tailwind tone for a response status chip. */
|
|
51
|
+
export function statusTone(status: number): string {
|
|
52
|
+
if (status >= 500) return 'border-red-300 bg-red-50 text-red-800'
|
|
53
|
+
if (status >= 400) return 'border-amber-300 bg-amber-50 text-amber-900'
|
|
54
|
+
if (status >= 300) return 'border-sky-300 bg-sky-50 text-sky-900'
|
|
55
|
+
return 'border-emerald-300 bg-emerald-50 text-emerald-900'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True when a response status redirects and carries a Location header. */
|
|
59
|
+
export function isRedirectResponse(
|
|
60
|
+
status: number,
|
|
61
|
+
headers: Record<string, string>
|
|
62
|
+
): boolean {
|
|
63
|
+
return status >= 300 && status < 400 && headerValue(headers, 'location') !== ''
|
|
64
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Zap } from 'lucide-react'
|
|
3
|
+
import type { Severity, Vulnerability, VulnerabilityListItem } from '@shared/types'
|
|
4
|
+
import { YakitSendPanel } from '@renderer/components/retest/YakitSendPanel'
|
|
5
|
+
import { SeverityBadge, StatusBadge } from '@renderer/components/common/Badges'
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue
|
|
12
|
+
} from '@renderer/components/ui/select'
|
|
13
|
+
import { Skeleton } from '@renderer/components/ui/skeleton'
|
|
14
|
+
import { useI18n } from '@renderer/i18n'
|
|
15
|
+
import { getApi } from '@renderer/lib/api'
|
|
16
|
+
|
|
17
|
+
interface FuzzerPageProps {
|
|
18
|
+
dataVersion: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TARGET_STORAGE_KEY = 'cortex.fuzzer.target'
|
|
22
|
+
|
|
23
|
+
function storedTargetId(): string {
|
|
24
|
+
try {
|
|
25
|
+
return window.localStorage.getItem(TARGET_STORAGE_KEY) ?? ''
|
|
26
|
+
} catch {
|
|
27
|
+
return ''
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rememberTarget(id: string): void {
|
|
32
|
+
try {
|
|
33
|
+
window.localStorage.setItem(TARGET_STORAGE_KEY, id)
|
|
34
|
+
} catch {
|
|
35
|
+
/* localStorage may be unavailable in a hardened WebView */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dedicated Web Fuzzer page: a Yakit-style manual HTTP sender with the request
|
|
41
|
+
* editor on the left, the response viewer on the right and both panes filling
|
|
42
|
+
* the available height. There is deliberately no ported vulnerability list and
|
|
43
|
+
* no batch fuzzing: a compact target selector in the header is the only link to
|
|
44
|
+
* a finding, because the Host derives the outbound origin allowlist from it.
|
|
45
|
+
*/
|
|
46
|
+
export function FuzzerPage({ dataVersion }: FuzzerPageProps) {
|
|
47
|
+
const { t } = useI18n()
|
|
48
|
+
const [items, setItems] = useState<VulnerabilityListItem[] | null>(null)
|
|
49
|
+
const [listLoading, setListLoading] = useState(true)
|
|
50
|
+
const [listError, setListError] = useState<string | null>(null)
|
|
51
|
+
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
52
|
+
const [detail, setDetail] = useState<Vulnerability | null>(null)
|
|
53
|
+
const [detailLoading, setDetailLoading] = useState(false)
|
|
54
|
+
const [detailError, setDetailError] = useState<string | null>(null)
|
|
55
|
+
const detailCache = useRef(new Map<string, Vulnerability>())
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const api = getApi()
|
|
59
|
+
if (!api) {
|
|
60
|
+
setListError(t('common.apiUnavailable'))
|
|
61
|
+
setListLoading(false)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
let alive = true
|
|
65
|
+
setListLoading(true)
|
|
66
|
+
api
|
|
67
|
+
.listVulnerabilities({ realm: 'web', page: 1, pageSize: 200 })
|
|
68
|
+
.then(
|
|
69
|
+
(result) => {
|
|
70
|
+
if (!alive) return
|
|
71
|
+
setItems(result.items)
|
|
72
|
+
setListError(null)
|
|
73
|
+
setListLoading(false)
|
|
74
|
+
const remembered = storedTargetId()
|
|
75
|
+
setSelectedId((current) => {
|
|
76
|
+
const preferred = current ?? remembered
|
|
77
|
+
if (preferred !== '' && result.items.some((item) => item.id === preferred)) {
|
|
78
|
+
return preferred
|
|
79
|
+
}
|
|
80
|
+
return result.items[0]?.id ?? null
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
(cause: unknown) => {
|
|
84
|
+
if (!alive) return
|
|
85
|
+
setListError(cause instanceof Error ? cause.message : String(cause))
|
|
86
|
+
setListLoading(false)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
return () => {
|
|
90
|
+
alive = false
|
|
91
|
+
}
|
|
92
|
+
}, [dataVersion, t])
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (selectedId === null) {
|
|
96
|
+
setDetail(null)
|
|
97
|
+
setDetailLoading(false)
|
|
98
|
+
setDetailError(null)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
rememberTarget(selectedId)
|
|
102
|
+
const cached = detailCache.current.get(selectedId)
|
|
103
|
+
if (cached !== undefined) {
|
|
104
|
+
setDetail(cached)
|
|
105
|
+
setDetailLoading(false)
|
|
106
|
+
setDetailError(null)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
const api = getApi()
|
|
110
|
+
if (!api) {
|
|
111
|
+
setDetailError(t('common.apiUnavailable'))
|
|
112
|
+
setDetailLoading(false)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
let alive = true
|
|
116
|
+
setDetail(null)
|
|
117
|
+
setDetailLoading(true)
|
|
118
|
+
setDetailError(null)
|
|
119
|
+
api.getVulnerability(selectedId).then(
|
|
120
|
+
(next) => {
|
|
121
|
+
if (!alive) return
|
|
122
|
+
if (next === null) {
|
|
123
|
+
setDetailError(t('fuzzer.target.detailFailed'))
|
|
124
|
+
} else {
|
|
125
|
+
detailCache.current.set(next.id, next)
|
|
126
|
+
setDetail(next)
|
|
127
|
+
}
|
|
128
|
+
setDetailLoading(false)
|
|
129
|
+
},
|
|
130
|
+
(cause: unknown) => {
|
|
131
|
+
if (!alive) return
|
|
132
|
+
setDetailError(cause instanceof Error ? cause.message : String(cause))
|
|
133
|
+
setDetailLoading(false)
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
return () => {
|
|
137
|
+
alive = false
|
|
138
|
+
}
|
|
139
|
+
}, [selectedId, dataVersion, t])
|
|
140
|
+
|
|
141
|
+
const selectedItem = items?.find((item) => item.id === selectedId) ?? null
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
data-testid="fuzzer-page"
|
|
146
|
+
className="mx-auto flex h-full min-h-0 w-full max-w-[1680px] flex-col px-6 py-4"
|
|
147
|
+
>
|
|
148
|
+
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
|
|
149
|
+
<h1 className="text-[19px] font-semibold tracking-tight text-foreground">
|
|
150
|
+
{t('nav.fuzzer')}
|
|
151
|
+
</h1>
|
|
152
|
+
|
|
153
|
+
<div className="flex min-w-0 flex-wrap items-center justify-end gap-2">
|
|
154
|
+
<span className="text-[10.5px] text-muted-foreground">{t('fuzzer.target.title')}</span>
|
|
155
|
+
{listLoading && items === null ? (
|
|
156
|
+
<Skeleton className="h-8 w-[300px]" />
|
|
157
|
+
) : (
|
|
158
|
+
<Select
|
|
159
|
+
value={selectedId ?? undefined}
|
|
160
|
+
onValueChange={(value) => setSelectedId(value)}
|
|
161
|
+
disabled={items === null || items.length === 0}
|
|
162
|
+
>
|
|
163
|
+
<SelectTrigger
|
|
164
|
+
data-testid="fuzzer-target-select"
|
|
165
|
+
aria-label={t('fuzzer.target.title')}
|
|
166
|
+
className="h-8 w-[300px] text-[11.5px]"
|
|
167
|
+
>
|
|
168
|
+
<SelectValue placeholder={t('fuzzer.target.select')} />
|
|
169
|
+
</SelectTrigger>
|
|
170
|
+
<SelectContent className="max-h-[320px]">
|
|
171
|
+
{(items ?? []).map((item) => (
|
|
172
|
+
<SelectItem key={item.id} value={item.id} className="text-[11.5px]">
|
|
173
|
+
{item.vuln_name}
|
|
174
|
+
</SelectItem>
|
|
175
|
+
))}
|
|
176
|
+
</SelectContent>
|
|
177
|
+
</Select>
|
|
178
|
+
)}
|
|
179
|
+
{selectedItem !== null ? (
|
|
180
|
+
<span className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
181
|
+
<SeverityBadge severity={selectedItem.severity as Severity} />
|
|
182
|
+
<span className="max-w-[180px] truncate font-mono">{selectedItem.host}</span>
|
|
183
|
+
<StatusBadge status={selectedItem.status} />
|
|
184
|
+
</span>
|
|
185
|
+
) : null}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{listError !== null ? (
|
|
190
|
+
<div role="alert" className="mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[11.5px] text-red-700">
|
|
191
|
+
{t('fuzzer.target.loadFailed', { error: listError })}
|
|
192
|
+
</div>
|
|
193
|
+
) : null}
|
|
194
|
+
|
|
195
|
+
<div className="mt-3 flex min-h-0 flex-1 flex-col">
|
|
196
|
+
{detailError !== null ? (
|
|
197
|
+
<div role="alert" className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[11.5px] text-red-700">
|
|
198
|
+
{detailError}
|
|
199
|
+
</div>
|
|
200
|
+
) : detail === null && listError === null && !listLoading && (items?.length ?? 0) === 0 ? (
|
|
201
|
+
<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">
|
|
202
|
+
{t('fuzzer.target.empty')}
|
|
203
|
+
</div>
|
|
204
|
+
) : detail === null ? (
|
|
205
|
+
<YakitSendPanel vuln={null} loading={detailLoading || listLoading || (items?.length ?? 0) > 0} />
|
|
206
|
+
) : (
|
|
207
|
+
<YakitSendPanel key={detail.id} vuln={detail} loading={detailLoading} />
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|