npm-scan-plus 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 (56) hide show
  1. package/.eslintrc.json +32 -0
  2. package/.github/CODEOWNERS +3 -0
  3. package/.github/workflows/ci.yml +105 -0
  4. package/.prettierrc +10 -0
  5. package/FUNDING.yml +1 -0
  6. package/PLAN.md +151 -0
  7. package/README.md +150 -0
  8. package/bin/npm-scan +13 -0
  9. package/bin/npm-scan-wrap +100 -0
  10. package/dist/cli/index.d.ts +18 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +299 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/lib/blocklist.d.ts +45 -0
  15. package/dist/lib/blocklist.d.ts.map +1 -0
  16. package/dist/lib/blocklist.js +256 -0
  17. package/dist/lib/blocklist.js.map +1 -0
  18. package/dist/lib/extended.js +314 -0
  19. package/dist/lib/extended.js.map +1 -0
  20. package/dist/lib/integrity.js +247 -0
  21. package/dist/lib/integrity.js.map +1 -0
  22. package/dist/lib/patterns.d.ts +76 -0
  23. package/dist/lib/patterns.d.ts.map +1 -0
  24. package/dist/lib/patterns.js +414 -0
  25. package/dist/lib/patterns.js.map +1 -0
  26. package/dist/lib/registry.d.ts +42 -0
  27. package/dist/lib/registry.d.ts.map +1 -0
  28. package/dist/lib/registry.js +157 -0
  29. package/dist/lib/registry.js.map +1 -0
  30. package/dist/lib/scanner.d.ts +43 -0
  31. package/dist/lib/scanner.d.ts.map +1 -0
  32. package/dist/lib/scanner.js +432 -0
  33. package/dist/lib/scanner.js.map +1 -0
  34. package/dist/lib/vuln.js +284 -0
  35. package/dist/lib/vuln.js.map +1 -0
  36. package/dist/types.d.ts +85 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +6 -0
  39. package/dist/types.js.map +1 -0
  40. package/jest.config.js +18 -0
  41. package/package.json +56 -0
  42. package/src/cli/index.ts +336 -0
  43. package/src/lib/blocklist.ts +239 -0
  44. package/src/lib/extended.ts +384 -0
  45. package/src/lib/integrity.ts +253 -0
  46. package/src/lib/patterns.ts +404 -0
  47. package/src/lib/registry.ts +146 -0
  48. package/src/lib/scanner.ts +447 -0
  49. package/src/lib/vuln.ts +321 -0
  50. package/src/types.ts +102 -0
  51. package/tests/blocklist.test.ts +89 -0
  52. package/tests/extended.test.ts +204 -0
  53. package/tests/patterns.test.ts +147 -0
  54. package/tests/scanner.test.ts +116 -0
  55. package/tests/vuln.test.ts +66 -0
  56. package/tsconfig.json +20 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Extended security analysis module
