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,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
+ });