@cyberhub/shieldpm 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/analyzer/static.d.ts +35 -0
  4. package/dist/analyzer/static.d.ts.map +1 -0
  5. package/dist/analyzer/static.js +416 -0
  6. package/dist/analyzer/static.js.map +1 -0
  7. package/dist/analyzer/typosquat.d.ts +30 -0
  8. package/dist/analyzer/typosquat.d.ts.map +1 -0
  9. package/dist/analyzer/typosquat.js +211 -0
  10. package/dist/analyzer/typosquat.js.map +1 -0
  11. package/dist/cli.d.ts +10 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +621 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/diff/dependency.d.ts +51 -0
  16. package/dist/diff/dependency.d.ts.map +1 -0
  17. package/dist/diff/dependency.js +222 -0
  18. package/dist/diff/dependency.js.map +1 -0
  19. package/dist/fingerprint/profile.d.ts +68 -0
  20. package/dist/fingerprint/profile.d.ts.map +1 -0
  21. package/dist/fingerprint/profile.js +233 -0
  22. package/dist/fingerprint/profile.js.map +1 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +22 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/monitor/permissions.d.ts +45 -0
  28. package/dist/monitor/permissions.d.ts.map +1 -0
  29. package/dist/monitor/permissions.js +265 -0
  30. package/dist/monitor/permissions.js.map +1 -0
  31. package/dist/sandbox/runner.d.ts +46 -0
  32. package/dist/sandbox/runner.d.ts.map +1 -0
  33. package/dist/sandbox/runner.js +216 -0
  34. package/dist/sandbox/runner.js.map +1 -0
  35. package/dist/utils/colors.d.ts +31 -0
  36. package/dist/utils/colors.d.ts.map +1 -0
  37. package/dist/utils/colors.js +54 -0
  38. package/dist/utils/colors.js.map +1 -0
  39. package/dist/utils/logger.d.ts +26 -0
  40. package/dist/utils/logger.d.ts.map +1 -0
  41. package/dist/utils/logger.js +77 -0
  42. package/dist/utils/logger.js.map +1 -0
  43. package/package.json +24 -0
  44. package/src/analyzer/static.ts +483 -0
  45. package/src/analyzer/typosquat.ts +272 -0
  46. package/src/cli.ts +700 -0
  47. package/src/diff/dependency.ts +297 -0
  48. package/src/fingerprint/profile.ts +333 -0
  49. package/src/index.ts +34 -0
  50. package/src/monitor/permissions.ts +330 -0
  51. package/src/sandbox/runner.ts +302 -0
  52. package/src/utils/colors.ts +58 -0
  53. package/src/utils/logger.ts +87 -0
  54. package/tsconfig.json +19 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * ShieldPM — Static Analysis Engine
