@hhxhhxhhx/hhx-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.
- package/package.json +18 -0
- package/src/audit/currentAudit.js +50 -0
- package/src/audit/getDepChain.js +47 -0
- package/src/audit/index.js +28 -0
- package/src/audit/normalizeAuditResult.js +47 -0
- package/src/audit/npmAudit.js +10 -0
- package/src/audit/remoteAudit.js +24 -0
- package/src/common/utils.js +35 -0
- package/src/entry/index.js +28 -0
- package/src/generateLock/generateLock.js +27 -0
- package/src/generateLock/index.js +1 -0
- package/src/main/index.js +23 -0
- package/src/mcpServer.js +56 -0
- package/src/parseProject/index.js +18 -0
- package/src/parseProject/parseLocalProject.js +8 -0
- package/src/parseProject/parseRemoteProject.js +65 -0
- package/src/render/index.js +24 -0
- package/src/render/markdown.js +17 -0
- package/src/render/template/audit.ejs +30 -0
- package/src/render/template/detail-item.ejs +32 -0
- package/src/render/template/detail.ejs +7 -0
- package/src/render/template/index.ejs +8 -0
- package/src/workDir/index.js +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hhxhhxhhx/hhx-mcp-audit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "src/mcpServer.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/mcpServer.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.17.1",
|
|
15
|
+
"ejs": "^3.1.10",
|
|
16
|
+
"zod": "^3.25.76"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -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,35 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec); // 将 exec 转换为返回 Promise 的函数
|
|
7
|
+
|
|
8
|
+
export async function runCommand(cmd, cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const stdout = await execAsync(cmd, {
|
|
11
|
+
cwd,
|
|
12
|
+
encoding: 'utf-8',
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
});
|
|
15
|
+
// 返回 audit 的 JSON 结果
|
|
16
|
+
return stdout.stdout.toString();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.stdout) {
|
|
19
|
+
return err.stdout.toString();
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function uniqueId() {
|
|
26
|
+
return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getFilename(importMetaUrl) {
|
|
30
|
+
return fileURLToPath(importMetaUrl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDirname(importMetaUrl) {
|
|
34
|
+
return dirname(getFilename(importMetaUrl));
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createWorkDir, deleteWorkDir } from '../workDir/index.js';
|
|
2
|
+
import { parseProject } from '../parseProject/index.js';
|
|
3
|
+
import { generateLock } from '../generateLock/index.js';
|
|
4
|
+
import { audit } from '../audit/index.js';
|
|
5
|
+
import { render } from '../render/index.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 根据项目根目录,审计项目中所有的包(含项目本身)
|
|
10
|
+
* @param {string} projectRoot 项目根目录,可以是本地目录的绝对路径,也可以是远程仓库的URL
|
|
11
|
+
* @param {string} savePath 保存审计结果的文件名,审计结果是一个标准格式的markdown字符串
|
|
12
|
+
*/
|
|
13
|
+
export async function auditPackage(projectRoot, savePath) {
|
|
14
|
+
// 1. 创建工作目录
|
|
15
|
+
const workDir = await createWorkDir();
|
|
16
|
+
// 2. 解析项目,向工作目录添加pacakge.json
|
|
17
|
+
const packageJson = await parseProject(projectRoot);
|
|
18
|
+
// // 3. 生成lock文件
|
|
19
|
+
await generateLock(workDir, packageJson);
|
|
20
|
+
// // 4. 对工作目录进行审计
|
|
21
|
+
const auditResult = await audit(workDir, packageJson);
|
|
22
|
+
// // 5. 渲染审计结果
|
|
23
|
+
const renderedResult = await render(auditResult, packageJson);
|
|
24
|
+
// // 6. 删除工作目录
|
|
25
|
+
await deleteWorkDir(workDir);
|
|
26
|
+
// // 7. 将结果保存到指定路径
|
|
27
|
+
await fs.promises.writeFile(savePath, renderedResult);
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { runCommand } from '../common/utils.js';
|
|
4
|
+
|
|
5
|
+
// 写入 package.json
|
|
6
|
+
async function writePackageJson(workDir, packageJson) {
|
|
7
|
+
const packageJsonPath = join(workDir, 'package.json');
|
|
8
|
+
fs.mkdirSync(dirname(packageJsonPath), { recursive: true });
|
|
9
|
+
await fs.promises.writeFile(
|
|
10
|
+
packageJsonPath,
|
|
11
|
+
JSON.stringify(packageJson),
|
|
12
|
+
'utf8'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 创建 lock 文件
|
|
17
|
+
async function createLockFile(workDir) {
|
|
18
|
+
const cmd = `npm install --package-lock-only --force`;
|
|
19
|
+
await runCommand(cmd, workDir); // 在工作目录中执行命令
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function generateLock(workDir, packageJson) {
|
|
23
|
+
// 1. 将 package.json 写入工作目录
|
|
24
|
+
await writePackageJson(workDir, packageJson);
|
|
25
|
+
// 2. 生成 lock 文件
|
|
26
|
+
await createLockFile(workDir);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { generateLock } from './generateLock.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createWorkDir, deleteWorkDir } from '../workDir/index.js';
|
|
2
|
+
import { parseProject } from '../parseProject/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 对项目本身以及其所有直接和间接依赖进行安全审计
|
|
6
|
+
* @param {string} projectRoot 项目根目录,可以是本地项目的路径或远程项目的URL
|
|
7
|
+
*/
|
|
8
|
+
export async function auditProject(projectRoot) {
|
|
9
|
+
// 1. 创建工作目录
|
|
10
|
+
const workDir = await createWorkDir();
|
|
11
|
+
// 2. 解析项目的package.json文件
|
|
12
|
+
const packageJSON = await parseProject(projectRoot);
|
|
13
|
+
// 3. 在工作目录中写入
|
|
14
|
+
const depTree = await generateDepTree(packageJSON);
|
|
15
|
+
// 3. 解析依赖树,获取每个包的依赖关系
|
|
16
|
+
const parsedTree = await parseTree(depTree);
|
|
17
|
+
// 4. 创建审计任务
|
|
18
|
+
const tasks = createTasks(parsedTree);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
auditProject(
|
|
22
|
+
'/Users/yuanjin/工作/课/录播课/付费课/60 天任务式学习/08. vue从入门到实战/案例/my-site'
|
|
23
|
+
);
|
package/src/mcpServer.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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 { auditPackage } from './entry/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
|
+
'auditPackage',
|
|
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
|
+
try {
|
|
31
|
+
await auditPackage(projectRoot, savePath);
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: `审计完成,结果已保存到: ${savePath}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: `审计失败: ${message}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const transport = new StdioServerTransport();
|
|
56
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseLocalProject } from './parseLocalProject.js';
|
|
2
|
+
import { parseRemoteProject } from './parseRemoteProject.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 解析工程根目录下的package.json文件
|
|
6
|
+
* @param {string} projectRoot 工程本地的根目录或远程仓库的URL
|
|
7
|
+
* @example
|
|
8
|
+
* parseProject('/path/to/local/project');
|
|
9
|
+
* parseProject('https://github.com/webpack/webpack');
|
|
10
|
+
* @returns {Promise<Object>} 返回解析后的package.json内容
|
|
11
|
+
* @throws {Error} 如果解析失败或文件不存在
|
|
12
|
+
*/
|
|
13
|
+
export function parseProject(projectRoot) {
|
|
14
|
+
if (projectRoot.startsWith('http://') || projectRoot.startsWith('https://')) {
|
|
15
|
+
return parseRemoteProject(projectRoot);
|
|
16
|
+
}
|
|
17
|
+
return parseLocalProject(projectRoot);
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function parseLocalProject(projectRoot) {
|
|
5
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
6
|
+
const json = await fs.promises.readFile(packageJsonPath, 'utf8');
|
|
7
|
+
return JSON.parse(json);
|
|
8
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 GitHub 仓库 URL,提取 owner、repo 和后续路径
|
|
3
|
+
* 支持格式:
|
|
4
|
+
* - https://github.com/owner/repo
|
|
5
|
+
* - https://github.com/owner/repo/tree/branch
|
|
6
|
+
* - https://github.com/owner/repo/blame/...
|
|
7
|
+
* 等等
|
|
8
|
+
*
|
|
9
|
+
* @param {string} url - GitHub 仓库 URL
|
|
10
|
+
* @returns {Object} { owner, repo, path }
|
|
11
|
+
* @throws {Error} 如果 URL 格式不合法或无法解析
|
|
12
|
+
*/
|
|
13
|
+
function parseGithubUrl(url) {
|
|
14
|
+
try {
|
|
15
|
+
const parsedUrl = new URL(url);
|
|
16
|
+
|
|
17
|
+
// 确保是 github.com
|
|
18
|
+
if (parsedUrl.hostname !== 'github.com') {
|
|
19
|
+
throw new Error('Only github.com URLs are supported');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 获取路径并去除空字符串(如开头的 /)
|
|
23
|
+
const parts = parsedUrl.pathname.split('/').filter(Boolean);
|
|
24
|
+
|
|
25
|
+
// 至少需要 owner 和 repo 两段
|
|
26
|
+
if (parts.length < 2) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Invalid GitHub repository URL: insufficient path segments'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const owner = parts[0];
|
|
33
|
+
const repo = parts[1];
|
|
34
|
+
const restPath = parts.slice(2); // 剩余路径,如 ['tree', 'v5.2.2']
|
|
35
|
+
|
|
36
|
+
// 构造 path:如果有后续路径,则以 '/' 开头拼接;否则为空字符串
|
|
37
|
+
const path = restPath.length > 0 ? '/' + restPath.join('/') : '';
|
|
38
|
+
|
|
39
|
+
return { owner, repo, path };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof TypeError) {
|
|
42
|
+
throw new Error('Invalid URL: malformed or missing');
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getPackageJsonUrl(gitInfo) {
|
|
49
|
+
let { owner, repo, path } = gitInfo;
|
|
50
|
+
if (path.startsWith('/tree/')) {
|
|
51
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
52
|
+
path = `tags/${pathParts[1]}`;
|
|
53
|
+
} else {
|
|
54
|
+
const url = `https://api.github.com/repos/${owner}/${repo}`;
|
|
55
|
+
const info = await fetch(url).then((resp) => resp.json());
|
|
56
|
+
path = `heads/${info.default_branch}`;
|
|
57
|
+
}
|
|
58
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${path}/package.json`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function parseRemoteProject(githubUrl) {
|
|
62
|
+
const gitInfo = parseGithubUrl(githubUrl);
|
|
63
|
+
const packgeJsonUrl = await getPackageJsonUrl(gitInfo);
|
|
64
|
+
return await fetch(packgeJsonUrl).then((resp) => resp.json());
|
|
65
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { renderMarkdown } from './markdown.js';
|
|
2
|
+
|
|
3
|
+
const desc = {
|
|
4
|
+
severityLevels: {
|
|
5
|
+
low: '低危',
|
|
6
|
+
moderate: '中危',
|
|
7
|
+
high: '高危',
|
|
8
|
+
critical: '严重',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 讲auditResult渲染为markdown格式的字符串
|
|
14
|
+
* @param {object} auditResult 规范化的审计结果
|
|
15
|
+
* @param {object} packageJson 包的package.json内容
|
|
16
|
+
*/
|
|
17
|
+
export async function render(auditResult, packageJson) {
|
|
18
|
+
const data = {
|
|
19
|
+
audit: auditResult,
|
|
20
|
+
desc,
|
|
21
|
+
packageJson,
|
|
22
|
+
};
|
|
23
|
+
return await renderMarkdown(data);
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ejs from 'ejs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { getDirname } from '../common/utils.js';
|
|
4
|
+
|
|
5
|
+
const templatePath = join(getDirname(import.meta.url), './template/index.ejs');
|
|
6
|
+
|
|
7
|
+
export function renderMarkdown(data) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
ejs.renderFile(templatePath, data, (err, str) => {
|
|
10
|
+
if (err) {
|
|
11
|
+
reject(err);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
resolve(str);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
您所审计的工程总共有 **<%- audit.summary.total %>** 个风险漏洞。
|
|
2
|
+
|
|
3
|
+
其中:
|
|
4
|
+
|
|
5
|
+
- **<%- desc.severityLevels.critical %>漏洞**:共计 **<%- audit.summary.critical %>** 个
|
|
6
|
+
- **<%- desc.severityLevels.high %>漏洞**:共计 **<%- audit.summary.high %>** 个
|
|
7
|
+
- **<%- desc.severityLevels.moderate %>漏洞**:共计 **<%- audit.summary.moderate %>** 个
|
|
8
|
+
- **<%- desc.severityLevels.low %>漏洞**:共计 **<%- audit.summary.low %>** 个
|
|
9
|
+
|
|
10
|
+
> 说明:
|
|
11
|
+
>
|
|
12
|
+
> - **<%- desc.severityLevels.critical %>**漏洞被认为是极其严重的,应该立即修复。
|
|
13
|
+
> - **<%- desc.severityLevels.high %>**漏洞被认为是严重的,应该尽快修复。
|
|
14
|
+
> - **<%- desc.severityLevels.moderate %>**漏洞被认为是中等严重的,可以选择在时间允许时修复。
|
|
15
|
+
> - **<%- desc.severityLevels.low %>**漏洞被认为是轻微的,可以根据自行需要进行修复。
|
|
16
|
+
|
|
17
|
+
下面是漏洞的详细信息
|
|
18
|
+
|
|
19
|
+
<% if (audit.summary.critical) { %>
|
|
20
|
+
<%- include('./detail.ejs', {type:'critical'}); %>
|
|
21
|
+
<% } %>
|
|
22
|
+
<% if (audit.summary.high) { %>
|
|
23
|
+
<%- include('./detail.ejs', {type:'high'}); %>
|
|
24
|
+
<% } %>
|
|
25
|
+
<% if (audit.summary.moderate) { %>
|
|
26
|
+
<%- include('./detail.ejs', {type:'moderate'}); %>
|
|
27
|
+
<% } %>
|
|
28
|
+
<% if (audit.summary.low) { %>
|
|
29
|
+
<%- include('./detail.ejs', {type:'low'}); %>
|
|
30
|
+
<% } %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
### `<%- item.name -%>`
|
|
2
|
+
|
|
3
|
+
**漏洞描述**:
|
|
4
|
+
<% item.problems.forEach((problem) => { %>
|
|
5
|
+
- <%- problem.title %>
|
|
6
|
+
- npm漏洞编号:`<%- problem.source %>`
|
|
7
|
+
- 漏洞详细说明:<%- problem.url %>
|
|
8
|
+
- 漏洞等级:<%- desc.severityLevels[problem.severity] %>
|
|
9
|
+
- 受影响的版本:`<%- problem.range %>`
|
|
10
|
+
<% }); %>
|
|
11
|
+
|
|
12
|
+
**依赖关系**:
|
|
13
|
+
<% if(item.depChains.length === 0) { %>
|
|
14
|
+
<% if(item.name === packageJson.name) { %>
|
|
15
|
+
当前工程
|
|
16
|
+
<% } else { %>
|
|
17
|
+
- `<%- packageJson.name %>` / <%- item.name %>
|
|
18
|
+
<% } %>
|
|
19
|
+
<% } else { %>
|
|
20
|
+
<% item.depChains.forEach((chain) => { %>
|
|
21
|
+
<% if(chain.length === 1 && chain[0] === packageJson.name) { %>
|
|
22
|
+
当前工程
|
|
23
|
+
<% } else { %>
|
|
24
|
+
- `<%- packageJson.name %>` / <%- chain.map(c=>`\`${c}\``).join(' / ') %>
|
|
25
|
+
<% } %>
|
|
26
|
+
<% }); %>
|
|
27
|
+
<% } %>
|
|
28
|
+
|
|
29
|
+
**漏洞包所在目录**:
|
|
30
|
+
<% item.nodes.forEach((path) => { %>
|
|
31
|
+
- `<%- path %>`
|
|
32
|
+
<% }); %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { uniqueId, getDirname } from '../common/utils.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = getDirname(import.meta.url); // 获取当前文件的目录名
|
|
7
|
+
|
|
8
|
+
const basePath = join(__dirname, '../..'); // 获取上两级目录
|
|
9
|
+
const workBasePath = join(basePath, 'work'); // 定义工作目录路径
|
|
10
|
+
fs.mkdirSync(workBasePath, { recursive: true }); // 确保工作目录存在
|
|
11
|
+
|
|
12
|
+
export async function createWorkDir() {
|
|
13
|
+
const workDir = join(workBasePath, uniqueId());
|
|
14
|
+
await fs.promises.mkdir(workDir, { recursive: true }); // 创建工作目录
|
|
15
|
+
return workDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 删除工作目录
|
|
19
|
+
export async function deleteWorkDir(workDir) {
|
|
20
|
+
await fs.promises.rm(workDir, { recursive: true }); // 删除工作目录
|
|
21
|
+
}
|