3
+ * Additional checks: license, repo validation, maintainer trust, download anomalies
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+
9
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
10
+
11
+ /** License risk classification */
12
+ const LICENSE_RISK = {
13
+ // High risk - requires source sharing or has legal issues
14
+ 'gpl-3.0': { level: 'high', reason: 'GPLv3 - copyleft, may affect proprietary code' },
15
+ 'gpl-2.0': { level: 'medium', reason: 'GPLv2 - copyleft' },
16
+ 'agpl-3.0': { level: 'high', reason: 'AGPLv3 - strong copyleft, SaaS exposure' },
17
+ 'lgpl-3.0': { level: 'medium', reason: 'LGPLv3 - more permissive than GPL' },
18
+ 'mpl-2.0': { level: 'low', reason: 'Mozilla Public License' },
19
+ ' EPL-2.0': { level: 'low', reason: 'Eclipse Public License' },
20
+ 'eupl-1.2': { level: 'medium', reason: 'European Union Public License' },
21
+ // Low risk - permissive
22
+ 'mit': { level: 'low', reason: 'Permissive' },
23
+ 'bsd': { level: 'low', reason: 'Permissive' },
24
+ 'apache-2.0': { level: 'low', reason: 'Permissive' },
25
+ 'isc': { level: 'low', reason: 'Permissive' },
26
+ 'unlicense': { level: 'low', reason: 'Public domain' },
27
+ // Suspicious - requires investigation
28
+ 'custom': { level: 'medium', reason: 'Custom license - review required' },
29
+ 'none': { level: 'high', reason: 'No license - copyright issues' },
30
+ 'proprietary': { level: 'high', reason: 'Proprietary - may restrict use' }
31
+ };
32
+
33
+ /** Known package maintainers with trust scores */
34
+ const TRUSTED_MAINTAINERS = new Set([
35
+ // Popular package maintainers
36
+ 'ljharb', // Jordan Harband (lodash)
37
+ 'sindresorhus', // Sindre Sohrus
38
+ 'jdalton', // John-David Dalton (lodash creator)
39
+ 'substack',
40
+ 'juliangruber',
41
+ 'rvagg',
42
+ 'addaleax',
43
+ 'bnb',
44
+ 'fy',
45
+ 'watson',
46
+ 'leorom',
47
+ 'd3',
48
+ 'mbostock',
49
+ 'mikaelbr',
50
+ 'analog',
51
+ // Large organizations
52
+ 'facebook',
53
+ 'google',
54
+ 'microsoft',
55
+ 'amazon',
56
+ 'netflix',
57
+ 'stripe',
58
+ 'paypal',
59
+ 'twitter',
60
+ 'uber'
61
+ ]);
62
+
63
+ /** Suspicious TLDs for typosquatting detection */
64
+ const SUSPICIOUS_TLDS = ['xyz', 'app', 'dev', 'io', 'co', 'sh', 'js', 'cn', 'ru'];
65
+
66
+ /**
67
+ * Analyze license risk
68
+ */
69
+ export function analyzeLicense(license: string | undefined): {
70
+ risk: 'low' | 'medium' | 'high' | 'critical';
71
+ details: string;
72
+ permissive: boolean;
73
+ } {
74
+ if (!license) {
75
+ return {
76
+ risk: 'high',
77
+ details: 'No license specified - copyright issues',
78
+ permissive: false
79
+ };
80
+ }
81
+
82
+ // Normalize license string
83
+ const normalized = license.toString().replace(/^(\d+\.)+/, '').trim().toLowerCase();
84
+ const known = LICENSE_RISK[normalized as keyof typeof LICENSE_RISK];
85
+
86
+ if (known) {
87
+ return {
88
+ risk: known.level as any,
89
+ details: known.reason,
90
+ permissive: known.level === 'low'
91
+ };
92
+ }
93
+
94
+ // Check for OR patterns (multiple licenses)
95
+ if (license.includes(' OR ')) {
96
+ return {
97
+ risk: 'medium',
98
+ details: 'Multiple licenses - review required',
99
+ permissive: false
100
+ };
101
+ }
102
+
103
+ return {
104
+ risk: 'low',
105
+ details: 'Unknown but present',
106
+ permissive: true
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Check if maintainer is trusted/known
112
+ */
113
+ export function checkMaintainerTrust(maintainers: any[], publisher: any): {
114
+ score: number;
115
+ isTrusted: boolean;
116
+ details: string;
117
+ } {
118
+ const allAuthors = [...(maintainers || []), publisher].filter(Boolean);
119
+ const names = allAuthors.map(m => m.username || m.name).filter(Boolean);
120
+
121
+ if (names.length === 0) {
122
+ return {
123
+ score: 0,
124
+ isTrusted: false,
125
+ details: 'No maintainers identified'
126
+ };
127
+ }
128
+
129
+ // Check if any trusted maintainer
130
+ const trustedCount = names.filter(n => TRUSTED_MAINTAINERS.has(n.toLowerCase())).length;
131
+ const trustRatio = trustedCount / names.length;
132
+
133
+ if (trustRatio > 0.5) {
134
+ return {
135
+ score: 100,
136
+ isTrusted: true,
137
+ details: `${trustedCount} trusted maintainer(s): ${names.join(', ')}`
138
+ };
139
+ }
140
+
141
+ if (trustedCount > 0) {
142
+ return {
143
+ score: 50,
144
+ isTrusted: false,
145
+ details: `Some trusted maintainer(s): ${names.join(', ')}`
146
+ };
147
+ }
148
+
149
+ return {
150
+ score: 20,
151
+ isTrusted: false,
152
+ details: `Unknown maintainer(s): ${names.join(', ')}`
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Validate repository URL exists and matches package
158
+ */
159
+ export async function validateRepository(
160
+ repoUrl: string | undefined,
161
+ packageName: string
162
+ ): Promise<{
163
+ valid: boolean;
164
+ details: string;
165
+ issues: string[];
166
+ }> {
167
+ const issues: string[] = [];
168
+
169
+ if (!repoUrl) {
170
+ return {
171
+ valid: false,
172
+ details: 'No repository URL',
173
+ issues: ['missing repository']
174
+ };
175
+ }
176
+
177
+ // Parse different repo formats
178
+ let normalizedUrl = repoUrl;
179
+
180
+ // Handle shorthand
181
+ if (!repoUrl.startsWith('http')) {
182
+ if (repoUrl.startsWith('github:')) {
183
+ normalizedUrl = 'https://' + repoUrl.replace('github:', '');
184
+ } else if (repoUrl.startsWith('gist:')) {
185
+ normalizedUrl = 'https://gist.github.com/' + repoUrl.replace('gist:', '');
186
+ } else if (repoUrl.startsWith('bitbucket:')) {
187
+ normalizedUrl = 'https://bitbucket.org/' + repoUrl.replace('bitbucket:', '');
188
+ } else if (repoUrl.startsWith('gitlab:')) {
189
+ normalizedUrl = 'https://gitlab.com/' + repoUrl.replace('gitlab:', '');
190
+ } else {
191
+ normalizedUrl = 'https://' + repoUrl;
192
+ }
193
+ }
194
+
195
+ // Extract owner/repo for GitHub
196
+ const githubMatch = normalizedUrl.match(/github\.com[/:]([\w-]+)\/([\w-.]+)/);
197
+ if (githubMatch) {
198
+ const [, owner, repo] = githubMatch;
199
+ const cleanRepo = repo.replace(/\.git$/, '');
200
+
201
+ // Check if repo name roughly matches package name
202
+ const expectedPackage = packageName.replace(/^@[\w-]+\//, ''); // Handle scoped packages
203
+ const nameMatch = cleanRepo.toLowerCase() === expectedPackage.toLowerCase() ||
204
+ cleanRepo.toLowerCase() === expectedPackage.replace(/[-_]/g, '').toLowerCase();
205
+
206
+ if (!nameMatch) {
207
+ issues.push(`repo name "${cleanRepo}" != package "${expectedPackage}"`);
208
+ }
209
+ }
210
+
211
+ // Check for suspicious domains
212
+ const url = new URL(normalizedUrl);
213
+ if (SUSPICIOUS_TLDS.includes(url.hostname.split('.').pop() || '')) {
214
+ issues.push(`suspicious TLD: ${url.hostname}`);
215
+ }
216
+
217
+ return {
218
+ valid: issues.length === 0,
219
+ details: normalizedUrl,
220
+ issues
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Check for release anomalies (sudden popularity spike = typosquatting indicator)
226
+ */
227
+ export async function checkReleaseAnomalies(
228
+ packageName: string,
229
+ metadata: any
230
+ ): Promise<{
231
+ suspicious: boolean;
232
+ details: string;
233
+ metrics: any;
234
+ }> {
235
+ const metrics: any = {
236
+ versionCount: 0,
237
+ hasNewVersions: false,
238
+ rapidRelease: false
239
+ };
240
+
241
+ try {
242
+ // Fetch full package data
243
+ const response = await fetch(`${NPM_REGISTRY}/${packageName}`);
244
+ const data: any = await response.json();
245
+
246
+ const versions = Object.keys(data.versions || {});
247
+ metrics.versionCount = versions.length;
248
+
249
+ // Check for recent rapid releases (potential automated publishing)
250
+ if (data.time) {
251
+ const timeKeys = Object.keys(data.time).filter(k => k !== 'created' && k !== 'modified');
252
+ if (timeKeys.length > 1) {
253
+ const sorted = timeKeys.sort((a, b) =>
254
+ new Date(data.time[a]).getTime() - new Date(data.time[b]).getTime()
255
+ );
256
+
257
+ // Check if multiple releases within hours
258
+ if (sorted.length >= 2) {
259
+ const lastRelease = new Date(data.time[sorted[sorted.length - 1]]);
260
+ const prevRelease = new Date(data.time[sorted[sorted.length - 2]]);
261
+ const hoursApart = (lastRelease.getTime() - prevRelease.getTime()) / (1000 * 60 * 60);
262
+
263
+ if (hoursApart < 1) {
264
+ metrics.rapidRelease = true;
265
+ metrics.hoursApart = hoursApart;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ } catch (e) {
271
+ // Ignore - just for metrics
272
+ }
273
+
274
+ const suspicious = metrics.rapidRelease;
275
+
276
+ return {
277
+ suspicious,
278
+ details: suspicious ? 'Unusual rapid release pattern' : 'Normal release pattern',
279
+ metrics
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Analyze dependencies for freshness and known issues
285
+ */
286
+ export function analyzeDependencies(
287
+ dependencies: Record<string, string> | undefined,
288
+ devDependencies: Record<string, string> | undefined
289
+ ): {
290
+ outdatedCount: number;
291
+ issues: string[];
292
+ details: string[];
293
+ } {
294
+ const allDeps = { ...dependencies, ...devDependencies };
295
+ const issues: string[] = [];
296
+ const details: string[] = [];
297
+
298
+ if (!allDeps || Object.keys(allDeps).length === 0) {
299
+ return { outdatedCount: 0, issues: [], details: [] };
300
+ }
301
+
302
+ // Check for very old/deprecated packages
303
+ const deprecated = [
304
+ 'request', // Deprecated in favor of fetch, axios
305
+ 'moment', // Deprecated in favor of date-fns, dayjs
306
+ 'lodash', // Should use lodash-es for tree-shaking
307
+ 'querystring', // Built into Node.js
308
+ 'underscore' // Use lodash or native
309
+ ];
310
+
311
+ for (const [dep, ver] of Object.entries(allDeps)) {
312
+ if (deprecated.includes(dep)) {
313
+ issues.push(`${dep} is deprecated or not recommended`);
314
+ details.push(`${dep}@${ver}: consider alternatives`);
315
+ }
316
+
317
+ // Check for very old major versions still in use
318
+ if (ver.startsWith('^0.') || ver.startsWith('~0.')) {
319
+ issues.push(`${dep} using old major version`);
320
+ details.push(`${dep}@${ver}: may have vulnerabilities`);
321
+ }
322
+ }
323
+
324
+ return {
325
+ outdatedCount: issues.length,
326
+ issues,
327
+ details
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Check file structure for suspicious patterns
333
+ */
334
+ export function analyzeFileStructure(files: string[]): {
335
+ suspicious: boolean;
336
+ issues: string[];
337
+ } {
338
+ const issues: string[] = [];
339
+
340
+ // Check for hidden files that shouldn't be there
341
+ const suspiciousFiles = [
342
+ /^\./,
343
+ /\.sh$/,
344
+ /\.bash$/,
345
+ / bash/,
346
+ /script/i
347
+ ];
348
+
349
+ // Check for common attack vectors
350
+ const suspiciousPaths = [
351
+ /proc\/self/,
352
+ /\/etc\//,
353
+ /~\/ /,
354
+ /\$HOME/,
355
+ /\.ssh\//
356
+ ];
357
+
358
+ for (const file of files) {
359
+ for (const pattern of suspiciousPaths) {
360
+ if (pattern.test(file)) {
361
+ issues.push(`suspicious path: ${file}`);
362
+ }
363
+ }
364
+ }
365
+
366
+ return {
367
+ suspicious: issues.length > 0,
368
+ issues
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Combined extended analysis (sync parts only)
374
+ */
375
+ export function extendedAnalysis(
376
+ packageName: string,
377
+ metadata: any
378
+ ) {
379
+ const license = analyzeLicense(metadata.license);
380
+ const maintainer = checkMaintainerTrust(metadata.maintainers, metadata.publisher);
381
+ const dependencies = analyzeDependencies(metadata.dependencies, metadata.devDependencies);
382
+
383
+ return { license, maintainer, repository: { valid: true, details: '', issues: [] }, dependencies };
384
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Package integrity verification
3
+ * Hash verification, signature checking, tarball analysis
4
+ */
5
+
6
+ import * as crypto from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import * as tar from 'tar';
11
+
12
+ const NPM_REGISTRY = 'https://registry.npmjs.org';
13
+
14
+ /**
15
+ * Verify package integrity using npm registry integrity field
16
+ */
17
+ export async function verifyIntegrity(
18
+ packageName: string,
19
+ version: string,
20
+ registry: string = NPM_REGISTRY
21
+ ): Promise<any> {
22
+ try {
23
+ const response = await fetch(`${registry}/${packageName}/${version}`);
24
+ const data: any = await response.json();
25
+
26
+ const expectedHash = data.dist?.integrity || null;
27
+
28
+ if (!expectedHash) {
29
+ return {
30
+ valid: true,
31
+ expectedHash: null,
32
+ actualHash: null,
33
+ algorithm: 'none',
34
+ details: 'No integrity hash in registry'
35
+ };
36
+ }
37
+
38
+ const algorithm = expectedHash.startsWith('sha512') ? 'sha512' : 'sha256';
39
+ const tarballUrl = data.dist?.tarball;
40
+
41
+ if (!tarballUrl) {
42
+ return {
43
+ valid: false,
44
+ expectedHash,
45
+ actualHash: null,
46
+ algorithm,
47
+ details: 'No tarball URL found'
48
+ };
49
+ }
50
+
51
+ const tarballResponse = await fetch(tarballUrl);
52
+ const buffer = Buffer.from(await tarballResponse.arrayBuffer());
53
+
54
+ const actualHash = algorithm === 'sha512'
55
+ ? 'sha512-' + crypto.createHash('sha512').update(buffer).digest('base64')
56
+ : 'sha256-' + crypto.createHash('sha256').update(buffer).digest('base64');
57
+
58
+ const valid = actualHash === expectedHash;
59
+
60
+ return {
61
+ valid,
62
+ expectedHash,
63
+ actualHash,
64
+ algorithm,
65
+ details: valid ? 'Integrity verified' : 'Integrity mismatch!'
66
+ };
67
+ } catch (error: any) {
68
+ return {
69
+ valid: false,
70
+ expectedHash: null,
71
+ actualHash: null,
72
+ algorithm: 'none',
73
+ details: `Verification failed: ${error.message}`
74
+ };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Analyze package size for anomalies
80
+ */
81
+ export async function analyzePackageSize(
82
+ packageName: string,
83
+ version: string,
84
+ registry: string = NPM_REGISTRY
85
+ ): Promise<any> {
86
+ const THRESHOLD_MB = 50;
87
+
88
+ try {
89
+ const response = await fetch(`${registry}/${packageName}/${version}`);
90
+ const data: any = await response.json();
91
+
92
+ const size = data.dist?.unpackedSize || data.dist?.size || 0;
93
+ const sizeFormatted = formatBytes(size);
94
+
95
+ return {
96
+ size,
97
+ sizeFormatted,
98
+ suspicious: size > THRESHOLD_MB * 1024 * 1024,
99
+ threshold: THRESHOLD_MB,
100
+ details: size > THRESHOLD_MB * 1024 * 1024
101
+ ? `Package size ${sizeFormatted} exceeds ${THRESHOLD_MB}MB threshold`
102
+ : `Package size ${sizeFormatted} is normal`
103
+ };
104
+ } catch (error: any) {
105
+ return {
106
+ size: 0,
107
+ sizeFormatted: '0 B',
108
+ suspicious: false,
109
+ threshold: THRESHOLD_MB,
110
+ details: 'Could not determine package size'
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Extract and analyze package.json from tarball
117
+ */
118
+ export async function analyzeTarball(
119
+ packageName: string,
120
+ version: string,
121
+ registry: string = NPM_REGISTRY
122
+ ): Promise<any> {
123
+ const tempDir = path.join(os.tmpdir(), `npm-scan-tarball-${Date.now()}`);
124
+ const tarballPath = path.join(tempDir, 'package.tgz');
125
+
126
+ try {
127
+ const response = await fetch(`${registry}/${packageName}/${version}`);
128
+ const data: any = await response.json();
129
+
130
+ const tarballUrl = data.dist?.tarball;
131
+ if (!tarballUrl) {
132
+ return {
133
+ hasPkgJson: false,
134
+ main: null,
135
+ bin: {},
136
+ files: [],
137
+ fileCount: 0,
138
+ hasNativeCode: false,
139
+ details: 'No tarball URL'
140
+ };
141
+ }
142
+
143
+ fs.mkdirSync(tempDir, { recursive: true });
144
+
145
+ const tarballResponse = await fetch(tarballUrl);
146
+ fs.writeFileSync(tarballPath, Buffer.from(await tarballResponse.arrayBuffer()));
147
+
148
+ const extractDir = path.join(tempDir, 'extracted');
149
+ fs.mkdirSync(extractDir, { recursive: true });
150
+
151
+ await tar.extract({
152
+ file: tarballPath,
153
+ cwd: extractDir
154
+ });
155
+
156
+ const entries = fs.readdirSync(extractDir);
157
+ const pkgDir = entries.find(e => e.startsWith('package'));
158
+ const packageJsonPath = path.join(extractDir, pkgDir || '', 'package.json');
159
+
160
+ if (!fs.existsSync(packageJsonPath)) {
161
+ return {
162
+ hasPkgJson: false,
163
+ main: null,
164
+ bin: {},
165
+ files: [],
166
+ fileCount: 0,
167
+ hasNativeCode: false,
168
+ details: 'package.json not found in tarball'
169
+ };
170
+ }
171
+
172
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
173
+ const allFiles = getAllFiles(path.join(extractDir, pkgDir || ''));
174
+
175
+ const nativeExtensions = ['.node', '.dll', '.dylib', '.so', '.a', '.o'];
176
+ const hasNativeCode = allFiles.some(f => nativeExtensions.some(ext => f.endsWith(ext)));
177
+
178
+ return {
179
+ hasPkgJson: true,
180
+ main: pkgJson.main || null,
181
+ bin: pkgJson.bin || {},
182
+ files: allFiles,
183
+ fileCount: allFiles.length,
184
+ hasNativeCode,
185
+ details: hasNativeCode ? 'Contains native code' : 'Pure JavaScript'
186
+ };
187
+ } catch (error: any) {
188
+ return {
189
+ hasPkgJson: false,
190
+ main: null,
191
+ bin: {},
192
+ files: [],
193
+ fileCount: 0,
194
+ hasNativeCode: false,
195
+ details: `Analysis failed: ${error.message}`
196
+ };
197
+ } finally {
198
+ try {
199
+ fs.rmSync(tempDir, { recursive: true, force: true });
200
+ } catch (e) {}
201
+ }
202
+ }
203
+
204
+ function formatBytes(bytes: number): string {
205
+ if (bytes === 0) return '0 B';
206
+
207
+ const units = ['B', 'KB', 'MB', 'GB'];
208
+ let unitIndex = 0;
209
+ let size = bytes;
210
+
211
+ while (size >= 1024 && unitIndex < units.length - 1) {
212
+ size = size / 1024;
213
+ unitIndex++;
214
+ }
215
+
216
+ return size.toFixed(2) + ' ' + units[unitIndex];
217
+ }
218
+
219
+ function getAllFiles(dir: string, baseDir?: string): string[] {
220
+ const files: string[] = [];
221
+ const base = baseDir || dir;
222
+
223
+ try {
224
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
225
+
226
+ for (const entry of entries) {
227
+ const fullPath = path.join(dir, entry.name);
228
+ const relativePath = path.relative(base, fullPath);
229
+
230
+ if (entry.isDirectory()) {
231
+ files.push(...getAllFiles(fullPath, base));
232
+ } else if (entry.isFile()) {
233
+ files.push(relativePath);
234
+ }
235
+ }
236
+ } catch (e) {}
237
+
238
+ return files;
239
+ }
240
+
241
+ export async function fullIntegrityCheck(
242
+ packageName: string,
243
+ version: string,
244
+ registry?: string
245
+ ): Promise<any> {
246
+ const [integrity, size, tarball] = await Promise.all([
247
+ verifyIntegrity(packageName, version, registry),
248
+ analyzePackageSize(packageName, version, registry),
249
+ analyzeTarball(packageName, version, registry)
250
+ ]);
251
+
252
+ return { integrity, size, tarball };
253
+ }