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

Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/client.js +434 -412
  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 -110
  7. package/src/cortex-ui/dsh/css-overrides.css +8 -0
  8. package/src/cortex-ui/renderer/App.tsx +39 -0
  9. package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
  10. package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
  11. package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
  12. package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
  13. package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
  14. package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
  15. package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +291 -137
  16. package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
  17. package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
  18. package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
  19. package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
  20. package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
  21. package/src/cortex-ui/renderer/i18n/messages/retest.ts +144 -2
  22. package/src/cortex-ui/renderer/lib/curl.ts +635 -0
  23. package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
  24. package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
  25. package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
  26. package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
  27. package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -17
  28. package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
  29. package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
  30. package/src/host/cortex/index.js +42 -1
  31. package/src/host/cortex/repeater-store.js +411 -0
  32. package/src/host/cortex/repeater.js +15 -2
  33. package/src/host/cortex/tools.js +2 -0
  34. package/src/host/miniapp/analyze.js +1078 -0
  35. package/src/host/miniapp/assets-import.js +294 -0
  36. package/src/host/miniapp/detect.js +426 -0
  37. package/src/host/miniapp/format.js +263 -0
  38. package/src/host/miniapp/roots.js +360 -0
  39. package/src/host/miniapp/routes.js +304 -0
  40. package/src/host/miniapp/service.js +559 -0
  41. package/src/host/miniapp/store.js +756 -0
  42. package/src/host/miniapp/tools.js +335 -0
  43. package/src/host/miniapp/unpack.js +1010 -0
  44. package/src/host/miniapp/wxapkg.js +416 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Import extracted mini-program intelligence into the existing Cortex asset
