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,248 @@
1
+ import * as t from "@babel/types";
2
+ import { parse } from "@babel/parser";
3
+
4
+ import _traverse, { type Visitor, type NodePath } from "@babel/traverse";
5
+
6
+ // 使用 require 方式导入以获得正确的类型
7
+ const traverse: (node: t.Node, visitor: Visitor) => void = _traverse.default as any;
8
+
9
+ export type GlobalUsageMap = Record<string, "r" | "rw">;
10
+
11
+ export const ECMAGlobals = [
12
+ "AggregateError",
13
+ "Array",
14
+ "ArrayBuffer",
15
+ "Atomics",
16
+ "BigInt",
17
+ "BigInt64Array",
18
+ "BigUint64Array",
19
+ "Boolean",
20
+ "DataView",
21
+ "Date",
22
+ "Error",
23
+ "EvalError",
24
+ "FinalizationRegistry",
25
+ "Float16Array",
26
+ "Float32Array",
27
+ "Float64Array",
28
+ "Function",
29
+ "Infinity",
30
+ "Int16Array",
31
+ "Int32Array",
32
+ "Int8Array",
33
+ "Intl",
34
+ "Iterator",
35
+ "JSON",
36
+ "Map",
37
+ "Math",
38
+ "NaN",
39
+ "Number",
40
+ "Object",
41
+ "Promise",
42
+ "Proxy",
43
+ "RangeError",
44
+ "ReferenceError",
45
+ "Reflect",
46
+ "RegExp",
47
+ "Set",
48
+ "SharedArrayBuffer",
49
+ "String",
50
+ "Symbol",
51
+ "SyntaxError",
52
+ "TypeError",
53
+ "Uint16Array",
54
+ "Uint32Array",
55
+ "Uint8Array",
56
+ "Uint8ClampedArray",
57
+ "URIError",
58
+ "WeakMap",
59
+ "WeakRef",
60
+ "WeakSet",
61
+ "decodeURI",
62
+ "decodeURIComponent",
63
+ "encodeURI",
64
+ "encodeURIComponent",
65
+ "escape",
66
+ // "eval",
67
+ "globalThis",
68
+ "isFinite",
69
+ "isNaN",
70
+ "parseFloat",
71
+ "parseInt",
72
+ "undefined",
73
+ "unescape",
74
+ ];
75
+
76
+ class GVPool {
77
+ public globals: Map<string, "r" | "rw"> = new Map();
78
+
79
+ add(name: string, perm: "r" | "rw") {
80
+ // 不管控除eval以外的ECMA全局变量的读权限
81
+ if (ECMAGlobals.includes(name) && perm === "r") {
82
+ return;
83
+ }
84
+
85
+ if (this.globals.has(name)) {
86
+ if (perm === "rw") {
87
+ this.globals.set(name, "rw");
88
+ }
89
+ } else {
90
+ this.globals.set(name, perm);
91
+ }
92
+ }
93
+ }
94
+
95
+ export function analyzeGlobals(code: string): GlobalUsageMap {
96
+ const pool = new GVPool();
97
+
98
+ const ast = parse(code, {
99
+ sourceType: "unambiguous",
100
+ });
101
+
102
+ traverse(ast, {
103
+ MemberExpression(path: NodePath<t.MemberExpression>) {
104
+ let expr: string[] = [];
105
+ let node: t.MemberExpression = path.node;
106
+ let flag = true;
107
+ while (t.isMemberExpression(node)) {
108
+ if (t.isIdentifier(node.property) && !node.computed) {
109
+ expr.unshift(node.property.name);
110
+ } else if (t.isStringLiteral(node.property)) {
111
+ expr.unshift(node.property.value);
112
+ } else {
113
+ flag = false;
114
+ break;
115
+ }
116
+
117
+ if (t.isIdentifier(node.object)) {
118
+ expr.unshift(node.object.name);
119
+ break;
120
+ } else if (t.isMemberExpression(node.object)) {
121
+ node = node.object;
122
+ } else {
123
+ flag = false;
124
+ break;
125
+ }
126
+ }
127
+
128
+ if (!flag) {
129
+ return;
130
+ }
131
+
132
+ // 处理 __webpack_require__.g.xxx 的情况, __webpack_require__.g 就是 globalThis
133
+ if (
134
+ expr.length >= 2 &&
135
+ expr[0] === "__webpack_require__" &&
136
+ expr[1] === "g"
137
+ ) {
138
+ expr = expr.slice(2);
139
+ }
140
+
141
+ const globalAlias = ["globalThis", "self", "window"];
142
+ let mustBeGlobal = false;
143
+ while (
144
+ expr.length > 0 &&
145
+ globalAlias.includes(expr[0]!) &&
146
+ !path.scope.hasBinding(expr[0]!, { noGlobals: true })
147
+ ) {
148
+ expr = expr.slice(1);
149
+ // 如果是globalThis/self/window开头的, 则一定是全局变量
150
+ mustBeGlobal = true;
151
+ }
152
+
153
+ if (
154
+ expr.length > 0 &&
155
+ (mustBeGlobal ||
156
+ !path.scope.hasBinding(expr[0]!, { noGlobals: true })) &&
157
+ expr[0] !== "arguments"
158
+ ) {
159
+ pool.add(
160
+ expr[0]!,
161
+
162
+ t.isAssignmentExpression(path.parent) &&
163
+ path.parent.left === path.node &&
164
+ expr.length === 1
165
+ ? "rw"
166
+ : "r"
167
+ );
168
+ }
169
+
170
+ path.skip();
171
+ },
172
+ Identifier(path: NodePath<t.Identifier>) {
173
+ if (!path.scope.hasBinding(path.node.name, { noGlobals: true })) {
174
+ // 排除一些不算真正变量读取的场景
175
+ if (
176
+ (t.isMemberExpression(path.parent) ||
177
+ t.isOptionalMemberExpression(path.parent)) &&
178
+ path.parent.property === path.node
179
+ ) {
180
+ return;
181
+ } else if (
182
+ t.isObjectProperty(path.parent) &&
183
+ path.parent.key === path.node
184
+ ) {
185
+ return;
186
+ } else if (
187
+ (t.isLabeledStatement(path.parent) ||
188
+ t.isBreakStatement(path.parent) ||
189
+ t.isContinueStatement(path.parent)) &&
190
+ path.parent.label === path.node
191
+ ) {
192
+ return;
193
+ } else if (
194
+ (t.isFunctionDeclaration(path.parent) ||
195
+ t.isFunctionExpression(path.parent)) &&
196
+ path.parent.id === path.node
197
+ ) {
198
+ return;
199
+ } else if (
200
+ t.isCatchClause(path.parent) &&
201
+ path.parent.param === path.node
202
+ ) {
203
+ return;
204
+ } else if (
205
+ t.isFunction(path.parent) &&
206
+ (path.parent.params.includes(path.node) ||
207
+ (path.parent as any).id === path.node ||
208
+ (path.parent as any).kind === path.node.name ||
209
+ (path.parent as any).key === path.node)
210
+ ) {
211
+ return;
212
+ } else if (t.isMetaProperty(path.parent)) {
213
+ return;
214
+ } else if (
215
+ t.isClassProperty(path.parent) &&
216
+ path.parent.key === path.node
217
+ ) {
218
+ return;
219
+ // globalAlias默认可访问
220
+ } else if (
221
+ path.node.name === "globalThis" ||
222
+ path.node.name === "window" ||
223
+ path.node.name === "self"
224
+ ) {
225
+ return;
226
+ } else if (path.node.name === "arguments") {
227
+ return;
228
+ }
229
+
230
+ pool.add(
231
+ path.node.name,
232
+
233
+ t.isAssignmentExpression(path.parent) &&
234
+ path.parent.left === path.node
235
+ ? "rw"
236
+ : "r"
237
+ );
238
+ }
239
+ },
240
+ });
241
+
242
+ const obj: GlobalUsageMap = Object.create(null);
243
+ for (const [name, perm] of pool.globals.entries()) {
244
+ obj[name] = perm;
245
+ }
246
+
247
+ return obj;
248
+ }
@@ -0,0 +1,184 @@
1
+ import tarStream from 'tar-stream';
2
+ import gunzip from 'gunzip-maybe';
3
+ import { Readable } from 'stream';
4
+ import got from 'got';
5
+
6
+ export interface ExtractedFile {
7
+ path: string;
8
+ content: Buffer;
9
+ size: number;
10
+ type: 'file' | 'directory' | 'symlink';
11
+ mode: number;
12
+ mtime: Date;
13
+ }
14
+
15
+ export interface ExtractionOptions {
16
+ /** 只解压匹配的文件路径 */
17
+ filter?: (path: string) => boolean;
18
+ /** 最大文件大小限制 (bytes) */
19
+ maxFileSize?: number;
20
+ /** 最大总解压大小限制 (bytes) */
21
+ maxTotalSize?: number;
22
+ }
23
+
24
+ /**
25
+ * 在内存中解压 .tar.gz Buffer
26
+ */
27
+ export async function extractTarGzFromBuffer(
28
+ tarGzBuffer: Buffer,
29
+ options: ExtractionOptions = {}
30
+ ): Promise<ExtractedFile[]> {
31
+ const { filter, maxFileSize = 50 * 1024 * 1024, maxTotalSize = 500 * 1024 * 1024 } = options;
32
+ const files: ExtractedFile[] = [];
33
+ let totalSize = 0;
34
+
35
+ return new Promise((resolve, reject) => {
36
+ const extract = tarStream.extract();
37
+
38
+ extract.on('entry', (header, stream, next) => {
39
+ // 应用过滤器
40
+ if (filter && !filter(header.name)) {
41
+ stream.on('end', next);
42
+ stream.resume();
43
+ return;
44
+ }
45
+
46
+ // 检查文件大小限制
47
+ if (header.size && header.size > maxFileSize) {
48
+ stream.on('end', next);
49
+ stream.resume();
50
+ console.warn(`File ${header.name} exceeds size limit, skipping`);
51
+ return;
52
+ }
53
+
54
+ const chunks: Buffer[] = [];
55
+ let fileSize = 0;
56
+
57
+ stream.on('data', (chunk: Buffer) => {
58
+ fileSize += chunk.length;
59
+ totalSize += chunk.length;
60
+
61
+ // 检查总大小限制
62
+ if (totalSize > maxTotalSize) {
63
+ reject(new Error('Total extraction size limit exceeded'));
64
+ return;
65
+ }
66
+
67
+ chunks.push(chunk);
68
+ });
69
+
70
+ stream.on('end', () => {
71
+ if (header.type === 'file') {
72
+ files.push({
73
+ path: header.name,
74
+ content: Buffer.concat(chunks),
75
+ size: fileSize,
76
+ type: 'file',
77
+ mode: header.mode || 0,
78
+ mtime: header.mtime || new Date()
79
+ });
80
+ } else if (header.type === 'directory') {
81
+ files.push({
82
+ path: header.name,
83
+ content: Buffer.alloc(0),
84
+ size: 0,
85
+ type: 'directory',
86
+ mode: header.mode || 0,
87
+ mtime: header.mtime || new Date()
88
+ });
89
+ }
90
+ next();
91
+ });
92
+
93
+ stream.on('error', reject);
94
+ });
95
+
96
+ extract.on('finish', () => {
97
+ resolve(files);
98
+ });
99
+
100
+ extract.on('error', reject);
101
+
102
+ // 处理压缩流
103
+ Readable.from(tarGzBuffer)
104
+ .pipe(gunzip())
105
+ .pipe(extract);
106
+ });
107
+ }
108
+
109
+
110
+ /**
111
+ * 从URL下载并在内存中解压
112
+ */
113
+ export async function extractTarGzFromUrl(
114
+ url: string,
115
+ options: ExtractionOptions = {}
116
+ ): Promise<ExtractedFile[]> {
117
+ const response = await got(url);
118
+ if (response.statusCode !== 200) {
119
+ throw new Error(`Failed to download: ${response.statusMessage}`);
120
+ }
121
+
122
+ const buffer = response.rawBody;
123
+ return extractTarGzFromBuffer(buffer, options);
124
+ }
125
+
126
+ /**
127
+ * 获取特定文件内容
128
+ */
129
+ export async function getFileFromTarGz(
130
+ tarGzBuffer: Buffer,
131
+ filePath: string
132
+ ): Promise<Buffer | null> {
133
+ const files = await extractTarGzFromBuffer(tarGzBuffer, {
134
+ filter: (path) => path === filePath
135
+ });
136
+
137
+ return files.length > 0 ? files[0]!.content : null;
138
+ }
139
+
140
+ /**
141
+ * 只获取文件列表,不提取内容
142
+ */
143
+ export async function listTarGzContents(tarGzBuffer: Buffer): Promise<string[]> {
144
+ const fileList: string[] = [];
145
+
146
+ return new Promise((resolve, reject) => {
147
+ const extract = tarStream.extract();
148
+
149
+ extract.on('entry', (header, stream, next) => {
150
+ fileList.push(header.name);
151
+ stream.on('end', next);
152
+ stream.resume(); // 跳过内容读取
153
+ });
154
+
155
+ extract.on('finish', () => {
156
+ resolve(fileList);
157
+ });
158
+
159
+ extract.on('error', reject);
160
+
161
+ Readable.from(tarGzBuffer)
162
+ .pipe(gunzip())
163
+ .pipe(extract);
164
+ });
165
+ }
166
+
167
+ /**
168
+ * 按文件扩展名过滤提取
169
+ */
170
+ export async function extractByExtensions(
171
+ tarGzBuffer: Buffer,
172
+ extensions: string[]
173
+ ): Promise<ExtractedFile[]> {
174
+ const normalizedExts = extensions.map(ext =>
175
+ ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`
176
+ );
177
+
178
+ return extractTarGzFromBuffer(tarGzBuffer, {
179
+ filter: (path) => {
180
+ const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
181
+ return normalizedExts.includes(ext);
182
+ }
183
+ });
184
+ }
@@ -0,0 +1,100 @@
1
+ import got from "got";
2
+
3
+ export interface NpmPackageInfo {
4
+ _id: string;
5
+ _rev: string;
6
+ bugs: {
7
+ url: string;
8
+ };
9
+ description: string;
10
+ "dist-tags": {
11
+ latest: string;
12
+ beta: string;
13
+ };
14
+ homepage: string;
15
+ keywords: string[];
16
+ license: string;
17
+ name: string;
18
+ repository: {
19
+ type: string;
20
+ url: string;
21
+ };
22
+ time: Record<string, string>;
23
+ versions: Record<
24
+ string,
25
+ {
26
+ _id: string;
27
+ version: string;
28
+ name: string;
29
+ license: string;
30
+ keywords: string[];
31
+ homepage: string;
32
+ description: string;
33
+ dist: {
34
+ fileCount: number;
35
+ unpackedSize: number;
36
+ tarball: string;
37
+ shasum: string;
38
+ integrity: string;
39
+ };
40
+ }
41
+ >;
42
+ }
43
+
44
+ export interface NpmDownloadStats {
45
+ downloads: number;
46
+ start: string;
47
+ end: string;
48
+ package: string;
49
+ }
50
+
51
+ const maxRetries = 3;
52
+
53
+ export async function getNpmPackageInfo(
54
+ packageName: string
55
+ ): Promise<NpmPackageInfo | null> {
56
+ for (let i = 0; i < maxRetries; i++) {
57
+ try {
58
+ const response = await got(
59
+ `https://registry.npmjs.org/${packageName}`
60
+ );
61
+
62
+ if (response.statusCode !== 200) {
63
+ throw new Error(
64
+ `HTTP ${response.statusCode}: ${response.statusMessage}`
65
+ );
66
+ }
67
+
68
+ const packageData = JSON.parse(response.body) as NpmPackageInfo;
69
+ return packageData;
70
+ } catch (error) {
71
+ continue;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export async function getNpmPackageDownloadStats(
78
+ packageName: string,
79
+ period: "last-day" | "last-week" | "last-month" | "last-year" = "last-month"
80
+ ): Promise<NpmDownloadStats | null> {
81
+ for (let i = 0; i < maxRetries; i++) {
82
+ try {
83
+ const response = await got(
84
+ `https://api.npmjs.org/downloads/point/${period}/${packageName}`
85
+ );
86
+
87
+ if (response.statusCode !== 200) {
88
+ throw new Error(
89
+ `HTTP ${response.statusCode}: ${response.statusMessage}`
90
+ );
91
+ }
92
+
93
+ const data = JSON.parse(response.body) as NpmDownloadStats;
94
+ return data || null;
95
+ } catch (error) {
96
+ continue;
97
+ }
98
+ }
99
+ return null;
100
+ }
@@ -0,0 +1,103 @@
1
+ import { parseSyml } from "@yarnpkg/parsers";
2
+ import * as yaml from "js-yaml";
3
+
4
+ abstract class LockParser {
5
+ abstract getDependencies(): Record<string, string[]>;
6
+ constructor(content: string) {}
7
+
8
+ public static deps2Set(deps: Record<string, string[]>): Set<string> {
9
+ const r = new Set<string>();
10
+ for (const key in deps) {
11
+ const versions = deps[key]!;
12
+ for (let i = 0; i < versions.length; i++) {
13
+ r.add(`${key}@${versions[i]}`);
14
+ }
15
+ }
16
+ return r;
17
+ }
18
+ }
19
+
20
+ export class PnpmLockParser extends LockParser {
21
+ private yaml: any;
22
+ constructor(content: string) {
23
+ super(content);
24
+ this.yaml = yaml.load(content) as any;
25
+ }
26
+
27
+ public getDependencies(): Record<string, string[]> {
28
+ const r: Record<string, string[]> = Object.create(null);
29
+ if (!this.yaml.packages) {
30
+ return r;
31
+ }
32
+ Object.entries(this.yaml.packages).forEach(([pkgKey, pkgInfo]) => {
33
+ // 解析包键,格式如: "/lodash/4.17.21" 或 "lodash@4.17.21"
34
+ let name, version;
35
+
36
+ if (pkgKey.startsWith("/")) {
37
+ // 旧格式: /lodash/4.17.21
38
+ const parts = pkgKey.split("/");
39
+ name = parts[1];
40
+ version = parts[2];
41
+ } else {
42
+ // 新格式: lodash@4.17.21
43
+ const match = pkgKey.match(/^(.+?)@(.+)$/);
44
+ if (match) {
45
+ name = match[1];
46
+ version = match[2];
47
+ }
48
+ }
49
+
50
+ if (name && version) {
51
+ // 处理 peer dependency 后缀,如 "babel-jest@27.5.1(@babel/core@7.23.6)"
52
+ let cleanVersion = version.split("(")[0];
53
+ if (cleanVersion == null) {
54
+ cleanVersion = version;
55
+ }
56
+ if (!r[name]) {
57
+ r[name] = [];
58
+ }
59
+ if (r[name]!.indexOf(cleanVersion) === -1) {
60
+ r[name]!.push(cleanVersion);
61
+ }
62
+ }
63
+ });
64
+ return r;
65
+ }
66
+ }
67
+
68
+ export class YarnLockParser extends LockParser {
69
+ private syml: any;
70
+
71
+ constructor(content: string) {
72
+ super(content);
73
+ this.syml = parseSyml(content);
74
+ }
75
+ private static getPackageName(key: string): string {
76
+ key = key.split(",")[0]!.trim();
77
+ key = key.split(":")[0]!.trim();
78
+ return key.lastIndexOf("@") > 0
79
+ ? key.slice(0, key.lastIndexOf("@"))
80
+ : key;
81
+ }
82
+
83
+ public getDependencies(): Record<string, string[]> {
84
+ const r: Record<string, string[]> = Object.create(null);
85
+ for (const key in this.syml) {
86
+ const entry = this.syml[key];
87
+ if (entry === "__metadata") {
88
+ continue;
89
+ }
90
+
91
+ const pkgName = YarnLockParser.getPackageName(key);
92
+ if (!r[pkgName]) {
93
+ r[pkgName] = [];
94
+ }
95
+
96
+ if (r[pkgName].indexOf(entry.version) === -1) {
97
+ r[pkgName].push(entry.version);
98
+ }
99
+ }
100
+
101
+ return r;
102
+ }
103
+ }