@anren-utils/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 (115) hide show
  1. package/.editorconfig +13 -0
  2. package/dist/audit/auditUtils.d.ts +12 -0
  3. package/dist/audit/auditUtils.d.ts.map +1 -0
  4. package/dist/audit/auditUtils.js +22 -0
  5. package/dist/audit/auditUtils.js.map +1 -0
  6. package/dist/audit/currentAudit.d.ts +53 -0
  7. package/dist/audit/currentAudit.d.ts.map +1 -0
  8. package/dist/audit/currentAudit.js +54 -0
  9. package/dist/audit/currentAudit.js.map +1 -0
  10. package/dist/audit/getDepChain.d.ts +16 -0
  11. package/dist/audit/getDepChain.d.ts.map +1 -0
  12. package/dist/audit/getDepChain.js +60 -0
  13. package/dist/audit/getDepChain.js.map +1 -0
  14. package/dist/audit/index.d.ts +11 -0
  15. package/dist/audit/index.d.ts.map +1 -0
  16. package/dist/audit/index.js +64 -0
  17. package/dist/audit/index.js.map +1 -0
  18. package/dist/audit/normalizeAuditResult.d.ts +13 -0
  19. package/dist/audit/normalizeAuditResult.d.ts.map +1 -0
  20. package/dist/audit/normalizeAuditResult.js +81 -0
  21. package/dist/audit/normalizeAuditResult.js.map +1 -0
  22. package/dist/audit/remoteAudit.d.ts +3 -0
  23. package/dist/audit/remoteAudit.d.ts.map +1 -0
  24. package/dist/audit/remoteAudit.js +24 -0
  25. package/dist/audit/remoteAudit.js.map +1 -0
  26. package/dist/generateLock/index.d.ts +17 -0
  27. package/dist/generateLock/index.d.ts.map +1 -0
  28. package/dist/generateLock/index.js +141 -0
  29. package/dist/generateLock/index.js.map +1 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +48 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/mcpServer.d.ts +2 -0
  35. package/dist/mcpServer.d.ts.map +1 -0
  36. package/dist/mcpServer.js +34 -0
  37. package/dist/mcpServer.js.map +1 -0
  38. package/dist/parseProject/detectPackageManager.d.ts +8 -0
  39. package/dist/parseProject/detectPackageManager.d.ts.map +1 -0
  40. package/dist/parseProject/detectPackageManager.js +22 -0
  41. package/dist/parseProject/detectPackageManager.js.map +1 -0
  42. package/dist/parseProject/index.d.ts +11 -0
  43. package/dist/parseProject/index.d.ts.map +1 -0
  44. package/dist/parseProject/index.js +20 -0
  45. package/dist/parseProject/index.js.map +1 -0
  46. package/dist/parseProject/parseLocalProject.d.ts +17 -0
  47. package/dist/parseProject/parseLocalProject.d.ts.map +1 -0
  48. package/dist/parseProject/parseLocalProject.js +28 -0
  49. package/dist/parseProject/parseLocalProject.js.map +1 -0
  50. package/dist/parseProject/parseLocalWorkspace.d.ts +2 -0
  51. package/dist/parseProject/parseLocalWorkspace.d.ts.map +1 -0
  52. package/dist/parseProject/parseLocalWorkspace.js +2 -0
  53. package/dist/parseProject/parseLocalWorkspace.js.map +1 -0
  54. package/dist/parseProject/parseRemoteProject.d.ts +41 -0
  55. package/dist/parseProject/parseRemoteProject.d.ts.map +1 -0
  56. package/dist/parseProject/parseRemoteProject.js +180 -0
  57. package/dist/parseProject/parseRemoteProject.js.map +1 -0
  58. package/dist/parseProject/parseRemoteWorkspace.d.ts +2 -0
  59. package/dist/parseProject/parseRemoteWorkspace.d.ts.map +1 -0
  60. package/dist/parseProject/parseRemoteWorkspace.js +2 -0
  61. package/dist/parseProject/parseRemoteWorkspace.js.map +1 -0
  62. package/dist/parseProject/parseWorkspace.d.ts +19 -0
  63. package/dist/parseProject/parseWorkspace.d.ts.map +1 -0
  64. package/dist/parseProject/parseWorkspace.js +140 -0
  65. package/dist/parseProject/parseWorkspace.js.map +1 -0
  66. package/dist/render/index.d.ts +9 -0
  67. package/dist/render/index.d.ts.map +1 -0
  68. package/dist/render/index.js +24 -0
  69. package/dist/render/index.js.map +1 -0
  70. package/dist/render/markdown.d.ts +12 -0
  71. package/dist/render/markdown.d.ts.map +1 -0
  72. package/dist/render/markdown.js +16 -0
  73. package/dist/render/markdown.js.map +1 -0
  74. package/dist/render/template/audit.ejs +30 -0
  75. package/dist/render/template/detail-item.ejs +32 -0
  76. package/dist/render/template/detail.ejs +7 -0
  77. package/dist/render/template/index.ejs +8 -0
  78. package/dist/types.d.ts +371 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +2 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/utils/dirUtils.d.ts +11 -0
  83. package/dist/utils/dirUtils.d.ts.map +1 -0
  84. package/dist/utils/dirUtils.js +28 -0
  85. package/dist/utils/dirUtils.js.map +1 -0
  86. package/dist/utils/index.d.ts +34 -0
  87. package/dist/utils/index.d.ts.map +1 -0
  88. package/dist/utils/index.js +74 -0
  89. package/dist/utils/index.js.map +1 -0
  90. package/eslint.config.js +38 -0
  91. package/package.json +38 -0
  92. package/src/audit/auditUtils.ts +24 -0
  93. package/src/audit/currentAudit.ts +116 -0
  94. package/src/audit/getDepChain.ts +71 -0
  95. package/src/audit/index.ts +90 -0
  96. package/src/audit/normalizeAuditResult.ts +99 -0
  97. package/src/audit/remoteAudit.ts +26 -0
  98. package/src/generateLock/index.ts +203 -0
  99. package/src/index.ts +48 -0
  100. package/src/mcpServer.ts +43 -0
  101. package/src/parseProject/detectPackageManager.ts +24 -0
  102. package/src/parseProject/index.ts +20 -0
  103. package/src/parseProject/parseLocalProject.ts +39 -0
  104. package/src/parseProject/parseRemoteProject.ts +225 -0
  105. package/src/parseProject/parseWorkspace.ts +202 -0
  106. package/src/render/index.ts +30 -0
  107. package/src/render/markdown.ts +29 -0
  108. package/src/render/template/audit.ejs +30 -0
  109. package/src/render/template/detail-item.ejs +32 -0
  110. package/src/render/template/detail.ejs +7 -0
  111. package/src/render/template/index.ejs +8 -0
  112. package/src/types.ts +429 -0
  113. package/src/utils/dirUtils.ts +31 -0
  114. package/src/utils/index.ts +88 -0
  115. package/tsconfig.json +42 -0
