Skip to content

MCP Audit安全审计

完整案例 By GitHub

实现流程

  • 创建工作目录:创建一个临时的工作目录,用于保存执行期间要用到的临时文件
  • 解析工程:解析本地工程目录或者远程仓库链接,得到对应的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缓存模型
  • 语义范围合并
    • 某个包的多个版本范围都有漏洞,如何根据多个语义版本求并集

By Modify.