@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.
- package/README.md +1 -1
- package/client.js +428 -406
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -106
- package/src/cortex-ui/dsh/api.ts +5 -2
- 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/asset/AssetDetail.tsx +9 -1
- 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 +333 -148
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- package/src/cortex-ui/renderer/components/vuln/DirectoryPickerDialog.tsx +187 -0
- package/src/cortex-ui/renderer/components/vuln/ExportReportDialog.tsx +173 -59
- package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +16 -4
- 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 +146 -2
- package/src/cortex-ui/renderer/i18n/messages/vuln.ts +26 -6
- 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 -0
- package/src/cortex-ui/renderer/lib/transport/httpClient.ts +1 -1
- package/src/cortex-ui/renderer/pages/DashboardPage.tsx +3 -3
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/cortex-ui/shared/ipc.ts +3 -4
- package/src/cortex-ui/shared/types.ts +9 -0
- package/src/host/cortex/fs.js +72 -0
- package/src/host/cortex/index.js +49 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/settings.js +54 -0
- package/src/host/cortex/tools.js +2 -0
- package/src/host/cortex/vulns.js +36 -13
- 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
- package/src/host/reports-office.js +341 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Office-format report generation for the ported Cortex UI.
|
|
3
|
+
*
|
|
4
|
+
* The Markdown report built by `reports.js` is the single source of truth; this
|
|
5
|
+
* module renders the same content as a Word document (docx) or a PDF. PDF text
|
|
6
|
+
* is Chinese, so a CJK TTF font from the host OS is embedded (PDFKit cannot
|
|
7
|
+
* embed the system .ttc collections, hence the TTF candidate list).
|
|
8
|
+
*
|
|
9
|
+
* Both renderers are pure Node (no browser APIs) and used by
|
|
10
|
+
* `POST /vulns/export` when `format` is `docx` or `pdf`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import PDFDocument from 'pdfkit'
|
|
15
|
+
import {
|
|
16
|
+
BorderStyle,
|
|
17
|
+
Document,
|
|
18
|
+
HeadingLevel,
|
|
19
|
+
Packer,
|
|
20
|
+
Paragraph,
|
|
21
|
+
TextRun,
|
|
22
|
+
} from 'docx'
|
|
23
|
+
|
|
24
|
+
/** Common single-face CJK TrueType fonts; PDFKit rejects .ttc collections. */
|
|
25
|
+
const CJK_FONT_CANDIDATES = [
|
|
26
|
+
'C:\\Windows\\Fonts\\simhei.ttf',
|
|
27
|
+
'C:\\Windows\\Fonts\\simkai.ttf',
|
|
28
|
+
'C:\\Windows\\Fonts\\simfang.ttf',
|
|
29
|
+
'C:\\Windows\\Fonts\\Deng.ttf',
|
|
30
|
+
'C:\\Windows\\Fonts\\msyh.ttf',
|
|
31
|
+
'/System/Library/Fonts/Supplemental/Arial Unicode.ttf',
|
|
32
|
+
'/Library/Fonts/Arial Unicode.ttf',
|
|
33
|
+
'/usr/share/fonts/truetype/arphic/uming.ttc',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const DOCX_BODY_FONT = 'Microsoft YaHei'
|
|
37
|
+
const DOCX_MONO_FONT = 'Consolas'
|
|
38
|
+
const PDF_PAGE_MARGIN = 56
|
|
39
|
+
|
|
40
|
+
export function findCjkFont() {
|
|
41
|
+
const explicit = process.env.CORTEX_PDF_FONT
|
|
42
|
+
if (explicit && existsSync(explicit)) return explicit
|
|
43
|
+
for (const candidate of CJK_FONT_CANDIDATES) {
|
|
44
|
+
if (existsSync(candidate)) return candidate
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ------------------------------------------------------------ markdown -- */
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse the report Markdown into simple blocks. Covers exactly what
|
|
53
|
+
* `buildVulnReport` emits plus fenced HTTP/JSON code inside reproduction steps.
|
|
54
|
+
* @param {string} markdown
|
|
55
|
+
* @returns {Array<{type:'heading'|'paragraph'|'list'|'code'|'hr', level?:number, text?:string, lines?:string[]}>}
|
|
56
|
+
*/
|
|
57
|
+
export function parseReportMarkdown(markdown) {
|
|
58
|
+
const lines = String(markdown ?? '')
|
|
59
|
+
.replace(/\r\n/g, '\n')
|
|
60
|
+
.split('\n')
|
|
61
|
+
const blocks = []
|
|
62
|
+
let index = 0
|
|
63
|
+
while (index < lines.length) {
|
|
64
|
+
const line = lines[index]
|
|
65
|
+
if (line.trim() === '') {
|
|
66
|
+
index += 1
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (/^```/.test(line)) {
|
|
70
|
+
const code = []
|
|
71
|
+
index += 1
|
|
72
|
+
while (index < lines.length && !/^```/.test(lines[index])) {
|
|
73
|
+
code.push(lines[index])
|
|
74
|
+
index += 1
|
|
75
|
+
}
|
|
76
|
+
index += 1
|
|
77
|
+
blocks.push({ type: 'code', text: code.join('\n') })
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line)
|
|
81
|
+
if (heading) {
|
|
82
|
+
blocks.push({ type: 'heading', level: heading[1].length, text: heading[2].trim() })
|
|
83
|
+
index += 1
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
87
|
+
const items = []
|
|
88
|
+
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index])) {
|
|
89
|
+
items.push(lines[index].replace(/^\s*[-*]\s+/, '').trim())
|
|
90
|
+
index += 1
|
|
91
|
+
}
|
|
92
|
+
blocks.push({ type: 'list', lines: items })
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (/^---+$/.test(line.trim())) {
|
|
96
|
+
blocks.push({ type: 'hr' })
|
|
97
|
+
index += 1
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
const paragraph = []
|
|
101
|
+
while (index < lines.length) {
|
|
102
|
+
const next = lines[index]
|
|
103
|
+
if (
|
|
104
|
+
next.trim() === '' ||
|
|
105
|
+
/^```/.test(next) ||
|
|
106
|
+
/^#{1,6}\s+/.test(next) ||
|
|
107
|
+
/^\s*[-*]\s+/.test(next) ||
|
|
108
|
+
/^---+$/.test(next.trim())
|
|
109
|
+
) {
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
paragraph.push(next.trim())
|
|
113
|
+
index += 1
|
|
114
|
+
}
|
|
115
|
+
blocks.push({ type: 'paragraph', text: paragraph.join(' ') })
|
|
116
|
+
}
|
|
117
|
+
return blocks
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Strip inline Markdown markers for plain-text renderers. */
|
|
121
|
+
export function plainInline(text) {
|
|
122
|
+
return String(text ?? '')
|
|
123
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
124
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
125
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
126
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ---------------------------------------------------------------- docx -- */
|
|
130
|
+
|
|
131
|
+
function docxInlineRuns(text) {
|
|
132
|
+
const runs = []
|
|
133
|
+
const pattern = /(\*\*[^*]+\*\*|`[^`]+`)/g
|
|
134
|
+
let last = 0
|
|
135
|
+
let match
|
|
136
|
+
while ((match = pattern.exec(String(text ?? ''))) !== null) {
|
|
137
|
+
if (match.index > last) {
|
|
138
|
+
runs.push(new TextRun({ text: String(text).slice(last, match.index) }))
|
|
139
|
+
}
|
|
140
|
+
const token = match[0]
|
|
141
|
+
if (token.startsWith('**')) {
|
|
142
|
+
runs.push(new TextRun({ text: token.slice(2, -2), bold: true }))
|
|
143
|
+
} else {
|
|
144
|
+
runs.push(new TextRun({ text: token.slice(1, -1), font: DOCX_MONO_FONT, shading: { fill: 'F3F4F6' } }))
|
|
145
|
+
}
|
|
146
|
+
last = match.index + token.length
|
|
147
|
+
}
|
|
148
|
+
if (last < String(text ?? '').length) runs.push(new TextRun({ text: String(text).slice(last) }))
|
|
149
|
+
return runs.length > 0 ? runs : [new TextRun({ text: '' })]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function headingLevel(level) {
|
|
153
|
+
if (level <= 1) return HeadingLevel.HEADING_1
|
|
154
|
+
if (level === 2) return HeadingLevel.HEADING_2
|
|
155
|
+
return HeadingLevel.HEADING_3
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render the Markdown report as a Word document.
|
|
160
|
+
* @param {string} markdown
|
|
161
|
+
* @param {{ title?: string, author?: string }} [meta]
|
|
162
|
+
* @returns {Promise<Buffer>}
|
|
163
|
+
*/
|
|
164
|
+
export async function renderDocxReport(markdown, meta = {}) {
|
|
165
|
+
const children = []
|
|
166
|
+
for (const block of parseReportMarkdown(markdown)) {
|
|
167
|
+
if (block.type === 'heading') {
|
|
168
|
+
children.push(
|
|
169
|
+
new Paragraph({
|
|
170
|
+
heading: headingLevel(block.level),
|
|
171
|
+
spacing: { before: block.level <= 1 ? 0 : 220, after: 120 },
|
|
172
|
+
children: docxInlineRuns(plainInline(block.text)),
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
} else if (block.type === 'paragraph') {
|
|
176
|
+
children.push(
|
|
177
|
+
new Paragraph({
|
|
178
|
+
spacing: { after: 140, line: 320 },
|
|
179
|
+
children: docxInlineRuns(block.text),
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
} else if (block.type === 'list') {
|
|
183
|
+
for (const item of block.lines) {
|
|
184
|
+
children.push(
|
|
185
|
+
new Paragraph({
|
|
186
|
+
bullet: { level: 0 },
|
|
187
|
+
spacing: { after: 80, line: 300 },
|
|
188
|
+
children: docxInlineRuns(item),
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
} else if (block.type === 'code') {
|
|
193
|
+
for (const line of block.text.split('\n')) {
|
|
194
|
+
children.push(
|
|
195
|
+
new Paragraph({
|
|
196
|
+
spacing: { after: 0 },
|
|
197
|
+
shading: { fill: 'F4F5F7' },
|
|
198
|
+
children: [new TextRun({ text: line === '' ? ' ' : line, font: DOCX_MONO_FONT, size: 18 })],
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
children.push(new Paragraph({ spacing: { after: 120 }, children: [] }))
|
|
203
|
+
} else if (block.type === 'hr') {
|
|
204
|
+
children.push(
|
|
205
|
+
new Paragraph({
|
|
206
|
+
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'D1D5DB' } },
|
|
207
|
+
spacing: { after: 120 },
|
|
208
|
+
children: [],
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const document = new Document({
|
|
215
|
+
creator: 'Cortex / DSH',
|
|
216
|
+
title: meta.title || 'Cortex vulnerability report',
|
|
217
|
+
styles: {
|
|
218
|
+
default: {
|
|
219
|
+
document: {
|
|
220
|
+
run: { font: DOCX_BODY_FONT, size: 21, color: '1F2937' },
|
|
221
|
+
paragraph: { spacing: { line: 320 } },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
sections: [{ properties: {}, children }],
|
|
226
|
+
})
|
|
227
|
+
return Packer.toBuffer(document)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ----------------------------------------------------------------- pdf -- */
|
|
231
|
+
|
|
232
|
+
function pdfCodeHeight(doc, text, width) {
|
|
233
|
+
return doc.heightOfString(text, { width, lineGap: 1, fontSize: 8.5 })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Render the Markdown report as a PDF with an embedded CJK font.
|
|
238
|
+
* @param {string} markdown
|
|
239
|
+
* @param {{ title?: string, author?: string }} [meta]
|
|
240
|
+
* @returns {Promise<Buffer>}
|
|
241
|
+
*/
|
|
242
|
+
export async function renderPdfReport(markdown, meta = {}) {
|
|
243
|
+
const fontPath = findCjkFont()
|
|
244
|
+
if (fontPath === null) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'PDF 导出需要系统中文字体(TTF):Windows 请安装“黑体/等线/楷体”,或设置环境变量 CORTEX_PDF_FONT 指向一个中文字体文件',
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const doc = new PDFDocument({
|
|
251
|
+
size: 'A4',
|
|
252
|
+
margins: { top: PDF_PAGE_MARGIN, left: PDF_PAGE_MARGIN, right: PDF_PAGE_MARGIN, bottom: PDF_PAGE_MARGIN + 18 },
|
|
253
|
+
bufferPages: true,
|
|
254
|
+
info: {
|
|
255
|
+
Title: meta.title || 'Cortex vulnerability report',
|
|
256
|
+
Author: meta.author || 'Cortex / DSH',
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
const chunks = []
|
|
260
|
+
doc.on('data', (chunk) => chunks.push(chunk))
|
|
261
|
+
const finished = new Promise((resolve, reject) => {
|
|
262
|
+
doc.on('end', () => resolve(Buffer.concat(chunks)))
|
|
263
|
+
doc.on('error', reject)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
doc.registerFont('body', fontPath)
|
|
267
|
+
const contentWidth = doc.page.width - PDF_PAGE_MARGIN * 2
|
|
268
|
+
const bottom = doc.page.height - PDF_PAGE_MARGIN - 18
|
|
269
|
+
|
|
270
|
+
const ensureSpace = (height) => {
|
|
271
|
+
if (doc.y + height > bottom) doc.addPage()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const block of parseReportMarkdown(markdown)) {
|
|
275
|
+
if (block.type === 'heading') {
|
|
276
|
+
const size = block.level <= 1 ? 19 : block.level === 2 ? 13.5 : 11.5
|
|
277
|
+
ensureSpace(size + 24)
|
|
278
|
+
doc.moveDown(block.level <= 1 ? 0 : 0.4)
|
|
279
|
+
doc.font('body').fontSize(size).fillColor('#111111').text(plainInline(block.text), {
|
|
280
|
+
width: contentWidth,
|
|
281
|
+
lineGap: 2,
|
|
282
|
+
})
|
|
283
|
+
doc.moveDown(0.35)
|
|
284
|
+
} else if (block.type === 'paragraph') {
|
|
285
|
+
const text = plainInline(block.text)
|
|
286
|
+
ensureSpace(20)
|
|
287
|
+
doc.font('body').fontSize(10).fillColor('#26303C').text(text, {
|
|
288
|
+
width: contentWidth,
|
|
289
|
+
lineGap: 2.5,
|
|
290
|
+
align: 'left',
|
|
291
|
+
})
|
|
292
|
+
doc.moveDown(0.55)
|
|
293
|
+
} else if (block.type === 'list') {
|
|
294
|
+
for (const item of block.lines) {
|
|
295
|
+
ensureSpace(16)
|
|
296
|
+
doc.font('body').fontSize(10).fillColor('#26303C').text(`• ${plainInline(item)}`, {
|
|
297
|
+
width: contentWidth,
|
|
298
|
+
indent: 10,
|
|
299
|
+
lineGap: 2,
|
|
300
|
+
})
|
|
301
|
+
doc.moveDown(0.2)
|
|
302
|
+
}
|
|
303
|
+
doc.moveDown(0.25)
|
|
304
|
+
} else if (block.type === 'code') {
|
|
305
|
+
const text = block.text.length > 0 ? block.text : ' '
|
|
306
|
+
const height = pdfCodeHeight(doc, text, contentWidth - 16) + 10
|
|
307
|
+
ensureSpace(height + 6)
|
|
308
|
+
const top = doc.y
|
|
309
|
+
doc.save().roundedRect(PDF_PAGE_MARGIN, top, contentWidth, height, 4).fill('#F3F4F6').restore()
|
|
310
|
+
doc
|
|
311
|
+
.font('body')
|
|
312
|
+
.fontSize(8.5)
|
|
313
|
+
.fillColor('#1F2937')
|
|
314
|
+
.text(text, PDF_PAGE_MARGIN + 8, top + 5, { width: contentWidth - 16, lineGap: 1 })
|
|
315
|
+
doc.moveDown(0.5)
|
|
316
|
+
} else if (block.type === 'hr') {
|
|
317
|
+
ensureSpace(14)
|
|
318
|
+
doc.moveDown(0.2)
|
|
319
|
+
const y = doc.y
|
|
320
|
+
doc.save().moveTo(PDF_PAGE_MARGIN, y).lineTo(PDF_PAGE_MARGIN + contentWidth, y)
|
|
321
|
+
.lineWidth(0.6).strokeColor('#D1D5DB').stroke().restore()
|
|
322
|
+
doc.moveDown(0.6)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const pageCount = doc.bufferedPageRange().count
|
|
327
|
+
for (let index = 0; index < pageCount; index += 1) {
|
|
328
|
+
doc.switchToPage(index)
|
|
329
|
+
doc
|
|
330
|
+
.font('body')
|
|
331
|
+
.fontSize(8)
|
|
332
|
+
.fillColor('#94A3B8')
|
|
333
|
+
.text(`第 ${index + 1} / ${pageCount} 页`, PDF_PAGE_MARGIN, doc.page.height - PDF_PAGE_MARGIN - 8, {
|
|
334
|
+
width: contentWidth,
|
|
335
|
+
align: 'right',
|
|
336
|
+
lineBreak: false,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
doc.end()
|
|
340
|
+
return finished
|
|
341
|
+
}
|