prscan 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 (63) hide show
  1. package/.vscode/launch.json +14 -0
  2. package/README.MD +32 -0
  3. package/dist/bot/lark.d.ts +2 -0
  4. package/dist/bot/lark.d.ts.map +1 -0
  5. package/dist/bot/lark.js +156 -0
  6. package/dist/bot/lark.js.map +1 -0
  7. package/dist/cli/cli.d.ts +2 -0
  8. package/dist/cli/cli.d.ts.map +1 -0
  9. package/dist/cli/cli.js +77 -0
  10. package/dist/cli/cli.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +46 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/report/index.d.ts +7 -0
  16. package/dist/report/index.d.ts.map +1 -0
  17. package/dist/report/index.js +45 -0
  18. package/dist/report/index.js.map +1 -0
  19. package/dist/tool/prscan.d.ts +72 -0
  20. package/dist/tool/prscan.d.ts.map +1 -0
  21. package/dist/tool/prscan.js +477 -0
  22. package/dist/tool/prscan.js.map +1 -0
  23. package/dist/util/analyze.d.ts +4 -0
  24. package/dist/util/analyze.d.ts.map +1 -0
  25. package/dist/util/analyze.js +213 -0
  26. package/dist/util/analyze.js.map +1 -0
  27. package/dist/util/archive.d.ts +34 -0
  28. package/dist/util/archive.d.ts.map +1 -0
  29. package/dist/util/archive.js +110 -0
  30. package/dist/util/archive.js.map +1 -0
  31. package/dist/util/memory-archive.d.ts +37 -0
  32. package/dist/util/memory-archive.d.ts.map +1 -0
  33. package/dist/util/memory-archive.js +128 -0
  34. package/dist/util/memory-archive.js.map +1 -0
  35. package/dist/util/npm.d.ts +46 -0
  36. package/dist/util/npm.d.ts.map +1 -0
  37. package/dist/util/npm.js +35 -0
  38. package/dist/util/npm.js.map +1 -0
  39. package/dist/util/parse.d.ts +18 -0
  40. package/dist/util/parse.d.ts.map +1 -0
  41. package/dist/util/parse.js +92 -0
  42. package/dist/util/parse.js.map +1 -0
  43. package/dist/util/proxy.d.ts +45 -0
  44. package/dist/util/proxy.d.ts.map +1 -0
  45. package/dist/util/proxy.js +143 -0
  46. package/dist/util/proxy.js.map +1 -0
  47. package/dist/util/repo.d.ts +103 -0
  48. package/dist/util/repo.d.ts.map +1 -0
  49. package/dist/util/repo.js +170 -0
  50. package/dist/util/repo.js.map +1 -0
  51. package/package.json +35 -0
  52. package/report.png +0 -0
  53. package/src/bot/lark.ts +184 -0
  54. package/src/cli/cli.ts +80 -0
  55. package/src/index.ts +67 -0
  56. package/src/report/index.ts +50 -0
  57. package/src/tool/prscan.ts +634 -0
  58. package/src/util/analyze.ts +248 -0
  59. package/src/util/memory-archive.ts +184 -0
  60. package/src/util/npm.ts +100 -0
  61. package/src/util/parse.ts +103 -0
  62. package/src/util/repo.ts +224 -0
  63. package/tsconfig.json +43 -0
