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.
- package/.eslintrc.json +32 -0
- package/.github/CODEOWNERS +3 -0
- package/.github/workflows/ci.yml +105 -0
- package/.prettierrc +10 -0
- package/FUNDING.yml +1 -0
- package/PLAN.md +151 -0
- package/README.md +150 -0
- package/bin/npm-scan +13 -0
- package/bin/npm-scan-wrap +100 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +299 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/lib/blocklist.d.ts +45 -0
- package/dist/lib/blocklist.d.ts.map +1 -0
- package/dist/lib/blocklist.js +256 -0
- package/dist/lib/blocklist.js.map +1 -0
- package/dist/lib/extended.js +314 -0
- package/dist/lib/extended.js.map +1 -0
- package/dist/lib/integrity.js +247 -0
- package/dist/lib/integrity.js.map +1 -0
- package/dist/lib/patterns.d.ts +76 -0
- package/dist/lib/patterns.d.ts.map +1 -0
- package/dist/lib/patterns.js +414 -0
- package/dist/lib/patterns.js.map +1 -0
- package/dist/lib/registry.d.ts +42 -0
- package/dist/lib/registry.d.ts.map +1 -0
- package/dist/lib/registry.js +157 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/scanner.d.ts +43 -0
- package/dist/lib/scanner.d.ts.map +1 -0
- package/dist/lib/scanner.js +432 -0
- package/dist/lib/scanner.js.map +1 -0
- package/dist/lib/vuln.js +284 -0
- package/dist/lib/vuln.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/jest.config.js +18 -0
- package/package.json +56 -0
- package/src/cli/index.ts +336 -0
- package/src/lib/blocklist.ts +239 -0
- package/src/lib/extended.ts +384 -0
- package/src/lib/integrity.ts +253 -0
- package/src/lib/patterns.ts +404 -0
- package/src/lib/registry.ts +146 -0
- package/src/lib/scanner.ts +447 -0
- package/src/lib/vuln.ts +321 -0
- package/src/types.ts +102 -0
- package/tests/blocklist.test.ts +89 -0
- package/tests/extended.test.ts +204 -0
- package/tests/patterns.test.ts +147 -0
- package/tests/scanner.test.ts +116 -0
- package/tests/vuln.test.ts +66 -0
- package/tsconfig.json +20 -0
package/src/lib/vuln.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vulnerability databases API integration
|
|
3
|
+
* OSV, GitHub Advisory, npm Audit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const OSV_API = 'https://api.osv.dev/v1/query';
|
|
7
|
+
const GITHUB_ADVISORIES_API = 'https://api.github.com/advisories';
|
|
8
|
+
const NPM_AUDIT_API = 'https://registry.npmjs.org/-/npm/v1/security/advisors';
|
|
9
|
+
|
|
10
|
+
export interface Vulnerability {
|
|
11
|
+
id: string;
|
|
12
|
+
source: 'osv' | 'github' | 'npm';
|
|
13
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
14
|
+
summary?: string;
|
|
15
|
+
details?: string;
|
|
16
|
+
aliases?: string[];
|
|
17
|
+
patched_versions?: string;
|
|
18
|
+
affected_versions?: string;
|
|
19
|
+
references?: string[];
|
|
20
|
+
published?: string;
|
|
21
|
+
modified?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VulnerabilityResult {
|
|
25
|
+
packageName: string;
|
|
26
|
+
vulnerabilities: Vulnerability[];
|
|
27
|
+
checkedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* OSV (Open Source Vulnerabilities) API client
|
|
32
|
+
* Free database maintained by Google
|
|
33
|
+
*/
|
|
34
|
+
export class OSVClient {
|
|
35
|
+
private apiUrl = OSV_API;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Query OSV for vulnerabilities in a package
|
|
39
|
+
*/
|
|
40
|
+
async checkPackage(packageName: string, version?: string): Promise<Vulnerability[]> {
|
|
41
|
+
const query: any = {
|
|
42
|
+
package: {
|
|
43
|
+
name: packageName,
|
|
44
|
+
ecosystem: 'npm'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (version) {
|
|
49
|
+
query.version = version;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(this.apiUrl, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json'
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(query)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data: any = await response.json();
|
|
66
|
+
const vulns = data.vulns || [];
|
|
67
|
+
|
|
68
|
+
return vulns.map(v => ({
|
|
69
|
+
id: v.id,
|
|
70
|
+
source: 'osv' as const,
|
|
71
|
+
severity: this.mapSeverity(v.severity),
|
|
72
|
+
summary: v.summary,
|
|
73
|
+
details: v.details,
|
|
74
|
+
aliases: v.aliases,
|
|
75
|
+
patched_versions: this.getPatchedVersions(v.affected),
|
|
76
|
+
affected_versions: this.getAffectedVersions(v.affected),
|
|
77
|
+
references: v.references?.map((r: any) => r.url),
|
|
78
|
+
published: v.published,
|
|
79
|
+
modified: v.modified
|
|
80
|
+
}));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('OSV API error:', error);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map OSV severity to our severity
|
|
89
|
+
*/
|
|
90
|
+
private mapSeverity(severity: any): 'low' | 'medium' | 'high' | 'critical' {
|
|
91
|
+
const s = String(severity || '').toLowerCase();
|
|
92
|
+
if (s === 'critical') return 'critical';
|
|
93
|
+
if (s === 'high') return 'high';
|
|
94
|
+
if (s === 'medium') return 'medium';
|
|
95
|
+
return 'low';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract patched versions from OSV affected field
|
|
100
|
+
*/
|
|
101
|
+
private getPatchedVersions(affected: any[]): string | undefined {
|
|
102
|
+
if (!affected || affected.length === 0) return undefined;
|
|
103
|
+
|
|
104
|
+
for (const a of affected) {
|
|
105
|
+
if (a.versions && a.ranges) {
|
|
106
|
+
for (const range of a.ranges) {
|
|
107
|
+
if (range.type === 'semver' && range.events) {
|
|
108
|
+
for (const event of range.events) {
|
|
109
|
+
if (event.introduced || event.fixed) {
|
|
110
|
+
return event.fixed || '<' + event.introduced;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get affected versions string
|
|
122
|
+
*/
|
|
123
|
+
private getAffectedVersions(affected: any[]): string | undefined {
|
|
124
|
+
if (!affected || affected.length === 0) return undefined;
|
|
125
|
+
const versions = affected.map(a => a.package?.version).filter(Boolean);
|
|
126
|
+
return versions.length > 0 ? versions.join(', ') : undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* GitHub Advisory Database API client
|
|
132
|
+
* Requires authentication for higher rate limits
|
|
133
|
+
*/
|
|
134
|
+
export class GitHubAdvisoryClient {
|
|
135
|
+
private token: string | undefined;
|
|
136
|
+
private apiUrl = GITHUB_ADVISORIES_API;
|
|
137
|
+
|
|
138
|
+
constructor(token?: string) {
|
|
139
|
+
this.token = token || process.env.GITHUB_TOKEN;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Fetch advisories for a specific package
|
|
144
|
+
*/
|
|
145
|
+
async checkPackage(packageName: string): Promise<Vulnerability[]> {
|
|
146
|
+
// GitHub advisories are fetched from the ecosystem-wide list
|
|
147
|
+
// We filter by package name client-side
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(
|
|
150
|
+
`${this.apiUrl}?ecosystem=npm&package=${packageName}`,
|
|
151
|
+
{
|
|
152
|
+
headers: this.getHeaders()
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
if (response.status === 403) {
|
|
158
|
+
console.warn('GitHub API rate limited. Set GITHUB_TOKEN for higher limits.');
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const data: any = await response.json();
|
|
164
|
+
const advisories = data.advisories || [];
|
|
165
|
+
|
|
166
|
+
return advisories.map((a: any) => ({
|
|
167
|
+
id: a.ghsa_id || a.cve_id,
|
|
168
|
+
source: 'github' as const,
|
|
169
|
+
severity: this.mapSeverity(a.severity),
|
|
170
|
+
summary: a.summary,
|
|
171
|
+
details: a.description,
|
|
172
|
+
aliases: a.cve_id ? [a.cve_id] : [],
|
|
173
|
+
patched_versions: a.vulnerabilities?.[0]?.patched_versions,
|
|
174
|
+
affected_versions: a.vulnerabilities?.[0]?.vulnerable_version_range,
|
|
175
|
+
references: a.references?.map((r: any) => r.url),
|
|
176
|
+
published: a.published_at,
|
|
177
|
+
modified: a.updated_at
|
|
178
|
+
}));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('GitHub Advisory API error:', error);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Map GitHub severity to our severity
|
|
187
|
+
*/
|
|
188
|
+
private mapSeverity(severity: any): 'low' | 'medium' | 'high' | 'critical' {
|
|
189
|
+
const s = String(severity || '').toLowerCase();
|
|
190
|
+
if (s === 'critical') return 'critical';
|
|
191
|
+
if (s === 'high') return 'high';
|
|
192
|
+
if (s === 'medium') return 'medium';
|
|
193
|
+
return 'low';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get HTTP headers
|
|
198
|
+
*/
|
|
199
|
+
private getHeaders(): Record<string, string> {
|
|
200
|
+
const headers: Record<string, string> = {
|
|
201
|
+
'Accept': 'application/vnd.github+json',
|
|
202
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
203
|
+
};
|
|
204
|
+
if (this.token) {
|
|
205
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
206
|
+
}
|
|
207
|
+
return headers;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* npm Audit API client
|
|
213
|
+
* Official npm security advisories
|
|
214
|
+
*/
|
|
215
|
+
export class NpmAuditClient {
|
|
216
|
+
private apiUrl = NPM_AUDIT_API;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Fetch advisories from npm
|
|
220
|
+
*/
|
|
221
|
+
async checkPackage(packageName: string): Promise<Vulnerability[]> {
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(
|
|
224
|
+
`${this.apiUrl}?package=${packageName}`,
|
|
225
|
+
{
|
|
226
|
+
headers: {
|
|
227
|
+
'Accept': 'application/json'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const data: any = await response.json();
|
|
237
|
+
const advisories = data.data || [];
|
|
238
|
+
|
|
239
|
+
return advisories.map((a: any) => ({
|
|
240
|
+
id: a.id,
|
|
241
|
+
source: 'npm' as const,
|
|
242
|
+
severity: this.mapSeverity(a.severity),
|
|
243
|
+
summary: a.title,
|
|
244
|
+
details: a.url,
|
|
245
|
+
patched_versions: a.patched_versions,
|
|
246
|
+
affected_versions: a.vulnerable_versions,
|
|
247
|
+
references: [a.url].filter(Boolean),
|
|
248
|
+
published: a.published,
|
|
249
|
+
modified: a.modified
|
|
250
|
+
}));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('npm Audit API error:', error);
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Map npm severity to our severity
|
|
259
|
+
*/
|
|
260
|
+
private mapSeverity(severity: any): 'low' | 'medium' | 'high' | 'critical' {
|
|
261
|
+
const s = String(severity || '').toLowerCase();
|
|
262
|
+
if (s === 'critical') return 'critical';
|
|
263
|
+
if (s === 'high') return 'high';
|
|
264
|
+
if (s === 'medium') return 'medium';
|
|
265
|
+
return 'low';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Combined vulnerability checker
|
|
271
|
+
* Queries all three sources
|
|
272
|
+
*/
|
|
273
|
+
export class VulnerabilityChecker {
|
|
274
|
+
private osv: OSVClient;
|
|
275
|
+
private github: GitHubAdvisoryClient;
|
|
276
|
+
private npmAudit: NpmAuditClient;
|
|
277
|
+
|
|
278
|
+
constructor(githubToken?: string) {
|
|
279
|
+
this.osv = new OSVClient();
|
|
280
|
+
this.github = new GitHubAdvisoryClient(githubToken);
|
|
281
|
+
this.npmAudit = new NpmAuditClient();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check a package against all vulnerability databases
|
|
286
|
+
*/
|
|
287
|
+
async checkPackage(packageName: string, version?: string): Promise<VulnerabilityResult> {
|
|
288
|
+
const [osvVulns, githubVulns, npmVulns] = await Promise.all([
|
|
289
|
+
this.osv.checkPackage(packageName, version).catch(() => []),
|
|
290
|
+
this.github.checkPackage(packageName).catch(() => []),
|
|
291
|
+
this.npmAudit.checkPackage(packageName).catch(() => [])
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
// Deduplicate by CVE ID
|
|
295
|
+
const seen = new Set<string>();
|
|
296
|
+
const combined: Vulnerability[] = [];
|
|
297
|
+
|
|
298
|
+
for (const v of [...osvVulns, ...githubVulns, ...npmVulns]) {
|
|
299
|
+
const id = v.id || v.aliases?.[0];
|
|
300
|
+
if (id && !seen.has(id)) {
|
|
301
|
+
seen.add(id);
|
|
302
|
+
combined.push(v);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
packageName,
|
|
308
|
+
vulnerabilities: combined,
|
|
309
|
+
checkedAt: new Date().toISOString()
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Export factory functions
|
|
315
|
+
export function createVulnerabilityChecker(token?: string): VulnerabilityChecker {
|
|
316
|
+
return new VulnerabilityChecker(token);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const osvClient = new OSVClient();
|
|
320
|
+
export const githubAdvisoryClient = new GitHubAdvisoryClient();
|
|
321
|
+
export const npmAuditClient = new NpmAuditClient();
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm-scan type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PackageMetadata {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
publisher?: {
|
|
10
|
+
username: string;
|
|
11
|
+
email: string;
|
|
12
|
+
};
|
|
13
|
+
maintainers?: Array<{
|
|
14
|
+
username: string;
|
|
15
|
+
email: string;
|
|
16
|
+
}>;
|
|
17
|
+
repository?: {
|
|
18
|
+
type: string;
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
homepage?: string;
|
|
22
|
+
license?: string;
|
|
23
|
+
dependencies?: Record<string, string>;
|
|
24
|
+
devDependencies?: Record<string, string>;
|
|
25
|
+
peerDependencies?: Record<string, string>;
|
|
26
|
+
scripts?: Record<string, string>;
|
|
27
|
+
latest?: string;
|
|
28
|
+
time?: {
|
|
29
|
+
created: string;
|
|
30
|
+
modified: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ScanResult {
|
|
35
|
+
packageName: string;
|
|
36
|
+
version: string;
|
|
37
|
+
status: 'safe' | 'warning' | 'danger' | 'blocked';
|
|
38
|
+
threats: Threat[];
|
|
39
|
+
metadata?: PackageMetadata;
|
|
40
|
+
score: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Threat {
|
|
44
|
+
type: ThreatType;
|
|
45
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
46
|
+
message: string;
|
|
47
|
+
details?: string;
|
|
48
|
+
location?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ThreatType =
|
|
52
|
+
| 'blocklisted'
|
|
53
|
+
| 'vulnerability'
|
|
54
|
+
| 'obfuscation'
|
|
55
|
+
| 'suspicious_code'
|
|
56
|
+
| 'typosquatting'
|
|
57
|
+
| 'supply_chain'
|
|
58
|
+
| 'suspicious_script'
|
|
59
|
+
| 'unknown_publisher'
|
|
60
|
+
| 'dependency_confusion';
|
|
61
|
+
|
|
62
|
+
export interface BlocklistEntry {
|
|
63
|
+
package: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
addedAt: string;
|
|
66
|
+
severity: 'high' | 'critical';
|
|
67
|
+
source?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ScanOptions {
|
|
71
|
+
registry?: string;
|
|
72
|
+
blocklist?: string[];
|
|
73
|
+
checkVulnerabilities?: boolean;
|
|
74
|
+
checkObfuscation?: boolean;
|
|
75
|
+
checkPatterns?: boolean;
|
|
76
|
+
timeout?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PostInstallScanResult {
|
|
80
|
+
scannedPackages: number;
|
|
81
|
+
threats: FileThreat[];
|
|
82
|
+
duration: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface FileThreat {
|
|
86
|
+
package: string;
|
|
87
|
+
file: string;
|
|
88
|
+
type: ThreatType;
|
|
89
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
90
|
+
message: string;
|
|
91
|
+
line?: number;
|
|
92
|
+
code?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CliOptions {
|
|
96
|
+
command: 'pre' | 'post' | 'scan' | 'blocklist';
|
|
97
|
+
subcommand?: 'add' | 'remove' | 'list';
|
|
98
|
+
package?: string;
|
|
99
|
+
version?: string;
|
|
100
|
+
folder?: string;
|
|
101
|
+
verbose?: boolean;
|
|
102
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for blocklist module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { blocklistManager, TYPOSQUATTING_PATTERNS } from '../src/lib/blocklist';
|
|
6
|
+
|
|
7
|
+
describe('BlocklistManager', () => {
|
|
8
|
+
describe('isBlocklisted', () => {
|
|
9
|
+
it('should return blocklist entry for known malicious package', () => {
|
|
10
|
+
const result = blocklistManager.isBlocklisted('event-stream');
|
|
11
|
+
expect(result).not.toBeNull();
|
|
12
|
+
expect(result?.package).toBe('event-stream');
|
|
13
|
+
expect(result?.severity).toBe('critical');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return null for safe package', () => {
|
|
17
|
+
const result = blocklistManager.isBlocklisted('lodash');
|
|
18
|
+
expect(result).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should be case-insensitive', () => {
|
|
22
|
+
const result = blocklistManager.isBlocklisted('EVENT-STREAM');
|
|
23
|
+
expect(result).not.toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should work for flatmap-stream', () => {
|
|
27
|
+
const result = blocklistManager.isBlocklisted('flatmap-stream');
|
|
28
|
+
expect(result).not.toBeNull();
|
|
29
|
+
expect(result?.severity).toBe('critical');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('detectTyposquatting', () => {
|
|
34
|
+
it('should detect typosquatting variations', () => {
|
|
35
|
+
// Test a known typosquat
|
|
36
|
+
const result = blocklistManager.detectTyposquatting('lodsh');
|
|
37
|
+
expect(result).toContain('lodash');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return empty array for legitimate packages', () => {
|
|
41
|
+
const result = blocklistManager.detectTyposquatting('axios');
|
|
42
|
+
expect(result).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('addToBlocklist / removeFromBlocklist', () => {
|
|
47
|
+
it('should add package to user blocklist', () => {
|
|
48
|
+
const testPackage = 'test-malicious-' + Date.now();
|
|
49
|
+
blocklistManager.addToBlocklist(testPackage, 'Test reason', 'high');
|
|
50
|
+
const result = blocklistManager.isBlocklisted(testPackage);
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
expect(result?.reason).toBe('Test reason');
|
|
53
|
+
|
|
54
|
+
// Cleanup
|
|
55
|
+
blocklistManager.removeFromBlocklist(testPackage);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should remove package from blocklist', () => {
|
|
59
|
+
const testPackage = 'test-remove-' + Date.now();
|
|
60
|
+
blocklistManager.addToBlocklist(testPackage, 'Test', 'high');
|
|
61
|
+
blocklistManager.removeFromBlocklist(testPackage);
|
|
62
|
+
const result = blocklistManager.isBlocklisted(testPackage);
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getBlocklist', () => {
|
|
68
|
+
it('should return array of blocked packages', () => {
|
|
69
|
+
const list = blocklistManager.getBlocklist();
|
|
70
|
+
expect(Array.isArray(list)).toBe(true);
|
|
71
|
+
expect(list.length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should include known malicious packages', () => {
|
|
75
|
+
const list = blocklistManager.getBlocklist();
|
|
76
|
+
const names = list.map(e => e.package);
|
|
77
|
+
expect(names).toContain('event-stream');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('TYPOSQUATTING_PATTERNS', () => {
|
|
83
|
+
it('should include common packages', () => {
|
|
84
|
+
const patterns = TYPOSQUATTING_PATTERNS.map(p => p.pattern);
|
|
85
|
+
expect(patterns).toContain('lodash');
|
|
86
|
+
expect(patterns).toContain('axios');
|
|
87
|
+
expect(patterns).toContain('express');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for extended analysis module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
analyzeLicense,
|
|
7
|
+
checkMaintainerTrust,
|
|
8
|
+
validateRepository,
|
|
9
|
+
analyzeDependencies,
|
|
10
|
+
analyzeFileStructure
|
|
11
|
+
} from '../src/lib/extended';
|
|
12
|
+
|
|
13
|
+
describe('Extended Analysis', () => {
|
|
14
|
+
describe('analyzeLicense', () => {
|
|
15
|
+
it('should identify MIT as low risk', () => {
|
|
16
|
+
const result = analyzeLicense('MIT');
|
|
17
|
+
expect(result.risk).toBe('low');
|
|
18
|
+
expect(result.permissive).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should identify Apache-2.0 as low risk', () => {
|
|
22
|
+
const result = analyzeLicense('Apache-2.0');
|
|
23
|
+
expect(result.risk).toBe('low');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should identify BSD as low risk', () => {
|
|
27
|
+
const result = analyzeLicense('BSD');
|
|
28
|
+
expect(result.risk).toBe('low');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should identify ISC as low risk', () => {
|
|
32
|
+
const result = analyzeLicense('ISC');
|
|
33
|
+
expect(result.risk).toBe('low');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should identify GPL-3.0 as high risk', () => {
|
|
37
|
+
const result = analyzeLicense('GPL-3.0');
|
|
38
|
+
expect(result.risk).toBe('high');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should identify AGPL-3.0 as high risk', () => {
|
|
42
|
+
const result = analyzeLicense('AGPL-3.0');
|
|
43
|
+
expect(result.risk).toBe('high');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should identify missing license as high risk', () => {
|
|
47
|
+
const result = analyzeLicense(undefined);
|
|
48
|
+
expect(result.risk).toBe('high');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle no license string', () => {
|
|
52
|
+
const result = analyzeLicense(undefined);
|
|
53
|
+
expect(result.risk).toBe('high');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should identify custom licenses', () => {
|
|
57
|
+
const result = analyzeLicense('CUSTOM');
|
|
58
|
+
expect(result.risk).toBe('medium');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle OR patterns', () => {
|
|
62
|
+
const result = analyzeLicense('MIT OR Apache-2.0');
|
|
63
|
+
expect(result.risk).toBe('medium');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('checkMaintainerTrust', () => {
|
|
68
|
+
it('should recognize known maintainers', () => {
|
|
69
|
+
const maintainers = [{ username: 'ljharb' }];
|
|
70
|
+
const result = checkMaintainerTrust(maintainers, undefined);
|
|
71
|
+
expect(result.isTrusted).toBe(true);
|
|
72
|
+
expect(result.score).toBe(100);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should recognize jdalton as trusted', () => {
|
|
76
|
+
const maintainers = [{ username: 'jdalton' }];
|
|
77
|
+
const result = checkMaintainerTrust(maintainers, undefined);
|
|
78
|
+
expect(result.isTrusted).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should recognize org maintainers', () => {
|
|
82
|
+
const maintainers = [{ username: 'google' }];
|
|
83
|
+
const result = checkMaintainerTrust(maintainers, undefined);
|
|
84
|
+
expect(result.isTrusted).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle empty maintainers', () => {
|
|
88
|
+
const result = checkMaintainerTrust([], undefined);
|
|
89
|
+
expect(result.isTrusted).toBe(false);
|
|
90
|
+
expect(result.score).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle unknown maintainers', () => {
|
|
94
|
+
const maintainers = [{ username: 'unknownuser123' }];
|
|
95
|
+
const result = checkMaintainerTrust(maintainers, undefined);
|
|
96
|
+
expect(result.isTrusted).toBe(false);
|
|
97
|
+
expect(result.score).toBeLessThan(50);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle publisher', () => {
|
|
101
|
+
const publisher = { username: 'ljharb' };
|
|
102
|
+
const result = checkMaintainerTrust([], publisher);
|
|
103
|
+
expect(result.isTrusted).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle mixed trust levels', () => {
|
|
107
|
+
const maintainers = [{ username: 'ljharb' }, { username: 'unknown' }];
|
|
108
|
+
const result = checkMaintainerTrust(maintainers, undefined);
|
|
109
|
+
expect(result.isTrusted).toBe(false);
|
|
110
|
+
expect(result.score).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('validateRepository', () => {
|
|
115
|
+
it('should accept valid GitHub repo', async () => {
|
|
116
|
+
const result = await validateRepository(
|
|
117
|
+
'https://github.com/lodash/lodash',
|
|
118
|
+
'lodash'
|
|
119
|
+
);
|
|
120
|
+
expect(result.valid).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should reject missing repo', async () => {
|
|
124
|
+
const result = await validateRepository(undefined, 'lodash');
|
|
125
|
+
expect(result.valid).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle shorthand github:', async () => {
|
|
129
|
+
const result = await validateRepository(
|
|
130
|
+
'github:lodash/lodash',
|
|
131
|
+
'lodash'
|
|
132
|
+
);
|
|
133
|
+
// Shorthand converts to https://lodash/lodash
|
|
134
|
+
expect(result.details).toContain('https://');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle bitbucket shorthand', async () => {
|
|
138
|
+
const result = await validateRepository(
|
|
139
|
+
'bitbucket:owner/repo',
|
|
140
|
+
'repo'
|
|
141
|
+
);
|
|
142
|
+
expect(result.details).toContain('bitbucket.org');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('analyzeDependencies', () => {
|
|
147
|
+
it('should flag deprecated packages', () => {
|
|
148
|
+
const deps = { request: '^2.88.0' };
|
|
149
|
+
const result = analyzeDependencies(deps, undefined);
|
|
150
|
+
expect(result.outdatedCount).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should flag moment as deprecated', () => {
|
|
154
|
+
const deps = { moment: '^2.29.0' };
|
|
155
|
+
const result = analyzeDependencies(deps, undefined);
|
|
156
|
+
expect(result.outdatedCount).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should flag old versions', () => {
|
|
160
|
+
const deps = { lodash: '^0.1.0' };
|
|
161
|
+
const result = analyzeDependencies(deps, undefined);
|
|
162
|
+
expect(result.outdatedCount).toBeGreaterThan(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle empty dependencies', () => {
|
|
166
|
+
const result = analyzeDependencies(undefined, undefined);
|
|
167
|
+
expect(result.outdatedCount).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle modern packages', () => {
|
|
171
|
+
const deps = { axios: '^1.0.0', express: '^4.18.0' };
|
|
172
|
+
const result = analyzeDependencies(deps, undefined);
|
|
173
|
+
expect(result.outdatedCount).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should include devDependencies', () => {
|
|
177
|
+
const deps = {};
|
|
178
|
+
const devDeps = { jest: '^29.0.0' };
|
|
179
|
+
const result = analyzeDependencies(deps, devDeps);
|
|
180
|
+
// jest shouldn't be flagged
|
|
181
|
+
expect(result.outdatedCount).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('analyzeFileStructure', () => {
|
|
186
|
+
it('should detect suspicious paths', () => {
|
|
187
|
+
const files = ['../../etc/passwd', '/home/user/.ssh/id_rsa'];
|
|
188
|
+
const result = analyzeFileStructure(files);
|
|
189
|
+
expect(result.suspicious).toBe(true);
|
|
190
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should allow normal paths', () => {
|
|
194
|
+
const files = ['index.js', 'lib/utils.js', 'package.json'];
|
|
195
|
+
const result = analyzeFileStructure(files);
|
|
196
|
+
expect(result.suspicious).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle empty file list', () => {
|
|
200
|
+
const result = analyzeFileStructure([]);
|
|
201
|
+
expect(result.suspicious).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|