MCP Audit安全审计
实现流程
- 创建工作目录:创建一个临时的工作目录,用于保存执行期间要用到的临时文件
- 解析工程:解析本地工程目录或者远程仓库链接,得到对应的
package.json文件内容 - 生成lock文件:将
package.json写入到临时工作目录,同时根据它生成package-lock.json - 安全审计:进入到临时工作目录,使用
npm audit --json命令进行安全审计,并讲审计结果规格化 - 渲染:将上一步得到的规格化审计结果进行渲染,渲染成标准化的markdown内容,并保存到结果文件
创建工作目录
js
import path from 'path';
import fs from 'fs';
import {fileURLToPath} from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const workBasePath = path.join(__dirname, 'work');
const createWorkDir = async () => {
const workDir = path.join(workBasePath);
await fs.promises.mkdir(workDir, {recursive: true});
return workDir;
};解析工程
判断工程是否为远程仓库
js
/**
* @param {string} projectRoot 项目根目录
* @return {object} package.json
* */
const EXTERNAL_LINK_HTTP = 'http://';
const EXTERNAL_LINK_HTTPS = 'https://';
const parseProject = async projectRoot => {
if (projectRoot.startsWith(EXTERNAL_LINK_HTTP) || projectRoot.startsWith(EXTERNAL_LINK_HTTPS)) {
return parseRemoteProject(projectRoot);
}
return parseLocalProject(projectRoot);
};本地工程解析
读取package.json内容
js
const parseLocalProject = async projectRoot => {
const packageJsonPath = path.join(projectRoot, 'package.json');
const json = await fs.promises.readFile(packageJsonPath, 'utf-8');
return JSON.parse(json);
};远程工程解析
生成lock文件
js
const generateLock = async (workDir, packageJson) => {
await fs.promises.writeFile(path.join(workDir, 'package.json'), JSON.stringify(packageJson, null, 2));
projectName = filteredPackageJson.name;
// 生成 lock 文件
await createLockFile(workDir);
};在node下执行npm命令,生成lock文件
js
const createLockFile = async workDir => {
const cmd = `npm install --package-lock-only --force`;
await runCommand(cmd, workDir);
};执行命令行命令通用方法
js
import {exec} from 'child_process';
const execAsync = promisify(exec);
/**
* 执行命令行命令
* @param {string} command 要执行的命令
* @param {string} cwd 工作目录
* @returns {Promise<{stdout: string, stderr: string}>}
*/
const runCommand = async (command, cwd) => {
const {stdout, stderr} = await execAsync(command, {
cwd,
maxBuffer: 1024 * 1024 * 10 * 10 // 100MB buffer
});
if (stderr) {
console.error('命令执行警告:', stderr);
}
return {stdout, stderr};
};安全审计
js
const audit = async (workDir, savePath) => {
console.log('开始执行命令:', workDir);
const cmd = `npm audit --json`;
try {
const {stdout} = await runCommand(cmd, workDir);
const auditResult = JSON.parse(stdout);
await fs.promises.mkdir(savePath, {recursive: true});
const resultPath = path.join(savePath, `audit-result.json`);
await fs.promises.writeFile(resultPath, JSON.stringify(auditResult, null, 2));
console.log(`安全审计完成,结果已保存到:${resultPath}`);
} catch (error) {
if (error.stdout) {
const rawOutputPath = path.join(savePath, `audit-result.json`);
await fs.promises.mkdir(savePath, {recursive: true});
await fs.promises.writeFile(rawOutputPath, error.stdout);
console.log(`原始输出已保存到:${rawOutputPath}`);
}
}
};渲染
js
const generateReport = async (workDir, savePath) => {
// 确保保存目录存在
await fs.promises.mkdir(savePath, {recursive: true});
// 查找最新的审计结果文件
const files = await fs.promises.readdir(savePath);
const auditResultFiles = files.filter(file => file.startsWith('audit-result') && file.endsWith('.json'));
if (auditResultFiles.length === 0) {
throw new Error('未找到审计结果文件');
}
// 获取最新的审计结果文件
const latestFile = auditResultFiles.sort().pop();
const resultPath = path.join(savePath, latestFile);
const auditResult = JSON.parse(await fs.promises.readFile(resultPath, 'utf-8'));
// 生成 Markdown 报告
let report = `# 安全审计报告 ${projectName}\n\n`;
report += `**生成时间**: ${new Date().toLocaleString('zh-CN')}\n\n`;
report += `**审计文件**: ${latestFile}\n\n`;
// 摘要信息
report += `## 摘要\n\n`;
if (auditResult.metadata) {
report += `- **总依赖包数量**: ${auditResult.metadata.dependencies.total || 0}\n`;
}
report += `\n`;
// 漏洞统计
const vulnerabilities = auditResult.vulnerabilities || {};
const vulnKeys = Object.keys(vulnerabilities);
report += `## 漏洞统计\n\n`;
report += `**发现漏洞数量**: ${vulnKeys.length}\n\n`;
if (vulnKeys.length > 0) {
// 按严重程度分类统计
const severityCount = {info: 0, low: 0, moderate: 0, high: 0, critical: 0};
vulnKeys.forEach(name => {
const severity = vulnerabilities[name].severity;
if (severityCount[severity] !== undefined) {
severityCount[severity]++;
}
});
report += `### 按严重程度分布\n\n`;
report += `| 严重程度 | 数量 |\n`;
report += `|---------|------|\n`;
report += `| Critical | ${severityCount.critical} |\n`;
report += `| High | ${severityCount.high} |\n`;
report += `| Moderate | ${severityCount.moderate} |\n`;
report += `| Low | ${severityCount.low} |\n`;
report += `| Info | ${severityCount.info} |\n`;
report += `\n`;
// 详细漏洞信息
report += `## 漏洞详情\n\n`;
// 按严重程度排序
const severityOrder = {critical: 0, high: 1, moderate: 2, low: 3, info: 4};
vulnKeys.sort((a, b) => severityOrder[vulnerabilities[a].severity] - severityOrder[vulnerabilities[b].severity]);
vulnKeys.forEach(name => {
const vuln = vulnerabilities[name];
report += `### ${name}\n\n`;
report += `- **严重程度**: ${getSeverityBadge(vuln.severity)}\n`;
report += `- **受影响版本**: ${vuln.vulnerable_versions || '--'}\n`;
report += `- **修复版本**: ${vuln.fixed_in || '--'}\n`;
if (vuln.access) {
report += `- **访问类型**: ${vuln.access}\n`;
}
// 处理 via 属性的两种格式
if (vuln.via && vuln.via.length > 0) {
// 检查是字符串数组还是对象数组
if (typeof vuln.via[0] === 'string') {
report += `- **漏洞来源**: ${vuln.via.join(', ')}\n`;
} else if (typeof vuln.via[0] === 'object') {
report += `- **漏洞来源**:\n\n`;
vuln.via.forEach((viaItem, index) => {
report += ` #### ${index + 1}. ${viaItem.title || viaItem.name}\n\n`;
report += ` - **参考链接**: [查看](${viaItem.url || '--'})\n`;
report += ` - **CVSS 评分**: ${viaItem.cvss.score || '--'}`;
report += ` (${viaItem.cvss.vectorString || '--'})\n`;
if (viaItem.cwe && viaItem.cwe.length > 0) {
report += ` - **漏洞类型**: ${viaItem.cwe.join(', ')}\n`;
}
report += ` - **严重程度**: ${getSeverityBadge(viaItem.severity) || '--'}\n`;
report += ` - **影响范围**: ${viaItem.range || '--'}\n`;
report += `\n`;
});
}
}
report += `**描述**:\n${vuln.description || '--'}\n\n`;
report += `**建议**:\n${vuln.recommendation || '--'}\n\n`;
report += `---\n\n`;
});
} else {
report += `🎉 **未发现安全漏洞!**\n\n`;
}
// 保存报告
const reportPath = path.join(savePath, `audit-report.md`);
await fs.promises.writeFile(reportPath, report);
console.log(`审计报告已生成:${reportPath}`);
return reportPath;
};持续优化
- 使用TypeScript 重构
- 上传到 npm,并可以使用 npx 下载执行
- 从问题出发还是从依赖出发?
- monorepo工程如何处理?
- 工程特征检测、workspace api的使用
- 如何适配不同的仓库,比如gitee、gitlab等url特征检测、策略模式
- 如何适配不同的本地环境,比如npm版本不同容器技术(本地docker、远程服务器)固定版本
- 图形表达依赖关系
- mermaid、AntV、D3等
- 性能提升
- 包-版本审计结果缓存,本地/远程缓存
- 本地缓存可采用LRU缓存模型
- 语义范围合并
- 某个包的多个版本范围都有漏洞,如何根据多个语义版本求并集