@@ -0,0 +1,184 @@
1
+ // SDK 使用说明 SDK user guide:https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/server-side-sdk/nodejs-sdk/preparation-before-development
2
+ import http from "http";
3
+ import * as lark from "@larksuiteoapi/node-sdk";
4
+ import { scanPRRisks } from "../tool/prscan.js";
5
+ import { makeReportInMd, md2Img } from "../report/index.js";
6
+ import * as fs from "fs";
7
+ import * as os from "os";
8
+ import * as path from "path";
9
+
10
+ const encryptKey = process.env.key;
11
+ const verificationToken = process.env.token;
12
+ const appId = process.env.id;
13
+ const appSecret = process.env.secret;
14
+ const bindPath = process.env.path || "/larkbot/event";
15
+ const port = process.env.port || 12345;
16
+
17
+ console.log("Lark Bot starting...");
18
+ console.log("App ID:", appId);
19
+ console.log("App Secret:", appSecret);
20
+ console.log("Encrypt Key:", encryptKey);
21
+ console.log("Verification Token:", verificationToken);
22
+ console.log("Event Path:", bindPath);
23
+ console.log("Listening on Port:", port);
24
+
25
+ const client = new lark.Client({
26
+ appId: appId ?? "",
27
+ appSecret: appSecret ?? "",
28
+ });
29
+
30
+ // 注册事件 Register event
31
+ const eventDispatcher = new lark.EventDispatcher({
32
+ logger: console,
33
+ loggerLevel: 5,
34
+ encryptKey: encryptKey ?? "",
35
+ verificationToken: verificationToken ?? "",
36
+ }).register({
37
+ "im.message.receive_v1": async (data) => {
38
+ if (data.message.message_type !== "text") {
39
+ client.im.message.reply({
40
+ path: {
41
+ message_id: data.message.message_id,
42
+ },
43
+ data: {
44
+ content: JSON.stringify({
45
+ text:
46
+ "只支持文本消息,收到消息类型:" +
47
+ data.message.message_type,
48
+ }),
49
+ msg_type: "text",
50
+ },
51
+ });
52
+ return "success";
53
+ }
54
+
55
+ const msg = JSON.parse(data.message.content).text as any;
56
+
57
+ // 提取PR链接
58
+ const prRegex =
59
+ /(https:\/\/github\.com\/[^\s\/]+\/[^\s\/]+\/pull\/\d+)/g;
60
+ const prLinks = msg.match(prRegex);
61
+
62
+ if (prLinks == null || prLinks.length === 0) {
63
+ client.im.message.reply({
64
+ path: {
65
+ message_id: data.message.message_id,
66
+ },
67
+ data: {
68
+ content: JSON.stringify({ text: "未提取到PR链接" }),
69
+ msg_type: "text",
70
+ },
71
+ });
72
+ return "success";
73
+ }
74
+
75
+ for (const prLink of prLinks) {
76
+ // 提取owner、repo、pr_number
77
+ const match = prLink.match(
78
+ /https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/
79
+ );
80
+ if (match) {
81
+ const owner = match[1];
82
+ const repo = match[2];
83
+ const prNumber = match[3];
84
+
85
+ scanPRRisks(owner, repo, prNumber)
86
+ .then((result) => {
87
+ const report = makeReportInMd(result!);
88
+
89
+ client.im.message.reply({
90
+ path: {
91
+ message_id: data.message.message_id,
92
+ },
93
+ data: {
94
+ content: JSON.stringify({
95
+ zh_cn: {
96
+ title: `PR安全扫描结果 - ${owner}/${repo}#${prNumber}`,
97
+ content: [
98
+ [
99
+ {
100
+ tag: "text",
101
+ text: `摘要: ${report.abstract}\n详细结果见下图:`,
102
+ },
103
+ {
104
+ tag: "md",
105
+ text: report.report,
106
+ },
107
+ ],
108
+ ],
109
+ },
110
+ }),
111
+ msg_type: "post",
112
+ },
113
+ });
114
+ })
115
+ .catch((error) => {
116
+ client.im.message.reply({
117
+ path: {
118
+ message_id: data.message.message_id,
119
+ },
120
+ data: {
121
+ content: JSON.stringify({
122
+ text: `扫描PR ${prLink} 失败: ${error.message}`,
123
+ }),
124
+ msg_type: "text",
125
+ },
126
+ });
127
+ });
128
+ }
129
+ }
130
+ // console.log(data);
131
+
132
+ // client.im.message.reply({
133
+ // path: {
134
+ // message_id: data.message.message_id,
135
+ // },
136
+ // data: {
137
+ // content: JSON.stringify({"text": "收到消息:" + data.message.text}),
138
+ // msg_type: "text"
139
+ // }
140
+ // });
141
+ return "success";
142
+ },
143
+ });
144
+
145
+ const server = http.createServer();
146
+ // 创建路由处理器 Create route handler
147
+ server.on(
148
+ "request",
149
+ lark.adaptDefault(bindPath, eventDispatcher, {
150
+ autoChallenge: true,
151
+ })
152
+ );
153
+
154
+ server.listen(port);
155
+
156
+ function createReadStreamFromBuffer(buffer: Buffer, options = {}) {
157
+ // 在系统临时目录创建文件
158
+ const tempDir = os.tmpdir();
159
+ const tempFile = path.join(
160
+ tempDir,
161
+ `buffer-stream-${Date.now()}-${Math.random().toString(36).substr(2)}`
162
+ );
163
+
164
+ // 同步写入临时文件
165
+ fs.writeFileSync(tempFile, buffer);
166
+
167
+ // 创建 read stream
168
+ const readStream = fs.createReadStream(tempFile, options);
169
+
170
+ // 自动清理临时文件
171
+ const cleanup = () => {
172
+ fs.unlink(tempFile, (err) => {
173
+ if (err && err.code !== "ENOENT") {
174
+ console.error("清理临时文件失败:", err);
175
+ }
176
+ });
177
+ };
178
+
179
+ readStream.on("close", cleanup);
180
+ readStream.on("error", cleanup);
181
+ readStream.on("end", cleanup);
182
+
183
+ return readStream;
184
+ }
package/src/cli/cli.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { program } from "commander";
2
+ import { execSync } from "child_process";
3
+ import { scanByFileDiff, scanPRRisks } from "../tool/prscan.js";
4
+ import { writeFileSync } from "fs";
5
+ import { makeReportInMd } from "../report/index.js";
6
+ program
7
+ .command("branch")
8
+ .description("根据分支文件变更分析NPM依赖变更")
9
+ .argument("<base>", "基础分支")
10
+ .argument("<head>", "变更分支")
11
+ .option("-r, --repo <repo>", "仓库路径", ".")
12
+ .option("-o, --output <output>", "输出文件路径,若不指定则输出到控制台", "")
13
+ .action(async (base: string, head: string, options: { repo: string, output: string }) => {
14
+ console.log(`分析 ${options.repo} 仓库从 ${base} 到 ${head} 的变更`);
15
+ const output = execSync(`git diff --name-only ${base} ${head}`, { cwd: options.repo }).toString();
16
+ const files = [];
17
+ for (let line of output.split("\n")) {
18
+ line = line.trim();
19
+ if (line.endsWith("yarn.lock")) {
20
+ console.log(` 发现变更: ${line}`);
21
+ } else if (line.endsWith("pnpm-lock.yaml")) {
22
+ console.log(` 发现变更: ${line}`);
23
+ } else if (line.endsWith("package-lock.json")) {
24
+ console.error(` 发现变更: ${line} (暂不支持分析package-lock.json)`);
25
+ continue;
26
+ } else {
27
+ continue;
28
+ }
29
+
30
+ const file1 = execSync(`git show ${base}:${line}`, { cwd: options.repo }).toString();
31
+ const file2 = execSync(`git show ${head}:${line}`, { cwd: options.repo }).toString();
32
+ files.push({
33
+ filename: line,
34
+ oldContent: file1,
35
+ newContent: file2,
36
+ });
37
+ }
38
+
39
+ const sr = await scanByFileDiff(files);
40
+
41
+ const report = makeReportInMd(sr);
42
+ if (options.output.length > 0) {
43
+
44
+ writeFileSync(options.output, report.report, { encoding: "utf-8" });
45
+ console.log(`分析报告已写入 ${options.output}`);
46
+ } else {
47
+ console.log(report.report);
48
+ }
49
+ });
50
+
51
+ program.command("github").description("根据GitHub Pull Request分析NPM依赖变更")
52
+ .argument("<link>", "Pull Request链接")
53
+ .option("-t, --token <token>", "GitHub访问令牌", "")
54
+ .option("-o, --output <output>", "输出文件路径,若不指定则输出到控制台", "")
55
+ .action(async (link: string, options: { token: string, output: string }) => {
56
+ console.log(`分析 Pull Request: ${link}`);
57
+
58
+ const match = link.match(
59
+ /https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/
60
+ );
61
+ if (match) {
62
+ const owner = match[1];
63
+ const repo = match[2];
64
+ const prNumber = match[3];
65
+
66
+ const sr = await scanPRRisks(owner!, repo!, parseInt(prNumber!), options.token);
67
+ const report = makeReportInMd(sr!);
68
+ if (options.output.length > 0) {
69
+ writeFileSync(options.output, report.report, { encoding: "utf-8" });
70
+ console.log(`分析报告已写入 ${options.output}`);
71
+ } else {
72
+ console.log(report.report);
73
+ }
74
+ } else {
75
+ console.error("无法解析Pull Request链接");
76
+ return;
77
+ }
78
+ });
79
+
80
+ program.parse();
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { Octokit } from "octokit";
2
+ import { GitHubRepo } from "./util/repo.js";
3
+ import { parseSyml } from '@yarnpkg/parsers';
4
+ import { YarnLockParser } from "./util/parse.js";
5
+ import { getNpmPackageInfo, getNpmPackageDownloadStats } from "./util/npm.js";
6
+ import { scanPRRisks } from "./tool/prscan.js";
7
+ import { makeReportInMd, md2Img } from "./report/index.js";
8
+ import { writeFileSync } from "node:fs";
9
+
10
+
11
+ // https://github.com/DeBankDeFi/defi-insight-react/pull/2323
12
+
13
+ const pr = await scanPRRisks(
14
+ "DeBankDeFi",
15
+ "defi-insight-react",
16
+ 2323,
17
+ "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")
18
+
19
+ const r = makeReportInMd(pr!);
20
+
21
+ const img = await md2Img(r.report);
22
+ if (img) {
23
+ writeFileSync("report.png", img);
24
+ }
25
+
26
+ console.log(r.abstract, "\n", r.report);
27
+
28
+
29
+ // 取消注释来测试
30
+ // await testNpmDownloads();
31
+
32
+
33
+ // const repo = new GitHubRepo("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");
34
+
35
+ // const prinfo = await repo.getPRInfo("RabbyHub", "rabby-mobile", 1082);
36
+
37
+ // const prfiles = await repo.getPRChangedFiles("RabbyHub", "rabby-mobile", 1082, 100);
38
+ // // console.log(prfiles);
39
+
40
+ // for (let i = 0; i < prfiles.length; i++) {
41
+ // const file = prfiles[i]!;
42
+
43
+ // if (file.filename !== "yarn.lock") {
44
+ // continue;
45
+ // }
46
+
47
+ // if (file.status !== "modified") {
48
+ // continue;
49
+ // }
50
+
51
+ // const content = await repo.getTextFileContent(
52
+ // "RabbyHub",
53
+ // "rabby-mobile",
54
+ // file.filename,
55
+ // prinfo.head.sha
56
+ // );
57
+
58
+ // if (content === null) {
59
+ // console.log("Failed to fetch file content");
60
+ // continue;
61
+ // }
62
+
63
+ // const parser = new YarnLockParser(content);
64
+ // const deps = parser.getDependencies();
65
+ // console.log(YarnLockParser.deps2Set(deps));
66
+ // debugger
67
+ // }
@@ -0,0 +1,50 @@
1
+ import { type PRScanResult } from '../tool/prscan.js';
2
+ import got from 'got';
3
+
4
+ export function makeReportInMd(sr: PRScanResult): {
5
+ report: string;
6
+ abstract: string;
7
+ } {
8
+ const risksCount = sr.changedDeps.reduce((acc, dep) => acc + dep.risks.length, 0);
9
+ let abstract = "共有 " + sr.changedDeps.length + " 个依赖变更, 包含 " + risksCount + " 个风险";
10
+
11
+ let md = "# PR扫描结果\n";
12
+ md += `## 依赖变更\n\n`;
13
+ if (sr.changedDeps.length === 0) {
14
+ md += "无依赖变更\n";
15
+ } else {
16
+ md += "```\n";
17
+ md += "| 依赖 | 版本 | 风险 | 下载 | 周下载量 | 最新版 |\n";
18
+ md += "|:---- |:---- |:---- |:---- |:---- |:---- |\n";
19
+ const depSorted = sr.changedDeps.sort((a, b) => b.risks.length - a.risks.length);
20
+ for (const dep of depSorted) {
21
+ md += `| ${dep.name} | ${dep.version} | ${dep.risks.length} | ${dep.packageInfo.versions[dep.version]!.dist.tarball} | ${dep.downloadInfo.downloads} | ${dep.packageInfo["dist-tags"].latest} |\n`;
22
+ }
23
+ md += "\n```\n\n";
24
+
25
+ md += "\n## 详细风险信息\n\n";
26
+ for (const dep of depSorted) {
27
+ if (dep.risks.length === 0) {
28
+ continue;
29
+ }
30
+ md += `### ${dep.name} @ ${dep.version}\n\n`;
31
+ for (const risk of dep.risks) {
32
+ md += `- **【${risk.level === "low" ? "低危" : (risk.level === "medium" ? "中危" : "高危")}】** ${risk.desc}:\n\n\`\`\`\n${risk.info}\n\`\`\`\n\n`;
33
+ }
34
+ md += "\n";
35
+ }
36
+ }
37
+
38
+ return {
39
+ report: md,
40
+ abstract
41
+ }
42
+ }
43
+
44
+ export async function md2Img(md: string): Promise<Buffer | null> {
45
+ try {
46
+ return (await got(`https://readpo.com/p/${encodeURIComponent(md)}`)).rawBody;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }