@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.

Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/client.js +428 -406
  3. package/index.js +7 -0
  4. package/locale/en.json +6 -6
  5. package/locale/zh.json +6 -6
  6. package/package.json +115 -106
  7. package/src/cortex-ui/dsh/api.ts +5 -2
  8. package/src/cortex-ui/dsh/css-overrides.css +8 -0
  9. package/src/cortex-ui/renderer/App.tsx +39 -0
  10. package/src/cortex-ui/renderer/components/asset/AssetDetail.tsx +9 -1
  11. package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
  12. package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
  13. package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
  14. package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
  15. package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
  16. package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
  17. package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +333 -148
  18. package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
  19. package/src/cortex-ui/renderer/components/vuln/DirectoryPickerDialog.tsx +187 -0
  20. package/src/cortex-ui/renderer/components/vuln/ExportReportDialog.tsx +173 -59
  21. package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +16 -4
  22. package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
  23. package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
  24. package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
  25. package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
  26. package/src/cortex-ui/renderer/i18n/messages/retest.ts +146 -2
  27. package/src/cortex-ui/renderer/i18n/messages/vuln.ts +26 -6
  28. package/src/cortex-ui/renderer/lib/curl.ts +635 -0
  29. package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
  30. package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
  31. package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
  32. package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
  33. package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -0
  34. package/src/cortex-ui/renderer/lib/transport/httpClient.ts +1 -1
  35. package/src/cortex-ui/renderer/pages/DashboardPage.tsx +3 -3
  36. package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
  37. package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
  38. package/src/cortex-ui/shared/ipc.ts +3 -4
  39. package/src/cortex-ui/shared/types.ts +9 -0
  40. package/src/host/cortex/fs.js +72 -0
  41. package/src/host/cortex/index.js +49 -1
  42. package/src/host/cortex/repeater-store.js +411 -0
  43. package/src/host/cortex/repeater.js +15 -2
  44. package/src/host/cortex/settings.js +54 -0
  45. package/src/host/cortex/tools.js +2 -0
  46. package/src/host/cortex/vulns.js +36 -13
  47. package/src/host/miniapp/analyze.js +1078 -0
  48. package/src/host/miniapp/assets-import.js +294 -0
  49. package/src/host/miniapp/detect.js +426 -0
  50. package/src/host/miniapp/format.js +263 -0
  51. package/src/host/miniapp/roots.js +360 -0
  52. package/src/host/miniapp/routes.js +304 -0
  53. package/src/host/miniapp/service.js +559 -0
  54. package/src/host/miniapp/store.js +756 -0
  55. package/src/host/miniapp/tools.js +335 -0
  56. package/src/host/miniapp/unpack.js +1010 -0
  57. package/src/host/miniapp/wxapkg.js +416 -0
  58. package/src/host/reports-office.js +341 -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
+ }
@@ -0,0 +1,64 @@
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, '&quot;')}">`
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
+ }
@@ -263,7 +263,7 @@ export function createHttpApi(): CortexApi {
263
263
  */
264
264
  exportVulnerabilities: async (
265
265
  ids: string[],
266
- _outputDir?: string
266
+ _options?: { outputDir?: string; format?: string }
267
267
  ): Promise<ExportReportsResult> => {
268
268
  const response = await request<WebExportResponse>(WEB_API_PATHS.vulnerabilitiesExport, {
269
269
  method: 'POST',
@@ -304,7 +304,7 @@ export function DashboardPage({
304
304
  [items]
305
305
  )
306
306
 
307
- const handleExportSelected = async (outputDir?: string) => {
307
+ const handleExportSelected = async (outputDir?: string, format?: 'md' | 'docx' | 'pdf') => {
308
308
  const ids = [...selectedIds]
309
309
  if (ids.length === 0) return
310
310
  const api = getApi()
@@ -315,7 +315,7 @@ export function DashboardPage({
315
315
  }
316
316
  setExporting(true)
317
317
  try {
318
- const result = await api.exportVulnerabilities(ids, outputDir)
318
+ const result = await api.exportVulnerabilities(ids, { outputDir, format })
319
319
  const savedPath = result.savedPaths?.[0]
320
320
  if (!result.cancelled) {
321
321
  const message =
@@ -536,7 +536,7 @@ export function DashboardPage({
536
536
  count={selectedCount}
537
537
  pending={exporting}
538
538
  onOpenChange={setExportOpen}
539
- onConfirm={(outputDir) => void handleExportSelected(outputDir)}
539
+ onConfirm={(outputDir, format) => void handleExportSelected(outputDir, format)}
540
540
  />
541
541
  </>
542
542
  )
@@ -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
+ }