@chen_258/audit-tool 1.0.5 → 1.1.1
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/dist/audit/index.js +2 -1
- package/dist/entry/index.js +44 -11
- package/dist/factory/detector.js +77 -0
- package/dist/factory/index.js +101 -0
- package/dist/factory/strategies/base.js +95 -0
- package/dist/factory/strategies/hybrid.strategy.js +91 -0
- package/dist/factory/strategies/index.js +7 -0
- package/dist/factory/strategies/lerna.strategy.js +48 -0
- package/dist/factory/strategies/nx.strategy.js +47 -0
- package/dist/factory/strategies/rush.strategy.js +67 -0
- package/dist/factory/strategies/turborepo.strategy.js +49 -0
- package/dist/factory/strategies/workspaces.strategy.js +61 -0
- package/dist/factory/types.js +1 -0
- package/dist/mcp/server.js +41 -24
- package/dist/package-json/index.js +71 -41
- package/dist/report-generator/index.js +119 -0
- package/package.json +13 -3
package/dist/audit/index.js
CHANGED
|
@@ -36,7 +36,7 @@ class Audit {
|
|
|
36
36
|
return Promise.resolve(this.cachedResult);
|
|
37
37
|
}
|
|
38
38
|
return new Promise((resolve, reject) => {
|
|
39
|
-
const childProcess = spawn('npm', ['audit', '--json'], {
|
|
39
|
+
const childProcess = spawn('npm', ['audit', '--json', '--no-workspaces'], {
|
|
40
40
|
cwd: this.inputPath,
|
|
41
41
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
42
|
shell: true,
|
|
@@ -229,3 +229,4 @@ export async function audit(inputPath, outputPath) {
|
|
|
229
229
|
process.stderr.write('审计函数执行完成,程序可以继续...\n');
|
|
230
230
|
return report;
|
|
231
231
|
}
|
|
232
|
+
export { Audit };
|
package/dist/entry/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { createPageJson,
|
|
1
|
+
import { createPageJson, getPageJsonWithPaths } from '../package-json/index.js';
|
|
2
2
|
import { createWorkDir, deleteWorkDir } from '../file/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { Audit } from '../audit/index.js';
|
|
4
|
+
import { generateSummaryReport } from '../report-generator/index.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
4
7
|
/**
|
|
5
8
|
* CLI 入口 — 独立运行模式
|
|
6
9
|
* 直接在终端执行安全审计,结果输出为 Markdown 文件
|
|
@@ -9,18 +12,48 @@ import { audit } from '../audit/index.js';
|
|
|
9
12
|
*/
|
|
10
13
|
async function main(url) {
|
|
11
14
|
const workDir = await createWorkDir(); // 创建临时目录
|
|
12
|
-
// 获取本地或者远程工程文件的package.json
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// 获取本地或者远程工程文件的package.json(标准化扫描结果)
|
|
16
|
+
const scanResult = await getPageJsonWithPaths(url);
|
|
17
|
+
const { packages: packagesInfo, detectedTypes } = scanResult;
|
|
18
|
+
console.log(`\n📁 检测到工程类型: ${detectedTypes.join(', ')}`);
|
|
19
|
+
console.log(`📦 发现 ${packagesInfo.length} 个 package.json`);
|
|
20
|
+
// 在工作目录生成临时的 package.json
|
|
21
|
+
const packages = packagesInfo.map(pkg => pkg.content);
|
|
22
|
+
await createPageJson(workDir, packages);
|
|
23
|
+
// 获取 workDir 下的所有子目录(每个子目录对应一个 package)
|
|
24
|
+
const subDirs = fs.readdirSync(workDir).filter(item => {
|
|
25
|
+
const itemPath = path.join(workDir, item);
|
|
26
|
+
return fs.statSync(itemPath).isDirectory();
|
|
27
|
+
});
|
|
28
|
+
// 对每个子目录进行安全审计,并收集审计结果
|
|
29
|
+
const auditResults = [];
|
|
30
|
+
for (let i = 0; i < subDirs.length; i++) {
|
|
31
|
+
const subDir = subDirs[i];
|
|
32
|
+
const subDirPath = path.join(workDir, subDir);
|
|
33
|
+
console.log(`\n🔍 开始审计: ${subDir}`);
|
|
34
|
+
// 创建 Audit 实例并获取审计结果(不生成单独文件)
|
|
35
|
+
const auditInstance = new Audit(subDirPath, '');
|
|
36
|
+
const auditResult = JSON.parse(await auditInstance.command());
|
|
37
|
+
// 获取对应的 package.json 路径和工程类型
|
|
38
|
+
const packageJsonPath = packagesInfo[i]?.path || '';
|
|
39
|
+
const projectType = packagesInfo[i]?.projectType || 'unknown';
|
|
40
|
+
auditResults.push({
|
|
41
|
+
subDir,
|
|
42
|
+
packageJsonPath,
|
|
43
|
+
projectType,
|
|
44
|
+
auditResult
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// 生成汇总报告到项目根目录
|
|
48
|
+
console.log(`\n📊 生成汇总报告...`);
|
|
49
|
+
const summaryPath = path.join(process.cwd(), 'audit-summary.md');
|
|
50
|
+
await generateSummaryReport(auditResults, summaryPath);
|
|
18
51
|
await deleteWorkDir(workDir); // 删除临时目录
|
|
19
52
|
}
|
|
20
53
|
// 测试本地项目
|
|
21
|
-
main('https://raw.githubusercontent.com/
|
|
22
|
-
console.log('✅ 程序执行完毕');
|
|
54
|
+
main('https://raw.githubusercontent.com/lerna/lerna/refs/heads/main').then(() => {
|
|
55
|
+
console.log('\n✅ 程序执行完毕');
|
|
23
56
|
}).catch((error) => {
|
|
24
|
-
console.error('❌ 程序执行失败:', error.message);
|
|
57
|
+
console.error('\n❌ 程序执行失败:', error.message);
|
|
25
58
|
process.exit(1);
|
|
26
59
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 各架构类型的配置文件标识
|
|
3
|
+
* detector 通过检查这些文件是否存在来判断工程类型
|
|
4
|
+
*/
|
|
5
|
+
const DETECTION_MARKERS = {
|
|
6
|
+
'lerna.json': 'lerna',
|
|
7
|
+
'nx.json': 'nx',
|
|
8
|
+
'turbo.json': 'turborepo',
|
|
9
|
+
'rush.json': 'rush',
|
|
10
|
+
'pnpm-workspace.yaml': 'workspaces',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 工程类型检测器
|
|
14
|
+
*
|
|
15
|
+
* 通过读取根目录下的配置文件来判断 Monorepo 架构类型。
|
|
16
|
+
* 支持本地路径和远程 URL 两种来源。
|
|
17
|
+
*
|
|
18
|
+
* 检测优先级:
|
|
19
|
+
* 1. 检查各架构的专属配置文件(lerna.json, nx.json, turbo.json, rush.json)
|
|
20
|
+
* 2. 检查 pnpm-workspace.yaml
|
|
21
|
+
* 3. 检查根 package.json 中是否包含 workspaces 字段
|
|
22
|
+
* 4. 若检测到多种类型,自动判定为 hybrid(混合架构)
|
|
23
|
+
* 5. 若均未检测到,判定为 single(单项目)
|
|
24
|
+
*/
|
|
25
|
+
export class ProjectDetector {
|
|
26
|
+
/**
|
|
27
|
+
* 检测工程类型
|
|
28
|
+
* @param rootPath 项目根路径(本地路径或远程 URL)
|
|
29
|
+
* @param readFileFn 读取文件内容的函数(支持本地和远程)
|
|
30
|
+
* @param rootPackageJson 根目录 package.json 的解析内容
|
|
31
|
+
* @returns 检测到的工程类型数组
|
|
32
|
+
*/
|
|
33
|
+
static async detect(rootPath, readFileFn, rootPackageJson) {
|
|
34
|
+
const detectedTypes = [];
|
|
35
|
+
// 1. 通过配置文件标识检测架构类型
|
|
36
|
+
for (const [fileName, type] of Object.entries(DETECTION_MARKERS)) {
|
|
37
|
+
try {
|
|
38
|
+
const configPath = this.resolveConfigPath(rootPath, fileName);
|
|
39
|
+
await readFileFn(configPath);
|
|
40
|
+
// 文件存在,确认检测到该类型
|
|
41
|
+
if (!detectedTypes.includes(type)) {
|
|
42
|
+
detectedTypes.push(type);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 文件不存在或读取失败,跳过
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 2. 检查根 package.json 的 workspaces 字段
|
|
50
|
+
if (rootPackageJson && rootPackageJson.workspaces) {
|
|
51
|
+
if (!detectedTypes.includes('workspaces')) {
|
|
52
|
+
detectedTypes.push('workspaces');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 3. 判断结果
|
|
56
|
+
if (detectedTypes.length === 0) {
|
|
57
|
+
return ['single'];
|
|
58
|
+
}
|
|
59
|
+
// 只有一种类型时直接返回;多种类型时合并为 hybrid
|
|
60
|
+
if (detectedTypes.length === 1) {
|
|
61
|
+
return detectedTypes;
|
|
62
|
+
}
|
|
63
|
+
return ['hybrid', ...detectedTypes];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 拼接配置文件路径
|
|
67
|
+
* @param rootPath 项目根路径
|
|
68
|
+
* @param fileName 配置文件名
|
|
69
|
+
* @returns 配置文件的完整路径
|
|
70
|
+
*/
|
|
71
|
+
static resolveConfigPath(rootPath, fileName) {
|
|
72
|
+
if (rootPath.startsWith('http://') || rootPath.startsWith('https://')) {
|
|
73
|
+
return `${rootPath}/${fileName}`;
|
|
74
|
+
}
|
|
75
|
+
return rootPath.endsWith(fileName) ? rootPath : `${rootPath}/${fileName}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ProjectDetector } from './detector.js';
|
|
2
|
+
import { BaseStrategy } from './strategies/base.js';
|
|
3
|
+
import { WorkspacesStrategy, LernaStrategy, NxStrategy, TurborepoStrategy, RushStrategy, HybridStrategy, } from './strategies/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* 策略注册表:将 ProjectType 映射到对应的策略构造函数
|
|
6
|
+
* 扩展新架构类型时,只需在此注册新的策略即可
|
|
7
|
+
*/
|
|
8
|
+
const STRATEGY_REGISTRY = {
|
|
9
|
+
workspaces: WorkspacesStrategy,
|
|
10
|
+
lerna: LernaStrategy,
|
|
11
|
+
nx: NxStrategy,
|
|
12
|
+
turborepo: TurborepoStrategy,
|
|
13
|
+
rush: RushStrategy,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 策略工厂
|
|
17
|
+
*
|
|
18
|
+
* 统一入口:自动检测工程类型 → 创建对应策略 → 执行扫描 → 返回标准化结果。
|
|
19
|
+
* 替代原有的 Factory 类,支持所有 Monorepo 架构类型。
|
|
20
|
+
*
|
|
21
|
+
* 用法:
|
|
22
|
+
* ```ts
|
|
23
|
+
* const result = await StrategyFactory.scan(rootPath, readFileFn, rootPackageJson);
|
|
24
|
+
* // result.detectedTypes => ['workspaces']
|
|
25
|
+
* // result.packages => [{ path, content, projectType, isRoot }, ...]
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class StrategyFactory {
|
|
29
|
+
/**
|
|
30
|
+
* 执行完整的扫描流程
|
|
31
|
+
* @param rootPath 项目根路径(本地路径或远程 URL)
|
|
32
|
+
* @param readFileFn 读取文件的函数(支持本地和远程)
|
|
33
|
+
* @param rootPackageJson 根目录 package.json 的解析内容
|
|
34
|
+
* @returns 标准化扫描结果
|
|
35
|
+
*/
|
|
36
|
+
static async scan(rootPath, readFileFn, rootPackageJson) {
|
|
37
|
+
// 1. 检测工程类型
|
|
38
|
+
const detectedTypes = await ProjectDetector.detect(rootPath, readFileFn, rootPackageJson);
|
|
39
|
+
// 2. 创建策略实例
|
|
40
|
+
const strategy = StrategyFactory.createStrategy(detectedTypes);
|
|
41
|
+
// 3. 执行扫描
|
|
42
|
+
const childPackages = await strategy.scan(rootPath, rootPackageJson, readFileFn);
|
|
43
|
+
// 4. 构建根 package.json 的 PackageInfo
|
|
44
|
+
const rootPackagePath = StrategyFactory.resolvePackageJsonPath(rootPath);
|
|
45
|
+
const rootPackage = {
|
|
46
|
+
path: rootPackagePath,
|
|
47
|
+
content: rootPackageJson,
|
|
48
|
+
projectType: detectedTypes[0],
|
|
49
|
+
isRoot: true,
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
detectedTypes,
|
|
53
|
+
packages: [rootPackage, ...childPackages],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 根据检测到的工程类型创建对应的策略实例
|
|
58
|
+
* @param detectedTypes 检测到的工程类型数组
|
|
59
|
+
* @returns 策略实例
|
|
60
|
+
*/
|
|
61
|
+
static createStrategy(detectedTypes) {
|
|
62
|
+
const primaryType = detectedTypes[0];
|
|
63
|
+
// 混合架构:使用 HybridStrategy(内部委托多个子策略)
|
|
64
|
+
if (primaryType === 'hybrid') {
|
|
65
|
+
return new HybridStrategy(detectedTypes);
|
|
66
|
+
}
|
|
67
|
+
// 单项目:不需要扫描子包,返回一个空扫描的策略
|
|
68
|
+
if (primaryType === 'single') {
|
|
69
|
+
class SingleStrategy extends BaseStrategy {
|
|
70
|
+
constructor() {
|
|
71
|
+
super(...arguments);
|
|
72
|
+
Object.defineProperty(this, "type", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
configurable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
value: 'single'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async extractPatterns() { return []; }
|
|
80
|
+
}
|
|
81
|
+
return new SingleStrategy();
|
|
82
|
+
}
|
|
83
|
+
// 查找注册表中的策略
|
|
84
|
+
const StrategyClass = STRATEGY_REGISTRY[primaryType];
|
|
85
|
+
if (!StrategyClass) {
|
|
86
|
+
throw new Error(`未注册的策略类型: ${primaryType}`);
|
|
87
|
+
}
|
|
88
|
+
return new StrategyClass();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 解析根目录 package.json 的路径
|
|
92
|
+
* @param rootPath 项目根路径
|
|
93
|
+
* @returns package.json 的完整路径
|
|
94
|
+
*/
|
|
95
|
+
static resolvePackageJsonPath(rootPath) {
|
|
96
|
+
if (rootPath.endsWith('package.json')) {
|
|
97
|
+
return rootPath;
|
|
98
|
+
}
|
|
99
|
+
return `${rootPath}/package.json`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
/**
|
|
4
|
+
* 扫描策略抽象基类
|
|
5
|
+
*
|
|
6
|
+
* 提供通用的 glob 扫描能力,子类只需实现 extractPatterns 方法
|
|
7
|
+
* 来定义如何从配置文件中提取子包路径模式。
|
|
8
|
+
*/
|
|
9
|
+
export class BaseStrategy {
|
|
10
|
+
/**
|
|
11
|
+
* 执行扫描:提取路径模式 → glob 匹配 → 读取 package.json → 返回标准化结果
|
|
12
|
+
* @param rootPath 项目根路径
|
|
13
|
+
* @param rootPackageJson 根 package.json 内容
|
|
14
|
+
* @param readFileFn 读取文件函数
|
|
15
|
+
* @returns 子包 PackageInfo 数组(不包含根 package.json)
|
|
16
|
+
*/
|
|
17
|
+
async scan(rootPath, rootPackageJson, readFileFn) {
|
|
18
|
+
const patterns = await this.extractPatterns(rootPath, rootPackageJson, readFileFn);
|
|
19
|
+
if (patterns.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
// 通过 glob 匹配所有 package.json 文件路径
|
|
23
|
+
const allJsonPaths = this.matchPackageJsonFiles(rootPath, patterns);
|
|
24
|
+
// 读取每个 package.json 内容并构建标准化结果
|
|
25
|
+
const packages = [];
|
|
26
|
+
const visitedPaths = new Set();
|
|
27
|
+
for (const jsonPath of allJsonPaths) {
|
|
28
|
+
const normalizedPath = this.normalizePath(jsonPath);
|
|
29
|
+
if (visitedPaths.has(normalizedPath))
|
|
30
|
+
continue;
|
|
31
|
+
visitedPaths.add(normalizedPath);
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFileFn(jsonPath);
|
|
34
|
+
packages.push({
|
|
35
|
+
path: jsonPath,
|
|
36
|
+
content,
|
|
37
|
+
projectType: this.type,
|
|
38
|
+
isRoot: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 某些匹配到的文件可能读取失败(如权限问题),跳过继续
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return packages;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 使用 glob 匹配指定路径模式下的所有 package.json 文件
|
|
49
|
+
* @param rootPath 项目根路径
|
|
50
|
+
* @param patterns 路径模式数组
|
|
51
|
+
* @returns 匹配到的 package.json 文件路径数组(已去重)
|
|
52
|
+
*/
|
|
53
|
+
matchPackageJsonFiles(rootPath, patterns) {
|
|
54
|
+
const matchedFiles = [];
|
|
55
|
+
for (const pattern of patterns) {
|
|
56
|
+
const resolvedPattern = this.resolveGlobPattern(rootPath, pattern);
|
|
57
|
+
const files = glob.sync(resolvedPattern);
|
|
58
|
+
if (files.length > 0) {
|
|
59
|
+
matchedFiles.push(files);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return this.flatten(matchedFiles);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 解析 glob 模式:将路径模式和根路径拼接为完整的 glob 表达式
|
|
66
|
+
* @param rootPath 项目根路径
|
|
67
|
+
* @param pattern 子路径模式(如 "packages/*")
|
|
68
|
+
* @returns 完整的 glob 模式
|
|
69
|
+
*/
|
|
70
|
+
resolveGlobPattern(rootPath, pattern) {
|
|
71
|
+
// 模式本身已包含 **/package.json 格式则直接拼接
|
|
72
|
+
if (pattern.includes('package.json')) {
|
|
73
|
+
return path.resolve(rootPath, pattern);
|
|
74
|
+
}
|
|
75
|
+
// 否则默认在匹配目录下查找 **/package.json
|
|
76
|
+
return path.resolve(rootPath, pattern, '**', 'package.json');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 规范化路径:统一路径分隔符,去除尾部斜杠
|
|
80
|
+
*/
|
|
81
|
+
normalizePath(filePath) {
|
|
82
|
+
return filePath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 扁平化多维数组为一维数组
|
|
86
|
+
*/
|
|
87
|
+
flatten(arr) {
|
|
88
|
+
return arr.reduce((result, element) => {
|
|
89
|
+
if (Array.isArray(element)) {
|
|
90
|
+
return result.concat(this.flatten(element));
|
|
91
|
+
}
|
|
92
|
+
return result.concat(element);
|
|
93
|
+
}, []);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
import { WorkspacesStrategy } from './workspaces.strategy.js';
|
|
3
|
+
import { LernaStrategy } from './lerna.strategy.js';
|
|
4
|
+
import { NxStrategy } from './nx.strategy.js';
|
|
5
|
+
import { TurborepoStrategy } from './turborepo.strategy.js';
|
|
6
|
+
import { RushStrategy } from './rush.strategy.js';
|
|
7
|
+
/**
|
|
8
|
+
* 混合架构扫描策略
|
|
9
|
+
*
|
|
10
|
+
* 当检测到多种架构特征同时存在时(如 Workspaces + Turborepo),
|
|
11
|
+
* 合并多个策略的扫描结果并去重。
|
|
12
|
+
*
|
|
13
|
+
* 合并规则:
|
|
14
|
+
* - 同一路径的 package.json 只保留一次(按策略优先级取第一个匹配)
|
|
15
|
+
* - projectType 标记为 'hybrid'
|
|
16
|
+
*/
|
|
17
|
+
export class HybridStrategy extends BaseStrategy {
|
|
18
|
+
constructor(detectedTypes) {
|
|
19
|
+
super();
|
|
20
|
+
Object.defineProperty(this, "type", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: 'hybrid'
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* 混合策略中参与合并的子策略列表(按优先级排序)
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(this, "subStrategies", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
// 根据检测到的类型创建对应的子策略(排除 'hybrid' 和 'single')
|
|
36
|
+
this.subStrategies = this.createSubStrategies(detectedTypes);
|
|
37
|
+
}
|
|
38
|
+
async extractPatterns(rootPath, rootPackageJson, readFileFn) {
|
|
39
|
+
// 合并所有子策略的路径模式并去重
|
|
40
|
+
const allPatterns = new Set();
|
|
41
|
+
for (const strategy of this.subStrategies) {
|
|
42
|
+
const patterns = await strategy.extractPatterns(rootPath, rootPackageJson, readFileFn);
|
|
43
|
+
patterns.forEach(p => allPatterns.add(p));
|
|
44
|
+
}
|
|
45
|
+
return [...allPatterns];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 执行扫描:委托给各子策略,合并结果并去重
|
|
49
|
+
*/
|
|
50
|
+
async scan(rootPath, rootPackageJson, readFileFn) {
|
|
51
|
+
const allPackages = [];
|
|
52
|
+
const visitedPaths = new Set();
|
|
53
|
+
for (const strategy of this.subStrategies) {
|
|
54
|
+
const packages = await strategy.scan(rootPath, rootPackageJson, readFileFn);
|
|
55
|
+
for (const pkg of packages) {
|
|
56
|
+
const normalizedPath = this.normalizePath(pkg.path);
|
|
57
|
+
if (visitedPaths.has(normalizedPath))
|
|
58
|
+
continue;
|
|
59
|
+
visitedPaths.add(normalizedPath);
|
|
60
|
+
// 统一标记为 hybrid 类型
|
|
61
|
+
allPackages.push({
|
|
62
|
+
...pkg,
|
|
63
|
+
projectType: 'hybrid',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return allPackages;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 根据检测到的工程类型创建对应的子策略实例
|
|
71
|
+
*/
|
|
72
|
+
createSubStrategies(detectedTypes) {
|
|
73
|
+
const strategyMap = {
|
|
74
|
+
workspaces: () => new WorkspacesStrategy(),
|
|
75
|
+
lerna: () => new LernaStrategy(),
|
|
76
|
+
nx: () => new NxStrategy(),
|
|
77
|
+
turborepo: () => new TurborepoStrategy(),
|
|
78
|
+
rush: () => new RushStrategy(),
|
|
79
|
+
};
|
|
80
|
+
const strategies = [];
|
|
81
|
+
for (const type of detectedTypes) {
|
|
82
|
+
if (type === 'hybrid' || type === 'single')
|
|
83
|
+
continue;
|
|
84
|
+
const factory = strategyMap[type];
|
|
85
|
+
if (factory) {
|
|
86
|
+
strategies.push(factory());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return strategies;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BaseStrategy } from './base.js';
|
|
2
|
+
export { WorkspacesStrategy } from './workspaces.strategy.js';
|
|
3
|
+
export { LernaStrategy } from './lerna.strategy.js';
|
|
4
|
+
export { NxStrategy } from './nx.strategy.js';
|
|
5
|
+
export { TurborepoStrategy } from './turborepo.strategy.js';
|
|
6
|
+
export { RushStrategy } from './rush.strategy.js';
|
|
7
|
+
export { HybridStrategy } from './hybrid.strategy.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lerna 扫描策略
|
|
4
|
+
*
|
|
5
|
+
* 从 lerna.json 中提取子包路径:
|
|
6
|
+
* - 当 useWorkspaces: true 时,复用 workspaces 字段(避免重复扫描)
|
|
7
|
+
* - 否则从 lerna.json 的 packages 字段提取路径
|
|
8
|
+
*/
|
|
9
|
+
export class LernaStrategy extends BaseStrategy {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
Object.defineProperty(this, "type", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: 'lerna'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async extractPatterns(rootPath, rootPackageJson, readFileFn) {
|
|
20
|
+
const lernaPath = `${rootPath}/lerna.json`;
|
|
21
|
+
let lernaConfig;
|
|
22
|
+
try {
|
|
23
|
+
lernaConfig = await readFileFn(lernaPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
// useWorkspaces: true 时,复用根 package.json 的 workspaces 字段
|
|
29
|
+
if (lernaConfig?.useWorkspaces && rootPackageJson?.workspaces) {
|
|
30
|
+
if (Array.isArray(rootPackageJson.workspaces)) {
|
|
31
|
+
return rootPackageJson.workspaces.filter((w) => typeof w === 'string');
|
|
32
|
+
}
|
|
33
|
+
// 对象格式的 workspaces:合并所有值
|
|
34
|
+
const result = [];
|
|
35
|
+
for (const value of Object.values(rootPackageJson.workspaces)) {
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
result.push(...value.filter((v) => typeof v === 'string'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
// 否则从 lerna.json 的 packages 字段提取
|
|
43
|
+
if (lernaConfig?.packages && Array.isArray(lernaConfig.packages)) {
|
|
44
|
+
return lernaConfig.packages.filter((p) => typeof p === 'string');
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Nx 扫描策略
|
|
4
|
+
*
|
|
5
|
+
* Nx 不一定配置 workspaces 字段,子包路径通过以下方式确定:
|
|
6
|
+
* 1. 读取 nx.json,检查 implicitDependencies 或 workspaceLayout
|
|
7
|
+
* 2. 默认遍历 apps/、libs/、packages/ 目录下所有包含 package.json 的子目录
|
|
8
|
+
*/
|
|
9
|
+
export class NxStrategy extends BaseStrategy {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
Object.defineProperty(this, "type", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: 'nx'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async extractPatterns(rootPath, rootPackageJson, readFileFn) {
|
|
20
|
+
const patterns = [];
|
|
21
|
+
// 优先从 package.json 的 workspaces 字段提取(Nx 项目可能同时配置 workspaces)
|
|
22
|
+
if (rootPackageJson?.workspaces) {
|
|
23
|
+
if (Array.isArray(rootPackageJson.workspaces)) {
|
|
24
|
+
patterns.push(...rootPackageJson.workspaces.filter((w) => typeof w === 'string'));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// 如果 workspaces 已提供路径,直接使用
|
|
28
|
+
if (patterns.length > 0) {
|
|
29
|
+
return patterns;
|
|
30
|
+
}
|
|
31
|
+
// 尝试从 nx.json 中读取 workspaceLayout 配置
|
|
32
|
+
try {
|
|
33
|
+
const nxConfig = await readFileFn(`${rootPath}/nx.json`);
|
|
34
|
+
if (nxConfig?.workspaceLayout) {
|
|
35
|
+
const { appsDir = 'apps', libsDir = 'libs' } = nxConfig.workspaceLayout;
|
|
36
|
+
patterns.push(`${appsDir}/*`, `${libsDir}/*`);
|
|
37
|
+
return patterns;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// nx.json 读取失败,使用默认目录
|
|
42
|
+
}
|
|
43
|
+
// Nx 默认目录
|
|
44
|
+
patterns.push('apps/*', 'libs/*');
|
|
45
|
+
return patterns;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rush 扫描策略
|
|
4
|
+
*
|
|
5
|
+
* 从 rush.json 的 projects 数组中提取每个子包的 projectFolder 路径。
|
|
6
|
+
* Rush 的路径格式为直接目录路径(不带 glob 通配符),如 "apps/app-a"。
|
|
7
|
+
*/
|
|
8
|
+
export class RushStrategy extends BaseStrategy {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
Object.defineProperty(this, "type", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: 'rush'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async extractPatterns(rootPath, _rootPackageJson, readFileFn) {
|
|
19
|
+
let rushConfig;
|
|
20
|
+
try {
|
|
21
|
+
rushConfig = await readFileFn(`${rootPath}/rush.json`);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
if (!rushConfig?.projects || !Array.isArray(rushConfig.projects)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
// 从每个 project 中提取 projectFolder 作为直接路径
|
|
30
|
+
// Rush 的 projectFolder 是精确目录路径,不使用 glob 通配符
|
|
31
|
+
return rushConfig.projects
|
|
32
|
+
.map((project) => project.projectFolder)
|
|
33
|
+
.filter((folder) => typeof folder === 'string' && folder.length > 0);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Rush 的路径是精确目录,重写 scan 方法直接读取 package.json
|
|
37
|
+
*/
|
|
38
|
+
async scan(rootPath, rootPackageJson, readFileFn) {
|
|
39
|
+
const patterns = await this.extractPatterns(rootPath, rootPackageJson, readFileFn);
|
|
40
|
+
if (patterns.length === 0) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const packages = [];
|
|
44
|
+
const visitedPaths = new Set();
|
|
45
|
+
for (const folder of patterns) {
|
|
46
|
+
// Rush 路径直接指向包含 package.json 的目录
|
|
47
|
+
const jsonPath = `${rootPath}/${folder}/package.json`;
|
|
48
|
+
const normalizedPath = this.normalizePath(jsonPath);
|
|
49
|
+
if (visitedPaths.has(normalizedPath))
|
|
50
|
+
continue;
|
|
51
|
+
visitedPaths.add(normalizedPath);
|
|
52
|
+
try {
|
|
53
|
+
const content = await readFileFn(jsonPath);
|
|
54
|
+
packages.push({
|
|
55
|
+
path: jsonPath,
|
|
56
|
+
content,
|
|
57
|
+
projectType: this.type,
|
|
58
|
+
isRoot: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// 跳过读取失败的文件
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return packages;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turborepo 扫描策略
|
|
4
|
+
*
|
|
5
|
+
* turbo.json 本身不定义子包路径,仅定义构建管道。
|
|
6
|
+
* 因此 Turborepo 策略复用 workspaces 的路径配置:
|
|
7
|
+
* - package.json 的 workspaces 字段
|
|
8
|
+
* - pnpm-workspace.yaml 的 packages 字段
|
|
9
|
+
*/
|
|
10
|
+
export class TurborepoStrategy extends BaseStrategy {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
Object.defineProperty(this, "type", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: 'turborepo'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async extractPatterns(rootPath, rootPackageJson, readFileFn) {
|
|
21
|
+
const patterns = [];
|
|
22
|
+
// 从 package.json 的 workspaces 字段提取
|
|
23
|
+
if (rootPackageJson?.workspaces) {
|
|
24
|
+
if (Array.isArray(rootPackageJson.workspaces)) {
|
|
25
|
+
patterns.push(...rootPackageJson.workspaces.filter((w) => typeof w === 'string'));
|
|
26
|
+
}
|
|
27
|
+
else if (typeof rootPackageJson.workspaces === 'object') {
|
|
28
|
+
for (const value of Object.values(rootPackageJson.workspaces)) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
patterns.push(...value.filter((v) => typeof v === 'string'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 尝试从 pnpm-workspace.yaml 补充
|
|
36
|
+
if (patterns.length === 0) {
|
|
37
|
+
try {
|
|
38
|
+
const yamlContent = await readFileFn(`${rootPath}/pnpm-workspace.yaml`);
|
|
39
|
+
if (yamlContent?.packages && Array.isArray(yamlContent.packages)) {
|
|
40
|
+
patterns.push(...yamlContent.packages);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// 跳过
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return patterns;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseStrategy } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Workspaces 扫描策略
|
|
4
|
+
*
|
|
5
|
+
* 从 package.json 的 workspaces 字段或 pnpm-workspace.yaml 中提取子包路径。
|
|
6
|
+
* workspaces 字段支持字符串数组和对象数组两种格式:
|
|
7
|
+
* - 字符串: ["packages/*", "apps/*"]
|
|
8
|
+
* - 对象: [{ "packages": ["core/*", "ui/*"] }]
|
|
9
|
+
*/
|
|
10
|
+
export class WorkspacesStrategy extends BaseStrategy {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
Object.defineProperty(this, "type", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: 'workspaces'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async extractPatterns(rootPath, rootPackageJson, readFileFn) {
|
|
21
|
+
const patterns = [];
|
|
22
|
+
// 优先从 package.json 的 workspaces 字段提取
|
|
23
|
+
if (rootPackageJson?.workspaces) {
|
|
24
|
+
patterns.push(...this.parseWorkspacesField(rootPackageJson.workspaces));
|
|
25
|
+
}
|
|
26
|
+
// 尝试从 pnpm-workspace.yaml 补充(package.json 中未配置时)
|
|
27
|
+
if (patterns.length === 0) {
|
|
28
|
+
try {
|
|
29
|
+
const yamlPath = `${rootPath}/pnpm-workspace.yaml`;
|
|
30
|
+
const yamlContent = await readFileFn(yamlPath);
|
|
31
|
+
if (yamlContent?.packages && Array.isArray(yamlContent.packages)) {
|
|
32
|
+
patterns.push(...yamlContent.packages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// pnpm-workspace.yaml 不存在或非 YAML 格式,跳过
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return patterns;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 解析 workspaces 字段,兼容数组和对象两种格式
|
|
43
|
+
*/
|
|
44
|
+
parseWorkspacesField(workspaces) {
|
|
45
|
+
if (Array.isArray(workspaces)) {
|
|
46
|
+
// 字符串数组: ["packages/*", "apps/*"]
|
|
47
|
+
return workspaces.filter((w) => typeof w === 'string');
|
|
48
|
+
}
|
|
49
|
+
if (typeof workspaces === 'object' && workspaces !== null) {
|
|
50
|
+
// 对象格式: { "packages": ["core/*", "ui/*"] }
|
|
51
|
+
const result = [];
|
|
52
|
+
for (const value of Object.values(workspaces)) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
result.push(...value.filter((v) => typeof v === 'string'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { createPageJson,
|
|
3
|
+
import { createPageJson, getPageJsonWithPaths } from '../package-json/index.js';
|
|
4
4
|
import { createWorkDir, deleteWorkDir } from '../file/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Audit } from '../audit/index.js';
|
|
6
|
+
import { generateSummaryReport } from '../report-generator/index.js';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import fs from 'fs';
|
|
8
9
|
export function createAuditServer() {
|
|
@@ -16,29 +17,45 @@ export function createAuditServer() {
|
|
|
16
17
|
}, async ({ project_url }) => {
|
|
17
18
|
let workDir = null;
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const workDir = await createWorkDir(); // 创建临时目录
|
|
21
|
+
// 获取本地或者远程工程文件的package.json(标准化扫描结果)
|
|
22
|
+
const scanResult = await getPageJsonWithPaths(project_url);
|
|
23
|
+
const { packages: packagesInfo, detectedTypes } = scanResult;
|
|
24
|
+
console.log(`\n📁 检测到工程类型: ${detectedTypes.join(', ')}`);
|
|
25
|
+
console.log(`📦 发现 ${packagesInfo.length} 个 package.json`);
|
|
26
|
+
// 在工作目录生成临时的 package.json
|
|
27
|
+
const packages = packagesInfo.map((pkg) => pkg.content);
|
|
28
|
+
await createPageJson(workDir, packages);
|
|
29
|
+
// 获取 workDir 下的所有子目录(每个子目录对应一个 package)
|
|
30
|
+
const subDirs = fs.readdirSync(workDir).filter(item => {
|
|
31
|
+
const itemPath = path.join(workDir, item);
|
|
32
|
+
return fs.statSync(itemPath).isDirectory();
|
|
33
|
+
});
|
|
34
|
+
// 对每个子目录进行安全审计,并收集审计结果
|
|
35
|
+
const auditResults = [];
|
|
36
|
+
for (let i = 0; i < subDirs.length; i++) {
|
|
37
|
+
const subDir = subDirs[i];
|
|
38
|
+
const subDirPath = path.join(workDir, subDir);
|
|
39
|
+
console.log(`\n🔍 开始审计: ${subDir}`);
|
|
40
|
+
// 创建 Audit 实例并获取审计结果(不生成单独文件)
|
|
41
|
+
const auditInstance = new Audit(subDirPath, '');
|
|
42
|
+
const auditResult = JSON.parse(await auditInstance.command());
|
|
43
|
+
// 获取对应的 package.json 路径和工程类型
|
|
44
|
+
const packageJsonPath = packagesInfo[i]?.path || '';
|
|
45
|
+
const projectType = packagesInfo[i]?.projectType || 'unknown';
|
|
46
|
+
auditResults.push({
|
|
47
|
+
subDir,
|
|
48
|
+
packageJsonPath,
|
|
49
|
+
projectType,
|
|
50
|
+
auditResult
|
|
51
|
+
});
|
|
25
52
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// 1. 创建临时工作目录
|
|
31
|
-
workDir = await createWorkDir();
|
|
32
|
-
// 2. 获取目标项目的 package.json
|
|
33
|
-
const pkgJson = await getPageJson(project_url);
|
|
34
|
-
// 3. 在临时目录写入 package.json,优先复制源项目 lock 文件
|
|
35
|
-
const sourcePath = isLocal ? path.resolve(project_url) : undefined;
|
|
36
|
-
await createPageJson(workDir, pkgJson, sourcePath);
|
|
37
|
-
// 4. 执行 npm audit 并生成 Markdown 报告到目标目录
|
|
38
|
-
const reportPath = path.join(targetDir, 'audit.md');
|
|
39
|
-
const report = await audit(workDir, reportPath);
|
|
53
|
+
// 生成汇总报告到项目根目录
|
|
54
|
+
console.log(`\n📊 生成汇总报告...`);
|
|
55
|
+
const summaryPath = path.join(process.cwd(), 'audit-summary.md');
|
|
56
|
+
await generateSummaryReport(auditResults, summaryPath);
|
|
40
57
|
return {
|
|
41
|
-
content: [{ type: 'text', text:
|
|
58
|
+
content: [{ type: 'text', text: `审计报告已生成: ${summaryPath}` }],
|
|
42
59
|
};
|
|
43
60
|
}
|
|
44
61
|
catch (error) {
|
|
@@ -48,7 +65,7 @@ export function createAuditServer() {
|
|
|
48
65
|
};
|
|
49
66
|
}
|
|
50
67
|
finally {
|
|
51
|
-
//
|
|
68
|
+
// 7. 无论成功失败,清理临时目录
|
|
52
69
|
if (workDir) {
|
|
53
70
|
try {
|
|
54
71
|
await deleteWorkDir(workDir);
|
|
@@ -1,68 +1,98 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { runCommand } from '../common/utils.js';
|
|
4
|
+
import { StrategyFactory } from '@/factory/index.js';
|
|
4
5
|
/**
|
|
5
|
-
* 获取package.json
|
|
6
|
+
* 获取 package.json 文件内容(标准化结构,含路径和工程类型信息)
|
|
6
7
|
* @param url 判断当前项目的路径是远程还是本地
|
|
7
|
-
* @returns
|
|
8
|
+
* @returns 标准化扫描结果
|
|
8
9
|
*/
|
|
9
|
-
export async function
|
|
10
|
+
export async function getPageJsonWithPaths(url) {
|
|
10
11
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
11
|
-
|
|
12
|
-
return getRemotePageJson(url);
|
|
12
|
+
return getRemotePageJsonWithPaths(url);
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
|
|
16
|
-
return getLocalPageJson(url);
|
|
15
|
+
return getLocalPageJsonWithPaths(url);
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
|
-
* 获取本地package.json
|
|
19
|
+
* 获取本地 package.json(标准化结构)
|
|
21
20
|
* @param projectPath 本地路径
|
|
22
|
-
* @returns
|
|
21
|
+
* @returns 标准化扫描结果
|
|
23
22
|
*/
|
|
24
|
-
async function
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
23
|
+
async function getLocalPageJsonWithPaths(projectPath) {
|
|
24
|
+
// 定义本地读取文件的函数
|
|
25
|
+
const readFileFn = async (filePath) => {
|
|
26
|
+
let packageJsonPath = filePath;
|
|
27
|
+
if (!filePath.includes('package.json')) {
|
|
28
|
+
packageJsonPath = path.join(filePath, 'package.json');
|
|
29
|
+
}
|
|
30
|
+
const packageJson = await fs.promises.readFile(packageJsonPath, 'utf8');
|
|
31
|
+
return JSON.parse(packageJson);
|
|
32
|
+
};
|
|
33
|
+
// 读取根目录的 package.json
|
|
34
|
+
const rootJson = await readFileFn(projectPath);
|
|
35
|
+
// 使用 StrategyFactory 自动检测架构类型并扫描所有子包
|
|
36
|
+
return StrategyFactory.scan(projectPath, readFileFn, rootJson);
|
|
28
37
|
}
|
|
29
38
|
/**
|
|
30
|
-
* 获取远程package.json
|
|
39
|
+
* 获取远程 package.json(标准化结构)
|
|
31
40
|
* @param url 远程URL
|
|
32
|
-
* @returns
|
|
33
|
-
* https://github.com/djdjjsfsks/manage/blob/main
|
|
41
|
+
* @returns 标准化扫描结果
|
|
34
42
|
*/
|
|
35
|
-
async function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
async function getRemotePageJsonWithPaths(url) {
|
|
44
|
+
// 定义远程读取文件的函数
|
|
45
|
+
const readFileFn = async (filePath) => {
|
|
46
|
+
const response = await fetch(filePath);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to fetch ${filePath}`);
|
|
49
|
+
}
|
|
50
|
+
const packageJson = await response.text();
|
|
51
|
+
return JSON.parse(packageJson);
|
|
52
|
+
};
|
|
53
|
+
// 读取根目录的 package.json
|
|
54
|
+
const rootJson = await readFileFn(`${url}/package.json`);
|
|
55
|
+
// 使用 StrategyFactory 自动检测架构类型并扫描所有子包
|
|
56
|
+
return StrategyFactory.scan(url, readFileFn, rootJson);
|
|
42
57
|
}
|
|
43
58
|
/**
|
|
44
|
-
* 创建package.json
|
|
45
|
-
* 仅在无 lock 文件时才执行 npm install 生成
|
|
59
|
+
* 创建 package.json 文件,为每个包生成独立的 package.json 和 lock 文件
|
|
46
60
|
* @param workDir 工作目录路径
|
|
47
|
-
* @param val
|
|
48
|
-
* @param sourcePath 源项目路径(用于查找已有的 lock 文件)
|
|
61
|
+
* @param val 页面配置对象或对象数组(支持 Monorepo 多个 package.json)
|
|
49
62
|
*/
|
|
50
|
-
export async function createPageJson(workDir, val
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
export async function createPageJson(workDir, val) {
|
|
64
|
+
// 处理数组或单个对象
|
|
65
|
+
const packages = Array.isArray(val) ? val : [val];
|
|
66
|
+
// 记录已使用的目录名,避免重复
|
|
67
|
+
const usedNames = new Map();
|
|
68
|
+
for (let i = 0; i < packages.length; i++) {
|
|
69
|
+
const pkg = packages[i];
|
|
70
|
+
// 获取包名作为子目录名
|
|
71
|
+
const pkgName = pkg.name || 'package';
|
|
72
|
+
// 清理包名中的特殊字符,用作目录名
|
|
73
|
+
let safeName = pkgName.replace(/^@/, '').replace(/\//g, '--');
|
|
74
|
+
// 如果目录名已存在,添加索引
|
|
75
|
+
if (usedNames.has(safeName)) {
|
|
76
|
+
usedNames.set(safeName, usedNames.get(safeName) + 1);
|
|
77
|
+
safeName = `${safeName}-${usedNames.get(safeName)}`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
usedNames.set(safeName, 0);
|
|
81
|
+
}
|
|
82
|
+
const pkgDir = path.join(workDir, safeName);
|
|
83
|
+
// 创建子目录
|
|
84
|
+
await fs.promises.mkdir(pkgDir, { recursive: true });
|
|
85
|
+
// 移除 workspaces 字段,避免子包被误认为是 workspace 根目录
|
|
86
|
+
const { workspaces, ...pkgWithoutWorkspaces } = pkg;
|
|
87
|
+
// 写入 package.json
|
|
88
|
+
const packageJsonPath = path.join(pkgDir, 'package.json');
|
|
89
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(pkgWithoutWorkspaces, null, 2));
|
|
90
|
+
// 直接执行 npm install 生成干净的 lock 文件
|
|
91
|
+
// 使用 --no-workspaces 防止 npm 识别父目录的 workspace 配置
|
|
63
92
|
await runCommand('npm', [
|
|
64
93
|
'install', '--package-lock-only', '--force',
|
|
65
94
|
'--ignore-scripts', '--no-audit', '--no-fund',
|
|
66
|
-
|
|
95
|
+
'--no-workspaces'
|
|
96
|
+
], { path: pkgDir });
|
|
67
97
|
}
|
|
68
98
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* 生成汇总报告(格式与单个项目 audit.md 一致)
|
|
4
|
+
* @param auditResults 审计结果数组
|
|
5
|
+
* @param outputPath 输出路径
|
|
6
|
+
* @param isMcpMode 是否为 MCP 模式(MCP 模式不写入文件,直接返回内容)
|
|
7
|
+
*/
|
|
8
|
+
export async function generateSummaryReport(auditResults, outputPath, isMcpMode = false) {
|
|
9
|
+
let markdown = `# Monorepo 安全审计汇总报告\n\n`;
|
|
10
|
+
markdown += `**生成时间:** ${new Date().toLocaleString()}\n\n`;
|
|
11
|
+
for (const result of auditResults) {
|
|
12
|
+
const { packageJsonPath, auditResult } = result;
|
|
13
|
+
// 使用 package.json 路径作为标题
|
|
14
|
+
markdown += `# ${packageJsonPath} 项目安全审计报告\n\n`;
|
|
15
|
+
markdown += `**生成时间:** ${new Date().toLocaleString()}\n`;
|
|
16
|
+
markdown += `**审计路径:** ${process.cwd()}\n\n`;
|
|
17
|
+
// 漏洞统计 — 兼容 npm audit JSON 格式
|
|
18
|
+
const vulnerabilities = auditResult.vulnerabilities || {};
|
|
19
|
+
const metadata = auditResult.metadata || {};
|
|
20
|
+
const stats = metadata.vulnerabilities || {};
|
|
21
|
+
// 处理无漏洞场景
|
|
22
|
+
const vulnCount = Object.keys(vulnerabilities).length;
|
|
23
|
+
if (vulnCount === 0 && (!stats.total || stats.total === 0)) {
|
|
24
|
+
markdown += `## 审计结果\n\n`;
|
|
25
|
+
markdown += `**未发现安全漏洞。** 所有依赖均通过安全审计。\n\n`;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
markdown += `## 漏洞统计\n\n`;
|
|
29
|
+
markdown += `| 严重程度 | 数量 |\n`;
|
|
30
|
+
markdown += `|----------|------|\n`;
|
|
31
|
+
markdown += `| **严重** | ${stats.critical || 0} |\n`;
|
|
32
|
+
markdown += `| **高危** | ${stats.high || 0} |\n`;
|
|
33
|
+
markdown += `| **中危** | ${stats.moderate || 0} |\n`;
|
|
34
|
+
markdown += `| **低危** | ${stats.low || 0} |\n`;
|
|
35
|
+
markdown += `| **信息** | ${stats.info || 0} |\n`;
|
|
36
|
+
markdown += `| **总计** | ${stats.total || vulnCount} |\n\n`;
|
|
37
|
+
// 按严重程度分组漏洞
|
|
38
|
+
const vulnerabilitiesBySeverity = {};
|
|
39
|
+
Object.entries(vulnerabilities).forEach(([key, vuln]) => {
|
|
40
|
+
const severity = vuln.severity || 'unknown';
|
|
41
|
+
if (!vulnerabilitiesBySeverity[severity]) {
|
|
42
|
+
vulnerabilitiesBySeverity[severity] = [];
|
|
43
|
+
}
|
|
44
|
+
vulnerabilitiesBySeverity[severity].push({
|
|
45
|
+
key,
|
|
46
|
+
...vuln
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// 严重程度排序(从高到低)
|
|
50
|
+
const severityOrder = ['critical', 'high', 'moderate', 'low', 'info', 'unknown'];
|
|
51
|
+
severityOrder.forEach(severity => {
|
|
52
|
+
const vulnList = vulnerabilitiesBySeverity[severity];
|
|
53
|
+
if (vulnList && vulnList.length > 0) {
|
|
54
|
+
const severityZh = {
|
|
55
|
+
'critical': '严重',
|
|
56
|
+
'high': '高危',
|
|
57
|
+
'moderate': '中危',
|
|
58
|
+
'low': '低危',
|
|
59
|
+
'info': '信息',
|
|
60
|
+
'unknown': '未知'
|
|
61
|
+
}[severity] || severity;
|
|
62
|
+
markdown += `## ${severityZh} 漏洞\n\n`;
|
|
63
|
+
vulnList.forEach((vuln, index) => {
|
|
64
|
+
markdown += `### ${index + 1}. ${vuln.name} (${vuln.key})\n\n`;
|
|
65
|
+
markdown += `**漏洞编号:** ${vuln.via?.[0]?.url || vuln.via?.[0]?.id || vuln.via?.[0]?.title || 'N/A'}\n\n`;
|
|
66
|
+
if (vuln.cvss?.score) {
|
|
67
|
+
markdown += `**CVSS 分数:** ${vuln.cvss.score}\n`;
|
|
68
|
+
markdown += `**CVSS 等级:** ${vuln.cvss.severity}\n\n`;
|
|
69
|
+
}
|
|
70
|
+
markdown += `**漏洞等级:** ${vuln.severity}\n`;
|
|
71
|
+
markdown += `**依赖路径:** \`${vuln.effects?.join(' → ') || vuln.key}\`\n`;
|
|
72
|
+
markdown += `**受影响的版本:** ${vuln.range}\n`;
|
|
73
|
+
markdown += `**修复版本:** ${vuln.fixAvailable?.name ? `${vuln.fixAvailable.name}@${vuln.fixAvailable.version}` : '暂无修复'}\n\n`;
|
|
74
|
+
markdown += `**依赖关系:**\n`;
|
|
75
|
+
if (vuln.nodes && vuln.nodes.length > 0) {
|
|
76
|
+
vuln.nodes.forEach((node) => {
|
|
77
|
+
markdown += `- ${node}\n`;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
markdown += `- 直接依赖\n`;
|
|
82
|
+
}
|
|
83
|
+
markdown += `\n`;
|
|
84
|
+
if (vuln.range === '*' || vuln.range === '>=0.0.0') {
|
|
85
|
+
markdown += `⚠️ **警告:** 此依赖影响所有版本\n\n`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// 修复建议
|
|
92
|
+
markdown += `## 修复建议\n\n`;
|
|
93
|
+
if (auditResult.actions && auditResult.actions.length > 0) {
|
|
94
|
+
auditResult.actions.forEach((action, index) => {
|
|
95
|
+
markdown += `${index + 1}. **${action.action}**\n`;
|
|
96
|
+
if (action.resolves && action.resolves.length > 0) {
|
|
97
|
+
action.resolves.forEach((resolve) => {
|
|
98
|
+
markdown += ` - 修复 \`${resolve.id}\`: ${resolve.path}\n`;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
markdown += `\n`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
markdown += `1. 运行 \`npm audit fix\` 自动修复可修复的漏洞\n`;
|
|
106
|
+
markdown += `2. 运行 \`npm audit fix --force\` 强制修复所有漏洞(可能有破坏性变更)\n`;
|
|
107
|
+
markdown += `3. 手动更新受影响包的版本\n\n`;
|
|
108
|
+
}
|
|
109
|
+
markdown += `\n---\n\n`; // 分隔符
|
|
110
|
+
}
|
|
111
|
+
// MCP 模式直接返回内容,不写入文件
|
|
112
|
+
if (isMcpMode) {
|
|
113
|
+
return markdown;
|
|
114
|
+
}
|
|
115
|
+
// CLI 模式写入文件
|
|
116
|
+
await fs.promises.writeFile(outputPath, markdown, 'utf-8');
|
|
117
|
+
console.log(`✅ 汇总报告已生成: ${outputPath}`);
|
|
118
|
+
return markdown;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chen_258/audit-tool",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/mcp/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,12 +8,21 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/"
|
|
11
|
-
],
|
|
11
|
+
],
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": "16.0.0"
|
|
14
14
|
},
|
|
15
|
+
"workspaces": [
|
|
16
|
+
"src/common",
|
|
17
|
+
"src/audit"
|
|
18
|
+
],
|
|
15
19
|
"description": "基于 npm audit 的 MCP 安全审计服务器",
|
|
16
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"npm-audit",
|
|
23
|
+
"security",
|
|
24
|
+
"vulnerability"
|
|
25
|
+
],
|
|
17
26
|
"license": "MIT",
|
|
18
27
|
"scripts": {
|
|
19
28
|
"dev": "vite",
|
|
@@ -28,6 +37,7 @@
|
|
|
28
37
|
},
|
|
29
38
|
"dependencies": {
|
|
30
39
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
40
|
+
"glob": "^13.0.6",
|
|
31
41
|
"zod": "^4.3.6"
|
|
32
42
|
},
|
|
33
43
|
"devDependencies": {
|