3
+ * library (schema v5).
4
+ *
5
+ * The analyzer produces domains/endpoints/secrets/parameters from the unpacked
6
+ * workspace; this module maps them onto the canonical asset shape and merges
7
+ * them with the same semantics as `cortex_sync_asset_info`:
8
+ *
9
+ * - `host_port` derived from the most-used business domain (https preferred),
10
+ * - `interfaces[]` from endpoint candidates (path/method/params),
11
+ * - `leaked_infos[]` from masked-verified hard-coded secrets (full value is
12
+ * persisted locally, never logged),
13
+ * - `test_parameters[]` from non-secret id/tenant/order candidates.
14
+ *
15
+ * Nothing here uploads package content or calls the network.
16
+ */
17
+
18
+ import { basename } from 'node:path'
19
+ import { withBusyRetry } from '../db.js'
20
+ import {
21
+ normalizeInterfaces,
22
+ normalizeLeaks,
23
+ normalizeProjectName,
24
+ normalizeTestParameters,
25
+ } from '../cortex/normalize-asset.js'
26
+
27
+ /** Domains that belong to the WeChat runtime / generic references, not the target. */
28
+ const NOISE_DOMAIN_PATTERNS = [
29
+ /(^|\.)qq\.com$/i,
30
+ /(^|\.)weixin\.qq\.com$/i,
31
+ /(^|\.)servicewechat\.com$/i,
32
+ /(^|\.)qpic\.cn$/i,
33
+ /(^|\.)qlogo\.cn$/i,
34
+ /(^|\.)w3\.org$/i,
35
+ /(^|\.)schema\.org$/i,
36
+ /(^|\.)example\.com$/i,
37
+ /(^|\.)localhost$/i,
38
+ ]
39
+
40
+ const MAX_INTERFACES = 200
41
+ const MAX_LEAKS = 100
42
+ const MAX_PARAMETERS = 100
43
+
44
+ function isNoiseDomain(host) {
45
+ if (!host || /^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true
46
+ return NOISE_DOMAIN_PATTERNS.some((pattern) => pattern.test(host))
47
+ }
48
+
49
+ /**
50
+ * Pick the asset host_port from overview domains (or an explicit value).
51
+ * @param {Array<{host:string,count:number,examples?:string[]}>} domains
52
+ * @returns {string} '' when no usable domain exists
53
+ */
54
+ export function chooseAssetHostPort(domains) {
55
+ const entries = Array.isArray(domains) ? domains : []
56
+ const usable = entries.filter((entry) => entry && !isNoiseDomain(String(entry.host || '')))
57
+ if (usable.length === 0) return ''
58
+ const ranked = [...usable].sort((left, right) => {
59
+ const leftHttps = (left.examples || []).some((value) => String(value).startsWith('https://'))
60
+ const rightHttps = (right.examples || []).some((value) => String(value).startsWith('https://'))
61
+ if (leftHttps !== rightHttps) return leftHttps ? -1 : 1
62
+ return Number(right.count || 0) - Number(left.count || 0)
63
+ })
64
+ const host = String((ranked[0] && ranked[0].host) || '').trim().toLowerCase()
65
+ if (host.length === 0) return ''
66
+ const https = (ranked[0].examples || []).some((value) => String(value).startsWith('https://'))
67
+ return `${https ? 'https' : 'http'}://${host}`
68
+ }
69
+
70
+ /** Build canonical interface entries from analyzer endpoint candidates. */
71
+ export function buildInterfaces(endpoints) {
72
+ const candidates = []
73
+ for (const item of Array.isArray(endpoints) ? endpoints : []) {
74
+ if (!item) continue
75
+ let path = String(item.path || '').trim()
76
+ if (path.length === 0) continue
77
+ if (path.length > 2000) path = path.slice(0, 2000)
78
+ let params = ''
79
+ const queryIndex = path.indexOf('?')
80
+ if (queryIndex >= 0) {
81
+ params = path.slice(queryIndex + 1)
82
+ path = path.slice(0, queryIndex)
83
+ }
84
+ if (path.length === 0 || !path.startsWith('/')) continue
85
+ const entry = { path }
86
+ const method = String(item.method || '').toUpperCase()
87
+ if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'].includes(method)) entry.method = method
88
+ if (params.length > 0) entry.params = params
89
+ candidates.push(entry)
90
+ }
91
+ return normalizeInterfaces(candidates).slice(0, MAX_INTERFACES)
92
+ }
93
+
94
+ /** Build leaked-info entries from analyzer secrets (never logs the value). */
95
+ export function buildLeaks(secrets, appid) {
96
+ const entries = []
97
+ for (const item of Array.isArray(secrets) ? secrets : []) {
98
+ if (!item || typeof item.value !== 'string' || item.value.trim().length === 0) continue
99
+ const source = `${String(item.source || 'unknown')}${item.line ? `:${item.line}` : ''}`
100
+ entries.push({
101
+ info_type: ['Credential', 'API_Key', 'Token', 'PII', 'Internal_IP', 'Other'].includes(item.info_type)
102
+ ? item.info_type
103
+ : 'Other',
104
+ value: item.value,
105
+ source: `miniapp:${appid}:${source}`,
106
+ potential_impact:
107
+ String(item.potential_impact || '').trim() ||
108
+ '小程序包内硬编码的凭据/密钥,可能被用于伪造请求或访问后端资源',
109
+ })
110
+ }
111
+ return normalizeLeaks(entries).slice(0, MAX_LEAKS)
112
+ }
113
+
114
+ /** Build canonical test-parameter seeds from analyzer candidates. */
115
+ export function buildTestParameters(parameters, appid) {
116
+ const entries = []
117
+ for (const item of Array.isArray(parameters) ? parameters : []) {
118
+ if (!item) continue
119
+ entries.push({
120
+ name: item.name,
121
+ endpoint: item.endpoint,
122
+ location: item.location,
123
+ values: Array.isArray(item.values) ? item.values : [],
124
+ source: `miniapp:${appid}:${String(item.source || '')}`,
125
+ pattern: item.pattern,
126
+ })
127
+ }
128
+ return normalizeTestParameters(entries).slice(0, MAX_PARAMETERS)
129
+ }
130
+
131
+ function readJsonArray(value) {
132
+ try {
133
+ const parsed = JSON.parse(String(value ?? '[]'))
134
+ return Array.isArray(parsed) ? parsed : []
135
+ } catch {
136
+ return []
137
+ }
138
+ }
139
+
140
+ function uniqueMerge(existingRaw, additions) {
141
+ const merged = [...readJsonArray(existingRaw)]
142
+ const seen = new Set(merged.map((entry) => JSON.stringify(entry)))
143
+ for (const entry of Array.isArray(additions) ? additions : []) {
144
+ const key = JSON.stringify(entry)
145
+ if (seen.has(key)) continue
146
+ seen.add(key)
147
+ merged.push(entry)
148
+ }
149
+ return merged
150
+ }
151
+
152
+ /**
153
+ * Merge one source's extracted intelligence into the asset library.
154
+ *
155
+ * @param {{holder:any, source:any, overview:any, projectName?:string, hostPort?:string}} input
156
+ * @returns {{ok:boolean, project:string, assets:Array<object>, totals:object, warnings:string[]}}
157
+ */
158
+ export async function importMiniappAssets(input) {
159
+ const holder = input && input.holder
160
+ const source = input && input.source
161
+ const overview = input && input.overview
162
+ if (!holder || !source || !overview) throw new Error('holder, source and overview are required')
163
+ if (typeof holder.ensure === 'function') holder.ensure()
164
+ if (typeof holder.assertUsable === 'function') holder.assertUsable()
165
+ if (holder.readOnly) throw new Error('Cortex database is in read-only mode; asset import rejected.')
166
+ if (typeof holder.assertWritable === 'function') holder.assertWritable()
167
+
168
+ const appid = String(source.appid || '').trim()
169
+ const version = String(source.version || '').trim()
170
+ const explicitHostPort =
171
+ input.hostPort !== undefined && input.hostPort !== null ? String(input.hostPort).trim() : ''
172
+ const hostPort = explicitHostPort || chooseAssetHostPort(overview.domains)
173
+ const project = normalizeProjectName(
174
+ input.projectName !== undefined && input.projectName !== null && String(input.projectName).trim().length > 0
175
+ ? String(input.projectName)
176
+ : `小程序 ${appid}`,
177
+ )
178
+
179
+ const interfaces = buildInterfaces(overview.endpoints)
180
+ const leaks = buildLeaks(overview.secrets, appid)
181
+ const parameters = buildTestParameters(overview.parameters, appid)
182
+
183
+ const warnings = []
184
+ if (!hostPort) {
185
+ warnings.push('no business domain was extracted; provide host_port explicitly to create an asset')
186
+ return {
187
+ ok: false,
188
+ project,
189
+ assets: [],
190
+ totals: { assets: 0, interfaces: interfaces.length, leaks: leaks.length, parameters: parameters.length },
191
+ warnings,
192
+ }
193
+ }
194
+ if (interfaces.length === 0) warnings.push('no endpoint candidates were extracted')
195
+ if (leaks.length === 0) warnings.push('no hard-coded secret was extracted')
196
+ if (parameters.length === 0) warnings.push('no test-parameter candidate was extracted')
197
+
198
+ const db = holder.db
199
+ if (!db) throw new Error('Cortex database is unavailable')
200
+ const existing = db.prepare('SELECT * FROM assets WHERE host_port = ?').get(hostPort)
201
+ const now = new Date().toISOString()
202
+ const techStack = existing && existing.tech_stack ? String(existing.tech_stack) : ''
203
+ const marker = `wechat-miniapp ${appid}${version ? `@${version}` : ''}`
204
+ const nextTechStack =
205
+ techStack.length === 0
206
+ ? marker
207
+ : techStack.toLowerCase().includes(marker.toLowerCase())
208
+ ? techStack
209
+ : `${techStack}, ${marker}`
210
+ const projectName =
211
+ !existing || !String(existing.project_name || '').trim() ? project : normalizeProjectName(existing.project_name)
212
+ const mergedInterfaces = uniqueMerge(existing && existing.interfaces, interfaces)
213
+ const mergedLeaks = uniqueMerge(existing && existing.leaked_infos, leaks)
214
+ const mergedParameters = uniqueMerge(existing && existing.test_parameters, parameters)
215
+
216
+ await withBusyRetry(() => {
217
+ db.exec('BEGIN IMMEDIATE')
218
+ try {
219
+ if (existing) {
220
+ db.prepare(
221
+ `UPDATE assets SET host = ?, project_name = ?, tech_stack = ?, status = 'Alive',
222
+ interfaces = ?, leaked_infos = ?, test_parameters = ?, last_seen = ?
223
+ WHERE host_port = ?`,
224
+ ).run(
225
+ existing.host || hostPort.replace(/^https?:\/\//, ''),
226
+ projectName,
227
+ nextTechStack,
228
+ JSON.stringify(mergedInterfaces),
229
+ JSON.stringify(mergedLeaks),
230
+ JSON.stringify(mergedParameters),
231
+ now,
232
+ hostPort,
233
+ )
234
+ } else {
235
+ db.prepare(
236
+ `INSERT INTO assets
237
+ (host_port, host, project_name, tech_stack, auth_required, status,
238
+ interfaces, leaked_infos, test_parameters, related_vuln_ids, last_seen)
239
+ VALUES (?, ?, ?, ?, 0, 'Alive', ?, ?, ?, '[]', ?)`,
240
+ ).run(
241
+ hostPort,
242
+ hostPort.replace(/^https?:\/\//, ''),
243
+ projectName,
244
+ nextTechStack,
245
+ JSON.stringify(mergedInterfaces),
246
+ JSON.stringify(mergedLeaks),
247
+ JSON.stringify(mergedParameters),
248
+ now,
249
+ )
250
+ }
251
+ db.exec('COMMIT')
252
+ } catch (error) {
253
+ try {
254
+ db.exec('ROLLBACK')
255
+ } catch {
256
+ /* ignore */
257
+ }
258
+ throw error
259
+ }
260
+ })
261
+ if (typeof holder.refreshFileStamp === 'function') holder.refreshFileStamp()
262
+
263
+ return {
264
+ ok: true,
265
+ project: projectName,
266
+ assets: [
267
+ {
268
+ host_port: hostPort,
269
+ created: !existing,
270
+ interfaces: mergedInterfaces.length,
271
+ leaks: mergedLeaks.length,
272
+ parameters: mergedParameters.length,
273
+ },
274
+ ],
275
+ totals: {
276
+ assets: 1,
277
+ interfaces: mergedInterfaces.length,
278
+ leaks: mergedLeaks.length,
279
+ parameters: mergedParameters.length,
280
+ added_interfaces: interfaces.length,
281
+ added_leaks: leaks.length,
282
+ added_parameters: parameters.length,
283
+ },
284
+ warnings,
285
+ }
286
+ }
287
+
288
+ /** Short label for the source package (no absolute path). */
289
+ export function miniappLabel(source) {
290
+ const appid = String((source && source.appid) || 'unknown')
291
+ const version = String((source && source.version) || '')
292
+ const name = String((source && source.app_name) || '') || basename(String((source && source.package_dir) || ''))
293
+ return `${appid}${version ? `@${version}` : ''}${name ? ` (${name})` : ''}`
294
+ }