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,336 @@
1
+ /**
2
+ * CLI entry point
3
+ * Pre and post-install npm security scanner
4
+ */
5
+
6
+ import * as path from 'path';
7
+ import { createScanner } from '../lib/scanner';
8
+ import { blocklistManager } from '../lib/blocklist';
9
+ import type { CliOptions, ScanResult, PostInstallScanResult } from '../types';
10
+
11
+ /**
12
+ * Main CLI runner
13
+ */
14
+ export async function run(argv: string[]): Promise<void> {
15
+ const args = argv.slice(2);
16
+ const options = parseArgs(args);
17
+
18
+ if (!options.command) {
19
+ printHelp();
20
+ return;
21
+ }
22
+
23
+ switch (options.command) {
24
+ case 'pre':
25
+ await runPreInstall(options);
26
+ break;
27
+ case 'post':
28
+ await runPostInstall(options);
29
+ break;
30
+ case 'scan':
31
+ await runFullScan(options);
32
+ break;
33
+ case 'blocklist':
34
+ await runBlocklist(options);
35
+ break;
36
+ default:
37
+ printHelp();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Parse command line arguments
43
+ */
44
+ function parseArgs(args: string[]): CliOptions {
45
+ const options: CliOptions = {
46
+ command: 'scan' as const
47
+ };
48
+
49
+ let i = 0;
50
+ while (i < args.length) {
51
+ const arg = args[i];
52
+
53
+ switch (arg) {
54
+ case 'pre':
55
+ case 'post':
56
+ case 'scan':
57
+ case 'blocklist':
58
+ options.command = arg as CliOptions['command'];
59
+ break;
60
+
61
+ case 'install':
62
+ options.command = 'pre';
63
+ if (args[i + 1] && !args[i + 1].startsWith('-')) {
64
+ options.package = args[++i];
65
+ }
66
+ break;
67
+
68
+ case '--version':
69
+ case '-v':
70
+ if (args[i + 1]) {
71
+ options.version = args[++i];
72
+ }
73
+ break;
74
+
75
+ case '--folder':
76
+ case '-f':
77
+ if (args[i + 1]) {
78
+ options.folder = args[++i];
79
+ }
80
+ break;
81
+
82
+ case '--verbose':
83
+ case '-V':
84
+ options.verbose = true;
85
+ break;
86
+
87
+ case 'add':
88
+ if (args[i + 1]) {
89
+ options.subcommand = 'add';
90
+ options.package = args[++i];
91
+ }
92
+ break;
93
+
94
+ case 'remove':
95
+ case 'rm':
96
+ if (args[i + 1]) {
97
+ options.subcommand = 'remove';
98
+ options.package = args[++i];
99
+ }
100
+ break;
101
+
102
+ case 'list':
103
+ options.subcommand = 'list';
104
+ break;
105
+
106
+ case 'help':
107
+ case '--help':
108
+ case '-h':
109
+ printHelp();
110
+ process.exit(0);
111
+ break;
112
+
113
+ default:
114
+ if (!arg.startsWith('-') && !options.package) {
115
+ options.package = arg;
116
+ }
117
+ }
118
+ i++;
119
+ }
120
+
121
+ return options;
122
+ }
123
+
124
+ /**
125
+ * Run pre-install scan
126
+ */
127
+ async function runPreInstall(options: CliOptions): Promise<void> {
128
+ if (!options.package) {
129
+ console.error('Error: Package name required');
130
+ console.error('Usage: npm-scan pre install <package> [--version <version>');
131
+ process.exit(1);
132
+ }
133
+
134
+ console.log(`\n🔍 Scanning package: ${options.package}${options.version ? '@' + options.version : ''}\n`);
135
+
136
+ const scanner = createScanner({ checkVulnerabilities: true });
137
+ const result = await scanner.preInstallScan(options.package, options.version);
138
+
139
+ printScanResult(result, options.verbose);
140
+ }
141
+
142
+ /**
143
+ * Run post-install scan
144
+ */
145
+ async function runPostInstall(options: CliOptions): Promise<void> {
146
+ console.log(`\n🔍 Scanning installed packages...\n`);
147
+
148
+ const scanner = createScanner({ checkVulnerabilities: true });
149
+ const result = await scanner.postInstallScan(options.folder);
150
+
151
+ printPostScanResult(result);
152
+ }
153
+
154
+ /**
155
+ * Run full scan (pre + post)
156
+ */
157
+ async function runFullScan(options: CliOptions): Promise<void> {
158
+ if (!options.package) {
159
+ console.error('Error: Package name required');
160
+ console.error('Usage: npm-scan scan <package> [--version <version>]');
161
+ process.exit(1);
162
+ }
163
+
164
+ console.log(`\n🔍 Full scan of: ${options.package}${options.version ? '@' + options.version : ''}\n`);
165
+
166
+ const scanner = createScanner({ checkVulnerabilities: true });
167
+ const pre = await scanner.preInstallScan(options.package, options.version);
168
+
169
+ printScanResult(pre, options.verbose);
170
+
171
+ if (pre.status !== 'blocked') {
172
+ const post = await scanner.postInstallScan(options.folder);
173
+ printPostScanResult(post);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Blocklist management
179
+ */
180
+ async function runBlocklist(options: CliOptions): Promise<void> {
181
+ switch (options.subcommand) {
182
+ case 'add':
183
+ if (!options.package) {
184
+ console.error('Error: Package name required');
185
+ process.exit(1);
186
+ }
187
+ blocklistManager.addToBlocklist(options.package, 'Manually added by user');
188
+ console.log(`✅ Added ${options.package} to blocklist`);
189
+ break;
190
+
191
+ case 'remove':
192
+ if (!options.package) {
193
+ console.error('Error: Package name required');
194
+ process.exit(1);
195
+ }
196
+ const removed = blocklistManager.removeFromBlocklist(options.package);
197
+ if (removed) {
198
+ console.log(`✅ Removed ${options.package} from blocklist`);
199
+ } else {
200
+ console.log(`ℹ️ ${options.package} was not in blocklist`);
201
+ }
202
+ break;
203
+
204
+ case 'list':
205
+ default:
206
+ const list = blocklistManager.getBlocklist();
207
+ console.log(`\n📋 Blocklisted packages (${list.length}):\n`);
208
+ for (const entry of list.slice(0, 50)) {
209
+ console.log(` • ${entry.package} [${entry.severity}]`);
210
+ console.log(` ${entry.reason}`);
211
+ }
212
+ if (list.length > 50) {
213
+ console.log(` ... and ${list.length - 50} more`);
214
+ }
215
+ break;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Print pre-install scan result
221
+ */
222
+ function printScanResult(result: ScanResult, verbose = false): void {
223
+ const statusEmoji = {
224
+ safe: '✅',
225
+ warning: '⚠️',
226
+ danger: '🚨',
227
+ blocked: '🛑'
228
+ }[result.status];
229
+
230
+ console.log(`${statusEmoji} ${result.packageName}@${result.version} - ${result.status.toUpperCase()}`);
231
+ console.log(` Score: ${result.score}/100`);
232
+ console.log('');
233
+
234
+ if (result.threats.length > 0) {
235
+ console.log('📌 Threats detected:');
236
+ for (const threat of result.threats) {
237
+ const severityIcon = {
238
+ low: '🔵',
239
+ medium: '🟡',
240
+ high: '🟠',
241
+ critical: '🔴'
242
+ }[threat.severity];
243
+
244
+ console.log(` ${severityIcon} [${threat.severity.toUpperCase()}] ${threat.type}: ${threat.message}`);
245
+ if (threat.details && verbose) {
246
+ console.log(` ${threat.details}`);
247
+ }
248
+ }
249
+ } else {
250
+ console.log(' No threats detected');
251
+ }
252
+
253
+ console.log('');
254
+
255
+ // Print metadata if verbose
256
+ if (verbose && result.metadata) {
257
+ const meta = result.metadata;
258
+ console.log('📦 Package info:');
259
+ if (meta.description) console.log(` ${meta.description}`);
260
+ if (meta.publisher) console.log(` Publisher: ${meta.publisher.username}`);
261
+ if (meta.license) console.log(` License: ${meta.license}`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Print post-install scan result
267
+ */
268
+ function printPostScanResult(result: PostInstallScanResult): void {
269
+ console.log(`📊 Scanned ${result.scannedPackages} packages in ${result.duration}ms`);
270
+ console.log('');
271
+
272
+ if (result.threats.length > 0) {
273
+ console.log(`🚨 Found ${result.threats.length} threats:\n`);
274
+
275
+ const bySeverity = {
276
+ critical: [] as typeof result.threats,
277
+ high: [] as typeof result.threats,
278
+ medium: [] as typeof result.threats,
279
+ low: [] as typeof result.threats
280
+ };
281
+
282
+ for (const threat of result.threats) {
283
+ bySeverity[threat.severity].push(threat);
284
+ }
285
+
286
+ for (const severity of ['critical', 'high', 'medium', 'low'] as const) {
287
+ const threats = bySeverity[severity];
288
+ if (threats.length > 0) {
289
+ console.log(` ${severity.toUpperCase()} (${threats.length}):`);
290
+ for (const threat of threats.slice(0, 10)) {
291
+ console.log(` ${threat.package}/${threat.file}`);
292
+ console.log(` ${threat.message}`);
293
+ }
294
+ if (threats.length > 10) {
295
+ console.log(` ... and ${threats.length - 10} more`);
296
+ }
297
+ console.log('');
298
+ }
299
+ }
300
+ } else {
301
+ console.log('✅ No threats detected');
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Print help
307
+ */
308
+ function printHelp(): void {
309
+ console.log(`
310
+ npm-scan 🔒 Security scanner for npm packages
311
+
312
+ Usage:
313
+ npm-scan pre install <package> [--version <version>] Pre-install security scan
314
+ npm-scan post [--folder <path>] Post-install scan
315
+ npm-scan scan <package> [--version <version>] Full scan (pre + post)
316
+ npm-scan blocklist add <package> Add to blocklist
317
+ npm-scan blocklist remove <package> Remove from blocklist
318
+ npm-scan blocklist list List blocked packages
319
+ npm-scan help This help message
320
+
321
+ Options:
322
+ -v, --version <version> Package version to scan
323
+ -f, --folder <path> node_modules folder path
324
+ -V, --verbose Verbose output
325
+ -h, --help Show this help
326
+
327
+ Examples:
328
+ npm-scan pre install lodash
329
+ npm-scan pre install axios --version 1.6.0
330
+ npm-scan post
331
+ npm-scan blocklist list
332
+ `);
333
+ }
334
+
335
+ export { printHelp };
336
+ export default { run };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Blocklist management
3
+ * Maintains list of known malicious packages
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import type { BlocklistEntry } from '../types';
10
+
11
+ // Known malicious packages (community-sourced & well-documented attacks)
12
+ const KNOWN_MALICIOUS: Record<string, Omit<BlocklistEntry, 'addedAt'>> = {
13
+ // Famous supply chain attacks
14
+ 'event-stream': {
15
+ package: 'event-stream',
16
+ reason: 'flatmap-stream malicious code injection (Dec 2018) - copay窃取加密货币',
17
+ severity: 'critical',
18
+ source: 'npm security incident'
19
+ },
20
+ 'flatmap-stream': {
21
+ package: 'flatmap-stream',
22
+ reason: 'Malicious dependency of event-stream that stole cryptocurrency wallet keys',
23
+ severity: 'critical',
24
+ source: 'npm security incident'
25
+ },
26
+ 'popcorn-native': {
27
+ package: 'popcorn-native',
28
+ reason: 'Malicious package that exfiltrated environment variables',
29
+ severity: 'critical',
30
+ source: 'npm advisory'
31
+ },
32
+ 'ngx-rocket': {
33
+ package: 'ngx-rocket',
34
+ reason: 'Contains malicious postinstall script that crypto mine',
35
+ severity: 'critical',
36
+ source: 'npm advisory'
37
+ },
38
+ 'ruddy': {
39
+ package: 'ruddy',
40
+ reason: '_typosquatting of rudder - steals credentials',
41
+ severity: 'critical',
42
+ source: 'npm advisory'
43
+ },
44
+ 'rudder': {
45
+ package: 'rudder',
46
+ reason: 'Typosquatting of rudder - steals credentials',
47
+ severity: 'critical',
48
+ source: 'npm advisory'
49
+ },
50
+ 'jquery-mobile': {
51
+ package: 'jquery-mobile',
52
+ reason: 'Malicious version with backdoor',
53
+ severity: 'critical',
54
+ source: 'npm advisory'
55
+ },
56
+ 'vue-tetris': {
57
+ package: 'vue-tetris',
58
+ reason: 'Malicious package - sends user data to remote server',
59
+ severity: 'critical',
60
+ source: 'npm advisory'
61
+ },
62
+ 'react-scripts': {
63
+ package: 'react-scripts',
64
+ reason: 'Malicious version that created reverse shell',
65
+ severity: 'critical',
66
+ source: 'npm advisory'
67
+ },
68
+ 'ariatosys-logger': {
69
+ package: 'ariatosys-logger',
70
+ reason: 'logger typosquat with malicious code',
71
+ severity: 'critical',
72
+ source: 'npm community'
73
+ },
74
+ 'syslog': {
75
+ package: 'syslog',
76
+ reason: 'Malicious version - installs rootkit',
77
+ severity: 'critical',
78
+ source: 'npm community'
79
+ },
80
+ 'buffertools': {
81
+ package: 'buffertools',
82
+ reason: 'Malicious version with native code that executes arbitrary commands',
83
+ severity: 'critical',
84
+ source: 'npm advisory'
85
+ }
86
+ };
87
+
88
+ // Common typosquatting patterns to detect
89
+ const TYPOSQUATTING_PATTERNS = [
90
+ // Popular packages often typosquatted
91
+ { pattern: 'lodash', variations: ['lodsh', 'lodas', 'lodah', 'lodih', 'loadsh'] },
92
+ { pattern: 'axios', variations: ['axio', 'axois', 'azios', 'axuox'] },
93
+ { pattern: 'express', variations: ['expres', 'exprss', 'expresz', 'exprress'] },
94
+ { pattern: 'react', variations: ['reactt', 'reacet', 'reacgt', 'reacvt'] },
95
+ { pattern: 'vue', variations: ['vu', 'vve', 'vuer', 'fvue'] },
96
+ { pattern: 'moment', variations: ['momemt', 'momnet', 'momnt', 'momemtn'] },
97
+ { pattern: 'lodash', variations: ['lodsh', 'lodas', 'lodah', 'lodih', 'loadsh'] },
98
+ { pattern: 'mongoose', variations: ['mongeose', 'mognoose', 'mangos'] },
99
+ { pattern: 'webpack', variations: ['webback', 'webpak', 'webpac'] },
100
+ { pattern: 'eslint', variations: ['easylint', 'eslint', 'esline'] },
101
+ { pattern: 'nodemon', variations: ['nodomen', 'nodmon', 'nodon'] },
102
+ { pattern: 'pm2', variations: ['pm', 'p2m', 'pn2'] },
103
+ { pattern: 'typescript', variations: ['typesript', 'type-script', 'tsc'] },
104
+ { pattern: 'dotenv', variations: ['doten', 'dotenV', 'dotenvn'] },
105
+ { pattern: 'async', variations: ['asycn', 'asyc', 'assync'] },
106
+ { pattern: 'qs', variations: ['qss', 's', 'q'] },
107
+ { pattern: 'minimist', variations: ['minimist', 'minimist-', 'minimist_'] },
108
+ { pattern: 'extend', variations: ['exten', 'extnd', 'extend_'] },
109
+ { pattern: 'request', variations: ['requst', 'reqest', 'rqequest'] },
110
+ { pattern: 'chalk', variations: ['chalck', 'chalk-', 'halck'] },
111
+ { pattern: 'commander', variations: ['comman', 'commender', 'commnad'] },
112
+ { pattern: 'inquirer', variations: ['inquir', 'inquer', 'enquire'] },
113
+ { pattern: 'bluebird', variations: ['bluebir', 'bluebd', 'bloodbird'] },
114
+ { pattern: 'underscore', variations: ['undrscore', 'underscro', '_score'] },
115
+ { pattern: 'lodash-es', variations: ['lodashes', 'lodash_es', 'loadsh-es'] }
116
+ ];
117
+
118
+ export class BlocklistManager {
119
+ private userBlocklist: Map<string, BlocklistEntry> = new Map();
120
+ private blocklistPath: string;
121
+
122
+ constructor() {
123
+ this.blocklistPath = path.join(os.homedir(), '.npm-scan-blocklist.json');
124
+ this.loadUserBlocklist();
125
+ }
126
+
127
+ /**
128
+ * Check if a package is blocklisted
129
+ */
130
+ isBlocklisted(packageName: string): BlocklistEntry | null {
131
+ const lowerName = packageName.toLowerCase();
132
+
133
+ // Check known malicious
134
+ if (KNOWN_MALICIOUS[lowerName]) {
135
+ return {
136
+ ...KNOWN_MALICIOUS[lowerName],
137
+ addedAt: '2018-12-01'
138
+ };
139
+ }
140
+
141
+ // Check user blocklist
142
+ if (this.userBlocklist.has(lowerName)) {
143
+ return this.userBlocklist.get(lowerName)!;
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Check for typosquatting
151
+ */
152
+ detectTyposquatting(packageName: string): string[] {
153
+ const lowerName = packageName.toLowerCase();
154
+ const matches: string[] = [];
155
+
156
+ for (const { pattern, variations } of TYPOSQUATTING_PATTERNS) {
157
+ if (variations.includes(lowerName)) {
158
+ matches.push(pattern);
159
+ }
160
+ }
161
+
162
+ return matches;
163
+ }
164
+
165
+ /**
166
+ * Add package to user blocklist
167
+ */
168
+ addToBlocklist(packageName: string, reason: string, severity: 'high' | 'critical' = 'high'): void {
169
+ const entry: BlocklistEntry = {
170
+ package: packageName.toLowerCase(),
171
+ reason,
172
+ addedAt: new Date().toISOString(),
173
+ severity
174
+ };
175
+
176
+ this.userBlocklist.set(packageName.toLowerCase(), entry);
177
+ this.saveUserBlocklist();
178
+ }
179
+
180
+ /**
181
+ * Remove package from user blocklist
182
+ */
183
+ removeFromBlocklist(packageName: string): boolean {
184
+ const removed = this.userBlocklist.delete(packageName.toLowerCase());
185
+ if (removed) {
186
+ this.saveUserBlocklist();
187
+ }
188
+ return removed;
189
+ }
190
+
191
+ /**
192
+ * Get all blocklisted packages
193
+ */
194
+ getBlocklist(): BlocklistEntry[] {
195
+ const entries: BlocklistEntry[] = [];
196
+
197
+ for (const entry of Object.values(KNOWN_MALICIOUS)) {
198
+ entries.push({ ...entry, addedAt: '2018-12-01' });
199
+ }
200
+
201
+ for (const entry of this.userBlocklist.values()) {
202
+ entries.push(entry);
203
+ }
204
+
205
+ return entries;
206
+ }
207
+
208
+ /**
209
+ * Load user blocklist from disk
210
+ */
211
+ private loadUserBlocklist(): void {
212
+ try {
213
+ if (fs.existsSync(this.blocklistPath)) {
214
+ const data = fs.readFileSync(this.blocklistPath, 'utf-8');
215
+ const entries = JSON.parse(data) as BlocklistEntry[];
216
+ for (const entry of entries) {
217
+ this.userBlocklist.set(entry.package, entry);
218
+ }
219
+ }
220
+ } catch (e) {
221
+ // Ignore errors
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Save user blocklist to disk
227
+ */
228
+ private saveUserBlocklist(): void {
229
+ try {
230
+ const entries = Array.from(this.userBlocklist.values());
231
+ fs.writeFileSync(this.blocklistPath, JSON.stringify(entries, null, 2));
232
+ } catch (e) {
233
+ throw new Error(`Failed to save blocklist: ${e}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ export const blocklistManager = new BlocklistManager();
239
+ export { TYPOSQUATTING_PATTERNS };