@sunchao116/mcp-audit 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.
Files changed (48) hide show
  1. package/package.json +36 -0
  2. package/src/audit/currentAudit.js +50 -0
  3. package/src/audit/getDepChain.js +47 -0
  4. package/src/audit/index.js +28 -0
  5. package/src/audit/normalizeAuditResult.js +47 -0
  6. package/src/audit/npmAudit.js +10 -0
  7. package/src/audit/remoteAudit.js +24 -0
  8. package/src/audit/test/test-currentAudit.js +15 -0
  9. package/src/audit/test/test-getDepChain.js +13 -0
  10. package/src/audit/test/test-index.js +17 -0
  11. package/src/audit/test/test-normalizeAuditResult.js +18 -0
  12. package/src/audit/test/test-npmAudit.js +15 -0
  13. package/src/audit/test/test-remoteAudit.js +15 -0
  14. package/src/audit/test/workdir/audit.json +2130 -0
  15. package/src/audit/test/workdir/current.json +10 -0
  16. package/src/audit/test/workdir/index.json +2398 -0
  17. package/src/audit/test/workdir/normalized.json +2581 -0
  18. package/src/audit/test/workdir/package-lock.json +16137 -0
  19. package/src/audit/test/workdir/package.json +1 -0
  20. package/src/audit/test/workdir/remote.json +75 -0
  21. package/src/common/utils.js +35 -0
  22. package/src/entry/index.js +28 -0
  23. package/src/entry/test/result/result-local.md +1177 -0
  24. package/src/entry/test/result/result-remote.md +151 -0
  25. package/src/entry/test/test-index.js +15 -0
  26. package/src/generateLock/generateLock.js +27 -0
  27. package/src/generateLock/index.js +1 -0
  28. package/src/generateLock/test/1.json +1 -0
  29. package/src/generateLock/test/test.js +15 -0
  30. package/src/generateLock/test/workdir/package-lock.json +16137 -0
  31. package/src/generateLock/test/workdir/package.json +1 -0
  32. package/src/main/index.js +23 -0
  33. package/src/mcpServer.js +43 -0
  34. package/src/parseProject/index.js +18 -0
  35. package/src/parseProject/parseLocalProject.js +8 -0
  36. package/src/parseProject/parseRemoteProject.js +65 -0
  37. package/src/parseProject/test/test.js +26 -0
  38. package/src/render/index.js +24 -0
  39. package/src/render/markdown.js +17 -0
  40. package/src/render/template/audit.ejs +30 -0
  41. package/src/render/template/detail-item.ejs +32 -0
  42. package/src/render/template/detail.ejs +7 -0
  43. package/src/render/template/index.ejs +8 -0
  44. package/src/render/test/test-index.js +27 -0
  45. package/src/render/test/workdir/auditResult.json +2101 -0
  46. package/src/render/test/workdir/index.md +1221 -0
  47. package/src/render/test/workdir/package.json +38 -0
  48. package/src/workDir/index.js +21 -0
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@sunchao116/mcp-audit",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol (MCP) server tool for auditing npm package dependencies, supporting both local and remote repository security audits",
5
+ "main": "src/main/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-audit": "./src/mcpServer.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "audit",
17
+ "npm-audit",
18
+ "security",
19
+ "dependency-audit",
20
+ "vulnerability-scan"
21
+ ],
22
+ "author": "sunchao <2413526139@qq.com>",
23
+ "files": [
24
+ "src",
25
+ "package.json",
26
+ "README.md"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.17.1",
33
+ "ejs": "^3.1.10",
34
+ "zod": "^3.25.76"
35
+ }
36
+ }
@@ -0,0 +1,50 @@
1
+ import { remoteAudit } from './remoteAudit.js';
2
+ const severityLevelsMap = {
3
+ info: 0,
4
+ low: 1,
5
+ moderate: 2,
6
+ high: 3,
7
+ critical: 4,
8
+ };
9
+
10
+ // 添加当前工程的审计结果
11
+ export async function currentAudit(name, version) {
12
+ // 1. 调用 remoteAudit 函数获取审计结果
13
+ const auditResult = await remoteAudit(name, version);
14
+
15
+ // 2. 规格化审计结果
16
+ if (
17
+ !auditResult.advisories ||
18
+ Object.keys(auditResult.advisories).length === 0
19
+ ) {
20
+ return null;
21
+ }
22
+ const result = {
23
+ name,
24
+ range: version,
25
+ nodes: ['.'],
26
+ depChains: [],
27
+ };
28
+ const advisories = Object.values(auditResult.advisories);
29
+ let maxSeverity = 'info';
30
+ result.problems = advisories.map((advisory) => {
31
+ const problem = {
32
+ source: advisory.id,
33
+ name,
34
+ dependency: name,
35
+ title: advisory.title,
36
+ url: advisory.url,
37
+ severity: advisory.severity,
38
+ cwe: advisory.cwe,
39
+ cvss: advisory.cvss,
40
+ range: advisory.vulnerable_versions,
41
+ };
42
+ // 更新最大严重性
43
+ if (severityLevelsMap[problem.severity] > severityLevelsMap[maxSeverity]) {
44
+ maxSeverity = problem.severity;
45
+ }
46
+ return problem;
47
+ });
48
+ result.severity = maxSeverity;
49
+ return result;
50
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 给定图结构中的一个节点,获取从该节点的依赖节点出发一直走到终点,一共走出的所有链条
3
+ * 注意:图结构中可能存在环,遇到环时,环所在的节点直接作为终点即可
4
+ * @param {Node} node
5
+ * @returns {Array<Set<string>>} 返回所有依赖链,每个链是一个字符串集合,每个字符串是一个节点名称
6
+ */
7
+ export function getDepChains(node, globalNodeMap) {
8
+ // 存储所有找到的依赖链
9
+ const chains = [];
10
+
11
+ // 当前DFS路径(用于检测环)
12
+ const currentPath = [];
13
+
14
+ /**
15
+ * 深度优先搜索函数
16
+ * @param {Node} currentNode - 当前处理的节点
17
+ */
18
+ function dfs(currentNode) {
19
+ if (!currentNode) return;
20
+
21
+ // 检查是否形成环(当前节点已在路径中)
22
+ if (currentPath.includes(currentNode.name)) {
23
+ chains.push([...currentPath]);
24
+ return;
25
+ }
26
+
27
+ // 将当前节点加入路径
28
+ currentPath.unshift(currentNode.name);
29
+
30
+ // 如果没有依赖节点,说明到达终点
31
+ if (!currentNode.effects || currentNode.effects.length === 0) {
32
+ chains.push([...currentPath]);
33
+ } else {
34
+ // 递归处理所有依赖节点
35
+ for (const effect of currentNode.effects) {
36
+ dfs(globalNodeMap[effect]);
37
+ }
38
+ }
39
+ // 回溯:移除当前节点
40
+ currentPath.shift();
41
+ }
42
+
43
+ // 从给定节点开始DFS
44
+ dfs(node);
45
+
46
+ return chains;
47
+ }
@@ -0,0 +1,28 @@
1
+ import { npmAudit } from './npmAudit.js';
2
+ import { normalizeAuditResult } from './normalizeAuditResult.js';
3
+ import { currentAudit } from './currentAudit.js';
4
+
5
+ export async function audit(workDir, packageJson) {
6
+ // 调用 npmAudit 获取审计结果
7
+ const auditResult = await npmAudit(workDir);
8
+ // 规范化审计结果
9
+ const normalizedResult = normalizeAuditResult(auditResult);
10
+
11
+ // 添加当前工程的审计结果
12
+ const current = await currentAudit(packageJson.name, packageJson.version);
13
+ if (current) {
14
+ normalizedResult.vulnerabilities[current.severity].unshift(current);
15
+ }
16
+ // 添加汇总信息
17
+ normalizedResult.summary = {
18
+ total: Object.values(normalizedResult.vulnerabilities).reduce(
19
+ (sum, arr) => sum + arr.length,
20
+ 0
21
+ ),
22
+ critical: normalizedResult.vulnerabilities.critical.length,
23
+ high: normalizedResult.vulnerabilities.high.length,
24
+ moderate: normalizedResult.vulnerabilities.moderate.length,
25
+ low: normalizedResult.vulnerabilities.low.length,
26
+ };
27
+ return normalizedResult;
28
+ }
@@ -0,0 +1,47 @@
1
+ import { getDepChains } from './getDepChain.js';
2
+
3
+ function _normalizeVulnerabilities(auditResult) {
4
+ const result = {
5
+ critical: [],
6
+ high: [],
7
+ moderate: [],
8
+ low: [],
9
+ };
10
+ for (const key in auditResult.vulnerabilities) {
11
+ const packageInfo = auditResult.vulnerabilities[key];
12
+ const normalizedPackage = _normalizePackage(packageInfo);
13
+ if (normalizedPackage) {
14
+ result[normalizedPackage.severity].push(normalizedPackage);
15
+ }
16
+ }
17
+ return result;
18
+
19
+ function _normalizePackage(packageInfo) {
20
+ const { via = [] } = packageInfo;
21
+ const validVia = via.filter((it) => typeof it === 'object');
22
+ if (validVia.length === 0) {
23
+ return null;
24
+ }
25
+ const info = {
26
+ name: packageInfo.name,
27
+ severity: packageInfo.severity,
28
+ problems: validVia,
29
+ nodes: packageInfo.nodes || [],
30
+ };
31
+ info.depChains = getDepChains(packageInfo, auditResult.vulnerabilities);
32
+ // info.depChains = info.depChains.filter(
33
+ // (chain) => !isInvalidChain(chain, packageInfo.name)
34
+ // );
35
+ return info;
36
+ }
37
+ }
38
+
39
+ function isInvalidChain(chain, packageName) {
40
+ return chain.length === 0 || (chain.length === 1 && chain[0] === packageName);
41
+ }
42
+
43
+ export function normalizeAuditResult(auditResult) {
44
+ return {
45
+ vulnerabilities: _normalizeVulnerabilities(auditResult),
46
+ };
47
+ }
@@ -0,0 +1,10 @@
1
+ import fs from 'fs';
2
+ import { join } from 'path';
3
+ import { runCommand } from '../common/utils.js';
4
+
5
+ export async function npmAudit(workDir) {
6
+ const cmd = `npm audit --json`;
7
+ const jsonResult = await runCommand(cmd, workDir); // 在工作目录中执行命令
8
+ const auditData = JSON.parse(jsonResult);
9
+ return auditData;
10
+ }
@@ -0,0 +1,24 @@
1
+ const URL = 'https://registry.npmjs.org/-/npm/v1/security/audits';
2
+
3
+ export async function remoteAudit(packageName, pacakgeVersion) {
4
+ const body = {
5
+ name: 'example-audit', // 项目名字随便写
6
+ version: '1.0.0', // 项目的版本,随便写
7
+ requires: {
8
+ [packageName]: pacakgeVersion,
9
+ },
10
+ dependencies: {
11
+ [packageName]: {
12
+ version: pacakgeVersion,
13
+ },
14
+ },
15
+ };
16
+ const resp = await fetch(URL, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ body: JSON.stringify(body),
22
+ });
23
+ return await resp.json();
24
+ }
@@ -0,0 +1,15 @@
1
+ import { currentAudit } from '../currentAudit.js';
2
+ import { getDirname } from '../../common/utils.js';
3
+ import { join } from 'path';
4
+ import fs from 'fs';
5
+
6
+ const workDir = join(getDirname(import.meta.url), './workdir');
7
+ const currentJson = join(workDir, './current.json');
8
+
9
+ async function test() {
10
+ const result = await currentAudit('my-site', '0.1.0');
11
+ fs.writeFileSync(currentJson, JSON.stringify(result, null, 2), 'utf8');
12
+ console.log('ok');
13
+ }
14
+
15
+ test();
@@ -0,0 +1,13 @@
1
+ import { getDepChains } from '../getDepChain.js';
2
+
3
+ const globalNodeMap = {
4
+ A: { name: 'A', effects: ['B', 'C'] },
5
+ B: { name: 'B', effects: ['C'] },
6
+ C: { name: 'C', effects: ['D'] },
7
+ D: { name: 'D', effects: ['B', 'E'] }, // recursive dependency
8
+ E: { name: 'E', effects: [] },
9
+ };
10
+ const nodeA = globalNodeMap['A'];
11
+
12
+ const chains = getDepChains(nodeA, globalNodeMap);
13
+ console.log(chains); // 输出所有依赖链
@@ -0,0 +1,17 @@
1
+ import { audit } from '../index.js';
2
+ import { getDirname } from '../../common/utils.js';
3
+ import { join } from 'path';
4
+ import fs from 'fs';
5
+
6
+ const workDir = join(getDirname(import.meta.url), './workdir');
7
+ const indexJson = join(workDir, './index.json');
8
+ const packageJsonPath = join(workDir, 'package.json');
9
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
10
+
11
+ async function test() {
12
+ const result = await audit(workDir, packageJson);
13
+ fs.writeFileSync(indexJson, JSON.stringify(result, null, 2), 'utf8');
14
+ console.log('ok');
15
+ }
16
+
17
+ test();
@@ -0,0 +1,18 @@
1
+ import { normalizeAuditResult } from '../normalizeAuditResult.js';
2
+ import fs from 'fs';
3
+ import { join } from 'path';
4
+ import { getDirname } from '../../common/utils.js';
5
+
6
+ const auditJson = join(getDirname(import.meta.url), './workdir/audit.json');
7
+ const auditResult = JSON.parse(fs.readFileSync(auditJson, 'utf8'));
8
+ function test() {
9
+ const r = normalizeAuditResult(auditResult);
10
+ const normalizedPath = join(
11
+ getDirname(import.meta.url),
12
+ './workdir/normalized.json'
13
+ );
14
+ fs.writeFileSync(normalizedPath, JSON.stringify(r, null, 2), 'utf8');
15
+ console.log('ok');
16
+ }
17
+
18
+ test();
@@ -0,0 +1,15 @@
1
+ import { npmAudit } from '../npmAudit.js';
2
+ import { getDirname } from '../../common/utils.js';
3
+ import { join } from 'path';
4
+ import fs from 'fs';
5
+
6
+ const workDir = join(getDirname(import.meta.url), './workdir');
7
+ const auditJson = join(workDir, './audit.json');
8
+
9
+ async function test() {
10
+ const result = await npmAudit(workDir);
11
+ fs.writeFileSync(auditJson, JSON.stringify(result, null, 2), 'utf8');
12
+ console.log('ok');
13
+ }
14
+
15
+ test();
@@ -0,0 +1,15 @@
1
+ import { remoteAudit } from '../remoteAudit.js';
2
+ import { getDirname } from '../../common/utils.js';
3
+ import { join } from 'path';
4
+ import fs from 'fs';
5
+
6
+ const workDir = join(getDirname(import.meta.url), './workdir');
7
+ const remoteJson = join(workDir, './remote.json');
8
+
9
+ async function test() {
10
+ const result = await remoteAudit('ejs', '3.1.9');
11
+ fs.writeFileSync(remoteJson, JSON.stringify(result, null, 2), 'utf8');
12
+ console.log('ok');
13
+ }
14
+
15
+ test();