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,466 @@
|
|
|
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 renderAuditResultDashboard(
|
|
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 metadata = auditData.metadata;
|
|
46
|
+
const vulnerabilities = auditData.vulnerabilities || {};
|
|
47
|
+
|
|
48
|
+
// 漏洞统计
|
|
49
|
+
const vulnStats = metadata?.vulnerabilities || {};
|
|
50
|
+
const vulnData = [
|
|
51
|
+
{
|
|
52
|
+
severity: "critical",
|
|
53
|
+
count: vulnStats.critical || 0,
|
|
54
|
+
color: "#d32f2f",
|
|
55
|
+
},
|
|
56
|
+
{ severity: "high", count: vulnStats.high || 0, color: "#f57c00" },
|
|
57
|
+
{
|
|
58
|
+
severity: "moderate",
|
|
59
|
+
count: vulnStats.moderate || 0,
|
|
60
|
+
color: "#fbc02d",
|
|
61
|
+
},
|
|
62
|
+
{ severity: "low", count: vulnStats.low || 0, color: "#388e3c" },
|
|
63
|
+
{ severity: "info", count: vulnStats.info || 0, color: "#1976d2" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// 依赖统计
|
|
67
|
+
const depStats = metadata?.dependencies || {};
|
|
68
|
+
const depData = [
|
|
69
|
+
{ type: "生产依赖", count: depStats.prod || 0, color: "#3498db" },
|
|
70
|
+
{ type: "开发依赖", count: depStats.dev || 0, color: "#9b59b6" },
|
|
71
|
+
{ type: "可选依赖", count: depStats.optional || 0, color: "#e74c3c" },
|
|
72
|
+
{ type: "Peer依赖", count: depStats.peer || 0, color: "#f39c12" },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// 漏洞包统计
|
|
76
|
+
const vulnPackages = Object.keys(vulnerabilities);
|
|
77
|
+
const severityCounts = {};
|
|
78
|
+
vulnPackages.forEach((pkgName) => {
|
|
79
|
+
const severity = vulnerabilities[pkgName].severity || "unknown";
|
|
80
|
+
severityCounts[severity] = (severityCounts[severity] || 0) + 1;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 获取项目显示名称
|
|
84
|
+
const getProjectDisplayName = (pkgInfo) => {
|
|
85
|
+
// 优先级 1: package.json 中的 name 属性
|
|
86
|
+
if (pkgInfo.name) {
|
|
87
|
+
return pkgInfo.name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 优先级 2: GitHub 仓库信息
|
|
91
|
+
if (pkgInfo._remote && pkgInfo._remote.repo) {
|
|
92
|
+
return pkgInfo._remote.repo;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 优先级 3: 项目路径(如果是本地项目)
|
|
96
|
+
if (pkgInfo._localPath) {
|
|
97
|
+
const pathParts = pkgInfo._localPath.split('/');
|
|
98
|
+
return pathParts[pathParts.length - 1] || "未命名项目";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 默认值
|
|
102
|
+
return "未命名项目";
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const projectDisplayName = getProjectDisplayName(packageJsonInfo);
|
|
106
|
+
|
|
107
|
+
// 获取项目版本
|
|
108
|
+
const projectVersion = packageJsonInfo.version ||
|
|
109
|
+
(packageJsonInfo._remote && packageJsonInfo._remote.branch) ||
|
|
110
|
+
"未知";
|
|
111
|
+
|
|
112
|
+
// 准备HTML内容
|
|
113
|
+
let html = `<!DOCTYPE html>
|
|
114
|
+
<html lang="zh-CN">
|
|
115
|
+
<head>
|
|
116
|
+
<meta charset="UTF-8">
|
|
117
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
118
|
+
<title>NPM Audit 可视化报告 - ${projectDisplayName}</title>
|
|
119
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
120
|
+
<style>
|
|
121
|
+
* { box-sizing: border-box; }
|
|
122
|
+
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #333; line-height: 1.6; }
|
|
123
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
124
|
+
.header { background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); margin-bottom: 30px; }
|
|
125
|
+
h1 { color: #2c3e50; margin: 0 0 10px 0; font-size: 2.5em; }
|
|
126
|
+
.subtitle { color: #7f8c8d; font-size: 1.2em; margin-bottom: 20px; }
|
|
127
|
+
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 30px; margin-bottom: 30px; }
|
|
128
|
+
.card { background: white; padding: 25px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); }
|
|
129
|
+
.card h2 { color: #34495e; margin-top: 0; padding-bottom: 10px; border-bottom: 2px solid #ecf0f1; }
|
|
130
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin: 20px 0; }
|
|
131
|
+
.stat-card { background: #f8f9fa; padding: 20px; border-radius: 10px; text-align: center; transition: transform 0.3s; }
|
|
132
|
+
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
|
|
133
|
+
.stat-value { font-size: 2.5em; font-weight: bold; margin: 10px 0; }
|
|
134
|
+
.stat-label { color: #7f8c8d; font-size: 0.9em; }
|
|
135
|
+
.severity-critical { color: #d32f2f; }
|
|
136
|
+
.severity-high { color: #f57c00; }
|
|
137
|
+
.severity-moderate { color: #fbc02d; }
|
|
138
|
+
.severity-low { color: #388e3c; }
|
|
139
|
+
.severity-info { color: #1976d2; }
|
|
140
|
+
.chart-container { position: relative; height: 300px; margin: 20px 0; }
|
|
141
|
+
.vuln-list { height: 100%; overflow-y: auto; }
|
|
142
|
+
.vuln-item { padding: 15px; border-bottom: 1px solid #ecf0f1; }
|
|
143
|
+
.vuln-item:last-child { border-bottom: none; }
|
|
144
|
+
.vuln-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
145
|
+
.vuln-name { font-weight: 600; font-size: 1.1em; }
|
|
146
|
+
.vuln-severity { padding: 5px 10px; border-radius: 20px; font-size: 0.8em; font-weight: 600; }
|
|
147
|
+
.severity-critical-bg { background: #ffebee; color: #d32f2f; }
|
|
148
|
+
.severity-high-bg { background: #fff3e0; color: #f57c00; }
|
|
149
|
+
.severity-moderate-bg { background: #fffde7; color: #fbc02d; }
|
|
150
|
+
.severity-low-bg { background: #e8f5e9; color: #388e3c; }
|
|
151
|
+
.severity-info-bg { background: #e3f2fd; color: #1976d2; }
|
|
152
|
+
.vuln-details { font-size: 0.9em; color: #666; }
|
|
153
|
+
.summary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin-top: 30px; }
|
|
154
|
+
.summary h2 { color: white; border-bottom: 2px solid rgba(255,255,255,0.3); }
|
|
155
|
+
.summary-content { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
|
156
|
+
.summary-item { background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; }
|
|
157
|
+
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.2); color: rgba(255,255,255,0.8); font-size: 0.9em; text-align: center; }
|
|
158
|
+
.timestamp { color: #7f8c8d; font-size: 0.9em; margin-top: 10px; }
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div class="container">
|
|
163
|
+
<div class="header">
|
|
164
|
+
<h1>📊 NPM Audit 可视化报告</h1>
|
|
165
|
+
<div class="subtitle">${projectDisplayName} - 版本 ${projectVersion}</div>
|
|
166
|
+
<div class="timestamp">审计时间: ${new Date().toLocaleString("zh-CN")}</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="dashboard-grid">
|
|
170
|
+
<!-- 漏洞统计卡片 -->
|
|
171
|
+
<div class="card">
|
|
172
|
+
<h2>🔍 漏洞统计</h2>
|
|
173
|
+
<div class="stats-grid">
|
|
174
|
+
<div class="stat-card">
|
|
175
|
+
<div class="stat-label">严重漏洞</div>
|
|
176
|
+
<div class="stat-value severity-critical">${vulnStats.critical || 0}</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="stat-card">
|
|
179
|
+
<div class="stat-label">高危漏洞</div>
|
|
180
|
+
<div class="stat-value severity-high">${vulnStats.high || 0}</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="stat-card">
|
|
183
|
+
<div class="stat-label">中危漏洞</div>
|
|
184
|
+
<div class="stat-value severity-moderate">${vulnStats.moderate || 0}</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="stat-card">
|
|
187
|
+
<div class="stat-label">低危漏洞</div>
|
|
188
|
+
<div class="stat-value severity-low">${vulnStats.low || 0}</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="stat-card">
|
|
191
|
+
<div class="stat-label">信息漏洞</div>
|
|
192
|
+
<div class="stat-value severity-info">${vulnStats.info || 0}</div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="stat-card">
|
|
195
|
+
<div class="stat-label">总计</div>
|
|
196
|
+
<div class="stat-value">${vulnStats.total || 0}</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="chart-container">
|
|
200
|
+
<canvas id="vulnerabilityChart"></canvas>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- 依赖统计卡片 -->
|
|
205
|
+
<div class="card">
|
|
206
|
+
<h2>📦 依赖统计</h2>
|
|
207
|
+
<div class="stats-grid">
|
|
208
|
+
<div class="stat-card">
|
|
209
|
+
<div class="stat-label">生产依赖</div>
|
|
210
|
+
<div class="stat-value">${depStats.prod || 0}</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="stat-card">
|
|
213
|
+
<div class="stat-label">开发依赖</div>
|
|
214
|
+
<div class="stat-value">${depStats.dev || 0}</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="stat-card">
|
|
217
|
+
<div class="stat-label">可选依赖</div>
|
|
218
|
+
<div class="stat-value">${depStats.optional || 0}</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="stat-card">
|
|
221
|
+
<div class="stat-label">Peer依赖</div>
|
|
222
|
+
<div class="stat-value">${depStats.peer || 0}</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="stat-card">
|
|
225
|
+
<div class="stat-label">总依赖数</div>
|
|
226
|
+
<div class="stat-value">${depStats.total || 0}</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="chart-container">
|
|
230
|
+
<canvas id="dependencyChart"></canvas>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- 漏洞列表卡片 -->
|
|
235
|
+
<div class="card">
|
|
236
|
+
<h2>📋 漏洞列表</h2>
|
|
237
|
+
<div class="vuln-list">`;
|
|
238
|
+
|
|
239
|
+
if (vulnPackages.length > 0) {
|
|
240
|
+
// 显示前10个漏洞
|
|
241
|
+
const displayPackages = vulnPackages.slice(0, 10);
|
|
242
|
+
displayPackages.forEach((pkgName) => {
|
|
243
|
+
const vuln = vulnerabilities[pkgName];
|
|
244
|
+
const severity = vuln.severity || "unknown";
|
|
245
|
+
const severityBgClass = `severity-${severity}-bg`;
|
|
246
|
+
|
|
247
|
+
html += `
|
|
248
|
+
<div class="vuln-item">
|
|
249
|
+
<div class="vuln-header">
|
|
250
|
+
<div class="vuln-name">${pkgName}</div>
|
|
251
|
+
<div class="vuln-severity ${severityBgClass}">${severity.toUpperCase()}</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="vuln-details">
|
|
254
|
+
受影响版本: ${vuln.range || "未知"} |
|
|
255
|
+
直接依赖: ${vuln.isDirect ? "是" : "否"}<br>`;
|
|
256
|
+
|
|
257
|
+
if (vuln.fixAvailable && vuln.fixAvailable !== false) {
|
|
258
|
+
html += `修复建议: 升级到 ${vuln.fixAvailable.version || "最新版本"}`;
|
|
259
|
+
} else {
|
|
260
|
+
html += `修复建议: ${vuln.fixAvailable === false ? "暂无可用修复" : "检查更新"}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
html += `
|
|
264
|
+
</div>
|
|
265
|
+
</div>`;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (vulnPackages.length > 10) {
|
|
269
|
+
html += `
|
|
270
|
+
<div class="vuln-item" style="text-align: center; color: #7f8c8d;">
|
|
271
|
+
还有 ${vulnPackages.length - 10} 个漏洞未显示...
|
|
272
|
+
</div>`;
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
html += `
|
|
276
|
+
<div class="vuln-item" style="text-align: center; color: #388e3c; padding: 40px 20px;">
|
|
277
|
+
<div style="font-size: 2em;">✅</div>
|
|
278
|
+
<div style="font-size: 1.2em; font-weight: 600; margin: 10px 0;">恭喜!</div>
|
|
279
|
+
<div>未发现任何安全漏洞</div>
|
|
280
|
+
</div>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
html += `
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- 修复建议总结 -->
|
|
289
|
+
<div class="summary">
|
|
290
|
+
<h2>🛠️ 修复建议</h2>
|
|
291
|
+
<div class="summary-content">`;
|
|
292
|
+
|
|
293
|
+
const vulnerabilitiesObj = auditData.vulnerabilities || {};
|
|
294
|
+
const hasFixAvailable = Object.values(vulnerabilitiesObj).some(
|
|
295
|
+
(vuln) => vuln.fixAvailable && vuln.fixAvailable !== false,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (hasFixAvailable) {
|
|
299
|
+
html += `
|
|
300
|
+
<div class="summary-item">
|
|
301
|
+
<h3>可用修复</h3>
|
|
302
|
+
<p>以下依赖有可用修复版本:</p>
|
|
303
|
+
<ul style="margin-left: 20px;">`;
|
|
304
|
+
|
|
305
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
306
|
+
if (vuln.fixAvailable && vuln.fixAvailable !== false) {
|
|
307
|
+
const fixVersion = vuln.fixAvailable.version || "最新版本";
|
|
308
|
+
html += `<li><strong>${pkgName}:</strong> 升级到 ${fixVersion}</li>`;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
html += `
|
|
313
|
+
</ul>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="summary-item">
|
|
316
|
+
<h3>修复命令</h3>
|
|
317
|
+
<p>执行以下命令修复漏洞:</p>
|
|
318
|
+
<pre style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 5px; overflow: auto;">`;
|
|
319
|
+
|
|
320
|
+
Object.entries(vulnerabilitiesObj).forEach(([pkgName, vuln]) => {
|
|
321
|
+
if (
|
|
322
|
+
vuln.fixAvailable &&
|
|
323
|
+
vuln.fixAvailable !== false &&
|
|
324
|
+
vuln.fixAvailable.name
|
|
325
|
+
) {
|
|
326
|
+
const fixVersion = vuln.fixAvailable.version || "latest";
|
|
327
|
+
html += `npm install ${vuln.fixAvailable.name}@${fixVersion}\n`;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
html += `
|
|
332
|
+
</pre>
|
|
333
|
+
</div>`;
|
|
334
|
+
} else if (Object.keys(vulnerabilitiesObj).length > 0) {
|
|
335
|
+
html += `
|
|
336
|
+
<div class="summary-item">
|
|
337
|
+
<h3>⚠️ 注意</h3>
|
|
338
|
+
<p>发现漏洞但暂无直接修复方案,请考虑以下替代方案:</p>
|
|
339
|
+
<ol>
|
|
340
|
+
<li>查看是否有可用的次要版本更新</li>
|
|
341
|
+
<li>考虑使用替代包</li>
|
|
342
|
+
<li>评估风险是否可接受</li>
|
|
343
|
+
</ol>
|
|
344
|
+
</div>`;
|
|
345
|
+
} else {
|
|
346
|
+
html += `
|
|
347
|
+
<div class="summary-item">
|
|
348
|
+
<h3>✅ 安全状态</h3>
|
|
349
|
+
<p>项目目前没有安全漏洞,无需修复。</p>
|
|
350
|
+
<p>继续保持良好的安全实践!</p>
|
|
351
|
+
</div>`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
html += `
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<!-- 图表脚本 -->
|
|
359
|
+
<script>
|
|
360
|
+
// 漏洞统计图表
|
|
361
|
+
const vulnCtx = document.getElementById('vulnerabilityChart').getContext('2d');
|
|
362
|
+
new Chart(vulnCtx, {
|
|
363
|
+
type: 'doughnut',
|
|
364
|
+
data: {
|
|
365
|
+
labels: ${JSON.stringify(vulnData.map((d) => d.severity.toUpperCase()))},
|
|
366
|
+
datasets: [{
|
|
367
|
+
data: ${JSON.stringify(vulnData.map((d) => d.count))},
|
|
368
|
+
backgroundColor: ${JSON.stringify(vulnData.map((d) => d.color))},
|
|
369
|
+
borderWidth: 2,
|
|
370
|
+
borderColor: 'white'
|
|
371
|
+
}]
|
|
372
|
+
},
|
|
373
|
+
options: {
|
|
374
|
+
responsive: true,
|
|
375
|
+
maintainAspectRatio: false,
|
|
376
|
+
plugins: {
|
|
377
|
+
legend: {
|
|
378
|
+
position: 'bottom',
|
|
379
|
+
labels: {
|
|
380
|
+
padding: 20,
|
|
381
|
+
usePointStyle: true
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
tooltip: {
|
|
385
|
+
callbacks: {
|
|
386
|
+
label: function(context) {
|
|
387
|
+
return context.label + ': ' + context.raw;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// 依赖统计图表
|
|
396
|
+
const depCtx = document.getElementById('dependencyChart').getContext('2d');
|
|
397
|
+
new Chart(depCtx, {
|
|
398
|
+
type: 'bar',
|
|
399
|
+
data: {
|
|
400
|
+
labels: ${JSON.stringify(depData.map((d) => d.type))},
|
|
401
|
+
datasets: [{
|
|
402
|
+
label: '依赖数量',
|
|
403
|
+
data: ${JSON.stringify(depData.map((d) => d.count))},
|
|
404
|
+
backgroundColor: ${JSON.stringify(depData.map((d) => d.color))},
|
|
405
|
+
borderWidth: 1,
|
|
406
|
+
borderColor: 'rgba(0,0,0,0.1)'
|
|
407
|
+
}]
|
|
408
|
+
},
|
|
409
|
+
options: {
|
|
410
|
+
responsive: true,
|
|
411
|
+
maintainAspectRatio: false,
|
|
412
|
+
scales: {
|
|
413
|
+
y: {
|
|
414
|
+
beginAtZero: true,
|
|
415
|
+
ticks: {
|
|
416
|
+
stepSize: 1
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
plugins: {
|
|
421
|
+
legend: {
|
|
422
|
+
display: false
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
</script>
|
|
428
|
+
|
|
429
|
+
<div class="footer">
|
|
430
|
+
<p>本报告基于 <code>npm audit --json</code> 命令生成 | 建议定期运行 <code>npm audit</code> 检查项目安全状况</p>
|
|
431
|
+
<p>更多信息请参考 <a href="https://docs.npmjs.com/auditing-package-dependencies-for-security-vulnerabilities" target="_blank" style="color: white; text-decoration: underline;">npm 安全文档</a></p>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</body>
|
|
435
|
+
</html>`;
|
|
436
|
+
|
|
437
|
+
// 将生成的HTML写入文件
|
|
438
|
+
await fs.promises.writeFile(savePath, html);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error(`Error rendering Dashboard audit result: ${error.message}`);
|
|
441
|
+
const errorHtml = `<!DOCTYPE html>
|
|
442
|
+
<html lang="zh-CN">
|
|
443
|
+
<head>
|
|
444
|
+
<meta charset="UTF-8">
|
|
445
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
446
|
+
<title>NPM Audit 报告渲染错误</title>
|
|
447
|
+
<style>
|
|
448
|
+
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
|
449
|
+
.error { color: #d32f2f; background: #ffebee; padding: 20px; border-radius: 5px; border-left: 5px solid #d32f2f; }
|
|
450
|
+
h1 { color: #333; }
|
|
451
|
+
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow: auto; }
|
|
452
|
+
</style>
|
|
453
|
+
</head>
|
|
454
|
+
<body>
|
|
455
|
+
<h1>NPM Audit 报告渲染错误</h1>
|
|
456
|
+
<div class="error">
|
|
457
|
+
<h2>❌ 错误</h2>
|
|
458
|
+
<p>${error.message}</p>
|
|
459
|
+
</div>
|
|
460
|
+
<h3>原始结果(前1000字符):</h3>
|
|
461
|
+
<pre>${auditResult.substring(0, 1000)}</pre>
|
|
462
|
+
</body>
|
|
463
|
+
</html>`;
|
|
464
|
+
await fs.promises.writeFile(savePath, errorHtml);
|
|
465
|
+
}
|
|
466
|
+
}
|