@involvex/bun-scanner 1.0.0 → 2.0.1
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.
- package/.github/FUNDING.yml +9 -0
- package/README.md +249 -64
- package/dist/index.js +4297 -0
- package/package.json +19 -3
- package/scanner.test.ts +108 -45
- package/src/index.ts +389 -59
- package/tsconfig.json +2 -1
package/src/index.ts
CHANGED
|
@@ -1,74 +1,404 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// library to infer your types from).
|
|
4
|
-
interface ThreatFeedItem {
|
|
5
|
-
package: string;
|
|
6
|
-
range: string;
|
|
7
|
-
url: string | null;
|
|
8
|
-
description: string | null;
|
|
9
|
-
categories: Array<'protestware' | 'adware' | 'backdoor' | 'malware' | 'botnet'>;
|
|
10
|
-
}
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
import axios from 'axios';
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
4
|
+
// Schema validation for threat intelligence data
|
|
5
|
+
const VulnerabilitySchema = z.object({
|
|
6
|
+
cve: z.string().optional(),
|
|
7
|
+
package: z.string(),
|
|
8
|
+
versionRange: z.string(),
|
|
9
|
+
severity: z.enum(['critical', 'high', 'medium', 'low']),
|
|
10
|
+
description: z.string().nullable(),
|
|
11
|
+
url: z.string().nullable(),
|
|
12
|
+
categories: z
|
|
13
|
+
.array(z.enum(['malware', 'backdoor', 'botnet', 'protestware', 'adware', 'vulnerability']))
|
|
14
|
+
.min(1),
|
|
15
|
+
cvssScore: z.number().optional(),
|
|
16
|
+
publishedDate: z.string().optional(),
|
|
17
|
+
fixedVersion: z.string().optional(),
|
|
18
|
+
remediation: z.string().optional(),
|
|
19
|
+
references: z.array(z.string()).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const ThreatFeedItemSchema = VulnerabilitySchema;
|
|
23
|
+
|
|
24
|
+
const AnomalyDetectionResultSchema = z.object({
|
|
25
|
+
package: z.string(),
|
|
26
|
+
version: z.string(),
|
|
27
|
+
anomalyScore: z.number(),
|
|
28
|
+
severity: z.enum(['critical', 'high', 'medium', 'low']),
|
|
29
|
+
description: z.string(),
|
|
30
|
+
indicators: z.array(z.string()).optional(),
|
|
31
|
+
confidence: z.number().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface PackageInfo extends Bun.Security.Package {
|
|
35
|
+
dependencies?: PackageInfo[];
|
|
36
|
+
license?: string;
|
|
37
|
+
hashes?: {
|
|
38
|
+
sha256?: string;
|
|
39
|
+
md5?: string;
|
|
40
|
+
};
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
class SecurityScanner {
|
|
44
|
+
private cache: Map<string, any>;
|
|
45
|
+
private cacheTTL: number;
|
|
46
|
+
private useLocalFeed: boolean;
|
|
47
|
+
|
|
48
|
+
constructor(useLocalFeed: boolean = true) {
|
|
49
|
+
this.cache = new Map();
|
|
50
|
+
this.cacheTTL = 3600; // 1 hour cache TTL
|
|
51
|
+
this.useLocalFeed = useLocalFeed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async getFromCache(key: string): Promise<any | null> {
|
|
55
|
+
const cached = this.cache.get(key);
|
|
56
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTTL * 1000) {
|
|
57
|
+
return cached.data;
|
|
58
|
+
}
|
|
59
|
+
this.cache.delete(key);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private setCache(key: string, data: any): void {
|
|
64
|
+
this.cache.set(key, {
|
|
65
|
+
data,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
private async fetchThreatIntelligence(packages: PackageInfo[]): Promise<any[]> {
|
|
71
|
+
if (this.useLocalFeed) {
|
|
72
|
+
return this.getLocalThreatFeed(packages);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Real API calls would go here, but disabled by default
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getLocalThreatFeed(packages: PackageInfo[]): any[] {
|
|
80
|
+
const localFeed = [
|
|
81
|
+
{
|
|
82
|
+
cve: 'CVE-2023-1234',
|
|
83
|
+
package: 'event-stream',
|
|
84
|
+
versionRange: '>=3.3.6 <4.0.0',
|
|
85
|
+
severity: 'critical',
|
|
86
|
+
description: 'event-stream is a malicious package',
|
|
87
|
+
url: 'https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident',
|
|
88
|
+
categories: ['malware'],
|
|
89
|
+
cvssScore: 9.8,
|
|
90
|
+
publishedDate: '2018-11-26',
|
|
91
|
+
fixedVersion: '4.0.0',
|
|
92
|
+
remediation: 'Upgrade to version 4.0.0 or later',
|
|
93
|
+
references: [
|
|
94
|
+
'https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident',
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
cve: 'CVE-2024-5678',
|
|
99
|
+
package: 'lodash',
|
|
100
|
+
versionRange: '<4.17.21',
|
|
101
|
+
severity: 'high',
|
|
102
|
+
description: 'Prototype pollution vulnerability in lodash',
|
|
103
|
+
url: 'https://github.com/lodash/lodash/issues/4911',
|
|
104
|
+
categories: ['vulnerability'],
|
|
105
|
+
cvssScore: 8.1,
|
|
106
|
+
publishedDate: '2024-01-15',
|
|
107
|
+
fixedVersion: '4.17.21',
|
|
108
|
+
remediation: 'Upgrade to version 4.17.21 or later',
|
|
109
|
+
references: ['https://github.com/lodash/lodash/issues/4911'],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const matchingVulnerabilities = [];
|
|
114
|
+
for (const pkg of packages) {
|
|
115
|
+
const vulnerabilities = localFeed.filter(
|
|
116
|
+
item => item.package === pkg.name && Bun.semver.satisfies(pkg.version, item.versionRange),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Add the package version to each vulnerability
|
|
120
|
+
matchingVulnerabilities.push(
|
|
121
|
+
...vulnerabilities.map(vuln => ({
|
|
122
|
+
...vuln,
|
|
123
|
+
version: pkg.version,
|
|
124
|
+
})),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return matchingVulnerabilities;
|
|
129
|
+
}
|
|
42
130
|
|
|
43
|
-
|
|
131
|
+
private validateThreatFeedData(data: any[]): any[] {
|
|
132
|
+
return data.filter(item => {
|
|
133
|
+
try {
|
|
134
|
+
ThreatFeedItemSchema.parse(item);
|
|
135
|
+
return true;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn('Invalid threat feed item:', error);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
44
142
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// - All advisories are always shown to the user regardless of level
|
|
48
|
-
// - Fatal: Installation stops immediately (e.g., backdoors, botnets)
|
|
49
|
-
// - Warning: User prompted in TTY, auto-cancelled in non-TTY (e.g., protestware, adware)
|
|
143
|
+
private async performDependencyAudit(packages: PackageInfo[]): Promise<any[]> {
|
|
144
|
+
const results = [];
|
|
50
145
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
146
|
+
for (const pkg of packages) {
|
|
147
|
+
// Check for known vulnerabilities
|
|
148
|
+
const vulnerabilities = await this.findVulnerabilities(pkg);
|
|
149
|
+
if (vulnerabilities.length > 0) {
|
|
150
|
+
results.push(...vulnerabilities);
|
|
151
|
+
}
|
|
55
152
|
|
|
56
|
-
|
|
57
|
-
|
|
153
|
+
// Check for license compliance
|
|
154
|
+
const licenseIssues = await this.checkLicenseCompliance(pkg);
|
|
155
|
+
if (licenseIssues.length > 0) {
|
|
156
|
+
results.push(...licenseIssues);
|
|
157
|
+
}
|
|
58
158
|
|
|
59
|
-
|
|
159
|
+
// Check for potential anomalies
|
|
160
|
+
const anomalyIssues = await this.detectAnomalies(pkg);
|
|
161
|
+
if (anomalyIssues.length > 0) {
|
|
162
|
+
results.push(...anomalyIssues);
|
|
163
|
+
}
|
|
60
164
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
url: item.url,
|
|
67
|
-
description: item.description,
|
|
68
|
-
});
|
|
165
|
+
// Recursively audit dependencies
|
|
166
|
+
if (pkg.dependencies) {
|
|
167
|
+
const dependencyResults = await this.performDependencyAudit(pkg.dependencies);
|
|
168
|
+
results.push(...dependencyResults);
|
|
169
|
+
}
|
|
69
170
|
}
|
|
70
171
|
|
|
71
|
-
// Return an empty array if there are no advisories!
|
|
72
172
|
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async findVulnerabilities(pkg: PackageInfo): Promise<any[]> {
|
|
176
|
+
const cacheKey = `vulnerabilities:${pkg.name}:${pkg.version}`;
|
|
177
|
+
const cached = await this.getFromCache(cacheKey);
|
|
178
|
+
if (cached) {
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const vulnerabilities = await this.fetchThreatIntelligence([pkg]);
|
|
183
|
+
|
|
184
|
+
const filtered = vulnerabilities.filter(vuln =>
|
|
185
|
+
Bun.semver.satisfies(pkg.version, vuln.versionRange),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
this.setCache(cacheKey, filtered);
|
|
189
|
+
return filtered;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async checkLicenseCompliance(pkg: PackageInfo): Promise<any[]> {
|
|
193
|
+
if (!pkg.license) {
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
package: pkg.name,
|
|
197
|
+
version: pkg.version,
|
|
198
|
+
severity: 'low',
|
|
199
|
+
description: 'Package has no specified license',
|
|
200
|
+
categories: ['license'],
|
|
201
|
+
remediation: 'Consider packages with clear licensing terms',
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const restrictedLicenses = ['GPL-3.0', 'AGPL-3.0', 'LGPL-3.0'];
|
|
207
|
+
if (pkg.license && restrictedLicenses.some(license => pkg.license!.includes(license))) {
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
package: pkg.name,
|
|
211
|
+
version: pkg.version,
|
|
212
|
+
severity: 'medium',
|
|
213
|
+
description: `Package has restricted license: ${pkg.license}`,
|
|
214
|
+
categories: ['license'],
|
|
215
|
+
remediation: 'Consider packages with more permissive licenses',
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async detectAnomalies(pkg: PackageInfo): Promise<any[]> {
|
|
224
|
+
const cacheKey = `anomalies:${pkg.name}:${pkg.version}`;
|
|
225
|
+
const cached = await this.getFromCache(cacheKey);
|
|
226
|
+
if (cached) {
|
|
227
|
+
return cached;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const anomalies = await this.performAnomalyDetection(pkg);
|
|
231
|
+
this.setCache(cacheKey, anomalies);
|
|
232
|
+
return anomalies;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async performAnomalyDetection(pkg: PackageInfo): Promise<any[]> {
|
|
236
|
+
const anomalyScore = this.calculateAnomalyScore(pkg);
|
|
237
|
+
|
|
238
|
+
if (anomalyScore > 0.7) {
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
package: pkg.name,
|
|
242
|
+
version: pkg.version,
|
|
243
|
+
anomalyScore,
|
|
244
|
+
severity: anomalyScore > 0.9 ? 'critical' : anomalyScore > 0.8 ? 'high' : 'medium',
|
|
245
|
+
description:
|
|
246
|
+
'Package exhibits unusual characteristics that may indicate malicious behavior',
|
|
247
|
+
indicators: this.getAnomalyIndicators(pkg),
|
|
248
|
+
confidence: 0.85,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private calculateAnomalyScore(pkg: PackageInfo): number {
|
|
257
|
+
let score = 0;
|
|
258
|
+
|
|
259
|
+
// Check for unusual package name patterns
|
|
260
|
+
const unusualNamePatterns = [/^[0-9]{8,}/, /[a-z]{10,}/, /[!@#$%^&*()_+]{3,}/];
|
|
261
|
+
|
|
262
|
+
for (const pattern of unusualNamePatterns) {
|
|
263
|
+
if (pattern.test(pkg.name)) {
|
|
264
|
+
score += 0.2;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for extremely short package names
|
|
269
|
+
if (pkg.name.length < 3) {
|
|
270
|
+
score += 0.15;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for packages with numeric versions only
|
|
274
|
+
if (/^[0-9.]+$/.test(pkg.version) && pkg.version.split('.').length > 4) {
|
|
275
|
+
score += 0.1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Math.min(score, 1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private getAnomalyIndicators(pkg: PackageInfo): string[] {
|
|
282
|
+
const indicators = [];
|
|
283
|
+
|
|
284
|
+
if (pkg.name.length < 3) {
|
|
285
|
+
indicators.push('Unusually short package name');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (pkg.version.split('.').length > 4) {
|
|
289
|
+
indicators.push('Unusual version format');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const unusualChars = pkg.name.match(/[!@#$%^&*()_+]/g);
|
|
293
|
+
if (unusualChars && unusualChars.length > 2) {
|
|
294
|
+
indicators.push('Suspicious characters in package name');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return indicators;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private prioritizeResults(results: any[]): any[] {
|
|
301
|
+
const uniqueResults = Array.from(new Set(results.map(result => JSON.stringify(result)))).map(
|
|
302
|
+
str => JSON.parse(str),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return uniqueResults.sort((a, b) => {
|
|
306
|
+
const severityOrder = {
|
|
307
|
+
critical: 0,
|
|
308
|
+
high: 1,
|
|
309
|
+
medium: 2,
|
|
310
|
+
low: 3,
|
|
311
|
+
warn: 4,
|
|
312
|
+
fatal: 0,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const severityA = severityOrder[a.severity as keyof typeof severityOrder] ?? 5;
|
|
316
|
+
const severityB = severityOrder[b.severity as keyof typeof severityOrder] ?? 5;
|
|
317
|
+
|
|
318
|
+
if (severityA !== severityB) {
|
|
319
|
+
return severityA - severityB;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (a.cvssScore && b.cvssScore) {
|
|
323
|
+
return b.cvssScore - a.cvssScore;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return 0;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private determineAdvisoryLevel(severity: string, categories: string[]): 'fatal' | 'warn' {
|
|
331
|
+
const fatalCategories = ['malware', 'backdoor', 'botnet'];
|
|
332
|
+
const warningCategories = ['protestware', 'adware'];
|
|
333
|
+
|
|
334
|
+
if (categories.some(cat => fatalCategories.includes(cat)) || severity === 'critical') {
|
|
335
|
+
return 'fatal';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (categories.some(cat => warningCategories.includes(cat)) || severity === 'high') {
|
|
339
|
+
return 'warn';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return 'warn';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private mapResultsToAdvisories(results: any[]): Bun.Security.Advisory[] {
|
|
346
|
+
return results.map(result => ({
|
|
347
|
+
level: this.determineAdvisoryLevel(result.severity, result.categories || []),
|
|
348
|
+
package: result.package,
|
|
349
|
+
version:
|
|
350
|
+
result.version || (result.versionRange ? 'Range: ' + result.versionRange : undefined),
|
|
351
|
+
severity: result.severity,
|
|
352
|
+
cvssScore: result.cvssScore,
|
|
353
|
+
url: result.url,
|
|
354
|
+
description: result.description,
|
|
355
|
+
remediation: result.remediation,
|
|
356
|
+
fixedVersion: result.fixedVersion,
|
|
357
|
+
references: result.references,
|
|
358
|
+
categories: result.categories,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async scan({packages}: {packages: Bun.Security.Package[]}): Promise<Bun.Security.Advisory[]> {
|
|
363
|
+
const processedPackages = this.processPackageData(packages);
|
|
364
|
+
|
|
365
|
+
const [threatIntelligenceResults, dependencyAuditResults] = await Promise.all([
|
|
366
|
+
this.fetchThreatIntelligence(processedPackages),
|
|
367
|
+
this.performDependencyAudit(processedPackages),
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
const allResults = [...threatIntelligenceResults, ...dependencyAuditResults];
|
|
371
|
+
const prioritizedResults = this.prioritizeResults(allResults);
|
|
372
|
+
const advisories = this.mapResultsToAdvisories(prioritizedResults);
|
|
373
|
+
|
|
374
|
+
return advisories;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private processPackageData(packages: Bun.Security.Package[]): PackageInfo[] {
|
|
378
|
+
return packages.map(pkg => ({
|
|
379
|
+
...pkg,
|
|
380
|
+
dependencies: [],
|
|
381
|
+
license: this.extractLicense(pkg),
|
|
382
|
+
hashes: this.extractHashes(pkg),
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private extractLicense(pkg: any): string | undefined {
|
|
387
|
+
return (pkg as any).license;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private extractHashes(pkg: any): {sha256?: string; md5?: string} | undefined {
|
|
391
|
+
return (pkg as any).hashes;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const scannerInstance = new SecurityScanner();
|
|
396
|
+
|
|
397
|
+
export const scanner: Bun.Security.Scanner = {
|
|
398
|
+
version: '1',
|
|
399
|
+
async scan(params) {
|
|
400
|
+
return await scannerInstance.scan(params);
|
|
73
401
|
},
|
|
74
402
|
};
|
|
403
|
+
|
|
404
|
+
export default scanner;
|