audit-project-server 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.
package/src/utils.js ADDED
@@ -0,0 +1,384 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const basePath = path.resolve(__dirname, ".."); // 向上回退一级到项目根目录
13
+ const workDirPath = path.join(basePath, "work");
14
+
15
+ fs.mkdirSync(workDirPath, { recursive: true });
16
+
17
+ /**
18
+ * 创建工作目录
19
+ * @returns 工作目录路径
20
+ */
21
+ export async function createdDirectory() {
22
+ // 创建一个workDir目录
23
+ try {
24
+ const workDir = path.join(workDirPath, Date.now().toString());
25
+ await fs.promises.mkdir(workDir, { recursive: true });
26
+ return workDir;
27
+ } catch (error) {
28
+ console.error(`Error creating directory: ${error.message}`);
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 删除工作目录
35
+ * @param workDir 工作目录路径
36
+ */
37
+ export async function deleteDirectory(workDir) {
38
+ await fs.promises.rm(workDir, { recursive: true, force: true });
39
+ }
40
+
41
+
42
+ /**
43
+ * 解析 GitHub 仓库 URL
44
+ * @param url GitHub 仓库 URL
45
+ * @returns { owner, repo, branch } 或 null
46
+ * 支持格式:
47
+ * - https://github.com/lodash/lodash
48
+ * - https://github.com/lodash/lodash/tree/4.17
49
+ * - https://github.com/lodash/lodash/tree/main
50
+ */
51
+ function parseGitHubUrl(url) {
52
+ // 匹配 GitHub URL 格式
53
+ // https://github.com/{owner}/{repo}/tree/{branch}
54
+ // https://github.com/{owner}/{repo}
55
+ const treeMatch = url.match(
56
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/(.+)$/
57
+ );
58
+ if (treeMatch) {
59
+ return {
60
+ owner: treeMatch[1],
61
+ repo: treeMatch[2],
62
+ branch: treeMatch[3], // 可以是分支名或版本号
63
+ };
64
+ }
65
+
66
+ // 没有指定分支/版本,默认使用 main 或 master
67
+ const basicMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/);
68
+ if (basicMatch) {
69
+ return {
70
+ owner: basicMatch[1],
71
+ repo: basicMatch[2],
72
+ branch: null, // 稍后尝试 main 或 master
73
+ };
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * 从 GitHub 远程仓库获取 package.json 内容
80
+ * @param owner 仓库所有者
81
+ * @param repo 仓库名称
82
+ * @param branch 分支名或版本号
83
+ * @returns package.json 内容
84
+ */
85
+ async function fetchGitHubPackageJson(owner, repo, branch) {
86
+ // GitHub raw 文件 URL 格式
87
+ // https://raw.githubusercontent.com/{owner}/{repo}/{branch}/package.json
88
+ const branches = branch ? [branch] : ["main", "master"];
89
+
90
+ for (const b of branches) {
91
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${b}/package.json`;
92
+ try {
93
+ const response = await fetch(rawUrl);
94
+ if (response.ok) {
95
+ const json = await response.json();
96
+ // 添加远程仓库元信息
97
+ json._remote = {
98
+ type: "github",
99
+ owner,
100
+ repo,
101
+ branch: b,
102
+ url: `https://github.com/${owner}/${repo}/tree/${b}`,
103
+ };
104
+ return json;
105
+ }
106
+ } catch (error) {
107
+ // 继续尝试下一个分支
108
+ console.error(`Failed to fetch from branch ${b}: ${error.message}`);
109
+ }
110
+ }
111
+
112
+ throw new Error(
113
+ `无法从 GitHub 仓库 ${owner}/${repo} 获取 package.json,尝试的分支: ${branches.join(", ")}`
114
+ );
115
+ }
116
+
117
+ /**
118
+ * 获取项目Package.json文件内容
119
+ * @param projectPath 项目路径 可能是本地项目 也可能是 远程项目
120
+ * @returns Package.json文件内容
121
+ *
122
+ * 支持的远程仓库格式:
123
+ * - https://github.com/lodash/lodash (默认使用 main 或 master 分支)
124
+ * - https://github.com/lodash/lodash/tree/4.17 (指定版本)
125
+ * - https://github.com/lodash/lodash/tree/main (指定分支)
126
+ */
127
+ export async function getPackageJsonInfo(projectPath) {
128
+ if (projectPath.includes("https://") || projectPath.includes("http://")) {
129
+ // 远程项目
130
+ const githubInfo = parseGitHubUrl(projectPath);
131
+
132
+ if (githubInfo) {
133
+ // GitHub 仓库
134
+ const { owner, repo, branch } = githubInfo;
135
+ return await fetchGitHubPackageJson(owner, repo, branch);
136
+ } else {
137
+ // 其他远程 URL,尝试直接获取 package.json
138
+ // 假设 URL 直接指向 package.json 文件或项目根目录
139
+ let packageJsonUrl = projectPath;
140
+ if (!projectPath.endsWith("package.json")) {
141
+ packageJsonUrl = projectPath.replace(/\/?$/, "/package.json");
142
+ }
143
+
144
+ try {
145
+ const response = await fetch(packageJsonUrl);
146
+ if (response.ok) {
147
+ return await response.json();
148
+ }
149
+ throw new Error(`HTTP ${response.status}`);
150
+ } catch (error) {
151
+ throw new Error(`无法从远程 URL 获取 package.json: ${error.message}`);
152
+ }
153
+ }
154
+ } else {
155
+ // 本地项目
156
+ const PackageJsonPath = path.join(projectPath, "package.json");
157
+ const json = await fs.promises.readFile(PackageJsonPath, "utf-8");
158
+ return JSON.parse(json);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * 检查依赖版本是否为特殊协议或非正常版本
164
+ * @param {string} version 版本字符串
165
+ * @returns {boolean} 是否为特殊协议或非正常版本
166
+ */
167
+ function isSpecialVersion(version) {
168
+ if (!version) return false;
169
+
170
+ // 检查是否为特殊协议
171
+ const specialProtocols = ['link:', 'file:', 'git:', 'git+ssh:', 'git+http:', 'git+https:', 'git+file:'];
172
+ for (const protocol of specialProtocols) {
173
+ if (version.startsWith(protocol)) {
174
+ return true;
175
+ }
176
+ }
177
+
178
+ // 检查是否为非正常版本(不是有效的semver版本)
179
+ // 有效的semver版本示例: 1.0.0, ^1.0.0, ~1.0.0, >=1.0.0 <2.0.0
180
+ // 非正常版本示例: latest, next, beta, alpha, 其他字符串
181
+ const semverRegex = /^[~^]?\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
182
+ const rangeRegex = /^[><=!~^]?\s*\d+\.\d+\.\d+/;
183
+
184
+ if (semverRegex.test(version) || rangeRegex.test(version)) {
185
+ return false;
186
+ }
187
+
188
+ // 如果是URL格式,也认为是特殊版本
189
+ if (version.includes('://') || version.includes('@')) {
190
+ return true;
191
+ }
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * 过滤特殊依赖,返回过滤后的package.json和跳过的依赖列表
198
+ * @param {Object} packageJsonInfo 原始package.json内容
199
+ * @returns {Object} 包含过滤后的package.json和跳过的依赖列表
200
+ */
201
+ function filterSpecialDependencies(packageJsonInfo) {
202
+ const filteredPackageJson = JSON.parse(JSON.stringify(packageJsonInfo));
203
+ const skippedDependencies = {
204
+ dependencies: [],
205
+ devDependencies: [],
206
+ peerDependencies: [],
207
+ optionalDependencies: []
208
+ };
209
+
210
+ // 检查并过滤各种依赖类型
211
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
212
+
213
+ for (const depType of depTypes) {
214
+ if (filteredPackageJson[depType]) {
215
+ const deps = filteredPackageJson[depType];
216
+ const filteredDeps = {};
217
+
218
+ for (const [pkgName, version] of Object.entries(deps)) {
219
+ if (isSpecialVersion(version)) {
220
+ skippedDependencies[depType].push({
221
+ name: pkgName,
222
+ version: version,
223
+ reason: `特殊协议或非正常版本: ${version}`
224
+ });
225
+ } else {
226
+ filteredDeps[pkgName] = version;
227
+ }
228
+ }
229
+
230
+ filteredPackageJson[depType] = filteredDeps;
231
+ }
232
+ }
233
+
234
+ return {
235
+ filteredPackageJson,
236
+ skippedDependencies
237
+ };
238
+ }
239
+
240
+ /**
241
+ * 生成package-lock.json文件
242
+ * @param workDir 工作目录路径
243
+ * @param PackageJsonInfo Package.json文件内容
244
+ * @returns {Object} 包含成功状态和跳过的依赖信息
245
+ */
246
+ export async function generatePackageLockJson(workDir, PackageJsonInfo) {
247
+ // 1. 过滤特殊依赖
248
+ const { filteredPackageJson, skippedDependencies } = filterSpecialDependencies(PackageJsonInfo);
249
+
250
+ // 2. 将过滤后的package.json文件内容写入到工作目录的package.json文件中
251
+ await fs.promises.writeFile(
252
+ path.join(workDir, "package.json"),
253
+ JSON.stringify(filteredPackageJson, null, 2),
254
+ );
255
+
256
+ // console.log('filteredPackageJson', filteredPackageJson);
257
+
258
+
259
+ // 3. 执行 npm install --package-lock-only --ignore-scripts --force 命令
260
+ const cmd = "npm install --package-lock-only --ignore-scripts --force --registry=https://registry.npmjs.org";
261
+
262
+ try {
263
+ await execAsync(cmd, {
264
+ cwd: workDir,
265
+ env: process.env, // 继承当前环境变量
266
+ });
267
+
268
+ return {
269
+ success: true,
270
+ skippedDependencies
271
+ };
272
+ } catch (error) {
273
+ // 如果命令失败,返回错误信息和跳过的依赖
274
+ console.error(`npm install failed: ${error.message}`);
275
+ return {
276
+ success: false,
277
+ error: error.message,
278
+ skippedDependencies
279
+ };
280
+ }
281
+ }
282
+
283
+ /**
284
+ * 执行npm audit命令
285
+ * @param workDir 工作目录路径
286
+ * @returns audit结果
287
+ */
288
+ export async function projectAudit(workDir) {
289
+ // 指定返回成json格式的数据
290
+ const cmd = "npm audit --registry=https://registry.npmjs.org --json";
291
+ try {
292
+ // 情况 1: 无漏洞 -> 正常返回 stdout
293
+ const { stdout } = await execAsync(cmd, { cwd: workDir });
294
+ try {
295
+ JSON.parse(stdout);
296
+ return stdout;
297
+ } catch (parseError) {
298
+ console.error(
299
+ `Invalid JSON returned from npm audit: ${parseError.message}`,
300
+ );
301
+ console.error(`Raw output: ${stdout.substring(0, 200)}...`);
302
+ return JSON.stringify({
303
+ error: "Invalid JSON response from npm audit",
304
+ raw: stdout.substring(0, 500),
305
+ });
306
+ }
307
+ } catch (error) {
308
+ // 情况 2: 有漏洞 -> npm 会报错,但报告内容在 error.stdout 里
309
+ // 直接返回它,忽略错误本身
310
+ if (error.stdout) {
311
+ try {
312
+ JSON.parse(error.stdout);
313
+ return error.stdout;
314
+ } catch (parseError) {
315
+ console.error(`Invalid JSON in error.stdout: ${parseError.message}`);
316
+ console.error(`Raw error output: ${error.stdout.substring(0, 200)}...`);
317
+ return JSON.stringify({
318
+ error: "Invalid JSON in npm audit error response",
319
+ raw: error.stdout.substring(0, 500),
320
+ });
321
+ }
322
+ }
323
+ // 极端情况:连 stdout 都没有(比如命令不存在),则返回错误信息
324
+ return JSON.stringify({ error: error.message || "Unknown audit error" });
325
+ }
326
+ }
327
+
328
+ /**
329
+ * 根据输出格式渲染审计结果
330
+ * @param {string} auditResult JSON格式的npm audit结果
331
+ * @param {Object} packageJsonInfo package.json文件内容
332
+ * @param {string} outputFormat 输出格式:md、html、txt、dashboard
333
+ * @param {string} savePath 保存路径(不含扩展名)
334
+ * @returns {Promise<{filePath: string, outputFormat: string}>} 返回文件路径和输出格式
335
+ */
336
+ export async function renderAuditResultByFormat(
337
+ auditResult,
338
+ packageJsonInfo,
339
+ outputFormat = "md",
340
+ savePath
341
+ ) {
342
+ // 导入渲染模块
343
+ const { renderAuditResult } = await import("./render/renderAuditResult.js");
344
+ const { renderAuditResultHTML } = await import("./render/renderAuditResultHTML.js");
345
+ const { renderAuditResultTXT } = await import("./render/renderAuditResultTXT.js");
346
+ const { renderAuditResultDashboard } = await import("./render/renderAuditResultDashboard.js");
347
+
348
+ // 定义格式扩展名映射
349
+ const formatExtensions = {
350
+ md: ".md",
351
+ html: ".html",
352
+ txt: ".txt",
353
+ dashboard: ".html",
354
+ };
355
+
356
+ const extension = formatExtensions[outputFormat] || ".md";
357
+ const fullSavePath = savePath.endsWith(extension)
358
+ ? savePath
359
+ : `${savePath}${extension}`;
360
+
361
+ let filePath;
362
+
363
+ switch (outputFormat) {
364
+ case "html":
365
+ await renderAuditResultHTML(auditResult, packageJsonInfo, fullSavePath);
366
+ filePath = fullSavePath;
367
+ break;
368
+ case "txt":
369
+ await renderAuditResultTXT(auditResult, packageJsonInfo, fullSavePath);
370
+ filePath = fullSavePath;
371
+ break;
372
+ case "dashboard":
373
+ await renderAuditResultDashboard(auditResult, packageJsonInfo, fullSavePath);
374
+ filePath = fullSavePath;
375
+ break;
376
+ case "md":
377
+ default:
378
+ await renderAuditResult(auditResult, packageJsonInfo, fullSavePath);
379
+ filePath = fullSavePath;
380
+ break;
381
+ }
382
+
383
+ return { filePath, outputFormat };
384
+ }