@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,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
|
+
}
|