@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
@@ -1,11 +1,15 @@
1
1
  import * as DialogPrimitive from '@radix-ui/react-dialog'
2
2
  import { Download, FolderOpen, Loader2 } from 'lucide-react'
3
3
  import { useEffect, useState } from 'react'
4
+ import type { ExportReportFormat } from '@shared/types'
4
5
  import { Button } from '@renderer/components/ui/button'
5
6
  import { useI18n } from '@renderer/i18n'
7
+ import { cn } from '@renderer/lib/utils'
6
8
  import { getPortalContainer } from '../../../dsh/portal'
9
+ import { DirectoryPickerDialog } from './DirectoryPickerDialog'
7
10
 
8
11
  const STORAGE_KEY = 'cortex.export.dir'
12
+ const FORMAT_KEY = 'cortex.export.format'
9
13
 
10
14
  function isAbsoluteLike(value: string): boolean {
11
15
  const trimmed = value.trim()
@@ -21,7 +25,7 @@ interface ExportReportDialogProps {
21
25
  count: number
22
26
  pending: boolean
23
27
  onOpenChange: (open: boolean) => void
24
- onConfirm: (outputDir: string | undefined) => void
28
+ onConfirm: (outputDir: string | undefined, format: ExportReportFormat) => void
25
29
  }
26
30
 
27
31
  /**
@@ -38,13 +42,49 @@ export function ExportReportDialog({
38
42
  }: ExportReportDialogProps) {
39
43
  const { t } = useI18n()
40
44
  const [directory, setDirectory] = useState('')
45
+ const [format, setFormat] = useState<ExportReportFormat>('md')
41
46
  const [invalid, setInvalid] = useState(false)
47
+ const [pickerOpen, setPickerOpen] = useState(false)
42
48
 
43
49
  useEffect(() => {
44
50
  if (!open) return
45
- const stored = window.localStorage.getItem(STORAGE_KEY) ?? ''
51
+ let alive = true
52
+ let stored = ''
53
+ try {
54
+ stored = window.localStorage.getItem(STORAGE_KEY) ?? ''
55
+ } catch {
56
+ stored = ''
57
+ }
58
+ const storedFormat = (() => {
59
+ try {
60
+ return window.localStorage.getItem(FORMAT_KEY)
61
+ } catch {
62
+ return null
63
+ }
64
+ })()
46
65
  setDirectory(stored)
66
+ setFormat(storedFormat === 'docx' || storedFormat === 'pdf' ? storedFormat : 'md')
47
67
  setInvalid(false)
68
+ // The Host remembers the last successful export directory; prefer it over
69
+ // any stale per-browser value so a fresh WebView still defaults correctly.
70
+ void fetch('/cortex-api/cortex/export/settings')
71
+ .then((response) => (response.ok ? response.json() : null))
72
+ .then((payload) => {
73
+ if (!alive) return
74
+ const hostDir = payload && typeof payload.outputDir === 'string' ? payload.outputDir.trim() : ''
75
+ if (hostDir.length > 0) {
76
+ setDirectory(hostDir)
77
+ try {
78
+ window.localStorage.setItem(STORAGE_KEY, hostDir)
79
+ } catch {
80
+ /* ignore */
81
+ }
82
+ }
83
+ })
84
+ .catch(() => {})
85
+ return () => {
86
+ alive = false
87
+ }
48
88
  }, [open])
49
89
 
