guardrail-security 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/dist/attack-surface/analyzer.d.ts +50 -0
- package/dist/attack-surface/analyzer.d.ts.map +1 -0
- package/dist/attack-surface/analyzer.js +83 -0
- package/dist/attack-surface/index.d.ts +5 -0
- package/dist/attack-surface/index.d.ts.map +1 -0
- package/dist/attack-surface/index.js +20 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/languages/index.d.ts +21 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +78 -0
- package/dist/languages/java-analyzer.d.ts +72 -0
- package/dist/languages/java-analyzer.d.ts.map +1 -0
- package/dist/languages/java-analyzer.js +417 -0
- package/dist/languages/python-analyzer.d.ts +70 -0
- package/dist/languages/python-analyzer.d.ts.map +1 -0
- package/dist/languages/python-analyzer.js +425 -0
- package/dist/license/compatibility-matrix.d.ts +28 -0
- package/dist/license/compatibility-matrix.d.ts.map +1 -0
- package/dist/license/compatibility-matrix.js +323 -0
- package/dist/license/engine.d.ts +77 -0
- package/dist/license/engine.d.ts.map +1 -0
- package/dist/license/engine.js +264 -0
- package/dist/license/index.d.ts +6 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/index.js +21 -0
- package/dist/sbom/generator.d.ts +108 -0
- package/dist/sbom/generator.d.ts.map +1 -0
- package/dist/sbom/generator.js +271 -0
- package/dist/sbom/index.d.ts +5 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +20 -0
- package/dist/secrets/guardian.d.ts +113 -0
- package/dist/secrets/guardian.d.ts.map +1 -0
- package/dist/secrets/guardian.js +334 -0
- package/dist/secrets/index.d.ts +10 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +30 -0
- package/dist/secrets/patterns.d.ts +42 -0
- package/dist/secrets/patterns.d.ts.map +1 -0
- package/dist/secrets/patterns.js +165 -0
- package/dist/secrets/pre-commit.d.ts +39 -0
- package/dist/secrets/pre-commit.d.ts.map +1 -0
- package/dist/secrets/pre-commit.js +127 -0
- package/dist/secrets/vault-integration.d.ts +83 -0
- package/dist/secrets/vault-integration.d.ts.map +1 -0
- package/dist/secrets/vault-integration.js +295 -0
- package/dist/secrets/vault-providers.d.ts +110 -0
- package/dist/secrets/vault-providers.d.ts.map +1 -0
- package/dist/secrets/vault-providers.js +417 -0
- package/dist/supply-chain/detector.d.ts +80 -0
- package/dist/supply-chain/detector.d.ts.map +1 -0
- package/dist/supply-chain/detector.js +168 -0
- package/dist/supply-chain/index.d.ts +11 -0
- package/dist/supply-chain/index.d.ts.map +1 -0
- package/dist/supply-chain/index.js +26 -0
- package/dist/supply-chain/malicious-db.d.ts +41 -0
- package/dist/supply-chain/malicious-db.d.ts.map +1 -0
- package/dist/supply-chain/malicious-db.js +82 -0
- package/dist/supply-chain/script-analyzer.d.ts +54 -0
- package/dist/supply-chain/script-analyzer.d.ts.map +1 -0
- package/dist/supply-chain/script-analyzer.js +160 -0
- package/dist/supply-chain/typosquat.d.ts +58 -0
- package/dist/supply-chain/typosquat.d.ts.map +1 -0
- package/dist/supply-chain/typosquat.js +257 -0
- package/dist/supply-chain/vulnerability-db.d.ts +114 -0
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -0
- package/dist/supply-chain/vulnerability-db.js +310 -0
- package/package.json +34 -0
- package/src/__tests__/license/engine.test.ts +250 -0
- package/src/__tests__/supply-chain/typosquat.test.ts +191 -0
- package/src/attack-surface/analyzer.ts +152 -0
- package/src/attack-surface/index.ts +5 -0
- package/src/index.ts +21 -0
- package/src/languages/index.ts +91 -0
- package/src/languages/java-analyzer.ts +490 -0
- package/src/languages/python-analyzer.ts +498 -0
- package/src/license/compatibility-matrix.ts +366 -0
- package/src/license/engine.ts +345 -0
- package/src/license/index.ts +6 -0
- package/src/sbom/generator.ts +355 -0
- package/src/sbom/index.ts +5 -0
- package/src/secrets/guardian.ts +448 -0
- package/src/secrets/index.ts +10 -0
- package/src/secrets/patterns.ts +186 -0
- package/src/secrets/pre-commit.ts +158 -0
- package/src/secrets/vault-integration.ts +360 -0
- package/src/secrets/vault-providers.ts +446 -0
- package/src/supply-chain/detector.ts +252 -0
- package/src/supply-chain/index.ts +11 -0
- package/src/supply-chain/malicious-db.ts +103 -0
- package/src/supply-chain/script-analyzer.ts +194 -0
- package/src/supply-chain/typosquat.ts +302 -0
- package/src/supply-chain/vulnerability-db.ts +386 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Malicious Package Database
|
|
3
|
+
*
|
|
4
|
+
* Checks packages against known malicious packages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MaliciousPackageInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
12
|
+
cve?: string;
|
|
13
|
+
reported: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Known malicious packages (this would be updated regularly from external sources)
|
|
18
|
+
*/
|
|
19
|
+
const KNOWN_MALICIOUS: MaliciousPackageInfo[] = [
|
|
20
|
+
// Example entries - in production, this would be fetched from:
|
|
21
|
+
// - npm security advisories
|
|
22
|
+
// - Snyk vulnerability database
|
|
23
|
+
// - GitHub Advisory Database
|
|
24
|
+
// - Custom threat intelligence feeds
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export class MaliciousPackageDB {
|
|
28
|
+
private maliciousPackages: Map<string, MaliciousPackageInfo[]> = new Map();
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.loadDatabase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if package is known to be malicious
|
|
36
|
+
*/
|
|
37
|
+
async checkPackage(name: string, version: string): Promise<{
|
|
38
|
+
isMalicious: boolean;
|
|
39
|
+
matches: MaliciousPackageInfo[];
|
|
40
|
+
}> {
|
|
41
|
+
const matches: MaliciousPackageInfo[] = [];
|
|
42
|
+
|
|
43
|
+
// Check exact name match
|
|
44
|
+
const nameMatches = this.maliciousPackages.get(name) || [];
|
|
45
|
+
|
|
46
|
+
for (const match of nameMatches) {
|
|
47
|
+
// If no version specified in DB, flag all versions
|
|
48
|
+
if (!match.version) {
|
|
49
|
+
matches.push(match);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check version match
|
|
54
|
+
if (match.version === version || match.version === '*') {
|
|
55
|
+
matches.push(match);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
isMalicious: matches.length > 0,
|
|
61
|
+
matches,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load malicious packages database
|
|
67
|
+
*/
|
|
68
|
+
private loadDatabase(): void {
|
|
69
|
+
for (const pkg of KNOWN_MALICIOUS) {
|
|
70
|
+
if (!this.maliciousPackages.has(pkg.name)) {
|
|
71
|
+
this.maliciousPackages.set(pkg.name, []);
|
|
72
|
+
}
|
|
73
|
+
this.maliciousPackages.get(pkg.name)!.push(pkg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update database from external sources
|
|
79
|
+
*/
|
|
80
|
+
async updateDatabase(): Promise<{ added: number; updated: number }> {
|
|
81
|
+
// In production, this would:
|
|
82
|
+
// 1. Fetch from npm security advisories API
|
|
83
|
+
// 2. Fetch from Snyk API
|
|
84
|
+
// 3. Fetch from GitHub Advisory Database
|
|
85
|
+
// 4. Merge with existing database
|
|
86
|
+
// 5. Return statistics
|
|
87
|
+
|
|
88
|
+
return { added: 0, updated: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add custom malicious package
|
|
93
|
+
*/
|
|
94
|
+
addMaliciousPackage(info: MaliciousPackageInfo): void {
|
|
95
|
+
if (!this.maliciousPackages.has(info.name)) {
|
|
96
|
+
this.maliciousPackages.set(info.name, []);
|
|
97
|
+
}
|
|
98
|
+
this.maliciousPackages.get(info.name)!.push(info);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Export singleton
|
|
103
|
+
export const maliciousPackageDB = new MaliciousPackageDB();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes package.json scripts for suspicious behavior
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ScriptAnalysisResult {
|
|
8
|
+
scriptName: string;
|
|
9
|
+
scriptContent: string;
|
|
10
|
+
isSuspicious: boolean;
|
|
11
|
+
threats: ScriptThreat[];
|
|
12
|
+
riskScore: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScriptThreat {
|
|
16
|
+
type: 'data_exfiltration' | 'crypto_mining' | 'backdoor' | 'malicious_download' | 'privilege_escalation';
|
|
17
|
+
pattern: string;
|
|
18
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ScriptAnalyzer {
|
|
23
|
+
/**
|
|
24
|
+
* Analyze package.json scripts
|
|
25
|
+
*/
|
|
26
|
+
async analyzeScripts(_packageName: string, _version: string): Promise<ScriptAnalysisResult[]> {
|
|
27
|
+
// In production, this would fetch package.json from npm registry
|
|
28
|
+
// For now, return empty array
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analyze a single script
|
|
34
|
+
*/
|
|
35
|
+
analyzeScript(scriptName: string, scriptContent: string): ScriptAnalysisResult {
|
|
36
|
+
const threats: ScriptThreat[] = [];
|
|
37
|
+
|
|
38
|
+
// Check for data exfiltration
|
|
39
|
+
if (this.detectExfiltration(scriptContent)) {
|
|
40
|
+
threats.push({
|
|
41
|
+
type: 'data_exfiltration',
|
|
42
|
+
pattern: 'network_request',
|
|
43
|
+
severity: 'high',
|
|
44
|
+
description: 'Script makes network requests that could exfiltrate data',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for crypto mining
|
|
49
|
+
if (this.detectCryptoMining(scriptContent)) {
|
|
50
|
+
threats.push({
|
|
51
|
+
type: 'crypto_mining',
|
|
52
|
+
pattern: 'crypto_miner',
|
|
53
|
+
severity: 'high',
|
|
54
|
+
description: 'Script contains crypto mining code',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for backdoors
|
|
59
|
+
if (this.detectBackdoor(scriptContent)) {
|
|
60
|
+
threats.push({
|
|
61
|
+
type: 'backdoor',
|
|
62
|
+
pattern: 'reverse_shell',
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
description: 'Script opens a backdoor or reverse shell',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for malicious downloads
|
|
69
|
+
if (this.detectMaliciousDownload(scriptContent)) {
|
|
70
|
+
threats.push({
|
|
71
|
+
type: 'malicious_download',
|
|
72
|
+
pattern: 'download_execute',
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
description: 'Script downloads and executes code',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for privilege escalation
|
|
79
|
+
if (this.detectPrivilegeEscalation(scriptContent)) {
|
|
80
|
+
threats.push({
|
|
81
|
+
type: 'privilege_escalation',
|
|
82
|
+
pattern: 'sudo_usage',
|
|
83
|
+
severity: 'high',
|
|
84
|
+
description: 'Script attempts privilege escalation',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate risk score
|
|
89
|
+
const riskScore = this.calculateRiskScore(threats);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
scriptName,
|
|
93
|
+
scriptContent,
|
|
94
|
+
isSuspicious: threats.length > 0,
|
|
95
|
+
threats,
|
|
96
|
+
riskScore,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect data exfiltration patterns
|
|
102
|
+
*/
|
|
103
|
+
detectExfiltration(script: string): boolean {
|
|
104
|
+
const patterns = [
|
|
105
|
+
/curl\s+.*\|\s*bash/i, // Pipe to bash
|
|
106
|
+
/wget\s+.*\|\s*sh/i, // Pipe to sh
|
|
107
|
+
/fetch\(['"]http/i, // HTTP requests
|
|
108
|
+
/axios\./i, // Axios requests
|
|
109
|
+
/http\.request/i, // HTTP module
|
|
110
|
+
/child_process\.exec.*curl/i, // Execute curl
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
return patterns.some((p) => p.test(script));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect crypto mining
|
|
118
|
+
*/
|
|
119
|
+
detectCryptoMining(script: string): boolean {
|
|
120
|
+
const patterns = [
|
|
121
|
+
/coinhive/i,
|
|
122
|
+
/cryptonight/i,
|
|
123
|
+
/monero/i,
|
|
124
|
+
/xmrig/i,
|
|
125
|
+
/stratum\+tcp/i,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
return patterns.some((p) => p.test(script));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect backdoor patterns
|
|
133
|
+
*/
|
|
134
|
+
private detectBackdoor(script: string): boolean {
|
|
135
|
+
const patterns = [
|
|
136
|
+
/nc\s+-l/i, // Netcat listener
|
|
137
|
+
/\/bin\/sh\s+-i/i, // Interactive shell
|
|
138
|
+
/bash\s+-i/i, // Interactive bash
|
|
139
|
+
/python.*socket/i, // Python socket
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
return patterns.some((p) => p.test(script));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Detect malicious downloads
|
|
147
|
+
*/
|
|
148
|
+
private detectMaliciousDownload(script: string): boolean {
|
|
149
|
+
const patterns = [
|
|
150
|
+
/curl.*\|\s*bash/i,
|
|
151
|
+
/wget.*&&.*chmod\s*\+x/i,
|
|
152
|
+
/download.*&&.*execute/i,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
return patterns.some((p) => p.test(script));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Detect privilege escalation
|
|
160
|
+
*/
|
|
161
|
+
private detectPrivilegeEscalation(script: string): boolean {
|
|
162
|
+
const patterns = [
|
|
163
|
+
/sudo\s+/i,
|
|
164
|
+
/su\s+-/i,
|
|
165
|
+
/chmod\s+777/i,
|
|
166
|
+
/chown\s+root/i,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return patterns.some((p) => p.test(script));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate risk score
|
|
174
|
+
*/
|
|
175
|
+
private calculateRiskScore(threats: ScriptThreat[]): number {
|
|
176
|
+
const severityScores = {
|
|
177
|
+
low: 25,
|
|
178
|
+
medium: 50,
|
|
179
|
+
high: 75,
|
|
180
|
+
critical: 100,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (threats.length === 0) return 0;
|
|
184
|
+
|
|
185
|
+
const totalScore = threats.reduce((sum, threat) => {
|
|
186
|
+
return sum + severityScores[threat.severity];
|
|
187
|
+
}, 0);
|
|
188
|
+
|
|
189
|
+
return Math.min(100, totalScore / threats.length);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Export singleton
|
|
194
|
+
export const scriptAnalyzer = new ScriptAnalyzer();
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typosquatting Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects potential typosquatting attacks against popular packages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Top 100 most popular npm packages (simplified list)
|
|
9
|
+
*/
|
|
10
|
+
const POPULAR_PACKAGES = [
|
|
11
|
+
'react', 'vue', 'angular', 'express', 'next', 'axios', 'lodash', 'webpack',
|
|
12
|
+
'typescript', 'eslint', 'prettier', 'jest', 'mocha', 'chai', 'babel',
|
|
13
|
+
'moment', 'dayjs', 'date-fns', 'redux', 'mobx', 'rxjs', 'socket.io',
|
|
14
|
+
'fastify', 'koa', 'hapi', 'nestjs', 'prisma', 'mongoose', 'sequelize',
|
|
15
|
+
'typeorm', 'knex', 'pg', 'mysql', 'redis', 'mongodb', 'sqlite3',
|
|
16
|
+
'passport', 'jsonwebtoken', 'bcrypt', 'crypto-js', 'uuid', 'nanoid',
|
|
17
|
+
'dotenv', 'config', 'yargs', 'commander', 'inquirer', 'chalk', 'ora',
|
|
18
|
+
'debug', 'winston', 'pino', 'morgan', 'cors', 'helmet', 'compression',
|
|
19
|
+
'multer', 'body-parser', 'cookie-parser', 'express-session', 'passport',
|
|
20
|
+
'nodemailer', 'sendgrid', 'twilio', 'stripe', 'aws-sdk', 'google-cloud',
|
|
21
|
+
'firebase', 'azure', 'docker', 'kubernetes', 'terraform', 'ansible',
|
|
22
|
+
'jenkins', 'gitlab', 'github', 'bitbucket', 'jira', 'confluence',
|
|
23
|
+
'slack', 'discord', 'telegram', 'whatsapp', 'sentry', 'datadog',
|
|
24
|
+
'newrelic', 'prometheus', 'grafana', 'elasticsearch', 'kibana', 'logstash',
|
|
25
|
+
'kafka', 'rabbitmq', 'celery', 'bull', 'agenda', 'cron', 'node-schedule',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export interface TyposquatResult {
|
|
29
|
+
isTyposquat: boolean;
|
|
30
|
+
suspiciousPackage: string;
|
|
31
|
+
targetPackage?: string;
|
|
32
|
+
similarity: number;
|
|
33
|
+
patterns: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TyposquatDetector {
|
|
37
|
+
private popularPackages: Set<string>;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.popularPackages = new Set(POPULAR_PACKAGES);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect typosquatting
|
|
45
|
+
*/
|
|
46
|
+
async detectTyposquatting(packageName: string): Promise<TyposquatResult> {
|
|
47
|
+
const patterns: string[] = [];
|
|
48
|
+
let targetPackage: string | undefined;
|
|
49
|
+
let maxSimilarity = 0;
|
|
50
|
+
|
|
51
|
+
// Check against popular packages
|
|
52
|
+
for (const popular of this.popularPackages) {
|
|
53
|
+
// Skip if exact match
|
|
54
|
+
if (packageName === popular) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check various typosquatting techniques
|
|
59
|
+
const techniques = [
|
|
60
|
+
this.checkCharacterSwap(packageName, popular),
|
|
61
|
+
this.checkMissingCharacter(packageName, popular),
|
|
62
|
+
this.checkExtraCharacter(packageName, popular),
|
|
63
|
+
this.checkHomoglyph(packageName, popular),
|
|
64
|
+
this.checkCombosquatting(packageName, popular),
|
|
65
|
+
this.checkLevenshtein(packageName, popular),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const technique of techniques) {
|
|
69
|
+
if (technique.isMatch) {
|
|
70
|
+
patterns.push(technique.pattern);
|
|
71
|
+
|
|
72
|
+
if (technique.similarity > maxSimilarity) {
|
|
73
|
+
maxSimilarity = technique.similarity;
|
|
74
|
+
targetPackage = popular;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
isTyposquat: patterns.length > 0,
|
|
82
|
+
suspiciousPackage: packageName,
|
|
83
|
+
targetPackage,
|
|
84
|
+
similarity: maxSimilarity,
|
|
85
|
+
patterns,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check for character swap (e.g., raect vs react)
|
|
91
|
+
*/
|
|
92
|
+
private checkCharacterSwap(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
93
|
+
if (Math.abs(pkg.length - popular.length) > 0) {
|
|
94
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try swapping adjacent characters
|
|
98
|
+
for (let i = 0; i < popular.length - 1; i++) {
|
|
99
|
+
const swapped = popular.substring(0, i) +
|
|
100
|
+
popular[i + 1] +
|
|
101
|
+
popular[i] +
|
|
102
|
+
popular.substring(i + 2);
|
|
103
|
+
|
|
104
|
+
if (swapped === pkg) {
|
|
105
|
+
return {
|
|
106
|
+
isMatch: true,
|
|
107
|
+
pattern: 'character_swap',
|
|
108
|
+
similarity: 0.95,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check for missing character (e.g., reat vs react)
|
|
118
|
+
*/
|
|
119
|
+
private checkMissingCharacter(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
120
|
+
if (pkg.length !== popular.length - 1) {
|
|
121
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try removing each character
|
|
125
|
+
for (let i = 0; i < popular.length; i++) {
|
|
126
|
+
const removed = popular.substring(0, i) + popular.substring(i + 1);
|
|
127
|
+
|
|
128
|
+
if (removed === pkg) {
|
|
129
|
+
return {
|
|
130
|
+
isMatch: true,
|
|
131
|
+
pattern: 'missing_character',
|
|
132
|
+
similarity: 0.9,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check for extra character (e.g., reactt vs react)
|
|
142
|
+
*/
|
|
143
|
+
private checkExtraCharacter(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
144
|
+
if (pkg.length !== popular.length + 1) {
|
|
145
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Try removing each character from pkg
|
|
149
|
+
for (let i = 0; i < pkg.length; i++) {
|
|
150
|
+
const removed = pkg.substring(0, i) + pkg.substring(i + 1);
|
|
151
|
+
|
|
152
|
+
if (removed === popular) {
|
|
153
|
+
return {
|
|
154
|
+
isMatch: true,
|
|
155
|
+
pattern: 'extra_character',
|
|
156
|
+
similarity: 0.9,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check for homoglyph substitution (e.g., react with Cyrillic 'а')
|
|
166
|
+
*/
|
|
167
|
+
private checkHomoglyph(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
168
|
+
// Common homoglyphs
|
|
169
|
+
const homoglyphs: Record<string, string[]> = {
|
|
170
|
+
'a': ['а', 'ɑ', 'α'], // Cyrillic/Greek a
|
|
171
|
+
'e': ['е', ' е'], // Cyrillic e
|
|
172
|
+
'o': ['о', 'ο', '0'], // Cyrillic/Greek o, zero
|
|
173
|
+
'i': ['і', 'ı', 'l', '1'], // Cyrillic i, Turkish i, l, one
|
|
174
|
+
'c': ['с', 'ϲ'], // Cyrillic c
|
|
175
|
+
'p': ['р'], // Cyrillic p
|
|
176
|
+
'x': ['х', 'χ'], // Cyrillic/Greek x
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Normalize both strings
|
|
180
|
+
const normalize = (str: string): string => {
|
|
181
|
+
let normalized = str;
|
|
182
|
+
for (const [latin, alternates] of Object.entries(homoglyphs)) {
|
|
183
|
+
for (const alt of alternates) {
|
|
184
|
+
normalized = normalized.replace(new RegExp(alt, 'g'), latin);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const normalizedPkg = normalize(pkg);
|
|
191
|
+
|
|
192
|
+
if (normalizedPkg === popular && normalizedPkg !== pkg) {
|
|
193
|
+
return {
|
|
194
|
+
isMatch: true,
|
|
195
|
+
pattern: 'homoglyph',
|
|
196
|
+
similarity: 0.95,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check for combosquatting (e.g., react-native-safe vs react)
|
|
205
|
+
*/
|
|
206
|
+
private checkCombosquatting(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
207
|
+
if (pkg.includes(popular) && pkg !== popular) {
|
|
208
|
+
// Check if it's just adding common suffixes/prefixes
|
|
209
|
+
const commonAdditions = ['-js', '-node', '-utils', '-core', '-plugin', '-webpack', '-babel'];
|
|
210
|
+
|
|
211
|
+
for (const addition of commonAdditions) {
|
|
212
|
+
if (pkg === popular + addition || pkg === addition + popular) {
|
|
213
|
+
return {
|
|
214
|
+
isMatch: true,
|
|
215
|
+
pattern: 'combosquatting',
|
|
216
|
+
similarity: 0.7,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check Levenshtein distance
|
|
227
|
+
*/
|
|
228
|
+
private checkLevenshtein(pkg: string, popular: string): { isMatch: boolean; pattern: string; similarity: number } {
|
|
229
|
+
const distance = this.levenshteinDistance(pkg, popular);
|
|
230
|
+
const maxLength = Math.max(pkg.length, popular.length);
|
|
231
|
+
const similarity = 1 - (distance / maxLength);
|
|
232
|
+
|
|
233
|
+
// Consider it suspicious if similarity > 0.8
|
|
234
|
+
if (similarity >= 0.8 && similarity < 1.0) {
|
|
235
|
+
return {
|
|
236
|
+
isMatch: true,
|
|
237
|
+
pattern: 'levenshtein_distance',
|
|
238
|
+
similarity,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { isMatch: false, pattern: '', similarity: 0 };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Calculate Levenshtein distance
|
|
247
|
+
*/
|
|
248
|
+
levenshteinDistance(a: string, b: string): number {
|
|
249
|
+
const matrix: number[][] = [];
|
|
250
|
+
|
|
251
|
+
// Initialize matrix with proper dimensions
|
|
252
|
+
for (let i = 0; i <= b.length; i++) {
|
|
253
|
+
matrix[i] = [];
|
|
254
|
+
for (let j = 0; j <= a.length; j++) {
|
|
255
|
+
matrix[i]![j] = 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Set first column
|
|
260
|
+
for (let i = 0; i <= b.length; i++) {
|
|
261
|
+
matrix[i]![0] = i;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Set first row
|
|
265
|
+
for (let j = 0; j <= a.length; j++) {
|
|
266
|
+
matrix[0]![j] = j;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (let i = 1; i <= b.length; i++) {
|
|
270
|
+
for (let j = 1; j <= a.length; j++) {
|
|
271
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
272
|
+
matrix[i]![j] = matrix[i - 1]![j - 1]!;
|
|
273
|
+
} else {
|
|
274
|
+
matrix[i]![j] = Math.min(
|
|
275
|
+
matrix[i - 1]![j - 1]! + 1, // substitution
|
|
276
|
+
matrix[i]![j - 1]! + 1, // insertion
|
|
277
|
+
matrix[i - 1]![j]! + 1 // deletion
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return matrix[b.length]![a.length]!;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get popular packages list
|
|
288
|
+
*/
|
|
289
|
+
async getPopularPackages(): Promise<string[]> {
|
|
290
|
+
return Array.from(this.popularPackages);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add custom popular package
|
|
295
|
+
*/
|
|
296
|
+
addPopularPackage(packageName: string): void {
|
|
297
|
+
this.popularPackages.add(packageName);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Export singleton
|
|
302
|
+
export const typosquatDetector = new TyposquatDetector();
|