3
+ * Scans package source code for suspicious patterns, network calls,
4
+ * filesystem access, obfuscation, and dynamic code execution.
5
+ */
6
+
7
+ import { readdir, readFile, stat } from 'node:fs/promises';
8
+ import { join, extname, relative } from 'node:path';
9
+
10
+ // ── Types ────────────────────────────────────────────────────────────────
11
+
12
+ export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
13
+
14
+ export interface Finding {
15
+ severity: Severity;
16
+ category: string;
17
+ message: string;
18
+ file: string;
19
+ line: number;
20
+ column: number;
21
+ snippet: string;
22
+ rule: string;
23
+ }
24
+
25
+ export interface RiskReport {
26
+ /** Overall risk score 0 (safe) – 10 (dangerous) */
27
+ score: number;
28
+ findings: Finding[];
29
+ summary: string;
30
+ /** Breakdown by category */
31
+ categoryCounts: Record<string, number>;
32
+ /** Total files scanned */
33
+ filesScanned: number;
34
+ }
35
+
36
+ // ── Pattern definitions ──────────────────────────────────────────────────
37
+
38
+ interface PatternRule {
39
+ rule: string;
40
+ pattern: RegExp;
41
+ severity: Severity;
42
+ category: string;
43
+ message: string;
44
+ }
45
+
46
+ const PATTERNS: PatternRule[] = [
47
+ // ── Dynamic code execution ──
48
+ {
49
+ rule: 'no-eval',
50
+ pattern: /\beval\s*\(/g,
51
+ severity: 'critical',
52
+ category: 'code-execution',
53
+ message: 'eval() can execute arbitrary code',
54
+ },
55
+ {
56
+ rule: 'no-function-constructor',
57
+ pattern: /\bnew\s+Function\s*\(/g,
58
+ severity: 'critical',
59
+ category: 'code-execution',
60
+ message: 'Function constructor can execute arbitrary code',
61
+ },
62
+ {
63
+ rule: 'no-vm-runInContext',
64
+ pattern: /\bvm\s*\.\s*(runInNewContext|runInThisContext|runInContext|compileFunction)\s*\(/g,
65
+ severity: 'high',
66
+ category: 'code-execution',
67
+ message: 'vm module can execute arbitrary code',
68
+ },
69
+ {
70
+ rule: 'no-buffer-eval',
71
+ pattern: /Buffer\.from\s*\([^)]+\)\s*\.\s*toString\s*\([^)]*\)[\s\S]{0,50}eval/g,
72
+ severity: 'critical',
73
+ category: 'code-execution',
74
+ message: 'Buffer decode + eval pattern — likely obfuscated code execution',
75
+ },
76
+
77
+ // ── Child process / shell ──
78
+ {
79
+ rule: 'no-child-process',
80
+ pattern: /require\s*\(\s*['"`]child_process['"`]\s*\)/g,
81
+ severity: 'high',
82
+ category: 'process',
83
+ message: 'child_process can spawn arbitrary system commands',
84
+ },
85
+ {
86
+ rule: 'no-child-process-import',
87
+ pattern: /from\s+['"`]child_process['"`]/g,
88
+ severity: 'high',
89
+ category: 'process',
90
+ message: 'child_process import — can spawn arbitrary system commands',
91
+ },
92
+ {
93
+ rule: 'no-exec-sync',
94
+ pattern: /\b(execSync|exec|spawn|spawnSync|fork|execFile|execFileSync)\s*\(/g,
95
+ severity: 'high',
96
+ category: 'process',
97
+ message: 'Process execution function detected',
98
+ },
99
+
100
+ // ── Network access ──
101
+ {
102
+ rule: 'no-http-require',
103
+ pattern: /require\s*\(\s*['"`](https?|net|tls|dgram)['"`]\s*\)/g,
104
+ severity: 'medium',
105
+ category: 'network',
106
+ message: 'Network module require',
107
+ },
108
+ {
109
+ rule: 'no-http-import',
110
+ pattern: /from\s+['"`](https?|net|tls|dgram)['"`]/g,
111
+ severity: 'medium',
112
+ category: 'network',
113
+ message: 'Network module import',
114
+ },
115
+ {
116
+ rule: 'no-http-request',
117
+ pattern: /\b(https?)\s*\.\s*(request|get)\s*\(/g,
118
+ severity: 'medium',
119
+ category: 'network',
120
+ message: 'HTTP request detected',
121
+ },
122
+ {
123
+ rule: 'no-fetch',
124
+ pattern: /\bfetch\s*\(\s*['"`]https?:/g,
125
+ severity: 'medium',
126
+ category: 'network',
127
+ message: 'fetch() call to external URL',
128
+ },
129
+ {
130
+ rule: 'no-fetch-dynamic',
131
+ pattern: /\bfetch\s*\(\s*[^'"`\s]/g,
132
+ severity: 'high',
133
+ category: 'network',
134
+ message: 'fetch() with dynamic URL — destination unknown',
135
+ },
136
+ {
137
+ rule: 'no-dns-lookup',
138
+ pattern: /\bdns\s*\.\s*(lookup|resolve|resolve4|resolve6)\s*\(/g,
139
+ severity: 'low',
140
+ category: 'network',
141
+ message: 'DNS lookup detected',
142
+ },
143
+ {
144
+ rule: 'no-xmlhttprequest',
145
+ pattern: /\bnew\s+XMLHttpRequest\s*\(/g,
146
+ severity: 'medium',
147
+ category: 'network',
148
+ message: 'XMLHttpRequest detected',
149
+ },
150
+ {
151
+ rule: 'no-websocket',
152
+ pattern: /\bnew\s+WebSocket\s*\(/g,
153
+ severity: 'medium',
154
+ category: 'network',
155
+ message: 'WebSocket connection detected',
156
+ },
157
+
158
+ // ── File system access ──
159
+ {
160
+ rule: 'no-fs-require',
161
+ pattern: /require\s*\(\s*['"`]fs['"`]\s*\)/g,
162
+ severity: 'low',
163
+ category: 'filesystem',
164
+ message: 'fs module require',
165
+ },
166
+ {
167
+ rule: 'no-fs-import',
168
+ pattern: /from\s+['"`](fs|node:fs|fs\/promises|node:fs\/promises)['"`]/g,
169
+ severity: 'low',
170
+ category: 'filesystem',
171
+ message: 'fs module import',
172
+ },
173
+ {
174
+ rule: 'no-sensitive-path-read',
175
+ pattern: /\b(readFile|readFileSync|createReadStream)\s*\(\s*['"`](\/etc\/passwd|\/etc\/shadow|~\/\.ssh|~\/\.aws|~\/\.npmrc|~\/\.env|\/proc\/)/g,
176
+ severity: 'critical',
177
+ category: 'filesystem',
178
+ message: 'Reading sensitive system file',
179
+ },
180
+ {
181
+ rule: 'no-sensitive-path-write',
182
+ pattern: /\b(writeFile|writeFileSync|appendFile|appendFileSync)\s*\(\s*['"`](\/etc\/|\/usr\/|\/bin\/|~\/\.bashrc|~\/\.profile)/g,
183
+ severity: 'critical',
184
+ category: 'filesystem',
185
+ message: 'Writing to sensitive system path',
186
+ },
187
+ {
188
+ rule: 'no-fs-unlink',
189
+ pattern: /\b(unlink|unlinkSync|rmdir|rmdirSync|rm)\s*\(/g,
190
+ severity: 'medium',
191
+ category: 'filesystem',
192
+ message: 'File/directory deletion detected',
193
+ },
194
+ {
195
+ rule: 'no-home-dir-readdir',
196
+ pattern: /\b(readdir|readdirSync)\s*\(\s*['"`](~|\/home\/|\/Users\/)/g,
197
+ severity: 'high',
198
+ category: 'filesystem',
199
+ message: 'Listing home directory contents',
200
+ },
201
+
202
+ // ── Environment variable access ──
203
+ {
204
+ rule: 'no-env-access',
205
+ pattern: /process\.env\b/g,
206
+ severity: 'low',
207
+ category: 'environment',
208
+ message: 'Accesses environment variables',
209
+ },
210
+ {
211
+ rule: 'no-env-sensitive',
212
+ pattern: /process\.env\s*\[\s*['"`](API_KEY|SECRET|TOKEN|PASSWORD|AWS_|GITHUB_TOKEN|NPM_TOKEN|DATABASE_URL|PRIVATE_KEY)/g,
213
+ severity: 'high',
214
+ category: 'environment',
215
+ message: 'Accesses sensitive environment variable',
216
+ },
217
+ {
218
+ rule: 'no-env-exfiltrate',
219
+ pattern: /JSON\.stringify\s*\(\s*process\.env\s*\)/g,
220
+ severity: 'critical',
221
+ category: 'environment',
222
+ message: 'Serializing entire process.env — possible credential exfiltration',
223
+ },
224
+
225
+ // ── Dynamic require / import ──
226
+ {
227
+ rule: 'no-dynamic-require',
228
+ pattern: /require\s*\(\s*[^'"`\s)][^)]*\)/g,
229
+ severity: 'medium',
230
+ category: 'code-execution',
231
+ message: 'Dynamic require with non-literal argument',
232
+ },
233
+ {
234
+ rule: 'no-dynamic-import',
235
+ pattern: /import\s*\(\s*[^'"`\s)][^)]*\)/g,
236
+ severity: 'medium',
237
+ category: 'code-execution',
238
+ message: 'Dynamic import with non-literal argument',
239
+ },
240
+
241
+ // ── Obfuscation ──
242
+ {
243
+ rule: 'no-charcode-build',
244
+ pattern: /String\.fromCharCode\s*\(/g,
245
+ severity: 'high',
246
+ category: 'obfuscation',
247
+ message: 'String.fromCharCode — common obfuscation technique',
248
+ },
249
+ {
250
+ rule: 'no-hex-string',
251
+ pattern: /\\x[0-9a-fA-F]{2}(\\x[0-9a-fA-F]{2}){3,}/g,
252
+ severity: 'high',
253
+ category: 'obfuscation',
254
+ message: 'Long hex escape sequence — possible obfuscated string',
255
+ },
256
+ {
257
+ rule: 'no-unicode-escape',
258
+ pattern: /\\u[0-9a-fA-F]{4}(\\u[0-9a-fA-F]{4}){5,}/g,
259
+ severity: 'high',
260
+ category: 'obfuscation',
261
+ message: 'Long unicode escape sequence — possible obfuscated string',
262
+ },
263
+ {
264
+ rule: 'no-atob',
265
+ pattern: /\batob\s*\(/g,
266
+ severity: 'medium',
267
+ category: 'obfuscation',
268
+ message: 'Base64 decode — may hide malicious strings',
269
+ },
270
+ {
271
+ rule: 'no-base64-decode',
272
+ pattern: /Buffer\.from\s*\([^,]+,\s*['"`]base64['"`]\s*\)/g,
273
+ severity: 'medium',
274
+ category: 'obfuscation',
275
+ message: 'Base64 decode via Buffer — may hide malicious strings',
276
+ },
277
+
278
+ // ── Prototype pollution ──
279
+ {
280
+ rule: 'no-proto-access',
281
+ pattern: /\[['"`]__proto__['"`]\]/g,
282
+ severity: 'high',
283
+ category: 'prototype-pollution',
284
+ message: '__proto__ access — possible prototype pollution',
285
+ },
286
+ {
287
+ rule: 'no-constructor-prototype',
288
+ pattern: /\bconstructor\s*\[\s*['"`]prototype['"`]\s*\]/g,
289
+ severity: 'high',
290
+ category: 'prototype-pollution',
291
+ message: 'constructor.prototype access — possible prototype pollution',
292
+ },
293
+
294
+ // ── Install scripts ──
295
+ {
296
+ rule: 'no-preinstall-script',
297
+ pattern: /"preinstall"\s*:\s*"/g,
298
+ severity: 'high',
299
+ category: 'install-script',
300
+ message: 'preinstall script can execute code before user reviews package',
301
+ },
302
+ {
303
+ rule: 'no-postinstall-script',
304
+ pattern: /"postinstall"\s*:\s*"/g,
305
+ severity: 'medium',
306
+ category: 'install-script',
307
+ message: 'postinstall script detected',
308
+ },
309
+ ];
310
+
311
+ // ── Severity weights for scoring ─────────────────────────────────────────
312
+
313
+ const SEVERITY_WEIGHT: Record<Severity, number> = {
314
+ critical: 3.0,
315
+ high: 2.0,
316
+ medium: 1.0,
317
+ low: 0.3,
318
+ info: 0.0,
319
+ };
320
+
321
+ // ── File collection ──────────────────────────────────────────────────────
322
+
323
+ const SCANNABLE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.json']);
324
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.shieldpm']);
325
+
326
+ async function collectFiles(dir: string): Promise<string[]> {
327
+ const files: string[] = [];
328
+
329
+ async function walk(d: string): Promise<void> {
330
+ let entries;
331
+ try {
332
+ entries = await readdir(d, { withFileTypes: true });
333
+ } catch {
334
+ return; // skip unreadable dirs
335
+ }
336
+ for (const entry of entries) {
337
+ const full = join(d, entry.name);
338
+ if (entry.isDirectory()) {
339
+ if (!SKIP_DIRS.has(entry.name)) {
340
+ await walk(full);
341
+ }
342
+ } else if (entry.isFile() && SCANNABLE_EXTENSIONS.has(extname(entry.name))) {
343
+ files.push(full);
344
+ }
345
+ }
346
+ }
347
+
348
+ await walk(dir);
349
+ return files;
350
+ }
351
+
352
+ // ── Core scan logic ──────────────────────────────────────────────────────
353
+
354
+ function scanContent(content: string, filePath: string, rules: PatternRule[]): Finding[] {
355
+ const findings: Finding[] = [];
356
+ const lines = content.split('\n');
357
+
358
+ for (const rule of rules) {
359
+ // Reset lastIndex for global regexes
360
+ rule.pattern.lastIndex = 0;
361
+ let match: RegExpExecArray | null;
362
+
363
+ while ((match = rule.pattern.exec(content)) !== null) {
364
+ // Calculate line and column
365
+ const beforeMatch = content.slice(0, match.index);
366
+ const line = (beforeMatch.match(/\n/g) || []).length + 1;
367
+ const lastNewline = beforeMatch.lastIndexOf('\n');
368
+ const column = match.index - lastNewline;
369
+
370
+ // Grab the snippet (the matched line)
371
+ const snippet = lines[line - 1]?.trim() ?? '';
372
+
373
+ findings.push({
374
+ severity: rule.severity,
375
+ category: rule.category,
376
+ message: rule.message,
377
+ file: filePath,
378
+ line,
379
+ column,
380
+ snippet: snippet.length > 120 ? snippet.slice(0, 117) + '...' : snippet,
381
+ rule: rule.rule,
382
+ });
383
+ }
384
+ }
385
+
386
+ return findings;
387
+ }
388
+
389
+ // ── Risk score calculation ───────────────────────────────────────────────
390
+
391
+ function calculateScore(findings: Finding[]): number {
392
+ if (findings.length === 0) return 0;
393
+
394
+ let raw = 0;
395
+ for (const f of findings) {
396
+ raw += SEVERITY_WEIGHT[f.severity];
397
+ }
398
+
399
+ // Diminishing returns — many low findings shouldn't max the score
400
+ // Score = 10 * (1 - e^(-raw/8))
401
+ const score = 10 * (1 - Math.exp(-raw / 8));
402
+ return Math.round(score * 10) / 10; // one decimal
403
+ }
404
+
405
+ function buildSummary(findings: Finding[], score: number): string {
406
+ if (findings.length === 0) return 'No suspicious patterns found.';
407
+
408
+ const critical = findings.filter((f) => f.severity === 'critical').length;
409
+ const high = findings.filter((f) => f.severity === 'high').length;
410
+ const medium = findings.filter((f) => f.severity === 'medium').length;
411
+ const low = findings.filter((f) => f.severity === 'low').length;
412
+
413
+ const parts: string[] = [];
414
+ if (critical > 0) parts.push(`${critical} critical`);
415
+ if (high > 0) parts.push(`${high} high`);
416
+ if (medium > 0) parts.push(`${medium} medium`);
417
+ if (low > 0) parts.push(`${low} low`);
418
+
419
+ const riskLabel = score >= 7 ? 'DANGEROUS' : score >= 4 ? 'SUSPICIOUS' : score >= 2 ? 'CAUTION' : 'LOW RISK';
420
+
421
+ return `Risk: ${riskLabel} (${score}/10) — ${findings.length} findings: ${parts.join(', ')}`;
422
+ }
423
+
424
+ // ── Public API ───────────────────────────────────────────────────────────
425
+
426
+ /**
427
+ * Analyze a package directory for security risks via static pattern matching.
428
+ */
429
+ export async function analyzePackage(packageDir: string): Promise<RiskReport> {
430
+ const files = await collectFiles(packageDir);
431
+ const allFindings: Finding[] = [];
432
+
433
+ for (const file of files) {
434
+ let content: string;
435
+ try {
436
+ content = await readFile(file, 'utf-8');
437
+ } catch {
438
+ continue;
439
+ }
440
+
441
+ const relPath = relative(packageDir, file);
442
+ const findings = scanContent(content, relPath, PATTERNS);
443
+ allFindings.push(...findings);
444
+ }
445
+
446
+ // Deduplicate same rule+file+line
447
+ const seen = new Set<string>();
448
+ const deduped = allFindings.filter((f) => {
449
+ const key = `${f.rule}:${f.file}:${f.line}`;
450
+ if (seen.has(key)) return false;
451
+ seen.add(key);
452
+ return true;
453
+ });
454
+
455
+ // Sort: critical first, then by file/line
456
+ deduped.sort((a, b) => {
457
+ const sw = SEVERITY_WEIGHT[b.severity] - SEVERITY_WEIGHT[a.severity];
458
+ if (sw !== 0) return sw;
459
+ return a.file.localeCompare(b.file) || a.line - b.line;
460
+ });
461
+
462
+ const score = calculateScore(deduped);
463
+
464
+ const categoryCounts: Record<string, number> = {};
465
+ for (const f of deduped) {
466
+ categoryCounts[f.category] = (categoryCounts[f.category] ?? 0) + 1;
467
+ }
468
+
469
+ return {
470
+ score,
471
+ findings: deduped,
472
+ summary: buildSummary(deduped, score),
473
+ categoryCounts,
474
+ filesScanned: files.length,
475
+ };
476
+ }
477
+
478
+ /**
479
+ * Analyze a single source string (useful for quick checks).
480
+ */
481
+ export function analyzeSource(source: string, filename = '<input>'): Finding[] {
482
+ return scanContent(source, filename, PATTERNS);
483
+ }