50
90
  const confirm = (): void => {
@@ -53,68 +93,142 @@ export function ExportReportDialog({
53
93
  setInvalid(true)
54
94
  return
55
95
  }
56
- if (trimmed !== '') window.localStorage.setItem(STORAGE_KEY, trimmed)
57
- else window.localStorage.removeItem(STORAGE_KEY)
58
- onConfirm(trimmed === '' ? undefined : trimmed)
96
+ try {
97
+ if (trimmed !== '') window.localStorage.setItem(STORAGE_KEY, trimmed)
98
+ else window.localStorage.removeItem(STORAGE_KEY)
99
+ window.localStorage.setItem(FORMAT_KEY, format)
100
+ } catch {
101
+ /* localStorage may be unavailable; the Host remembers the directory */
102
+ }
103
+ onConfirm(trimmed === '' ? undefined : trimmed, format)
59
104
  }
60
105
 
61
- return (
62
- <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
63
- <DialogPrimitive.Portal container={getPortalContainer()}>
64
- <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
65
- <DialogPrimitive.Content
66
- data-testid="export-report-dialog"
67
- className="fixed left-1/2 top-1/2 z-[70] w-[min(480px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
68
- >
69
- <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
70
- {t('vuln.export.title')}
71
- </DialogPrimitive.Title>
72
- <DialogPrimitive.Description className="mt-1.5 text-[12.5px] leading-relaxed text-muted-foreground">
73
- {t('vuln.export.description', { count })}
74
- </DialogPrimitive.Description>
106
+ const formats: Array<{ value: ExportReportFormat; label: string; testId: string }> = [
107
+ { value: 'md', label: t('vuln.export.formatMd'), testId: 'export-format-md' },
108
+ { value: 'docx', label: t('vuln.export.formatDocx'), testId: 'export-format-docx' },
109
+ { value: 'pdf', label: t('vuln.export.formatPdf'), testId: 'export-format-pdf' },
110
+ ]
75
111
 
76
- <label
77
- htmlFor="export-output-dir"
78
- className="mt-4 flex items-center gap-1.5 text-[11.5px] font-medium text-foreground"
79
- >
80
- <FolderOpen aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
81
- {t('vuln.export.dirLabel')}
82
- </label>
83
- <input
84
- id="export-output-dir"
85
- data-testid="export-output-dir"
86
- value={directory}
87
- spellCheck={false}
88
- disabled={pending}
89
- placeholder={t('vuln.export.dirPlaceholder')}
90
- onChange={(event) => {
91
- setDirectory(event.target.value)
92
- if (invalid) setInvalid(false)
112
+ return (
113
+ <>
114
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange} modal={false}>
115
+ <DialogPrimitive.Portal container={getPortalContainer()}>
116
+ <DialogPrimitive.Overlay className="sheet-overlay fixed inset-0 z-[70] bg-[hsl(224_18%_10%/0.3)]" />
117
+ <DialogPrimitive.Content
118
+ data-testid="export-report-dialog"
119
+ onInteractOutside={(event) => {
120
+ // The directory picker is a sibling Radix layer; keep this dialog
121
+ // open while the user browses folders.
122
+ if (pickerOpen) event.preventDefault()
93
123
  }}
94
- onKeyDown={(event) => {
95
- if (event.key === 'Enter') confirm()
124
+ onEscapeKeyDown={(event) => {
125
+ if (pickerOpen) event.preventDefault()
96
126
  }}
97
- className="mt-1.5 h-9 w-full rounded-md border border-border bg-background px-2.5 font-mono text-[11.5px] text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
98
- />
99
- <p className={invalid ? 'mt-1.5 text-[10.5px] text-red-700' : 'mt-1.5 text-[10.5px] text-muted-foreground'}>
100
- {invalid ? t('vuln.export.dirInvalid') : t('vuln.export.dirHint')}
101
- </p>
127
+ className="fixed left-1/2 top-1/2 z-[70] w-[min(480px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background p-5 shadow-panel focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
128
+ >
129
+ <DialogPrimitive.Title className="text-[16px] font-medium tracking-tight text-foreground">
130
+ {t('vuln.export.title')}
131
+ </DialogPrimitive.Title>
132
+ <DialogPrimitive.Description className="sr-only">
133
+ {t('vuln.export.title')}
134
+ </DialogPrimitive.Description>
135
+
136
+ <div className="mt-4">
137
+ <p className="text-[11.5px] font-medium text-foreground">{t('vuln.export.formatLabel')}</p>
138
+ <div
139
+ role="radiogroup"
140
+ aria-label={t('vuln.export.formatLabel')}
141
+ className="mt-1.5 inline-flex rounded-md border border-border bg-muted p-0.5"
142
+ >
143
+ {formats.map((item) => (
144
+ <button
145
+ key={item.value}
146
+ type="button"
147
+ role="radio"
148
+ aria-checked={format === item.value}
149
+ data-testid={item.testId}
150
+ disabled={pending}
151
+ onClick={() => setFormat(item.value)}
152
+ className={cn(
153
+ 'h-7 rounded px-2.5 text-[11.5px] transition-colors',
154
+ format === item.value
155
+ ? 'bg-background font-medium text-foreground shadow-sm'
156
+ : 'text-muted-foreground hover:text-foreground'
157
+ )}
158
+ >
159
+ {item.label}
160
+ </button>
161
+ ))}
162
+ </div>
163
+ </div>
164
+
165
+ <label
166
+ htmlFor="export-output-dir"
167
+ className="mt-4 flex items-center gap-1.5 text-[11.5px] font-medium text-foreground"
168
+ >
169
+ <FolderOpen aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
170
+ {t('vuln.export.dirLabel')}
171
+ </label>
172
+ <div className="mt-1.5 flex items-center gap-1.5">
173
+ <input
174
+ id="export-output-dir"
175
+ data-testid="export-output-dir"
176
+ value={directory}
177
+ spellCheck={false}
178
+ disabled={pending}
179
+ placeholder={t('vuln.export.dirPlaceholder')}
180
+ onChange={(event) => {
181
+ setDirectory(event.target.value)
182
+ if (invalid) setInvalid(false)
183
+ }}
184
+ onKeyDown={(event) => {
185
+ if (event.key === 'Enter') confirm()
186
+ }}
187
+ className="h-9 min-w-0 flex-1 rounded-md border border-border bg-background px-2.5 font-mono text-[11.5px] text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
188
+ />
189
+ <Button
190
+ variant="outline"
191
+ size="sm"
192
+ data-testid="export-browse-dir"
193
+ disabled={pending}
194
+ onClick={() => setPickerOpen(true)}
195
+ className="h-9 shrink-0 gap-1.5"
196
+ >
197
+ <FolderOpen aria-hidden="true" className="h-3.5 w-3.5" />
198
+ {t('vuln.export.browse')}
199
+ </Button>
200
+ </div>
201
+ {invalid ? (
202
+ <p className="mt-1.5 text-[10.5px] text-red-700">{t('vuln.export.dirInvalid')}</p>
203
+ ) : null}
204
+
205
+ <div className="mt-5 flex items-center justify-end gap-2">
206
+ <Button variant="outline" size="sm" onClick={() => onOpenChange(false)} disabled={pending}>
207
+ {t('common.cancel')}
208
+ </Button>
209
+ <Button size="sm" onClick={confirm} disabled={pending} className="gap-1.5">
210
+ {pending ? (
211
+ <Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
212
+ ) : (
213
+ <Download aria-hidden="true" className="h-3.5 w-3.5" />
214
+ )}
215
+ {pending ? t('vuln.detail.exporting') : t('vuln.export.confirm')}
216
+ </Button>
217
+ </div>
218
+ </DialogPrimitive.Content>
219
+ </DialogPrimitive.Portal>
220
+ </DialogPrimitive.Root>
102
221
 
103
- <div className="mt-5 flex items-center justify-end gap-2">
104
- <Button variant="outline" size="sm" onClick={() => onOpenChange(false)} disabled={pending}>
105
- {t('common.cancel')}
106
- </Button>
107
- <Button size="sm" onClick={confirm} disabled={pending} className="gap-1.5">
108
- {pending ? (
109
- <Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
110
- ) : (
111
- <Download aria-hidden="true" className="h-3.5 w-3.5" />
112
- )}
113
- {pending ? t('vuln.detail.exporting') : t('vuln.export.confirm')}
114
- </Button>
115
- </div>
116
- </DialogPrimitive.Content>
117
- </DialogPrimitive.Portal>
118
- </DialogPrimitive.Root>
222
+ <DirectoryPickerDialog
223
+ open={pickerOpen}
224
+ initialPath={directory}
225
+ onOpenChange={setPickerOpen}
226
+ onSelect={(path) => {
227
+ setDirectory(path)
228
+ setInvalid(false)
229
+ setPickerOpen(false)
230
+ }}
231
+ />
232
+ </>
119
233
  )
120
234
  }
@@ -254,16 +254,17 @@ export function VulnDetail({
254
254
  }
255
255
  }
256
256
 
257
- const handleExport = async (outputDir?: string) => {
257
+ const handleExport = async (outputDir?: string, format?: 'md' | 'docx' | 'pdf') => {
258
258
  if (!detail || loading) return
259
259
  const api = getApi()
260
260
  if (!api) {
261
261
  toast.error(apiUnavailableMessage(t))
262
+ setExportOpen(false)
262
263
  return
263
264
  }
264
265
  setExporting(true)
265
266
  try {
266
- const result = await api.exportVulnerabilities([detail.id], outputDir)
267
+ const result = await api.exportVulnerabilities([detail.id], { outputDir, format })
267
268
  if (!result.cancelled) {
268
269
  const savedPath = result.savedPaths?.[0]
269
270
  toast.success(
@@ -287,7 +288,18 @@ export function VulnDetail({
287
288
  if (!next) onClose()
288
289
  }}
289
290
  >
290
- <SheetContent data-testid="vuln-sheet">
291
+ <SheetContent
292
+ data-testid="vuln-sheet"
293
+ onInteractOutside={(event) => {
294
+ // Nested dialogs (export/build path, delete confirm) are portaled
295
+ // outside this sheet in the DOM; without this the sheet treats a
296
+ // click inside them as an outside click and closes itself.
297
+ if (exportOpen || confirmOpen) event.preventDefault()
298
+ }}
299
+ onEscapeKeyDown={(event) => {
300
+ if (exportOpen || confirmOpen) event.preventDefault()
301
+ }}
302
+ >
291
303
  <div className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-6">
292
304
  <span className="font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
293
305
  {t('vuln.detail.eyebrow')}
@@ -629,7 +641,7 @@ export function VulnDetail({
629
641
  count={1}
630
642
  pending={exporting}
631
643
  onOpenChange={setExportOpen}
632
- onConfirm={(outputDir) => void handleExport(outputDir)}
644
+ onConfirm={(outputDir, format) => void handleExport(outputDir, format)}
633
645
  />
634
646
  </>
635
647
  )
@@ -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> {