@@ -0,0 +1,116 @@
1
+ import type { AuditSeverity } from "../types.js";
2
+ import { remoteAudit } from "./remoteAudit.js";
3
+
4
+ /**
5
+ * 单个漏洞问题的详细信息
6
+ */
7
+ interface Problem {
8
+ /** 漏洞 ID (Advisory ID) */
9
+ source: number;
10
+
11
+ /** 包名 */
12
+ name: string;
13
+
14
+ /** 依赖名 */
15
+ dependency: string;
16
+
17
+ /** 漏洞标题 */
18
+ title: string;
19
+
20
+ /** 漏洞详情 URL */
21
+ url: string;
22
+
23
+ /** 严重程度 */
24
+ severity: AuditSeverity;
25
+
26
+ /** CWE 列表 */
27
+ cwe: string[];
28
+
29
+ /** CVSS 评分数据 */
30
+ cvss: {
31
+ score: number;
32
+ vectorString: string;
33
+ };
34
+
35
+ /** 受影响的版本范围 */
36
+ range: string;
37
+ }
38
+
39
+ /**
40
+ * currentAudit 函数返回的最终结果对象
41
+ */
42
+ interface CurrentAuditResult {
43
+ /** 包名 */
44
+ name: string;
45
+
46
+ /** 版本/范围 */
47
+ range: string;
48
+
49
+ /** 节点路径,当前工程通常为根目录 '.' */
50
+ nodes: string[];
51
+
52
+ /** 依赖链,当前工程通常为空 */
53
+ depChains: any[]; // 如果 depChains 有具体结构,可以替换 any
54
+
55
+ /** 检测到的所有漏洞问题列表 */
56
+ problems: Problem[];
57
+
58
+ /** 当前包的最高严重程度 */
59
+ severity: AuditSeverity;
60
+ }
61
+
62
+ const severityLevelsMap: Record<AuditSeverity, number> = {
63
+ info: 0,
64
+ low: 1,
65
+ moderate: 2,
66
+ high: 3,
67
+ critical: 4,
68
+ };
69
+
70
+ /**
71
+ * @Desc: 添加当前工程的审计结果
72
+ * @return {*}
73
+ * @param {string} name
74
+ * @param {string} version
75
+ */
76
+ export async function currentAudit(name: string, version: string) {
77
+ // 1. 调用 remoteAudit 函数获取审计结果
78
+ const auditResult = await remoteAudit(name, version);
79
+ // 2. 规格化审计结果
80
+ if (
81
+ !auditResult.advisories ||
82
+ Object.keys(auditResult.advisories).length === 0
83
+ ) {
84
+ return null;
85
+ }
86
+ const result: CurrentAuditResult = {
87
+ name,
88
+ range: version,
89
+ nodes: ["."],
90
+ depChains: [],
91
+ problems: [],
92
+ severity: "info",
93
+ };
94
+ const advisories = Object.values(auditResult.advisories);
95
+ let maxSeverity: AuditSeverity = "info";
96
+ result.problems = advisories.map((advisory) => {
97
+ const problem: Problem = {
98
+ source: advisory.id,
99
+ name,
100
+ dependency: name,
101
+ title: advisory.title,
102
+ url: advisory.url,
103
+ severity: advisory.severity,
104
+ cwe: advisory.cwe,
105
+ cvss: advisory.cvss,
106
+ range: advisory.vulnerable_versions,
107
+ };
108
+ // 更新最大严重性
109
+ if (severityLevelsMap[problem.severity] > severityLevelsMap[maxSeverity]) {
110
+ maxSeverity = problem.severity;
111
+ }
112
+ return problem;
113
+ });
114
+ result.severity = maxSeverity;
115
+ return result;
116
+ }
@@ -0,0 +1,71 @@
1
+ import type { VulnerabilityInfo } from "../types.js";
2
+
3
+ /**
4
+ * 图的伸缩算法,用于获取从给定节点出发的所有依赖链条
5
+ * 给定图结构中的一个节点,获取从该节点的依赖节点出发一直走到终点,一共走出的所有链条
6
+ * 注意:图结构中可能存在环,遇到环时,环所在的节点直接作为终点即可
7
+ * @param {VulnerabilityInfo} vulnerabilityInfo - 给定的漏洞信息
8
+ * @param {Record<string, VulnerabilityInfo>} vulnerabilities - 所有漏洞信息的映射表,键为漏洞名称,值为漏洞信息
9
+ * @returns {Array<string[][]>} 返回所有依赖链,每个链是一个字符串数组,每个字符串是一个漏洞名称
10
+ * 返回结果示例:"depChains": [["@vue/cli-plugin-babel","@vue/cli-service","@intervolga/optimize-cssnano-plugin","postcss"],[
11
+ "@vue/cli-plugin-router",
12
+ "@vue/cli-service",
13
+ "@intervolga/optimize-cssnano-plugin",
14
+ "postcss"]]
15
+ */
16
+ export function getDepChains(
17
+ vulnerabilityInfo: VulnerabilityInfo,
18
+ vulnerabilities: Record<string, VulnerabilityInfo>,
19
+ ) {
20
+ // 存储所有找到的依赖链(存储最终结果:所有找到的完整路径)
21
+ const chains: string[][] = [];
22
+
23
+ // 当前DFS路径(用于检测环)(当前正在探索的路径栈(用于记录路径和检测环))
24
+ const currentPath: string[] = [];
25
+
26
+ /**
27
+ * 深度优先搜索函数
28
+ * @param {VulnerabilityInfo} currentVulnerabilityInfo - 当前处理的漏洞信息
29
+ */
30
+ function dfs(currentVulnerabilityInfo?: VulnerabilityInfo) {
31
+ // 1. 健壮性检查
32
+ if (!currentVulnerabilityInfo) return;
33
+
34
+ // 2. 环检测
35
+ // 检查是否形成环(当前节点已在路径中)
36
+ // 如果当前节点的名字已经存在于当前路径中,说明我们遇到了一个环(A->B->C->A)。
37
+ // 将当前路径记录下来并停止该分支的搜索。
38
+ if (currentPath.includes(currentVulnerabilityInfo.name)) {
39
+ chains.push([...currentPath]); // 注意:这里并没有把重复的节点再次加入,而是截断
40
+ return;
41
+ }
42
+
43
+ // 3. 前进:将当前节点加入路径头部
44
+ // 使用 unshift 是为了让链条保持顺序:[起点, ..., 当前节点]
45
+ currentPath.unshift(currentVulnerabilityInfo.name);
46
+
47
+ // 4. 终点判断
48
+ // 如果没有依赖节点,说明到达终点
49
+ if (
50
+ !currentVulnerabilityInfo.effects ||
51
+ currentVulnerabilityInfo.effects.length === 0
52
+ ) {
53
+ chains.push([...currentPath]);
54
+ } else {
55
+ // 5. 递归遍历
56
+ // 遍历当前节点的所有依赖项,对每一个依赖项递归调用 dfs
57
+ for (const effect of currentVulnerabilityInfo.effects) {
58
+ dfs(vulnerabilities[effect]);
59
+ }
60
+ }
61
+ // 6. 回溯
62
+ // 当递归返回(无论是找到了终点还是遇到了环),
63
+ // 必须将当前节点从路径中移除,以便探索下一条可能的路径。
64
+ currentPath.shift();
65
+ }
66
+
67
+ // 从给定节点开始DFS
68
+ dfs(vulnerabilityInfo);
69
+
70
+ return chains;
71
+ }
@@ -0,0 +1,90 @@
1
+ import { join } from "path";
2
+ import type { RemoteWorkspaceInfo } from "../parseProject/parseWorkspace.js";
3
+ import type { PackageJsonInfo, PackageManager } from "../types.js";
4
+ import { npmAudit, pnpmAudit } from "./auditUtils.js";
5
+ import { currentAudit } from "./currentAudit.js";
6
+ import { normalizeAuditResult } from "./normalizeAuditResult.js";
7
+
8
+ interface AuditOptions {
9
+ workDir: string;
10
+ packageJson: PackageJsonInfo;
11
+ subPackageInfos: RemoteWorkspaceInfo | null;
12
+ currentPackageManager: PackageManager;
13
+ }
14
+
15
+ export async function audit(options: AuditOptions) {
16
+ const { subPackageInfos } = options;
17
+ if (subPackageInfos?.subPackageNames) {
18
+ return await monorepoAudit(options);
19
+ }
20
+ return await defaultAudit(options);
21
+ }
22
+
23
+ async function defaultAudit(options: AuditOptions) {
24
+ const { workDir, packageJson, currentPackageManager } = options;
25
+ const auditFn = currentPackageManager === "npm" ? npmAudit : pnpmAudit;
26
+ // 调用 npmAudit 获取审计结果
27
+ const auditResult = await auditFn(workDir);
28
+
29
+ // 规范化审计结果
30
+ const normalizedResult = normalizeAuditResult(auditResult);
31
+
32
+ // 添加当前工程的审计结果
33
+ const current = await currentAudit(packageJson.name, packageJson.version);
34
+ if (current) {
35
+ normalizedResult.vulnerabilities[current.severity].unshift(current);
36
+ }
37
+ // 添加汇总信息
38
+ normalizedResult.summary = {
39
+ total: Object.values(normalizedResult.vulnerabilities).reduce(
40
+ (sum, arr) => sum + arr.length,
41
+ 0,
42
+ ),
43
+ critical: normalizedResult.vulnerabilities.critical.length,
44
+ high: normalizedResult.vulnerabilities.high.length,
45
+ moderate: normalizedResult.vulnerabilities.moderate.length,
46
+ low: normalizedResult.vulnerabilities.low.length,
47
+ info: normalizedResult.vulnerabilities.info.length,
48
+ };
49
+ return normalizedResult;
50
+ }
51
+
52
+ async function monorepoAudit(options: AuditOptions) {
53
+ const { workDir, packageJson, subPackageInfos, currentPackageManager } =
54
+ options;
55
+ if (!subPackageInfos) {
56
+ throw new Error("monorepo工程必须包含子包信息");
57
+ }
58
+ const auditFn = currentPackageManager === "npm" ? npmAudit : pnpmAudit;
59
+ // 调用 npmAudit 获取审计结果
60
+ const auditResult = await auditFn(workDir);
61
+ // 得到所有子包的审计结果
62
+ const subPackageAuditResults = await Promise.all(
63
+ subPackageInfos.subPackageNames.map((subPackageName) =>
64
+ auditFn(join(workDir, subPackageName)),
65
+ ),
66
+ );
67
+ const allAuditResults = [auditResult, ...subPackageAuditResults];
68
+
69
+ // 规范化审计结果
70
+ const normalizedResult = normalizeAuditResult(allAuditResults);
71
+
72
+ // 添加当前工程的审计结果
73
+ const current = await currentAudit(packageJson.name, packageJson.version);
74
+ if (current) {
75
+ normalizedResult.vulnerabilities[current.severity].unshift(current);
76
+ }
77
+ // 添加汇总信息
78
+ normalizedResult.summary = {
79
+ total: Object.values(normalizedResult.vulnerabilities).reduce(
80
+ (sum, arr) => sum + arr.length,
81
+ 0,
82
+ ),
83
+ critical: normalizedResult.vulnerabilities.critical.length,
84
+ high: normalizedResult.vulnerabilities.high.length,
85
+ moderate: normalizedResult.vulnerabilities.moderate.length,
86
+ low: normalizedResult.vulnerabilities.low.length,
87
+ info: normalizedResult.vulnerabilities.info.length,
88
+ };
89
+ return normalizedResult;
90
+ }
@@ -0,0 +1,99 @@
1
+ import type {
2
+ AuditResult,
3
+ AuditSeverity,
4
+ NormalizedVulnerabilityInfo,
5
+ VulnerabilityInfo,
6
+ } from "../types.js";
7
+ import { getDepChains } from "./getDepChain.js";
8
+
9
+ export interface NormalizedResult {
10
+ vulnerabilities: Record<AuditSeverity, NormalizedVulnerabilityInfo[]>;
11
+ summary?: Record<AuditSeverity | "total", number>;
12
+ }
13
+
14
+ function _normalizeVulnerabilities(auditResult: AuditResult) {
15
+ // 漏洞级别分类组别,用于后续统计漏洞数量
16
+ const result: Record<AuditSeverity, NormalizedVulnerabilityInfo[]> = {
17
+ critical: [],
18
+ high: [],
19
+ moderate: [],
20
+ low: [],
21
+ info: [],
22
+ };
23
+ for (const key in auditResult.vulnerabilities) {
24
+ // 获取当前漏洞的包信息
25
+ const packageInfo = auditResult.vulnerabilities[key];
26
+ const normalizedPackage = _normalizePackage(packageInfo);
27
+ if (normalizedPackage) {
28
+ result[normalizedPackage.severity].push(normalizedPackage);
29
+ }
30
+ }
31
+ return result;
32
+
33
+ function _normalizePackage(packageInfo?: VulnerabilityInfo) {
34
+ if (!packageInfo) {
35
+ return null;
36
+ }
37
+ const { via = [] } = packageInfo;
38
+ // 只需要记录自身有漏洞的包,而不是依赖的包
39
+ const validVia = via.filter((it) => typeof it === "object");
40
+ if (validVia.length === 0) {
41
+ return null;
42
+ }
43
+ // 规格化漏洞信息
44
+ const info: NormalizedVulnerabilityInfo = {
45
+ name: packageInfo.name,
46
+ severity: packageInfo.severity,
47
+ problems: validVia,
48
+ nodes: packageInfo.nodes || [],
49
+ };
50
+ info.depChains = getDepChains(packageInfo, auditResult.vulnerabilities);
51
+ // info.depChains = info.depChains.filter(
52
+ // (chain) => !isInvalidChain(chain, packageInfo.name)
53
+ // );
54
+ return info;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 处理 auditResult 是数组的情况,合并多个审计结果
60
+ * @param auditResults AuditResult 数组
61
+ * @returns 合并后的标准化结果
62
+ */
63
+ export function normalizeAuditResults(auditResults: AuditResult[]) {
64
+ // 初始化结果对象
65
+ const result: Record<AuditSeverity, NormalizedVulnerabilityInfo[]> = {
66
+ critical: [],
67
+ high: [],
68
+ moderate: [],
69
+ low: [],
70
+ info: [],
71
+ };
72
+
73
+ // 遍历所有审计结果
74
+ for (const auditResult of auditResults) {
75
+ const normalizedResult = _normalizeVulnerabilities(auditResult);
76
+
77
+ // 合并每个级别的漏洞
78
+ for (const severity in normalizedResult) {
79
+ const vulnerabilities = normalizedResult[severity as AuditSeverity];
80
+ result[severity as AuditSeverity].push(...vulnerabilities);
81
+ }
82
+ }
83
+
84
+ return {
85
+ vulnerabilities: result,
86
+ } as NormalizedResult;
87
+ }
88
+
89
+ export function normalizeAuditResult(auditResult: AuditResult | AuditResult[]) {
90
+ if (Array.isArray(auditResult)) {
91
+ return {
92
+ vulnerabilities: normalizeAuditResults(auditResult).vulnerabilities,
93
+ } as NormalizedResult;
94
+ }
95
+
96
+ return {
97
+ vulnerabilities: _normalizeVulnerabilities(auditResult),
98
+ } as NormalizedResult;
99
+ }
@@ -0,0 +1,26 @@
1
+ import type { NpmAuditResponse } from "../types.js";
2
+
3
+ const URL = "https://registry.npmjs.org/-/npm/v1/security/audits";
4
+
5
+ export async function remoteAudit(packageName: string, packageVersion: string) {
6
+ const body = {
7
+ name: "example-audit", // 项目名字随便写
8
+ version: "1.0.0", // 项目的版本,随便写
9
+ requires: {
10
+ [packageName]: packageVersion,
11
+ },
12
+ dependencies: {
13
+ [packageName]: {
14
+ version: packageVersion,
15
+ },
16
+ },
17
+ };
18
+ const resp = await fetch(URL, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ },
23
+ body: JSON.stringify(body),
24
+ });
25
+ return (await resp.json()) as NpmAuditResponse;
26
+ }
@@ -0,0 +1,203 @@
1
+ import fs from "fs";
2
+ import { join, dirname } from "path";
3
+ import { getReadFileUrl, runCommand } from "../utils/index.js";
4
+ import type { PackageJsonInfo, PackageManager } from "../types.js";
5
+ import type { RemoteWorkspaceInfo } from "../parseProject/parseWorkspace.js";
6
+
7
+ interface GenerateLockOptions {
8
+ workDir: string;
9
+ packageJson: PackageJsonInfo;
10
+ subPackageInfos: RemoteWorkspaceInfo | null;
11
+ projectRoot: string;
12
+ currentPackageManager: PackageManager;
13
+ }
14
+
15
+ /**
16
+ * @Desc: 处理 package.json 中的 workspace 依赖
17
+ * @param {Object} packageJson package.json文件内容
18
+ * @returns {Object} 处理后的 package.json 文件内容
19
+ */
20
+ function processPackageJson(packageJson: Object) {
21
+ // 深拷贝 packageJson 以避免修改原始对象
22
+ const processedPackageJson = JSON.parse(JSON.stringify(packageJson));
23
+
24
+ // 处理 dependencies
25
+ if (processedPackageJson.dependencies) {
26
+ for (const [key, value] of Object.entries(
27
+ processedPackageJson.dependencies,
28
+ )) {
29
+ if (typeof value === "string" && value.startsWith("workspace:")) {
30
+ delete processedPackageJson.dependencies[key];
31
+ }
32
+ }
33
+ }
34
+
35
+ // 处理 devDependencies
36
+ if (processedPackageJson.devDependencies) {
37
+ for (const [key, value] of Object.entries(
38
+ processedPackageJson.devDependencies,
39
+ )) {
40
+ if (typeof value === "string" && value.startsWith("workspace:")) {
41
+ delete processedPackageJson.devDependencies[key];
42
+ }
43
+ }
44
+ }
45
+
46
+ // 处理 peerDependencies
47
+ if (processedPackageJson.peerDependencies) {
48
+ for (const [key, value] of Object.entries(
49
+ processedPackageJson.peerDependencies,
50
+ )) {
51
+ if (typeof value === "string" && value.startsWith("workspace:")) {
52
+ delete processedPackageJson.peerDependencies[key];
53
+ }
54
+ }
55
+ }
56
+
57
+ // 处理 optionalDependencies
58
+ if (processedPackageJson.optionalDependencies) {
59
+ for (const [key, value] of Object.entries(
60
+ processedPackageJson.optionalDependencies,
61
+ )) {
62
+ if (typeof value === "string" && value.startsWith("workspace:")) {
63
+ delete processedPackageJson.optionalDependencies[key];
64
+ }
65
+ }
66
+ }
67
+
68
+ return processedPackageJson;
69
+ }
70
+
71
+ /**
72
+ * @Desc: 根据子包的信息,生成子包的package.json文件
73
+ * @param {string} workDir 工作目录
74
+ * @param {string} projectRoot 项目根目录
75
+ * @param {RemoteWorkspaceInfo} subPackageInfos 子包信息
76
+ */
77
+ async function writeSubPackageJson(
78
+ workDir: string,
79
+ projectRoot: string,
80
+ subPackageInfos: RemoteWorkspaceInfo,
81
+ ) {
82
+ const { gitInfo, branchName, subPackageNames } = subPackageInfos;
83
+ // 根据subPackageNames,在workDir下创建子包目录并写入package.json文件
84
+ const dirPaths = subPackageNames.map((subPackageName) =>
85
+ join(workDir, subPackageName),
86
+ );
87
+ // 创建子包目录
88
+ dirPaths.forEach((dirPath) => {
89
+ fs.mkdirSync(dirPath, { recursive: true });
90
+ });
91
+ if (gitInfo && branchName) {
92
+ for (const subPackageName of subPackageNames) {
93
+ const filePath = subPackageName + "/package.json";
94
+ const packageJsonUrl = getReadFileUrl(gitInfo, branchName, filePath);
95
+ const subPackageJson = (await fetch(packageJsonUrl).then((resp) =>
96
+ resp.json(),
97
+ )) as Promise<PackageJsonInfo>;
98
+ // 处理 package.json 中的 workspace 依赖
99
+ const processedPackageJson = processPackageJson(subPackageJson);
100
+ const packageJsonPath = join(workDir, subPackageName, "package.json");
101
+ // 将处理后的 packageJson 写入文件
102
+ await fs.promises.writeFile(
103
+ packageJsonPath,
104
+ JSON.stringify(processedPackageJson),
105
+ "utf8",
106
+ );
107
+ }
108
+ } else {
109
+ // 解析本地工程根目录下的package.json文件
110
+ for (const subPackageName of subPackageNames) {
111
+ const subPackageJsonPath = join(
112
+ projectRoot,
113
+ subPackageName,
114
+ "package.json",
115
+ );
116
+ const subPackageJson = await fs.promises.readFile(
117
+ subPackageJsonPath,
118
+ "utf8",
119
+ );
120
+ const processedPackageJson = processPackageJson(
121
+ JSON.parse(subPackageJson),
122
+ );
123
+ await fs.promises.writeFile(
124
+ join(workDir, subPackageName, "package.json"),
125
+ JSON.stringify(processedPackageJson),
126
+ "utf8",
127
+ );
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @Desc: 向工作目录添加package.json文件
134
+ * @param {string} workDir 工作目录
135
+ * @param {Object} packageJson package.json文件内容
136
+ */
137
+ async function writePackageJson(options: GenerateLockOptions) {
138
+ const { workDir, packageJson, subPackageInfos, projectRoot } = options;
139
+ const packageJsonPath = join(workDir, "package.json");
140
+ fs.mkdirSync(dirname(packageJsonPath), { recursive: true });
141
+ if (subPackageInfos?.subPackageNames) {
142
+ // 处理 package.json 中的 workspace 依赖
143
+ const processedPackageJson = processPackageJson(packageJson);
144
+ // 处理子包
145
+ await writeSubPackageJson(workDir, projectRoot, subPackageInfos);
146
+ // 将处理后的 packageJson 写入文件
147
+ await fs.promises.writeFile(
148
+ packageJsonPath,
149
+ JSON.stringify(processedPackageJson),
150
+ "utf8",
151
+ );
152
+ } else {
153
+ await fs.promises.writeFile(
154
+ packageJsonPath,
155
+ JSON.stringify(packageJson),
156
+ "utf8",
157
+ );
158
+ }
159
+ }
160
+
161
+ /**
162
+ * @Desc: 在指定工作目录中生成lock文件
163
+ * @param {string} workDir 工作目录
164
+ */
165
+ async function createLockFile(
166
+ workDir: string,
167
+ subPackageInfos: RemoteWorkspaceInfo | null,
168
+ currentPackageManager: PackageManager,
169
+ ) {
170
+ // 只支持pnpm和npm
171
+ if (!subPackageInfos) {
172
+ const cmd = "npm install --package-lock-only --force";
173
+ // 在工作目录中执行命令
174
+ await runCommand(cmd, workDir);
175
+ } else {
176
+ const { subPackageNames } = subPackageInfos;
177
+ const cmd =
178
+ currentPackageManager === "pnpm"
179
+ ? `pnpm install --lockfile-only`
180
+ : `npm install --package-lock-only --force`;
181
+ // 在不同的工作目录中执行命令
182
+ for (const subPackageName of subPackageNames) {
183
+ const subPackageDir = join(workDir, subPackageName);
184
+ await runCommand(cmd, subPackageDir);
185
+ }
186
+ // 最后在工作目录中执行命令
187
+ await runCommand(cmd, workDir);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * @Desc: 向工作目录添加package.json文件,然后生成lock文件
193
+ * @param {string} workDir 工作目录
194
+ * @param {Object} packageJson package.json文件内容
195
+ */
196
+ export async function generateLock(options: GenerateLockOptions) {
197
+ const { workDir, subPackageInfos, currentPackageManager } = options;
198
+ // 1. 将 package.json 写入工作目录
199
+ // NOTE:不管是npm还是pnpm,package.json文件基本都是相同的
200
+ await writePackageJson(options);
201
+ // 2. 生成 lock 文件
202
+ await createLockFile(workDir, subPackageInfos, currentPackageManager);
203
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { audit } from "./audit/index.js";
2
+ import { generateLock } from "./generateLock/index.js";
3
+ import { detectPackageManager } from "./parseProject/detectPackageManager.js";
4
+ import { parseProject } from "./parseProject/index.js";
5
+ import { parseWorkspace } from "./parseProject/parseWorkspace.js";
6
+ import { render } from "./render/index.js";
7
+ import { createWorkDir, deleteWorkDir } from "./utils/dirUtils.js";
8
+ import fs from "fs";
9
+
10
+ /**
11
+ * 根据项目根目录,审计项目中所有的包(含项目本身)
12
+ * @param {string} projectRoot 项目根目录,可以是本地目录的绝对路径,也可以是远程仓库的URL
13
+ * @param {string} savePath 保存审计结果的文件名,审计结果是一个标准格式的markdown字符串
14
+ */
15
+ export async function auditProject(projectRoot: string, savePath: string) {
16
+ // 1. 创建工作目录
17
+ const workDir = await createWorkDir();
18
+ // 2. 判断项目的包管理器类型
19
+ const currentPackageManager = detectPackageManager(projectRoot);
20
+ // 3. 解析项目的package.json文件
21
+ const packageJson = await parseProject(projectRoot);
22
+ // 4· 判断项目是否是monorepo工程
23
+ const subPackageInfos = await parseWorkspace({
24
+ projectRoot,
25
+ packageJsonRoot: packageJson,
26
+ });
27
+ // 5. 向工作目录添加package.json文件,然后生成lock文件
28
+ await generateLock({
29
+ workDir,
30
+ packageJson,
31
+ subPackageInfos,
32
+ projectRoot,
33
+ currentPackageManager,
34
+ });
35
+ // 6. 对工作目录进行审计
36
+ const auditResult = await audit({
37
+ workDir,
38
+ packageJson,
39
+ subPackageInfos,
40
+ currentPackageManager,
41
+ });
42
+ // 7. 渲染审计结果
43
+ const renderedResult = await render(auditResult, packageJson);
44
+ // 8. 删除工作目录
45
+ await deleteWorkDir(workDir);
46
+ // 9. 将结果保存到指定路径
47
+ await fs.promises.writeFile(savePath, renderedResult);
48
+ }
@@ -0,0 +1,43 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { auditProject } from "./index.js";
5
+
6
+ const server = new McpServer({
7
+ name: "audit-server",
8
+ title: "前端工程安全审计服务",
9
+ version: "0.1.0",
10
+ });
11
+
12
+ server.registerTool(
13
+ "auditProject",
14
+ {
15
+ title: "审计前端工程",
16
+ description:
17
+ "审计前端工程的所有直接和间接依赖,得到安全审计结果。支持本地工程的审计,也支持远程仓库的审计。审计结果为标准格式的markdown字符串,不用修改,直接用于展示即可。",
18
+ inputSchema: {
19
+ projectRoot: z
20
+ .string()
21
+ .describe("本地工程的根路径,或者远程仓库的URL地址"),
22
+ savePath: z
23
+ .string()
24
+ .describe(
25
+ "保存审计结果的路径,传递当前工程的根路径下的工程明audit.md,如果没有当前工程,则传递桌面路径下的audit.md(注意,桌面路径必须传入绝对路径)",
26
+ ),
27
+ },
28
+ },
29
+ async ({ projectRoot, savePath }) => {
30
+ await auditProject(projectRoot, savePath);
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: `审计完成,结果已保存到: ${savePath}`,
36
+ },
37
+ ],
38
+ };
39
+ },
40
+ );
41
+
42
+ const transport = new StdioServerTransport();
43
+ server.connect(transport);