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
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 渲染npm audit结果到HTML格式
|
|
5
|
+
* @param {string} auditResult JSON格式的npm audit结果
|
|
6
|
+
* @param {Object} packageJsonInfo package.json文件内容
|
|
7
|
+
* @param {string} savePath 保存路径
|
|
8
|
+
* @returns {string} HTML格式的报告
|
|
9
|
+
*/
|
|
10
|
+
export async function renderAuditResultHTML(
|
|
11
|
+
auditResult,
|
|
12
|
+
packageJsonInfo,
|
|
13
|
+
savePath,
|
|
14
|
+
) {
|
|
15
|
+
try {
|
|
16
|
+
const auditData = JSON.parse(auditResult);
|
|
17
|
+
|
|
18
|
+
// 检查是否有错误
|
|
19
|
+
if (auditData.error) {
|
|
20
|
+
const html = `<!DOCTYPE html>
|
|
21
|
+
<html lang="zh-CN">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="UTF-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
25
|
+
<title>NPM Audit 报告 - 错误</title>
|
|
26
|
+
<style>
|
|
27
|
+
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
|
28
|
+
.error { color: #d32f2f; background: #ffebee; padding: 20px; border-radius: 5px; border-left: 5px solid #d32f2f; }
|
|
29
|
+
h1 { color: #333; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<h1>NPM Audit 报告</h1>
|
|
34
|
+
<div class="error">
|
|
35
|
+
<h2>❌ 错误</h2>
|
|
36
|
+
<p>${auditData.error}</p>
|
|
37
|
+
</div>
|
|
38
|
+
</body>
|
|
39
|
+
</html>`;
|
|
40
|
+
await fs.promises.writeFile(savePath, html);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 获取项目显示名称
|
|
45
|
+
const getProjectDisplayName = (pkgInfo) => {
|
|
46
|
+
// 优先级 1: package.json 中的 name 属性
|
|
47
|
+
if (pkgInfo.name) {
|
|
48
|
+
return pkgInfo.name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 优先级 2: GitHub 仓库信息
|
|
52
|
+
if (pkgInfo._remote && pkgInfo._remote.repo) {
|
|
53
|
+
return pkgInfo._remote.repo;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 优先级 3: 项目路径(如果是本地项目)
|
|
57
|
+
if (pkgInfo._localPath) {
|
|
58
|
+
const pathParts = pkgInfo._localPath.split('/');
|
|
59
|
+
return pathParts[pathParts.length - 1] || "未命名项目";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 默认值
|
|
63
|
+
return "未命名项目";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const projectDisplayName = getProjectDisplayName(packageJsonInfo);
|
|
67
|
+
|
|
68
|
+
// 获取项目版本
|
|
69
|
+
const projectVersion = packageJsonInfo.version ||
|
|
70
|
+
(packageJsonInfo._remote && packageJsonInfo._remote.branch) ||
|
|
71
|
+
"未知";
|
|
72
|
+
|
|
73
|
+
// 准备HTML内容
|
|
74
|
+
let html = `<!DOCTYPE html>
|
|
75
|
+
<html lang="zh-CN">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="UTF-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
79
|
+
<title>NPM Audit 安全审计报告 - ${projectDisplayName}</title>
|
|
80
|
+
<style>
|
|
81
|
+
* { box-sizing: border-box; }
|
|
82
|
+
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; color: #333; line-height: 1.6; }
|
|
83
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
84
|
+
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
|
|
85
|
+
h2 { color: #34495e; margin-top: 30px; padding-bottom: 5px; border-bottom: 2px solid #ecf0f1; }
|
|
86
|
+
h3 { color: #7f8c8d; }
|
|
87
|
+
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; }
|
|
88
|
+
.info-card { background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #3498db; }
|
|
89
|
+
.stats-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
90
|
+
.stats-table th, .stats-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
91
|
+
.stats-table th { background: #f8f9fa; font-weight: 600; }
|
|
92
|
+
.severity-critical { color: #d32f2f; font-weight: bold; }
|
|
93
|
+
.severity-high { color: #f57c00; font-weight: bold; }
|
|
94
|
+
.severity-moderate { color: #fbc02d; font-weight: bold; }
|
|
95
|
+
.severity-low { color: #388e3c; font-weight: bold; }
|
|
96
|
+
.severity-info { color: #1976d2; font-weight: bold; }
|
|
97
|
+
.vuln-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 5px; padding: 20px; margin: 15px 0; }
|
|
98
|
+
.vuln-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
|
99
|
+
.vuln-title { font-size: 1.2em; font-weight: 600; }
|
|
100
|
+
.vuln-severity { padding: 5px 10px; border-radius: 3px; font-size: 0.9em; }
|
|
101
|
+
.severity-critical-bg { background: #ffebee; color: #d32f2f; }
|
|
102
|
+
.severity-high-bg { background: #fff3e0; color: #f57c00; }
|
|
103
|
+
.severity-moderate-bg { background: #fffde7; color: #fbc02d; }
|
|
104
|
+
.severity-low-bg { background: #e8f5e9; color: #388e3c; }
|
|
105
|
+
.severity-info-bg { background: #e3f2fd; color: #1976d2; }
|
|
106
|
+
.details-table { width: 100%; border-collapse: collapse; margin: 10px 0; }
|
|
107
|
+
.details-table th, .details-table td { padding: 8px 12px; border: 1px solid #e0e0e0; }
|
|
108
|
+
.details-table th { background: #f5f5f5; font-weight: 500; }
|
|
109
|
+
.fix-commands { background: #f5f5f5; padding: 15px; border-radius: 5px; font-family: monospace; white-space: pre-wrap; }
|
|
110
|
+
.success { color: #388e3c; background: #e8f5e9; padding: 15px; border-radius: 5px; }
|
|
111
|
+
.warning { color: #f57c00; background: #fff3e0; padding: 15px; border-radius: 5px; }
|
|
112
|
+
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e0e0e0; color: #666; font-size: 0.9em; }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div class="container">
|
|
117
|
+
<h1>NPM Audit 安全审计报告</h1>
|
|
118
|
+
|
|
119
|
+
<!-- 项目信息 -->
|
|
120
|
+
<section>
|
|
121
|
+
<h2>项目信息</h2>
|
|
122
|
+
<div class="info-grid">
|
|
123
|
+
<div class="info-card">
|
|
124
|
+
<strong>项目名称:</strong> ${projectDisplayName}
|
|
125
|
+
</div>
|
|
126
|
+
<div class="info-card">
|
|
127
|
+
<strong>项目版本:</strong> ${projectVersion}
|
|
128
|
+
</div>
|
|
129
|
+
<div class="info-card">
|
|
130
|
+
<strong>审计时间:</strong> ${new Date().toLocaleString("zh-CN")}
|
|
131
|
+
</div>
|
|
132
|
+
<div class="info-card">
|
|
133
|
+
<strong>审计工具:</strong> npm audit
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</section>`;
|
|
137
|
+
|
|
138
|
+
// 漏洞统计摘要
|
|
139
|
+
const metadata = auditData.metadata;
|
|
140
|
+
if (metadata && metadata.vulnerabilities) {
|
|
141
|
+
const vulnStats = metadata.vulnerabilities;
|
|
142
|
+
html += `
|
|
143
|
+
<section>
|
|
144
|
+
<h2>漏洞统计摘要</h2>
|
|
145
|
+
<table class="stats-table">
|
|
146
|
+
<thead>
|
|
147
|
+
<tr>
|
|
148
|
+
<th>严重级别</th>
|
|
149
|
+
<th>数量</th>
|
|
150
|
+
</tr>
|
|
151
|
+
</thead>
|
|
152
|
+
<tbody>
|
|
153
|
+
<tr>
|
|
154
|
+
<td class="severity-critical">严重 (critical)</td>
|
|
155
|
+
<td>${vulnStats.critical || 0}</td>
|
|
156
|
+
</tr>
|
|
157
|
+
<tr>
|
|
158
|
+
<td class="severity-high">高 (high)</td>
|
|
159
|
+
<td>${vulnStats.high || 0}</td>
|
|
160
|
+
</tr>
|
|
161
|
+
<tr>
|
|
162
|
+
<td class="severity-moderate">中 (moderate)</td>
|
|
163
|
+
<td>${vulnStats.moderate || 0}</td>
|
|
164
|
+
</tr>
|
|
165
|
+
<tr>
|
|
166
|
+
<td class="severity-low">低 (low)</td>
|
|
167
|
+
<td>${vulnStats.low || 0}</td>
|
|
168
|
+
</tr>
|
|
169
|
+
<tr>
|
|
170
|
+
<td class="severity-info">信息 (info)</td>
|
|
171
|
+
<td>${vulnStats.info || 0}</td>
|
|
172
|
+
</tr>
|
|
173
|
+
<tr>
|
|
174
|
+
<td><strong>总计</strong></td>
|
|
175
|
+
<td><strong>${vulnStats.total || 0}</strong></td>
|
|
176
|
+
</tr>
|
|
177
|
+
</tbody>
|
|
178
|
+
</table>
|
|
179
|
+
</section>`;
|
|
180
|
+
|
|
181
|
+
// 依赖统计
|
|
182
|
+
const depStats = metadata.dependencies;
|
|
183
|
+
if (depStats) {
|
|
184
|
+
html += `
|
|
185
|
+
<section>
|
|
186
|
+
<h2>依赖统计</h2>
|
|
187
|
+
<div class="info-grid">
|
|
188
|
+
<div class="info-card">
|
|
189
|
+
<strong>生产依赖:</strong> ${depStats.prod || 0}
|
|
190
|
+
</div>
|
|
191
|
+
<div class="info-card">
|
|
192
|
+
<strong>开发依赖:</strong> ${depStats.dev || 0}
|
|
193
|
+
</div>
|
|
194
|
+
<div class="info-card">
|
|
195
|
+
<strong>可选依赖:</strong> ${depStats.optional || 0}
|
|
196
|
+
</div>
|
|
197
|
+
<div class="info-card">
|
|
198
|
+
<strong>Peer依赖:</strong> ${depStats.peer || 0}
|
|
199
|
+
</div>
|
|
200
|
+
<div class="info-card">
|
|
201
|
+
<strong>总依赖数:</strong> ${depStats.total || 0}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</section>`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 详细漏洞列表
|
|
209
|
+
const vulnerabilities = auditData.vulnerabilities;
|
|
210
|
+
if (vulnerabilities && Object.keys(vulnerabilities).length > 0) {
|
|
211
|
+
html += `
|
|
212
|
+
<section>
|
|
213
|
+
<h2>详细漏洞列表</h2>`;
|
|
214
|
+
|
|
215
|
+
Object.entries(vulnerabilities).forEach(([pkgName, vuln]) => {
|
|
216
|
+
const severity = vuln.severity || "unknown";
|
|
217
|
+
const severityClass = `severity-${severity}`;
|
|
218
|
+
const severityBgClass = `severity-${severity}-bg`;
|
|
219
|
+
|
|
220
|
+
html += `
|
|
221
|
+
<div class="vuln-card">
|
|
222
|
+
<div class="vuln-header">
|
|
223
|
+
<div class="vuln-title">${pkgName}</div>
|
|
224
|
+
<div class="vuln-severity ${severityBgClass}">${severity.toUpperCase()}</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<table class="details-table">
|
|
228
|
+
<tr>
|
|
229
|
+
<th>严重级别</th>
|
|
230
|
+
<td class="${severityClass}">${severity}</td>
|
|
231
|
+
</tr>
|
|
232
|
+
<tr>
|
|
233
|
+
<th>是否直接依赖</th>
|
|
234
|
+
<td>${vuln.isDirect ? "是" : "否"}</td>
|
|
235
|
+
</tr>
|
|
236
|
+
<tr>
|
|
237
|
+
<th>受影响版本</th>
|
|
238
|
+
<td>${vuln.range || "未知"}</td>
|
|
239
|
+
</tr>
|
|
240
|
+
<tr>
|
|
241
|
+
<th>修复建议</th>
|
|
242
|
+
<td>`;
|
|
243
|
+
|
|
244
|
+
if (vuln.fixAvailable) {
|
|
245
|
+
if (vuln.fixAvailable === false) {
|
|
246
|
+
html += `暂无可用修复`;
|
|
247
|
+
} else {
|
|
248
|
+
html += `升级到 ${vuln.fixAvailable.version || "最新版本"}`;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
html += `检查更新`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
html += `</td>
|
|
255
|
+
</tr>
|
|
256
|
+
</table>`;
|
|
257
|
+
|
|
258
|
+
// 漏洞详情
|
|
259
|
+
if (vuln.via && Array.isArray(vuln.via)) {
|
|
260
|
+
html += `
|
|
261
|
+
<h3>漏洞详情</h3>`;
|
|
262
|
+
|
|
263
|
+
vuln.via.forEach((issue, index) => {
|
|
264
|
+
if (typeof issue === "string") {
|
|
265
|
+
html += `<p><strong>${index + 1}. 间接漏洞:</strong> ${issue}</p>`;
|
|
266
|
+
} else {
|
|
267
|
+
html += `<p><strong>${index + 1}. ${issue.title || "未知漏洞"}</strong></p>
|
|
268
|
+
<ul>
|
|
269
|
+
<li><strong>严重级别:</strong> ${issue.severity || "未知"}</li>`;
|
|
270
|
+
|
|
271
|
+
if (issue.url) {
|
|
272
|
+
html += `<li><strong>参考链接:</strong> <a href="${issue.url}" target="_blank">${issue.url}</a></li>`;
|
|
273
|
+
}
|
|
274
|
+
if (issue.range) {
|
|
275
|
+
html += `<li><strong>受影响版本:</strong> ${issue.range}</li>`;
|
|
276
|
+
}
|
|
277
|
+
if (issue.cvss && issue.cvss.score !== undefined) {
|
|
278
|
+
html += `<li><strong>CVSS 评分:</strong> ${issue.cvss.score}</li>`;
|
|
279
|
+
}
|
|
280
|
+
if (issue.cwe && issue.cwe.length > 0) {
|
|
281
|
+
html += `<li><strong>CWE:</strong> ${issue.cwe.join(", ")}</li>`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
html += `</ul>`;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
html += `
|
|
290
|
+
</div>`;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
html += `
|
|
294
|
+
</section>`;
|
|
295
|
+
} else {
|
|
296
|
+
html += `
|
|
297
|
+
<section>
|
|
298
|
+
<h2>详细漏洞列表</h2>
|
|
299
|
+
<div class="success">
|
|
300
|
+
✅ <strong>恭喜!未发现任何安全漏洞。</strong>
|
|
301
|
+
</div>
|
|
302
|
+
</section>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 修复建议总结
|
|
306
|
+
html += `
|
|
307
|
+
<section>
|
|
308
|
+
<h2>修复建议总结</h2>`;
|
|
309
|
+
|
|
310
|
+
const vulnerabilitiesObj = auditData.vulnerabilities || {};
|
|
311
|
+
const hasFixAvailable = Object.values(vulnerabilitiesObj).some(
|
|
312
|
+
(vuln) => vuln.fixAvailable && vuln.fixAvailable !== false,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (hasFixAvailable) {
|
|
316
|
+
html += `
|
|
317
|
+
<p>以下依赖有可用修复:</p>
|
|
318
|
+
<ul>`;
|
|
319
|
+
|
|
320
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
321
|
+
if (vuln.fixAvailable && vuln.fixAvailable !== false) {
|
|
322
|
+
const fixVersion = vuln.fixAvailable.version || "最新版本";
|
|
323
|
+
html += `<li><strong>${pkgName}:</strong> 升级到版本 ${fixVersion}</li>`;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
html += `
|
|
328
|
+
</ul>
|
|
329
|
+
<p><strong>执行以下命令修复漏洞:</strong></p>
|
|
330
|
+
<div class="fix-commands">`;
|
|
331
|
+
|
|
332
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
333
|
+
if (
|
|
334
|
+
vuln.fixAvailable &&
|
|
335
|
+
vuln.fixAvailable !== false &&
|
|
336
|
+
vuln.fixAvailable.name
|
|
337
|
+
) {
|
|
338
|
+
const fixVersion = vuln.fixAvailable.version || "latest";
|
|
339
|
+
html += `npm install ${vuln.fixAvailable.name}@${fixVersion}\n`;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
html += `
|
|
344
|
+
</div>`;
|
|
345
|
+
} else if (Object.keys(vulnerabilitiesObj).length > 0) {
|
|
346
|
+
html += `
|
|
347
|
+
<div class="warning">
|
|
348
|
+
⚠️ <strong>注意:</strong> 发现漏洞但暂无直接修复方案,请考虑以下替代方案:
|
|
349
|
+
<ol>
|
|
350
|
+
<li>查看是否有可用的次要版本更新</li>
|
|
351
|
+
<li>考虑使用替代包</li>
|
|
352
|
+
<li>评估风险是否可接受</li>
|
|
353
|
+
</ol>
|
|
354
|
+
</div>`;
|
|
355
|
+
} else {
|
|
356
|
+
html += `
|
|
357
|
+
<div class="success">
|
|
358
|
+
✅ <strong>无需修复</strong> - 项目目前没有安全漏洞。
|
|
359
|
+
</div>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 结尾
|
|
363
|
+
html += `
|
|
364
|
+
</section>
|
|
365
|
+
|
|
366
|
+
<div class="footer">
|
|
367
|
+
<h3>报告说明</h3>
|
|
368
|
+
<ul>
|
|
369
|
+
<li>本报告基于 <code>npm audit --json</code> 命令生成</li>
|
|
370
|
+
<li>CVSS (Common Vulnerability Scoring System) 分数范围 0-10,分数越高风险越大</li>
|
|
371
|
+
<li>建议定期运行 <code>npm audit</code> 检查项目安全状况</li>
|
|
372
|
+
<li>更多信息请参考 <a href="https://docs.npmjs.com/auditing-package-dependencies-for-security-vulnerabilities" target="_blank">npm 安全文档</a></li>
|
|
373
|
+
</ul>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</body>
|
|
377
|
+
</html>`;
|
|
378
|
+
|
|
379
|
+
// 将生成的HTML写入文件
|
|
380
|
+
await fs.promises.writeFile(savePath, html);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error(`Error rendering HTML audit result: ${error.message}`);
|
|
383
|
+
const errorHtml = `<!DOCTYPE html>
|
|
384
|
+
<html lang="zh-CN">
|
|
385
|
+
<head>
|
|
386
|
+
<meta charset="UTF-8">
|
|
387
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
388
|
+
<title>NPM Audit 报告渲染错误</title>
|
|
389
|
+
<style>
|
|
390
|
+
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
|
391
|
+
.error { color: #d32f2f; background: #ffebee; padding: 20px; border-radius: 5px; border-left: 5px solid #d32f2f; }
|
|
392
|
+
h1 { color: #333; }
|
|
393
|
+
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow: auto; }
|
|
394
|
+
</style>
|
|
395
|
+
</head>
|
|
396
|
+
<body>
|
|
397
|
+
<h1>NPM Audit 报告渲染错误</h1>
|
|
398
|
+
<div class="error">
|
|
399
|
+
<h2>❌ 错误</h2>
|
|
400
|
+
<p>${error.message}</p>
|
|
401
|
+
</div>
|
|
402
|
+
<h3>原始结果(前1000字符):</h3>
|
|
403
|
+
<pre>${auditResult.substring(0, 1000)}</pre>
|
|
404
|
+
</body>
|
|
405
|
+
</html>`;
|
|
406
|
+
await fs.promises.writeFile(savePath, errorHtml);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 渲染npm audit结果到纯文本格式
|
|
5
|
+
* @param {string} auditResult JSON格式的npm audit结果
|
|
6
|
+
* @param {Object} packageJsonInfo package.json文件内容
|
|
7
|
+
* @param {string} savePath 保存路径
|
|
8
|
+
* @returns {string} 纯文本格式的报告
|
|
9
|
+
*/
|
|
10
|
+
export async function renderAuditResultTXT(
|
|
11
|
+
auditResult,
|
|
12
|
+
packageJsonInfo,
|
|
13
|
+
savePath,
|
|
14
|
+
) {
|
|
15
|
+
try {
|
|
16
|
+
const auditData = JSON.parse(auditResult);
|
|
17
|
+
|
|
18
|
+
// 检查是否有错误
|
|
19
|
+
if (auditData.error) {
|
|
20
|
+
const txt = `NPM Audit 报告\n\n❌ 错误: ${auditData.error}\n\n`;
|
|
21
|
+
await fs.promises.writeFile(savePath, txt);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 准备纯文本内容
|
|
26
|
+
let txt = `NPM Audit 安全审计报告\n`;
|
|
27
|
+
txt += `========================================\n\n`;
|
|
28
|
+
|
|
29
|
+
// 项目信息
|
|
30
|
+
txt += `项目信息\n`;
|
|
31
|
+
txt += `--------\n`;
|
|
32
|
+
txt += `项目名称: ${packageJsonInfo.name || "未知"}\n`;
|
|
33
|
+
txt += `项目版本: ${packageJsonInfo.version || "未知"}\n`;
|
|
34
|
+
txt += `审计时间: ${new Date().toLocaleString("zh-CN")}\n`;
|
|
35
|
+
txt += `审计工具: npm audit\n\n`;
|
|
36
|
+
|
|
37
|
+
// 漏洞统计摘要
|
|
38
|
+
const metadata = auditData.metadata;
|
|
39
|
+
if (metadata && metadata.vulnerabilities) {
|
|
40
|
+
const vulnStats = metadata.vulnerabilities;
|
|
41
|
+
txt += `漏洞统计摘要\n`;
|
|
42
|
+
txt += `------------\n`;
|
|
43
|
+
txt += `严重级别 数量\n`;
|
|
44
|
+
txt += `----------------------\n`;
|
|
45
|
+
txt += `严重 (critical) ${vulnStats.critical || 0}\n`;
|
|
46
|
+
txt += `高 (high) ${vulnStats.high || 0}\n`;
|
|
47
|
+
txt += `中 (moderate) ${vulnStats.moderate || 0}\n`;
|
|
48
|
+
txt += `低 (low) ${vulnStats.low || 0}\n`;
|
|
49
|
+
txt += `信息 (info) ${vulnStats.info || 0}\n`;
|
|
50
|
+
txt += `----------------------\n`;
|
|
51
|
+
txt += `总计 ${vulnStats.total || 0}\n\n`;
|
|
52
|
+
|
|
53
|
+
// 依赖统计
|
|
54
|
+
const depStats = metadata.dependencies;
|
|
55
|
+
if (depStats) {
|
|
56
|
+
txt += `依赖统计\n`;
|
|
57
|
+
txt += `--------\n`;
|
|
58
|
+
txt += `生产依赖: ${depStats.prod || 0}\n`;
|
|
59
|
+
txt += `开发依赖: ${depStats.dev || 0}\n`;
|
|
60
|
+
txt += `可选依赖: ${depStats.optional || 0}\n`;
|
|
61
|
+
txt += `Peer依赖: ${depStats.peer || 0}\n`;
|
|
62
|
+
txt += `总依赖数: ${depStats.total || 0}\n\n`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 详细漏洞列表
|
|
67
|
+
const vulnerabilities = auditData.vulnerabilities;
|
|
68
|
+
if (vulnerabilities && Object.keys(vulnerabilities).length > 0) {
|
|
69
|
+
txt += `详细漏洞列表\n`;
|
|
70
|
+
txt += `------------\n\n`;
|
|
71
|
+
|
|
72
|
+
Object.entries(vulnerabilities).forEach(([pkgName, vuln]) => {
|
|
73
|
+
const severity = vuln.severity || "unknown";
|
|
74
|
+
const severitySymbol = {
|
|
75
|
+
critical: "[CRITICAL]",
|
|
76
|
+
high: "[HIGH]",
|
|
77
|
+
moderate: "[MODERATE]",
|
|
78
|
+
low: "[LOW]",
|
|
79
|
+
info: "[INFO]",
|
|
80
|
+
}[severity] || "[UNKNOWN]";
|
|
81
|
+
|
|
82
|
+
txt += `${severitySymbol} ${pkgName}\n`;
|
|
83
|
+
txt += `严重级别: ${severity}\n`;
|
|
84
|
+
txt += `是否直接依赖: ${vuln.isDirect ? "是" : "否"}\n`;
|
|
85
|
+
txt += `受影响版本: ${vuln.range || "未知"}\n`;
|
|
86
|
+
|
|
87
|
+
// 修复建议
|
|
88
|
+
if (vuln.fixAvailable) {
|
|
89
|
+
if (vuln.fixAvailable === false) {
|
|
90
|
+
txt += `修复建议: 暂无可用修复\n`;
|
|
91
|
+
} else {
|
|
92
|
+
txt += `修复建议: 升级到 ${vuln.fixAvailable.version || "最新版本"}\n`;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
txt += `修复建议: 检查更新\n`;
|
|
96
|
+
}
|
|
97
|
+
txt += `\n`;
|
|
98
|
+
|
|
99
|
+
// 漏洞详情
|
|
100
|
+
if (vuln.via && Array.isArray(vuln.via)) {
|
|
101
|
+
txt += `漏洞详情:\n`;
|
|
102
|
+
vuln.via.forEach((issue, index) => {
|
|
103
|
+
if (typeof issue === "string") {
|
|
104
|
+
txt += ` ${index + 1}. 间接漏洞: ${issue}\n`;
|
|
105
|
+
} else {
|
|
106
|
+
txt += ` ${index + 1}. ${issue.title || "未知漏洞"}\n`;
|
|
107
|
+
txt += ` 严重级别: ${issue.severity || "未知"}\n`;
|
|
108
|
+
if (issue.url) {
|
|
109
|
+
txt += ` 参考链接: ${issue.url}\n`;
|
|
110
|
+
}
|
|
111
|
+
if (issue.range) {
|
|
112
|
+
txt += ` 受影响版本: ${issue.range}\n`;
|
|
113
|
+
}
|
|
114
|
+
if (issue.cvss && issue.cvss.score !== undefined) {
|
|
115
|
+
txt += ` CVSS 评分: ${issue.cvss.score}\n`;
|
|
116
|
+
}
|
|
117
|
+
if (issue.cwe && issue.cwe.length > 0) {
|
|
118
|
+
txt += ` CWE: ${issue.cwe.join(", ")}\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
txt += `\n`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
txt += `----------------------------------------\n\n`;
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
txt += `详细漏洞列表\n`;
|
|
128
|
+
txt += `------------\n`;
|
|
129
|
+
txt += `✅ 恭喜!未发现任何安全漏洞。\n\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 修复建议总结
|
|
133
|
+
txt += `修复建议总结\n`;
|
|
134
|
+
txt += `------------\n`;
|
|
135
|
+
const vulnerabilitiesObj = auditData.vulnerabilities || {};
|
|
136
|
+
const hasFixAvailable = Object.values(vulnerabilitiesObj).some(
|
|
137
|
+
(vuln) => vuln.fixAvailable && vuln.fixAvailable !== false,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (hasFixAvailable) {
|
|
141
|
+
txt += `以下依赖有可用修复:\n\n`;
|
|
142
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
143
|
+
if (vuln.fixAvailable && vuln.fixAvailable !== false) {
|
|
144
|
+
const fixVersion = vuln.fixAvailable.version || "最新版本";
|
|
145
|
+
txt += `- ${pkgName}: 升级到版本 ${fixVersion}\n`;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
txt += `\n`;
|
|
149
|
+
txt += `执行以下命令修复漏洞:\n\n`;
|
|
150
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
151
|
+
if (
|
|
152
|
+
vuln.fixAvailable &&
|
|
153
|
+
vuln.fixAvailable !== false &&
|
|
154
|
+
vuln.fixAvailable.name
|
|
155
|
+
) {
|
|
156
|
+
const fixVersion = vuln.fixAvailable.version || "latest";
|
|
157
|
+
txt += `npm install ${vuln.fixAvailable.name}@${fixVersion}\n`;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
txt += `\n`;
|
|
161
|
+
} else if (Object.keys(vulnerabilitiesObj).length > 0) {
|
|
162
|
+
txt += `⚠️ 注意: 发现漏洞但暂无直接修复方案,请考虑以下替代方案:\n`;
|
|
163
|
+
txt += `1. 查看是否有可用的次要版本更新\n`;
|
|
164
|
+
txt += `2. 考虑使用替代包\n`;
|
|
165
|
+
txt += `3. 评估风险是否可接受\n\n`;
|
|
166
|
+
} else {
|
|
167
|
+
txt += `✅ 无需修复 - 项目目前没有安全漏洞。\n\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 结尾
|
|
171
|
+
txt += `报告说明\n`;
|
|
172
|
+
txt += `--------\n`;
|
|
173
|
+
txt += `- 本报告基于 'npm audit --json' 命令生成\n`;
|
|
174
|
+
txt += `- CVSS (Common Vulnerability Scoring System) 分数范围 0-10,分数越高风险越大\n`;
|
|
175
|
+
txt += `- 建议定期运行 'npm audit' 检查项目安全状况\n`;
|
|
176
|
+
txt += `- 更多信息请参考 https://docs.npmjs.com/auditing-package-dependencies-for-security-vulnerabilities\n`;
|
|
177
|
+
|
|
178
|
+
// 将生成的纯文本写入文件
|
|
179
|
+
await fs.promises.writeFile(savePath, txt);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Error rendering TXT audit result: ${error.message}`);
|
|
182
|
+
const errorTxt = `NPM Audit 报告渲染错误\n\n❌ 错误: ${error.message}\n\n原始结果(前1000字符):\n\n${auditResult.substring(0, 1000)}`;
|
|
183
|
+
await fs.promises.writeFile(savePath, errorTxt);
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { auditProject } from "./index.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "project-audit-server",
|
|
8
|
+
title: "项目审计服务",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// auditProject("https://github.com/facebook/react/tree/v19.2.3", "./audit-result.md", "md");
|
|
13
|
+
|
|
14
|
+
server.registerTool(
|
|
15
|
+
"auditProject",
|
|
16
|
+
{
|
|
17
|
+
title: "auditProject",
|
|
18
|
+
description: "审计项目",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
projectPath: z.string().describe("当前项目路径(支持本地和远程项目)"),
|
|
21
|
+
savePath: z.string().describe("审计结果保存路径(当前项目根目录,不含扩展名,将根据格式自动添加)"),
|
|
22
|
+
outputFormat: z
|
|
23
|
+
.enum(["md", "html", "txt", "dashboard"])
|
|
24
|
+
.default("md")
|
|
25
|
+
.describe("输出格式:md(Markdown)、html(HTML页面)、txt(纯文本)、dashboard(带图表的可视化页面)"),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async ({ projectPath, savePath, outputFormat = "md" }) => {
|
|
29
|
+
const result = await auditProject(projectPath, savePath, outputFormat);
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: `项目审计完成,输出格式:${outputFormat},文件路径:${result.filePath}`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const transport = new StdioServerTransport();
|
|
42
|
+
server.connect(transport);
|