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/LICENSE +15 -0
- package/README.md +175 -0
- package/package.json +49 -0
- package/src/index.js +31 -0
- package/src/render/renderAuditResult.js +229 -0
- package/src/render/renderAuditResultDashboard.js +466 -0
- package/src/render/renderAuditResultHTML.js +408 -0
- package/src/render/renderAuditResultTXT.js +185 -0
- package/src/server.js +42 -0
- package/src/utils.js +384 -0
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
|
+
}
|