@lievclouds/dsh-cortex 0.4.1 → 0.4.2
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/client.js +257 -257
- package/package.json +1 -1
- package/src/cortex-ui/dsh/api.ts +10 -1
- package/src/cortex-ui/renderer/components/asset/AssetTable.tsx +21 -27
- package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +3 -2
- package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +7 -1
- package/src/cortex-ui/renderer/i18n/messages/vuln.ts +2 -0
- package/src/cortex-ui/renderer/lib/retestRequest.ts +17 -0
- package/src/cortex-ui/renderer/pages/DashboardPage.tsx +5 -1
- package/src/cortex-ui/shared/types.ts +3 -1
- package/src/cortex-ui/shared/web.ts +2 -0
- package/src/host/cortex/vulns.js +32 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lievclouds/dsh-cortex",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Cortex workbench for DeepSeek Harness: the original Cortex UI (vulnerabilities, assets, internal operations, Memory Nebula, snapshots, retest) as a native DSH panel, plus 12 native agent tools so DSH conversations use the local Cortex SQLite without MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/src/cortex-ui/dsh/api.ts
CHANGED
|
@@ -112,7 +112,16 @@ export function createCortexApi(): CortexApi {
|
|
|
112
112
|
patch(`/vulns/${encodeURIComponent(input.id)}/status`, { status: input.status }),
|
|
113
113
|
deleteVulnerability: (id) => del(`/vulns/${encodeURIComponent(id)}`),
|
|
114
114
|
deleteVulnerabilities: (ids) => post('/vulns/delete-many', { ids }),
|
|
115
|
-
exportVulnerabilities: (ids) =>
|
|
115
|
+
exportVulnerabilities: async (ids) => {
|
|
116
|
+
const response = await post('/vulns/export', { ids })
|
|
117
|
+
const files = Array.isArray(response?.files) ? response.files : []
|
|
118
|
+
const savedPaths = Array.isArray(response?.savedPaths) ? response.savedPaths : []
|
|
119
|
+
return {
|
|
120
|
+
cancelled: response?.cancelled === true || files.length === 0,
|
|
121
|
+
files: files.map((file) => (file && typeof file.filename === 'string' ? file.filename : String(file))),
|
|
122
|
+
savedPaths: savedPaths.filter((path) => typeof path === 'string'),
|
|
123
|
+
}
|
|
124
|
+
},
|
|
116
125
|
|
|
117
126
|
// ---------------------------------------------------------------- assets
|
|
118
127
|
listAssets: (filters) => get('/assets', filters as Record<string, unknown>),
|
|
@@ -23,12 +23,15 @@ interface AssetTableProps {
|
|
|
23
23
|
const thBase =
|
|
24
24
|
'sticky top-0 z-10 border-b border-border bg-background px-3 py-2 text-left text-[11px] font-medium text-foreground/70'
|
|
25
25
|
|
|
26
|
-
/** Table cells share
|
|
26
|
+
/** Table cells share a fixed row height and the hairline bottom border. */
|
|
27
27
|
function cellClass(last: boolean, extra?: string): string {
|
|
28
|
-
return cn('border-b border-border px-3 py-2
|
|
28
|
+
return cn('h-14 border-b border-border px-3 py-2 align-middle', last && 'border-b-0', extra)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Fixed single-line fingerprint summary: list rows keep one constant height, so
|
|
33
|
+
* a long stack is clipped with an ellipsis instead of growing the row.
|
|
34
|
+
*/
|
|
32
35
|
function TechStackTags({ techStack }: { techStack: string }) {
|
|
33
36
|
const { t } = useI18n()
|
|
34
37
|
const tags = techStackTags(techStack)
|
|
@@ -36,23 +39,14 @@ function TechStackTags({ techStack }: { techStack: string }) {
|
|
|
36
39
|
return <span className="text-[11px] text-muted-foreground">{t('asset.value.notRecorded')}</span>
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
const
|
|
40
|
-
const hiddenCount = tags.length - visibleTags.length
|
|
41
|
-
|
|
42
|
+
const label = tags.join(' · ')
|
|
42
43
|
return (
|
|
43
|
-
<span
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>
|
|
50
|
-
{tag}
|
|
51
|
-
</span>
|
|
52
|
-
))}
|
|
53
|
-
{hiddenCount > 0 ? (
|
|
54
|
-
<span className="font-mono text-[11px] tabular-nums text-foreground/60">+{hiddenCount}</span>
|
|
55
|
-
) : null}
|
|
44
|
+
<span
|
|
45
|
+
className="block truncate font-mono text-[11px] leading-5 text-foreground/75"
|
|
46
|
+
title={label}
|
|
47
|
+
data-testid="asset-tech-stack"
|
|
48
|
+
>
|
|
49
|
+
{label}
|
|
56
50
|
</span>
|
|
57
51
|
)
|
|
58
52
|
}
|
|
@@ -83,28 +77,28 @@ function TableSkeleton() {
|
|
|
83
77
|
<tbody>
|
|
84
78
|
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
85
79
|
<tr key={index}>
|
|
86
|
-
<td className="border-b border-border px-3 py-
|
|
80
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
87
81
|
<Skeleton className="h-3.5 w-[182px]" />
|
|
88
82
|
</td>
|
|
89
|
-
<td className="border-b border-border px-3 py-
|
|
83
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
90
84
|
<Skeleton className="h-3 w-[92px]" />
|
|
91
85
|
</td>
|
|
92
|
-
<td className="border-b border-border px-3 py-
|
|
86
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
93
87
|
<Skeleton className="h-3 w-[68%]" />
|
|
94
88
|
</td>
|
|
95
|
-
<td className="border-b border-border px-3 py-
|
|
89
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
96
90
|
<Skeleton className="mx-auto h-[22px] w-[56px] rounded-full" />
|
|
97
91
|
</td>
|
|
98
|
-
<td className="border-b border-border px-3 py-
|
|
92
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
99
93
|
<Skeleton className="mx-auto h-3 w-5" />
|
|
100
94
|
</td>
|
|
101
|
-
<td className="border-b border-border px-3 py-
|
|
95
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
102
96
|
<Skeleton className="mx-auto h-3 w-5" />
|
|
103
97
|
</td>
|
|
104
|
-
<td className="border-b border-border px-3 py-
|
|
98
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
105
99
|
<Skeleton className="mx-auto h-3 w-5" />
|
|
106
100
|
</td>
|
|
107
|
-
<td className="border-b border-border px-3 py-
|
|
101
|
+
<td className="h-14 border-b border-border px-3 py-0">
|
|
108
102
|
<Skeleton className="h-3 w-[62px]" />
|
|
109
103
|
</td>
|
|
110
104
|
<td className="border-b border-border py-3 pr-4" />
|
|
@@ -8,7 +8,8 @@ import { useI18n } from '@renderer/i18n'
|
|
|
8
8
|
import { getApi } from '@renderer/lib/api'
|
|
9
9
|
import {
|
|
10
10
|
defaultRequestIndex,
|
|
11
|
-
extractRepeaterRequests
|
|
11
|
+
extractRepeaterRequests,
|
|
12
|
+
resolveRepeaterUrl
|
|
12
13
|
} from '@renderer/lib/retestRequest'
|
|
13
14
|
|
|
14
15
|
interface RepeaterPanelProps {
|
|
@@ -159,7 +160,7 @@ export function RepeaterPanel({ vuln, loading }: RepeaterPanelProps) {
|
|
|
159
160
|
const response = await api.sendRepeaterRequest({
|
|
160
161
|
vulnId: vuln.id,
|
|
161
162
|
method: parsed.method,
|
|
162
|
-
url: parsed.url,
|
|
163
|
+
url: resolveRepeaterUrl(parsed.url, vuln.target_url),
|
|
163
164
|
headers: parsed.headers,
|
|
164
165
|
...(parsed.body === '' ? {} : { body: parsed.body }),
|
|
165
166
|
followRedirects
|
|
@@ -262,7 +262,13 @@ export function VulnDetail({
|
|
|
262
262
|
setExporting(true)
|
|
263
263
|
try {
|
|
264
264
|
const result = await api.exportVulnerabilities([detail.id])
|
|
265
|
-
if (!result.cancelled)
|
|
265
|
+
if (!result.cancelled) {
|
|
266
|
+
const savedPath = result.savedPaths?.[0]
|
|
267
|
+
toast.success(
|
|
268
|
+
t('vuln.toast.exported'),
|
|
269
|
+
savedPath ? { description: t('vuln.toast.exportedPath', { path: savedPath }) } : undefined
|
|
270
|
+
)
|
|
271
|
+
}
|
|
266
272
|
} catch (cause) {
|
|
267
273
|
toast.error(t('vuln.toast.exportFailed', { error: toErrorMessage(cause, t) }))
|
|
268
274
|
} finally {
|
|
@@ -91,6 +91,7 @@ export const vulnMessages: MessageBundle = {
|
|
|
91
91
|
'vuln.copy.codeBlockDone': '已复制代码块',
|
|
92
92
|
'vuln.toast.exportedMany': '已导出 {count} 份 Markdown 报告',
|
|
93
93
|
'vuln.toast.exported': 'Markdown 报告已导出',
|
|
94
|
+
'vuln.toast.exportedPath': '已保存到:{path}',
|
|
94
95
|
'vuln.toast.exportFailed': '导出失败:{error}',
|
|
95
96
|
'vuln.toast.deleted': '已删除 {id}',
|
|
96
97
|
'vuln.toast.bulkDeleted': '已删除 {count} 条漏洞',
|
|
@@ -201,6 +202,7 @@ export const vulnMessages: MessageBundle = {
|
|
|
201
202
|
'vuln.copy.codeBlockDone': 'Code block copied',
|
|
202
203
|
'vuln.toast.exportedMany': 'Exported {count} Markdown reports',
|
|
203
204
|
'vuln.toast.exported': 'Markdown report exported',
|
|
205
|
+
'vuln.toast.exportedPath': 'Saved to: {path}',
|
|
204
206
|
'vuln.toast.exportFailed': 'Export failed: {error}',
|
|
205
207
|
'vuln.toast.deleted': 'Deleted {id}',
|
|
206
208
|
'vuln.toast.bulkDeleted': 'Deleted {count} vulnerabilities',
|
|
@@ -11,6 +11,23 @@ export interface RepeaterRequestCandidate {
|
|
|
11
11
|
|
|
12
12
|
const REQUEST_LINE = /^([A-Z]+)\s+(\S+)\s+HTTP\/\d(?:\.\d)?$/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Raw report requests carry only the origin-form path (`GET /api/x HTTP/1.1`),
|
|
16
|
+
* but the Host send API requires an absolute URL. Resolve against the finding's
|
|
17
|
+
* target so http-only targets stay http and https targets stay https.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveRepeaterUrl(rawUrl: string, targetUrl: string): string {
|
|
20
|
+
const value = rawUrl.trim()
|
|
21
|
+
if (/^https?:\/\//i.test(value)) return value
|
|
22
|
+
try {
|
|
23
|
+
const target = new URL(targetUrl)
|
|
24
|
+
if (value.startsWith('//')) return `${target.protocol}${value}`
|
|
25
|
+
return new URL(value, target).href
|
|
26
|
+
} catch {
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
function normalizePath(input: string): string {
|
|
15
32
|
try {
|
|
16
33
|
if (/^https?:\/\//i.test(input)) return new URL(input).pathname
|
|
@@ -313,11 +313,15 @@ export function DashboardPage({
|
|
|
313
313
|
setExporting(true)
|
|
314
314
|
try {
|
|
315
315
|
const result = await api.exportVulnerabilities(ids)
|
|
316
|
+
const savedPath = result.savedPaths?.[0]
|
|
316
317
|
if (!result.cancelled) {
|
|
317
|
-
|
|
318
|
+
const message =
|
|
318
319
|
result.files.length > 1
|
|
319
320
|
? t('vuln.toast.exportedMany', { count: result.files.length })
|
|
320
321
|
: t('vuln.toast.exported')
|
|
322
|
+
toast.success(
|
|
323
|
+
message,
|
|
324
|
+
savedPath ? { description: t('vuln.toast.exportedPath', { path: savedPath }) } : undefined
|
|
321
325
|
)
|
|
322
326
|
}
|
|
323
327
|
} catch (cause) {
|
|
@@ -1020,8 +1020,10 @@ export interface DeleteVulnsResult {
|
|
|
1020
1020
|
|
|
1021
1021
|
export interface ExportReportsResult {
|
|
1022
1022
|
cancelled: boolean
|
|
1023
|
-
/**
|
|
1023
|
+
/** File names (or absolute paths for desktop transports) of exported reports. */
|
|
1024
1024
|
files: string[]
|
|
1025
|
+
/** DSH Host also writes reports under ~/.cortex/exports and returns them here. */
|
|
1026
|
+
savedPaths?: string[]
|
|
1025
1027
|
}
|
|
1026
1028
|
|
|
1027
1029
|
export interface SeedDemoResult {
|
|
@@ -72,6 +72,8 @@ export interface WebExportFile {
|
|
|
72
72
|
/** Response of `POST /api/vulns/export`; the browser downloads these, nothing is written server-side. */
|
|
73
73
|
export interface WebExportResponse {
|
|
74
74
|
files: WebExportFile[]
|
|
75
|
+
/** Optional Host-side saved copies (`~/.cortex/exports/...`) for desktop builds. */
|
|
76
|
+
savedPaths?: string[]
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/**
|
package/src/host/cortex/vulns.js
CHANGED
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
} from '../http.js'
|
|
35
35
|
import * as dto from '../dto.js'
|
|
36
36
|
import { buildVulnReport, reportFilename } from '../reports.js'
|
|
37
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
38
|
+
import { homedir } from 'node:os'
|
|
39
|
+
import { join } from 'node:path'
|
|
37
40
|
|
|
38
41
|
const SEVERITIES = ['Critical', 'High', 'Medium', 'Low', 'Info']
|
|
39
42
|
const STATUSES = ['Confirmed', 'Inconclusive', 'Retest_Passed']
|
|
@@ -497,7 +500,7 @@ export async function handleExport({ req, res, holder, bumpRev }) {
|
|
|
497
500
|
const body = await readJsonBody(req)
|
|
498
501
|
const ids = requireIdList(body, 'ids', MAX_EXPORT_IDS)
|
|
499
502
|
if (ids.length === 0) {
|
|
500
|
-
sendJson(res, 200, { cancelled: true, files: [] })
|
|
503
|
+
sendJson(res, 200, { cancelled: true, files: [], savedPaths: [] })
|
|
501
504
|
return
|
|
502
505
|
}
|
|
503
506
|
const db = holder.db
|
|
@@ -525,9 +528,36 @@ export async function handleExport({ req, res, holder, bumpRev }) {
|
|
|
525
528
|
content: buildVulnReport(detail, related),
|
|
526
529
|
})
|
|
527
530
|
}
|
|
531
|
+
// The DSH desktop WebView may block browser downloads, so the Host also
|
|
532
|
+
// writes the Markdown reports to ~/.cortex/exports/<timestamp>/ and returns
|
|
533
|
+
// the absolute paths. The renderer still attempts a normal download, but the
|
|
534
|
+
// toast can then point at a real file that always exists.
|
|
535
|
+
const savedPaths = []
|
|
536
|
+
if (files.length > 0) {
|
|
537
|
+
try {
|
|
538
|
+
const exportRoot = process.env.CORTEX_EXPORT_DIR || join(homedir(), '.cortex', 'exports')
|
|
539
|
+
const stamp = new Date()
|
|
540
|
+
.toISOString()
|
|
541
|
+
.replace(/[-:]/g, '')
|
|
542
|
+
.replace(/\..+$/, '')
|
|
543
|
+
.replace('T', '-')
|
|
544
|
+
const exportDir = join(exportRoot, stamp)
|
|
545
|
+
mkdirSync(exportDir, { recursive: true })
|
|
546
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
547
|
+
const file = files[index]
|
|
548
|
+
const safeName = String(file.filename).replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_').slice(0, 120) || file.id
|
|
549
|
+
const target = join(exportDir, `${String(index + 1).padStart(2, '0')}-${safeName.endsWith('.md') ? safeName : `${safeName}.md`}`)
|
|
550
|
+
writeFileSync(target, file.content, 'utf8')
|
|
551
|
+
savedPaths.push(target)
|
|
552
|
+
}
|
|
553
|
+
} catch {
|
|
554
|
+
// Filesystem failure must not break the export response; the inline
|
|
555
|
+
// files still let the browser download attempt proceed.
|
|
556
|
+
}
|
|
557
|
+
}
|
|
528
558
|
// Export is a read-only projection: no revision bump.
|
|
529
559
|
void bumpRev
|
|
530
|
-
sendJson(res, 200, { cancelled: files.length === 0, files })
|
|
560
|
+
sendJson(res, 200, { cancelled: files.length === 0, files, savedPaths })
|
|
531
561
|
}
|
|
532
562
|
|
|
533
563
|
export { SEVERITIES, STATUSES, REALM_FILTERS, VULN_ORDER }
|