@flagrix/scanner-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/github/api-error.d.ts +8 -0
  4. package/dist/github/api-error.js +36 -0
  5. package/dist/github/api-error.js.map +1 -0
  6. package/dist/github/repo-scanner.d.ts +15 -0
  7. package/dist/github/repo-scanner.js +1017 -0
  8. package/dist/github/repo-scanner.js.map +1 -0
  9. package/dist/github/user-profile-ruleset.d.ts +12 -0
  10. package/dist/github/user-profile-ruleset.js +155 -0
  11. package/dist/github/user-profile-ruleset.js.map +1 -0
  12. package/dist/github/user-scanner.d.ts +11 -0
  13. package/dist/github/user-scanner.js +141 -0
  14. package/dist/github/user-scanner.js.map +1 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +13 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/linkedin/profile-scorer.d.ts +8 -0
  19. package/dist/linkedin/profile-scorer.js +115 -0
  20. package/dist/linkedin/profile-scorer.js.map +1 -0
  21. package/dist/pdf/pdf-scanner.d.ts +36 -0
  22. package/dist/pdf/pdf-scanner.js +148 -0
  23. package/dist/pdf/pdf-scanner.js.map +1 -0
  24. package/dist/rules/rule-matcher.d.ts +3 -0
  25. package/dist/rules/rule-matcher.js +51 -0
  26. package/dist/rules/rule-matcher.js.map +1 -0
  27. package/dist/types/index.d.ts +234 -0
  28. package/dist/types/index.js +17 -0
  29. package/dist/types/index.js.map +1 -0
  30. package/dist/utils/evidence.d.ts +8 -0
  31. package/dist/utils/evidence.js +28 -0
  32. package/dist/utils/evidence.js.map +1 -0
  33. package/dist/utils/risk-calculator.d.ts +11 -0
  34. package/dist/utils/risk-calculator.js +35 -0
  35. package/dist/utils/risk-calculator.js.map +1 -0
  36. package/package.json +54 -0
@@ -0,0 +1,1017 @@
1
+ /**
2
+ * GitHub Repository Scanner
3
+ *
4
+ * Scans GitHub repositories for malware signatures, obfuscated code,
5
+ * suspicious dependencies, and postinstall scripts.
6
+ *
7
+ * Uses GitHub API to fetch repo contents without full clone.
8
+ * No chrome.storage dependency — token and signatures are passed as options.
9
+ */
10
+ import { franc } from "franc-min";
11
+ import { DEFAULT_DISCLAIMER } from "../types/index.js";
12
+ import { collectEvidence } from "../utils/evidence.js";
13
+ import { githubApiError } from "./api-error.js";
14
+ import { calculateRiskScore, getRiskLevel, getSeverityWeight } from "../utils/risk-calculator.js";
15
+ import { applyYaraRules, isTestFile } from "../rules/rule-matcher.js";
16
+ // High-risk files to always check
17
+ const PRIORITY_FILES = [
18
+ "package.json",
19
+ "package-lock.json",
20
+ "requirements.txt",
21
+ "setup.py",
22
+ "Pipfile",
23
+ ".npmrc",
24
+ ".yarnrc",
25
+ "Makefile",
26
+ ];
27
+ const SCANNABLE_EXTENSIONS = [".js", ".ts", ".py", ".sh", ".ps1", ".bat", ".cmd"];
28
+ const MAX_FILES_TO_SCAN = 50;
29
+ const NPM_DOWNLOAD_CACHE = new Map();
30
+ const NPM_CACHE_TTL_MS = 60 * 60 * 1000;
31
+ const NPM_HIGH_DOWNLOAD_THRESHOLD = 100_000;
32
+ export async function scanGitHubRepo(repo, options) {
33
+ const findings = [];
34
+ let filesScanned = 0;
35
+ let patternsMatched = 0;
36
+ let dependenciesChecked = 0;
37
+ try {
38
+ const headers = {
39
+ Accept: "application/vnd.github.v3+json",
40
+ "User-Agent": "Flagrix-Extension",
41
+ };
42
+ if (options.githubToken) {
43
+ headers["Authorization"] = `Bearer ${options.githubToken}`;
44
+ }
45
+ let branch = repo.branch;
46
+ if (!branch) {
47
+ const repoResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}`, { headers });
48
+ if (repoResponse.status === 404 && !options.githubToken) {
49
+ throw new Error("This appears to be a private repository. To scan private repos, add a GitHub personal access token in Flagrix settings.");
50
+ }
51
+ if (!repoResponse.ok) {
52
+ throw await githubApiError(repoResponse);
53
+ }
54
+ const repoData = await repoResponse.json();
55
+ branch = repoData.default_branch || "main";
56
+ }
57
+ // Pin the scan to the branch's current commit so the whole read — tree
58
+ // and every file — is one immutable snapshot. Without this, the repo can
59
+ // change between requests (or between scan and clone) while the verdict
60
+ // silently keeps referring to content nobody can see anymore (TOCTOU).
61
+ const commitResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/commits/${encodeURIComponent(branch)}`, { headers });
62
+ if (commitResponse.status === 404 && !options.githubToken) {
63
+ throw new Error("This appears to be a private repository. To scan private repos, add a GitHub personal access token in Flagrix settings.");
64
+ }
65
+ if (!commitResponse.ok) {
66
+ throw await githubApiError(commitResponse);
67
+ }
68
+ const commitSha = (await commitResponse.json()).sha;
69
+ const treeUrl = `https://api.github.com/repos/${repo.owner}/${repo.repo}/git/trees/${commitSha}?recursive=1`;
70
+ const treeResponse = await fetch(treeUrl, { headers });
71
+ if (treeResponse.status === 404 && !options.githubToken) {
72
+ throw new Error("This appears to be a private repository. To scan private repos, add a GitHub personal access token in Flagrix settings.");
73
+ }
74
+ if (!treeResponse.ok) {
75
+ throw await githubApiError(treeResponse);
76
+ }
77
+ const tree = await treeResponse.json();
78
+ const filesToScan = tree.tree
79
+ .filter((item) => item.type === "blob" &&
80
+ (PRIORITY_FILES.some((pf) => item.path.endsWith(pf)) ||
81
+ SCANNABLE_EXTENSIONS.some((ext) => item.path.endsWith(ext))))
82
+ .slice(0, MAX_FILES_TO_SCAN);
83
+ // Check for hidden files with suspicious names
84
+ const hiddenFiles = tree.tree.filter((item) => item.path.startsWith(".") &&
85
+ !item.path.startsWith(".github") &&
86
+ !item.path.startsWith(".git") &&
87
+ item.type === "blob");
88
+ for (const hidden of hiddenFiles) {
89
+ if (hidden.path.includes("backdoor") ||
90
+ hidden.path.includes("payload") ||
91
+ hidden.path.includes("shell")) {
92
+ findings.push({
93
+ severity: "high",
94
+ type: "HIDDEN_FILE",
95
+ file: hidden.path,
96
+ description: `Suspicious hidden file: ${hidden.path}`,
97
+ });
98
+ patternsMatched++;
99
+ }
100
+ }
101
+ const maliciousPackages = options.signatures.maliciousPackages;
102
+ const yaraRules = options.signatures.yaraRules;
103
+ const fileContents = [];
104
+ for (const file of filesToScan) {
105
+ filesScanned++;
106
+ const contentUrl = `https://api.github.com/repos/${repo.owner}/${repo.repo}/contents/${file.path}?ref=${commitSha}`;
107
+ const contentResponse = await fetch(contentUrl, { headers });
108
+ if (!contentResponse.ok)
109
+ continue;
110
+ const fileData = await contentResponse.json();
111
+ const content = atob(fileData.content || "");
112
+ if (SCANNABLE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) {
113
+ fileContents.push({ path: file.path, content });
114
+ }
115
+ if (file.path.endsWith("package.json")) {
116
+ const pkgFindings = await scanPackageJson(content, maliciousPackages, file.path);
117
+ findings.push(...pkgFindings);
118
+ patternsMatched += pkgFindings.length;
119
+ dependenciesChecked += countDependencies(content);
120
+ const postinstallFindings = checkPostinstallScripts(content, file.path);
121
+ findings.push(...postinstallFindings);
122
+ patternsMatched += postinstallFindings.length;
123
+ }
124
+ if (file.path.endsWith("requirements.txt") || file.path.endsWith("setup.py")) {
125
+ const pyFindings = scanPythonDeps(content, maliciousPackages, file.path);
126
+ findings.push(...pyFindings);
127
+ patternsMatched += pyFindings.length;
128
+ }
129
+ if (SCANNABLE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) {
130
+ const yaraFindings = applyYaraRules(content, yaraRules, file.path);
131
+ findings.push(...yaraFindings);
132
+ patternsMatched += yaraFindings.length;
133
+ const obfuscationFindings = detectObfuscation(content, file.path);
134
+ findings.push(...obfuscationFindings);
135
+ patternsMatched += obfuscationFindings.length;
136
+ }
137
+ }
138
+ // Non-English comment detection (source files only, not tests)
139
+ const sourceFiles = fileContents.filter((f) => !isTestFile(f.path));
140
+ const languageFindings = await detectNonEnglishComments(sourceFiles, repo);
141
+ findings.push(...languageFindings);
142
+ patternsMatched += languageFindings.length;
143
+ // Comprehensive security checks
144
+ for (const file of fileContents) {
145
+ const secretFindings = detectHardcodedSecrets(file.content, file.path);
146
+ findings.push(...secretFindings);
147
+ patternsMatched += secretFindings.length;
148
+ const networkFindings = detectNetworkPatterns(file.content, file.path);
149
+ findings.push(...networkFindings);
150
+ patternsMatched += networkFindings.length;
151
+ const miningFindings = detectCryptoMining(file.content, file.path);
152
+ findings.push(...miningFindings);
153
+ patternsMatched += miningFindings.length;
154
+ const exfiltrationFindings = detectDataExfiltration(file.content, file.path);
155
+ findings.push(...exfiltrationFindings);
156
+ patternsMatched += exfiltrationFindings.length;
157
+ const backdoorFindings = detectBackdoors(file.content, file.path);
158
+ findings.push(...backdoorFindings);
159
+ patternsMatched += backdoorFindings.length;
160
+ const fileAccessFindings = detectSuspiciousFileAccess(file.content, file.path);
161
+ findings.push(...fileAccessFindings);
162
+ patternsMatched += fileAccessFindings.length;
163
+ const socialEngFindings = detectSocialEngineering(file.content, file.path);
164
+ findings.push(...socialEngFindings);
165
+ patternsMatched += socialEngFindings.length;
166
+ }
167
+ for (const file of fileContents) {
168
+ if (file.path.endsWith("package.json")) {
169
+ const packageNameFindings = detectSuspiciousPackageNames(file.content, file.path);
170
+ findings.push(...packageNameFindings);
171
+ patternsMatched += packageNameFindings.length;
172
+ const supplyChainFindings = detectSupplyChainRisks(file.content, file.path);
173
+ findings.push(...supplyChainFindings);
174
+ patternsMatched += supplyChainFindings.length;
175
+ }
176
+ }
177
+ const integrityFindings = await detectCodeIntegrityIssues(fileContents, repo);
178
+ findings.push(...integrityFindings);
179
+ patternsMatched += integrityFindings.length;
180
+ const riskScore = calculateRiskScore(findings);
181
+ const riskLevel = getRiskLevel(riskScore, findings);
182
+ return {
183
+ riskScore,
184
+ riskLevel,
185
+ factors: findings.map((f) => ({
186
+ factor: f.type,
187
+ weight: getSeverityWeight(f.severity),
188
+ description: f.description,
189
+ })),
190
+ disclaimer: DEFAULT_DISCLAIMER,
191
+ repo,
192
+ commitSha,
193
+ scanSummary: { filesScanned, patternsMatched, dependenciesChecked },
194
+ findings,
195
+ safeToClone: riskLevel === "low",
196
+ scannedAt: new Date(),
197
+ };
198
+ }
199
+ catch (error) {
200
+ throw error;
201
+ }
202
+ }
203
+ // ─── Package scanning ─────────────────────────────────────────────────────────
204
+ async function scanPackageJson(content, maliciousPackages, filePath) {
205
+ const findings = [];
206
+ try {
207
+ const pkg = JSON.parse(content);
208
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
209
+ for (const [name] of Object.entries(allDeps)) {
210
+ const malicious = maliciousPackages.find((m) => m.name === name);
211
+ if (malicious) {
212
+ findings.push({
213
+ severity: malicious.severity,
214
+ type: "SUSPICIOUS_DEPENDENCY",
215
+ file: filePath,
216
+ package: name,
217
+ description: `Known malicious package: ${name}`,
218
+ });
219
+ }
220
+ if (!name.startsWith("@")) {
221
+ const typosquat = checkTyposquat(name);
222
+ if (typosquat) {
223
+ const weeklyDownloads = await getNpmWeeklyDownloads(name);
224
+ const shouldFlag = weeklyDownloads === null || weeklyDownloads < NPM_HIGH_DOWNLOAD_THRESHOLD;
225
+ if (shouldFlag) {
226
+ findings.push({
227
+ severity: "medium",
228
+ type: "TYPOSQUAT_PACKAGE",
229
+ file: filePath,
230
+ package: name,
231
+ description: `Possible typosquat of "${typosquat}": ${name}`,
232
+ });
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ catch {
239
+ // Invalid JSON, skip
240
+ }
241
+ return findings;
242
+ }
243
+ function checkPostinstallScripts(content, filePath) {
244
+ const findings = [];
245
+ try {
246
+ const pkg = JSON.parse(content);
247
+ const scripts = pkg.scripts || {};
248
+ const dangerousScripts = ["postinstall", "preinstall", "install"];
249
+ for (const scriptName of dangerousScripts) {
250
+ const script = scripts[scriptName];
251
+ if (script) {
252
+ if (script.includes("curl") ||
253
+ script.includes("wget") ||
254
+ script.includes("http") ||
255
+ script.includes("fetch")) {
256
+ findings.push({
257
+ severity: "high",
258
+ type: "POSTINSTALL_SCRIPT",
259
+ file: filePath,
260
+ description: `${scriptName} script makes network requests: "${script.slice(0, 50)}..."`,
261
+ });
262
+ }
263
+ if (script.includes("eval") || script.includes("exec")) {
264
+ findings.push({
265
+ severity: "critical",
266
+ type: "POSTINSTALL_SCRIPT",
267
+ file: filePath,
268
+ description: `${scriptName} script executes dynamic code`,
269
+ });
270
+ }
271
+ }
272
+ }
273
+ }
274
+ catch {
275
+ // Invalid JSON
276
+ }
277
+ return findings;
278
+ }
279
+ function scanPythonDeps(content, maliciousPackages, filePath) {
280
+ const findings = [];
281
+ const lines = content.split("\n");
282
+ for (const line of lines) {
283
+ const pkgMatch = line.match(/^([a-zA-Z0-9_-]+)/);
284
+ if (pkgMatch) {
285
+ const name = pkgMatch[1];
286
+ const malicious = maliciousPackages.find((m) => m.name.toLowerCase() === name.toLowerCase());
287
+ if (malicious) {
288
+ findings.push({
289
+ severity: malicious.severity,
290
+ type: "SUSPICIOUS_DEPENDENCY",
291
+ file: filePath,
292
+ package: name,
293
+ description: `Known malicious Python package: ${name}`,
294
+ });
295
+ }
296
+ }
297
+ }
298
+ return findings;
299
+ }
300
+ // ─── Obfuscation detection ────────────────────────────────────────────────────
301
+ function detectObfuscation(content, filePath) {
302
+ const findings = [];
303
+ if (isTestFile(filePath))
304
+ return findings;
305
+ // Heavy Base64 usage
306
+ const base64Matches = content.match(/[A-Za-z0-9+/]{50,}={0,2}/g);
307
+ if (base64Matches && base64Matches.length > 5) {
308
+ const snippet = base64Matches.slice(0, 3).join("\n") + (base64Matches.length > 3 ? "\n... and more" : "");
309
+ findings.push({
310
+ severity: "medium",
311
+ type: "OBFUSCATED_CODE",
312
+ file: filePath,
313
+ description: "Heavy Base64 encoding detected (possible data exfiltration)",
314
+ codeSnippet: snippet,
315
+ codeExplanation: "🔓 Multiple Base64-encoded strings found. Base64 is commonly used to hide malicious payloads, URLs, or commands from code reviewers.",
316
+ });
317
+ }
318
+ // Hex-encoded strings
319
+ const hexMatches = content.match(/\\x[0-9a-fA-F]{2}/g);
320
+ if (hexMatches && hexMatches.length > 20) {
321
+ findings.push({
322
+ severity: "medium",
323
+ type: "OBFUSCATED_CODE",
324
+ file: filePath,
325
+ description: "Extensive hex-encoded strings detected",
326
+ codeSnippet: hexMatches.slice(0, 15).join("") + "...",
327
+ codeExplanation: "🔤 Hex-encoded strings detected. This obfuscation technique hides the actual string content, often used to conceal malicious URLs or commands.",
328
+ });
329
+ }
330
+ // eval/Function constructor abuse
331
+ const evalPatterns = [
332
+ { pattern: /eval\s*\([^)]{0,200}\)/gi, name: "eval" },
333
+ { pattern: /new\s+Function\s*\([^)]{0,200}\)/gi, name: "Function constructor" },
334
+ { pattern: /setTimeout\s*\(\s*["'`][^"'`]{0,200}["'`]/gi, name: "setTimeout with string" },
335
+ { pattern: /setInterval\s*\(\s*["'`][^"'`]{0,200}["'`]/gi, name: "setInterval with string" },
336
+ ];
337
+ for (const { pattern, name } of evalPatterns) {
338
+ const matches = content.match(pattern);
339
+ if (matches && matches.length > 0) {
340
+ const snippet = matches[0];
341
+ findings.push({
342
+ severity: "high",
343
+ type: "OBFUSCATED_CODE",
344
+ file: filePath,
345
+ description: `Dynamic code execution pattern detected (${name})`,
346
+ codeSnippet: snippet.length > 200 ? snippet.substring(0, 200) + "..." : snippet,
347
+ codeExplanation: explainSuspiciousCode(snippet, "OBFUSCATED_CODE"),
348
+ });
349
+ break;
350
+ }
351
+ }
352
+ // Suspiciously long lines (hidden code attack)
353
+ const hiddenCodePatterns = [
354
+ /eval\s*\(/i,
355
+ /exec\s*\(/i,
356
+ /fetch\s*\(/i,
357
+ /XMLHttpRequest/i,
358
+ /\$\(.+\)\s*\(/,
359
+ /window\.location\s*=/i,
360
+ /document\.write\s*\(/i,
361
+ /\.innerHTML\s*=/i,
362
+ /child_process/i,
363
+ /require\s*\(\s*['"]fs['"]\s*\)/,
364
+ /atob\s*\(/i,
365
+ /fromCharCode/i,
366
+ ];
367
+ const lines = content.split("\n");
368
+ const suspiciousLines = [];
369
+ lines.forEach((line, index) => {
370
+ if (line.length > 5000) {
371
+ const hasMaliciousPattern = hiddenCodePatterns.some((pattern) => pattern.test(line));
372
+ suspiciousLines.push({ lineNum: index + 1, length: line.length, hasMaliciousPattern });
373
+ }
374
+ });
375
+ if (suspiciousLines.length > 0) {
376
+ const maxLength = Math.max(...suspiciousLines.map((l) => l.length));
377
+ const linesWithMaliciousCode = suspiciousLines.filter((l) => l.hasMaliciousPattern);
378
+ const lineDetails = suspiciousLines
379
+ .slice(0, 3)
380
+ .map((l) => `Line ${l.lineNum} (${l.length.toLocaleString()} chars${l.hasMaliciousPattern ? ", contains suspicious code" : ""})`)
381
+ .join(", ");
382
+ const severity = linesWithMaliciousCode.length > 0 ? "critical" : "high";
383
+ const firstLine = lines[suspiciousLines[0].lineNum - 1];
384
+ const codeSnippet = extractCodeSnippet(firstLine);
385
+ const codeExplanation = explainSuspiciousCode(firstLine, "OBFUSCATED_CODE");
386
+ findings.push({
387
+ severity,
388
+ type: "OBFUSCATED_CODE",
389
+ file: filePath,
390
+ line: suspiciousLines[0].lineNum,
391
+ description: `Extremely long line detected (${maxLength.toLocaleString()} characters). ${linesWithMaliciousCode.length > 0 ? "Contains suspicious code patterns like eval/exec/fetch. " : ""}Malicious code may be hidden far to the right. Affected: ${lineDetails}`,
392
+ codeSnippet,
393
+ codeExplanation,
394
+ });
395
+ }
396
+ return findings;
397
+ }
398
+ function explainSuspiciousCode(code, type) {
399
+ const explanations = [];
400
+ if (/eval\s*\(/i.test(code))
401
+ explanations.push("⚠️ Uses eval() to execute dynamic code - can run arbitrary malicious commands");
402
+ if (/exec\s*\(/i.test(code))
403
+ explanations.push("⚠️ Executes system commands - can run shell scripts and steal data");
404
+ if (/child_process/i.test(code))
405
+ explanations.push("⚠️ Spawns child processes - can execute arbitrary programs on your system");
406
+ if (/atob\s*\(/i.test(code))
407
+ explanations.push("🔓 Decodes Base64 data - often used to hide malicious payloads");
408
+ if (/fromCharCode/i.test(code))
409
+ explanations.push("🔤 Constructs strings from character codes - obfuscation technique to hide malicious code");
410
+ if (/fetch\s*\(|XMLHttpRequest|axios\./i.test(code))
411
+ explanations.push("🌐 Makes network requests - could send your data to attacker's server");
412
+ if (/document\.write|\.innerHTML\s*=/i.test(code))
413
+ explanations.push("📝 Injects HTML/scripts into page - can steal credentials or modify the page");
414
+ if (/window\.location\s*=/i.test(code))
415
+ explanations.push("🔀 Redirects browser - can send you to phishing sites");
416
+ if (/require\s*\(\s*['"]fs['"]\s*\)/i.test(code))
417
+ explanations.push("📁 Accesses file system - can read/write/delete files on your computer");
418
+ if (/process\.env/i.test(code))
419
+ explanations.push("🔐 Accesses environment variables - can steal API keys and credentials");
420
+ if (/crypto|password|token|api[_-]?key/i.test(code))
421
+ explanations.push("🔑 References sensitive data - may be attempting to steal credentials");
422
+ if (/discord\.gg|pastebin\.com|bit\.ly|tinyurl/i.test(code))
423
+ explanations.push("🔗 Contains suspicious URLs - likely data exfiltration endpoint");
424
+ if (type === "OBFUSCATED_CODE" && code.length > 1000)
425
+ explanations.push("📏 Extremely long/complex code - intentionally obfuscated to hide malicious intent");
426
+ return explanations.length > 0
427
+ ? explanations.join("\n")
428
+ : "This code contains suspicious patterns that may indicate malicious intent. Review carefully before running.";
429
+ }
430
+ function extractCodeSnippet(line, maxLength = 500) {
431
+ if (line.length > maxLength) {
432
+ const patterns = [
433
+ /eval\s*\([^)]{0,200}\)/i,
434
+ /exec\s*\([^)]{0,200}\)/i,
435
+ /atob\s*\([^)]{0,200}\)/i,
436
+ /fetch\s*\([^)]{0,200}\)/i,
437
+ /require\s*\(\s*['"][^'"]+['"]\s*\)/i,
438
+ ];
439
+ for (const pattern of patterns) {
440
+ const match = line.match(pattern);
441
+ if (match) {
442
+ const matchStart = match.index || 0;
443
+ const start = Math.max(0, matchStart - 100);
444
+ const end = Math.min(line.length, matchStart + match[0].length + 100);
445
+ const snippet = line.substring(start, end);
446
+ return (start > 0 ? "..." : "") + snippet + (end < line.length ? "..." : "");
447
+ }
448
+ }
449
+ return (line.substring(0, 250) +
450
+ " ... [" +
451
+ (line.length - 500).toLocaleString() +
452
+ " chars hidden] ... " +
453
+ line.substring(line.length - 250));
454
+ }
455
+ return line;
456
+ }
457
+ // ─── Non-English comment detection ───────────────────────────────────────────
458
+ const ENGLISH_CODES = ["eng", "sco"];
459
+ const MIN_WORD_LENGTH = 5;
460
+ // Thresholds are intentionally conservative: language detection on short code
461
+ // comments is noisy, so we only flag repos with a substantial volume of comments
462
+ // that are clearly (not marginally) non-English, to avoid false-flagging
463
+ // legitimate English or internationally-commented projects.
464
+ const MIN_WORDS_TO_ANALYZE = 30;
465
+ const NON_ENGLISH_THRESHOLD = 0.25;
466
+ const MIN_NON_ENGLISH_WORDS = 8;
467
+ // Only run language detection on comments long enough for franc to be reliable.
468
+ const MIN_COMMENT_LEN_FOR_DETECTION = 40;
469
+ const COMMENT_PATTERNS = {
470
+ javascript: [/\/\/(.*)$/gm, /\/\*[\s\S]*?\*\//g],
471
+ typescript: [/\/\/(.*)$/gm, /\/\*[\s\S]*?\*\//g],
472
+ python: [/#(.*)$/gm, /"""[\s\S]*?"""/g, /'''[\s\S]*?'''/g],
473
+ shell: [/#(.*)$/gm],
474
+ powershell: [/#(.*)$/gm, /<#[\s\S]*?#>/g],
475
+ batch: [/(?:^|\n)\s*REM\s+(.*)$/gim, /(?:^|\n)\s*::(.*)$/gm],
476
+ };
477
+ function extractComments(content, fileExtension) {
478
+ const comments = [];
479
+ let language = "";
480
+ if ([".js", ".jsx", ".mjs", ".cjs"].includes(fileExtension))
481
+ language = "javascript";
482
+ else if ([".ts", ".tsx"].includes(fileExtension))
483
+ language = "typescript";
484
+ else if ([".py"].includes(fileExtension))
485
+ language = "python";
486
+ else if ([".sh", ".bash"].includes(fileExtension))
487
+ language = "shell";
488
+ else if ([".ps1"].includes(fileExtension))
489
+ language = "powershell";
490
+ else if ([".bat", ".cmd"].includes(fileExtension))
491
+ language = "batch";
492
+ if (!language || !COMMENT_PATTERNS[language])
493
+ return comments;
494
+ for (const pattern of COMMENT_PATTERNS[language]) {
495
+ const matches = content.match(pattern);
496
+ if (matches)
497
+ comments.push(...matches);
498
+ }
499
+ return comments
500
+ .map((comment) => comment
501
+ .replace(/^\/\/\s*/, "")
502
+ .replace(/^\/\*\s*|\s*\*\/$/g, "")
503
+ .replace(/^#\s*/, "")
504
+ .replace(/^<#\s*|\s*#>$/g, "")
505
+ .replace(/^"""\s*|\s*"""$/g, "")
506
+ .replace(/^'''\s*|\s*'''$/g, "")
507
+ .replace(/^REM\s+/i, "")
508
+ .replace(/^::\s*/, "")
509
+ .trim())
510
+ .filter((comment) => comment.length > 0);
511
+ }
512
+ function analyzeCommentLanguages(comments, filePath, fileAnalysis) {
513
+ let fileNonEnglishWords = 0;
514
+ for (const comment of comments) {
515
+ const words = comment.split(/\s+/).filter((word) => word.length >= MIN_WORD_LENGTH);
516
+ fileAnalysis.totalWords += words.length;
517
+ fileAnalysis.totalComments += 1;
518
+ if (comment.length >= MIN_COMMENT_LEN_FOR_DETECTION) {
519
+ const detectedLang = franc(comment);
520
+ if (detectedLang && detectedLang !== "und") {
521
+ if (ENGLISH_CODES.includes(detectedLang)) {
522
+ fileAnalysis.englishWords += words.length;
523
+ }
524
+ else {
525
+ fileAnalysis.nonEnglishWords += words.length;
526
+ fileNonEnglishWords += words.length;
527
+ const currentCount = fileAnalysis.detectedLanguages.get(detectedLang) || 0;
528
+ fileAnalysis.detectedLanguages.set(detectedLang, currentCount + words.length);
529
+ }
530
+ }
531
+ else {
532
+ fileAnalysis.englishWords += words.length;
533
+ }
534
+ }
535
+ else {
536
+ fileAnalysis.englishWords += words.length;
537
+ }
538
+ }
539
+ if (fileNonEnglishWords > 0) {
540
+ fileAnalysis.filesWithNonEnglish.set(filePath, fileNonEnglishWords);
541
+ }
542
+ }
543
+ async function detectNonEnglishComments(files, _repo) {
544
+ const findings = [];
545
+ const analysis = {
546
+ totalComments: 0,
547
+ totalWords: 0,
548
+ englishWords: 0,
549
+ nonEnglishWords: 0,
550
+ nonEnglishPercentage: 0,
551
+ detectedLanguages: new Map(),
552
+ filesWithNonEnglish: new Map(),
553
+ };
554
+ for (const file of files) {
555
+ const ext = file.path.substring(file.path.lastIndexOf("."));
556
+ const comments = extractComments(file.content, ext);
557
+ if (comments.length > 0) {
558
+ analyzeCommentLanguages(comments, file.path, analysis);
559
+ }
560
+ }
561
+ if (analysis.totalWords >= MIN_WORDS_TO_ANALYZE) {
562
+ analysis.nonEnglishPercentage = analysis.nonEnglishWords / analysis.totalWords;
563
+ if (analysis.nonEnglishPercentage >= NON_ENGLISH_THRESHOLD &&
564
+ analysis.nonEnglishWords >= MIN_NON_ENGLISH_WORDS) {
565
+ const topFiles = Array.from(analysis.filesWithNonEnglish.entries())
566
+ .sort((a, b) => b[1] - a[1])
567
+ .slice(0, 3)
568
+ .map(([path]) => path);
569
+ const langBreakdown = Array.from(analysis.detectedLanguages.entries())
570
+ .map(([code, count]) => `${getLanguageName(code)}: ${count} words`)
571
+ .join(", ");
572
+ let severity = "medium";
573
+ let description = "";
574
+ if (analysis.englishWords === 0) {
575
+ severity = "high";
576
+ description = `100% of code comments are in non-English languages (${langBreakdown}). Repository contains NO English comments, which is highly suspicious for projects claiming international scope.`;
577
+ }
578
+ else {
579
+ const percentage = Math.round(analysis.nonEnglishPercentage * 100);
580
+ description = `${percentage}% of code comments are in non-English languages (${langBreakdown}). This may indicate deceptive code where malicious intent is hidden in foreign language comments.`;
581
+ }
582
+ findings.push({ severity, type: "NON_ENGLISH_COMMENTS", description, files: topFiles });
583
+ }
584
+ }
585
+ return findings;
586
+ }
587
+ function getLanguageName(code) {
588
+ const names = {
589
+ tur: "Turkish", rus: "Russian", zho: "Chinese", jpn: "Japanese", kor: "Korean",
590
+ ara: "Arabic", deu: "German", fra: "French", spa: "Spanish", por: "Portuguese",
591
+ ita: "Italian", nld: "Dutch", pol: "Polish", ukr: "Ukrainian", vie: "Vietnamese",
592
+ tha: "Thai", ind: "Indonesian", heb: "Hebrew", hin: "Hindi",
593
+ };
594
+ return names[code] || code;
595
+ }
596
+ // ─── Comprehensive security checks ───────────────────────────────────────────
597
+ function detectSuspiciousPackageNames(content, filePath) {
598
+ const findings = [];
599
+ try {
600
+ const pkg = JSON.parse(content);
601
+ const allDeps = {
602
+ ...pkg.dependencies,
603
+ ...pkg.devDependencies,
604
+ ...pkg.peerDependencies,
605
+ ...pkg.optionalDependencies,
606
+ };
607
+ const suspiciousPackages = [];
608
+ for (const packageName in allDeps) {
609
+ if (/^[a-z0-9]{6,8}$/.test(packageName) && !/[aeiou]{2}/.test(packageName)) {
610
+ suspiciousPackages.push(`${packageName} (random-looking name)`);
611
+ }
612
+ if (/(.)\1{4,}/.test(packageName)) {
613
+ suspiciousPackages.push(`${packageName} (repeated characters)`);
614
+ }
615
+ if (packageName.length === 1) {
616
+ suspiciousPackages.push(`${packageName} (single character)`);
617
+ }
618
+ if (/(test|temp|tmp|debug|hack|crack|backdoor|shell|payload)[_-]?[0-9]*$/.test(packageName)) {
619
+ suspiciousPackages.push(`${packageName} (suspicious name pattern)`);
620
+ }
621
+ }
622
+ if (suspiciousPackages.length > 0) {
623
+ findings.push({
624
+ severity: "medium",
625
+ type: "SUSPICIOUS_PACKAGE_NAME",
626
+ file: filePath,
627
+ description: `Suspicious package names detected: ${suspiciousPackages.slice(0, 5).join(", ")}${suspiciousPackages.length > 5 ? ` and ${suspiciousPackages.length - 5} more` : ""}`,
628
+ codeExplanation: "📦 These package names don't follow typical naming conventions and may be malicious packages attempting to hide their purpose.",
629
+ });
630
+ }
631
+ }
632
+ catch {
633
+ // Not valid JSON
634
+ }
635
+ return findings;
636
+ }
637
+ function detectHardcodedSecrets(content, filePath) {
638
+ const findings = [];
639
+ if (isTestFile(filePath))
640
+ return findings;
641
+ const secretPatterns = [
642
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]([^'"]{20,})['"]/gi, type: "API Key", severity: "critical" },
643
+ { pattern: /(?:secret[_-]?key|secret)\s*[:=]\s*['"]([^'"]{20,})['"]/gi, type: "Secret Key", severity: "critical" },
644
+ { pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"]([^'"]{8,})['"]/gi, type: "Password", severity: "critical" },
645
+ { pattern: /(?:token|auth[_-]?token)\s*[:=]\s*['"]([^'"]{20,})['"]/gi, type: "Auth Token", severity: "critical" },
646
+ { pattern: /(?:private[_-]?key|privatekey)\s*[:=]\s*['"]([^'"]{20,})['"]/gi, type: "Private Key", severity: "critical" },
647
+ { pattern: /AKIA[0-9A-Z]{16}/g, type: "AWS Access Key", severity: "critical" },
648
+ { pattern: /ghp_[a-zA-Z0-9]{36}/g, type: "GitHub Personal Access Token", severity: "critical" },
649
+ { pattern: /gho_[a-zA-Z0-9]{36}/g, type: "GitHub OAuth Token", severity: "critical" },
650
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/g, type: "Stripe Live Key", severity: "critical" },
651
+ { pattern: /(?:mongodb|mongo)[+:\/\/]{0,6}[^@\s]+:[^@\s]+@/gi, type: "MongoDB Connection String", severity: "high" },
652
+ { pattern: /postgres:\/\/[^@\s]+:[^@\s]+@/gi, type: "PostgreSQL Connection String", severity: "high" },
653
+ ];
654
+ const foundSecrets = [];
655
+ const matchedRegexes = [];
656
+ for (const { pattern, type, severity } of secretPatterns) {
657
+ const matches = content.match(pattern);
658
+ if (matches && matches.length > 0) {
659
+ foundSecrets.push({ type, match: matches[0].substring(0, 50) + "...", severity });
660
+ matchedRegexes.push(pattern);
661
+ }
662
+ }
663
+ if (foundSecrets.length > 0) {
664
+ const highestSeverity = foundSecrets.some((s) => s.severity === "critical") ? "critical" : "high";
665
+ findings.push({
666
+ severity: highestSeverity,
667
+ type: "HARDCODED_SECRETS",
668
+ file: filePath,
669
+ description: `Hardcoded credentials detected: ${foundSecrets.map((s) => s.type).join(", ")}`,
670
+ evidence: collectEvidence(content, matchedRegexes),
671
+ codeSnippet: foundSecrets[0].match,
672
+ codeExplanation: "🔐 Hardcoded credentials expose your application to unauthorized access. API keys, passwords, and tokens should NEVER be committed to source code. Use environment variables or secure credential management instead.",
673
+ });
674
+ }
675
+ return findings;
676
+ }
677
+ function detectNetworkPatterns(content, filePath) {
678
+ const findings = [];
679
+ if (isTestFile(filePath))
680
+ return findings;
681
+ const suspiciousPatterns = [
682
+ // Valid public IPv4 only: each octet bounded 0–255, not embedded in a longer
683
+ // dotted/number sequence (avoids flagging version strings like "1.2.3.4.5" or
684
+ // impossible IPs like "999.999.999.999"), and excluding private/link-local ranges.
685
+ { pattern: /(?:https?:\/\/)?(?<![\d.])(?!(?:127\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.))(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}(?![\d.])(?::\d{1,5})?/g, type: "Hardcoded IP Address", severity: "high" },
686
+ { pattern: /https?:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[a-zA-Z0-9_-]+/gi, type: "Discord Webhook", severity: "critical" },
687
+ { pattern: /\d{8,10}:[a-zA-Z0-9_-]{35}/g, type: "Telegram Bot Token", severity: "high" },
688
+ { pattern: /(?:pastebin\.com|hastebin\.com|paste\.ee)\/[a-zA-Z0-9]+/gi, type: "Pastebin URL", severity: "medium" },
689
+ { pattern: /(?:bit\.ly|tinyurl\.com|t\.co)\/[a-zA-Z0-9]+/gi, type: "URL Shortener", severity: "medium" },
690
+ { pattern: /https?:\/\/[a-zA-Z0-9-]+\.(?:tk|ml|ga|cf|gq|onion|xyz)(?:\/|$)/gi, type: "Suspicious Domain TLD", severity: "high" },
691
+ { pattern: /https?:\/\/[a-zA-Z0-9-]+\.ngrok\.io/gi, type: "Ngrok Tunnel", severity: "medium" },
692
+ ];
693
+ for (const { pattern, type, severity } of suspiciousPatterns) {
694
+ const matches = content.match(pattern);
695
+ if (matches && matches.length > 0) {
696
+ const validMatches = matches.filter((m) => {
697
+ const lines = content.split("\n");
698
+ const lineWithMatch = lines.find((l) => l.includes(m));
699
+ return lineWithMatch && !lineWithMatch.trim().startsWith("//") && !lineWithMatch.trim().startsWith("#");
700
+ });
701
+ if (validMatches.length > 0) {
702
+ findings.push({
703
+ severity,
704
+ type: "NETWORK_COMMUNICATION",
705
+ file: filePath,
706
+ description: `Suspicious network communication detected: ${type} (${validMatches.length} occurrence${validMatches.length > 1 ? "s" : ""})`,
707
+ evidence: collectEvidence(content, pattern),
708
+ codeSnippet: validMatches.slice(0, 3).join("\n"),
709
+ codeExplanation: `🌐 ${type} detected. ${type === "Discord Webhook"
710
+ ? "Discord webhooks are commonly used for data exfiltration - stolen data is sent to attacker's Discord server."
711
+ : type === "Hardcoded IP Address"
712
+ ? "Hardcoded IP addresses bypass DNS and make the code harder to audit. Often used to connect to command & control servers."
713
+ : "This URL may be used for data exfiltration or malware distribution."}`,
714
+ });
715
+ }
716
+ }
717
+ }
718
+ return findings;
719
+ }
720
+ function detectCryptoMining(content, filePath) {
721
+ const findings = [];
722
+ const miningPatterns = [
723
+ /coinhive|coin-hive|crypto-loot|cryptoloot|jsecoin/gi,
724
+ /stratum\+tcp|stratum\.|\bpool\./gi,
725
+ /monero|xmr|cryptonight/gi,
726
+ /(?:miner|mining)[\w]*\.(?:start|run|init)/gi,
727
+ /\.(?:setNumThreads|setThrottle|getHashesPerSecond)/gi,
728
+ ];
729
+ const miningIndicators = [];
730
+ const matches = [];
731
+ for (const pattern of miningPatterns) {
732
+ const found = content.match(pattern);
733
+ if (found) {
734
+ miningIndicators.push(...found);
735
+ matches.push(...found);
736
+ }
737
+ }
738
+ if (/while\s*\(\s*true\s*\)[\s\S]{0,200}(?:Math\.|crypto|hash)/i.test(content)) {
739
+ miningIndicators.push("Infinite loop with crypto/math operations");
740
+ }
741
+ if (miningIndicators.length > 0) {
742
+ findings.push({
743
+ severity: "critical",
744
+ type: "CRYPTO_MINER",
745
+ file: filePath,
746
+ description: `Cryptocurrency mining code detected: ${miningIndicators.slice(0, 3).join(", ")}`,
747
+ evidence: collectEvidence(content, miningPatterns),
748
+ codeSnippet: matches.slice(0, 2).join("\n"),
749
+ codeExplanation: "⛏️ Cryptocurrency miners use your CPU/GPU to mine coins for attackers. This drastically slows down your system and increases electricity costs. Miners are often bundled with legitimate software.",
750
+ });
751
+ }
752
+ return findings;
753
+ }
754
+ function detectDataExfiltration(content, filePath) {
755
+ const findings = [];
756
+ if (isTestFile(filePath))
757
+ return findings;
758
+ const exfiltrationPatterns = [
759
+ { pattern: /navigator\.clipboard\.read(?:Text)?\s*\(/gi, type: "Clipboard Access", severity: "high" },
760
+ { pattern: /localStorage\.getItem|sessionStorage\.getItem/gi, type: "Storage Access", severity: "medium" },
761
+ { pattern: /document\.cookie/gi, type: "Cookie Access", severity: "high" },
762
+ { pattern: /(?:addEventListener|on)\s*\(\s*['"](?:keydown|keypress|keyup)['"]/gi, type: "Keylogger Pattern", severity: "critical" },
763
+ { pattern: /(?:input|password|email)[\w-]*\.value/gi, type: "Form Data Access", severity: "medium" },
764
+ { pattern: /new\s+FormData\s*\([^)]*\)[\s\S]{0,100}(?:fetch|axios|XMLHttpRequest)/gi, type: "Form Data Transmission", severity: "high" },
765
+ ];
766
+ const detectedPatterns = [];
767
+ const matchedRegexes = [];
768
+ let highestSeverity = "medium";
769
+ for (const { pattern, type, severity } of exfiltrationPatterns) {
770
+ if (pattern.test(content)) {
771
+ detectedPatterns.push(type);
772
+ matchedRegexes.push(pattern);
773
+ if (severity === "critical" || (severity === "high" && highestSeverity !== "critical")) {
774
+ highestSeverity = severity;
775
+ }
776
+ }
777
+ }
778
+ if (detectedPatterns.length > 0) {
779
+ findings.push({
780
+ severity: highestSeverity,
781
+ type: "DATA_EXFILTRATION",
782
+ file: filePath,
783
+ description: `Data exfiltration patterns detected: ${detectedPatterns.join(", ")}`,
784
+ evidence: collectEvidence(content, matchedRegexes),
785
+ codeExplanation: "🕵️ This code accesses sensitive user data. Combined with network requests, this could indicate credential theft or data exfiltration to attacker's servers.",
786
+ });
787
+ }
788
+ return findings;
789
+ }
790
+ function detectBackdoors(content, filePath) {
791
+ const findings = [];
792
+ const backdoorPatterns = [
793
+ { pattern: /eval\s*\(\s*(?:request|req)(?:\.|\.body|\.query)/gi, type: "Remote Code Execution Endpoint", severity: "critical" },
794
+ { pattern: /exec\s*\(\s*(?:request|req)(?:\.|\.body|\.query)/gi, type: "Command Injection Endpoint", severity: "critical" },
795
+ { pattern: /require\s*\(\s*(?:request|req)(?:\.|\.body|\.query)/gi, type: "Dynamic Require", severity: "critical" },
796
+ { pattern: /new\s+Function\s*\([^)]*(?:request|req)/gi, type: "Dynamic Function from Request", severity: "critical" },
797
+ { pattern: /(?:admin|debug|backdoor|shell)[\w]*\s*[:=]\s*(?:true|1|"[^"]*")\s*(?:\/\/|#)?\s*(?:TODO|FIXME|HACK)?/gi, type: "Debug/Admin Flag", severity: "high" },
798
+ { pattern: /(?:password|auth)\s*(?:===?|==)\s*['"][^'"]{0,20}['"]\s*\)/gi, type: "Hardcoded Auth Bypass", severity: "critical" },
799
+ ];
800
+ for (const { pattern, type, severity } of backdoorPatterns) {
801
+ const matches = content.match(pattern);
802
+ if (matches) {
803
+ findings.push({
804
+ severity,
805
+ type: "BACKDOOR",
806
+ file: filePath,
807
+ description: `Backdoor detected: ${type}`,
808
+ codeSnippet: matches[0],
809
+ codeExplanation: `🚪 ${type} - This code allows remote attackers to execute arbitrary commands or bypass authentication. This is a CRITICAL security vulnerability.`,
810
+ evidence: collectEvidence(content, pattern),
811
+ });
812
+ }
813
+ }
814
+ return findings;
815
+ }
816
+ function detectSupplyChainRisks(content, filePath) {
817
+ const findings = [];
818
+ if (!filePath.endsWith("package.json"))
819
+ return findings;
820
+ try {
821
+ const pkg = JSON.parse(content);
822
+ const scripts = pkg.scripts || {};
823
+ const suspiciousScripts = [];
824
+ for (const [scriptName, scriptContent] of Object.entries(scripts)) {
825
+ if (typeof scriptContent === "string") {
826
+ if (/(?:curl|wget|fetch)[\s\S]*\.(?:exe|sh|py|bin|dmg|pkg|msi|dll)/i.test(scriptContent)) {
827
+ suspiciousScripts.push(`${scriptName}: Downloads executable files`);
828
+ }
829
+ if (/(?:cd|pushd)[\s\S]*node_modules/i.test(scriptContent)) {
830
+ suspiciousScripts.push(`${scriptName}: Modifies node_modules`);
831
+ }
832
+ if (/(?:>|>>|tee)[\s\S]*(?:package\.json|index\.js|\.\.\/)/i.test(scriptContent)) {
833
+ suspiciousScripts.push(`${scriptName}: Self-modifying script`);
834
+ }
835
+ if (/(?:preinstall|postinstall|install)/.test(scriptName) && /(?:curl|wget|fetch|http|https)/i.test(scriptContent)) {
836
+ suspiciousScripts.push(`${scriptName}: Network access during installation`);
837
+ }
838
+ }
839
+ }
840
+ if (suspiciousScripts.length > 0) {
841
+ findings.push({
842
+ severity: "critical",
843
+ type: "SUPPLY_CHAIN_RISK",
844
+ file: filePath,
845
+ description: `Supply chain attack patterns in package scripts: ${suspiciousScripts.join("; ")}`,
846
+ codeSnippet: JSON.stringify(scripts, null, 2).substring(0, 300) + "...",
847
+ codeExplanation: "📦 Supply chain attacks inject malicious code through package installation scripts. These scripts run automatically when you install the package and can compromise your entire system.",
848
+ });
849
+ }
850
+ }
851
+ catch {
852
+ // Not valid JSON
853
+ }
854
+ return findings;
855
+ }
856
+ function detectSuspiciousFileAccess(content, filePath) {
857
+ const findings = [];
858
+ if (isTestFile(filePath))
859
+ return findings;
860
+ const fileAccessPatterns = [
861
+ { pattern: /(?:fs\.read|readFileSync|readFile)\s*\([^)]*(?:\.ssh|\.aws|\.gnupg|\.docker|id_rsa|credentials)/gi, type: "SSH/AWS Credentials Access", severity: "critical" },
862
+ { pattern: /(?:fs\.read|readFileSync|readFile)\s*\([^)]*(?:etc\/passwd|etc\/shadow|\.bash_history|\.zsh_history)/gi, type: "System File Access", severity: "critical" },
863
+ { pattern: /(?:fs\.read|readFileSync|readFile)\s*\([^)]*(?:Chrome|Firefox|Safari|Edge)[\w\s/\\]*(?:Cookies|Login|History)/gi, type: "Browser Data Access", severity: "critical" },
864
+ { pattern: /(?:fs\.write|writeFileSync|writeFile)\s*\([^)]*(?:\/etc|\/sys|\/bin|C:\\\\Windows)/gi, type: "System Directory Write", severity: "critical" },
865
+ { pattern: /(?:fs\.unlink|unlinkSync|rmSync|rm\s+-rf)/gi, type: "File Deletion", severity: "medium" },
866
+ ];
867
+ for (const { pattern, type, severity } of fileAccessPatterns) {
868
+ const matches = content.match(pattern);
869
+ if (matches) {
870
+ findings.push({
871
+ severity,
872
+ type: "SUSPICIOUS_FILE_ACCESS",
873
+ file: filePath,
874
+ description: `Suspicious file access detected: ${type}`,
875
+ codeSnippet: matches[0],
876
+ codeExplanation: `📁 ${type} - This code attempts to access or modify sensitive system files. This could be credential theft, system compromise, or destructive malware.`,
877
+ evidence: collectEvidence(content, pattern),
878
+ });
879
+ }
880
+ }
881
+ return findings;
882
+ }
883
+ async function detectCodeIntegrityIssues(files, _repo) {
884
+ const findings = [];
885
+ for (const file of files) {
886
+ if (file.path.endsWith(".min.js") || file.path.endsWith(".min.css"))
887
+ continue;
888
+ const lines = file.content.split("\n");
889
+ const longLines = lines.filter((l) => l.length > 500);
890
+ const avgLineLength = lines.reduce((sum, l) => sum + l.length, 0) / lines.length;
891
+ if (longLines.length > 3 && avgLineLength > 200) {
892
+ const shortVars = (file.content.match(/\b[a-z]\b/g) || []).length;
893
+ const totalVars = (file.content.match(/\b[a-z][a-zA-Z0-9_]*\b/g) || []).length;
894
+ if (shortVars / totalVars > 0.3) {
895
+ findings.push({
896
+ severity: "medium",
897
+ type: "CODE_INTEGRITY_ISSUE",
898
+ file: file.path,
899
+ description: "Minified/obfuscated code detected in source repository",
900
+ codeExplanation: "🔍 Source code appears to be minified or obfuscated. Legitimate projects typically commit readable source code, not minified versions. This makes code review difficult and may hide malicious intent.",
901
+ });
902
+ }
903
+ }
904
+ }
905
+ const hasLicense = files.some((f) => /^LICENSE|^COPYING/i.test(f.path));
906
+ if (!hasLicense && files.length > 5) {
907
+ findings.push({
908
+ severity: "low",
909
+ type: "CODE_INTEGRITY_ISSUE",
910
+ description: "No LICENSE file found in repository",
911
+ codeExplanation: "📄 Missing license file. Legitimate open-source projects typically include a license. Absence may indicate a quickly assembled malicious repository.",
912
+ });
913
+ }
914
+ const hasReadme = files.some((f) => /^README/i.test(f.path));
915
+ if (!hasReadme && files.length > 5) {
916
+ findings.push({
917
+ severity: "low",
918
+ type: "CODE_INTEGRITY_ISSUE",
919
+ description: "No README file found in repository",
920
+ codeExplanation: "📝 Missing README. Legitimate projects typically have documentation. Absence may indicate a hastily created malicious repository.",
921
+ });
922
+ }
923
+ return findings;
924
+ }
925
+ function detectSocialEngineering(content, filePath) {
926
+ const findings = [];
927
+ if (!/README/i.test(filePath))
928
+ return findings;
929
+ const socialEngineeringPatterns = [
930
+ { pattern: /(?:urgent|immediately|quick|fast|act now|limited time)/gi, type: "Urgency Language", severity: "medium" },
931
+ { pattern: /(?:disable|turn off|bypass)[\s\w]*(?:antivirus|firewall|security|protection)/gi, type: "Security Bypass Instructions", severity: "high" },
932
+ { pattern: /(?:admin|root|sudo)[\s\w]*(?:rights|privileges|access|permission)/gi, type: "Privilege Escalation Language", severity: "medium" },
933
+ { pattern: /(?:100%|totally|completely|absolutely)[\s\w]*(?:safe|secure|trusted|legit)/gi, type: "Over-assurance Language", severity: "low" },
934
+ { pattern: /(?:millions?|thousands?)[\s\w]*(?:downloads?|users?|stars?)/gi, type: "Fake Popularity Claims", severity: "low" },
935
+ ];
936
+ const detectedPatterns = [];
937
+ let highestSeverity = "low";
938
+ for (const { pattern, type, severity } of socialEngineeringPatterns) {
939
+ if (pattern.test(content)) {
940
+ detectedPatterns.push(type);
941
+ if (severity === "high" || (severity === "medium" && highestSeverity === "low")) {
942
+ highestSeverity = severity;
943
+ }
944
+ }
945
+ }
946
+ if (detectedPatterns.length >= 2) {
947
+ findings.push({
948
+ severity: highestSeverity,
949
+ type: "SOCIAL_ENGINEERING",
950
+ file: filePath,
951
+ description: `Social engineering tactics detected in README: ${detectedPatterns.join(", ")}`,
952
+ codeExplanation: "🎣 README contains persuasive language designed to manipulate users. Scammers use urgency, fake credentials, and security bypass instructions to trick users into running malicious code.",
953
+ });
954
+ }
955
+ return findings;
956
+ }
957
+ // ─── Utilities ────────────────────────────────────────────────────────────────
958
+ async function getNpmWeeklyDownloads(packageName) {
959
+ const cached = NPM_DOWNLOAD_CACHE.get(packageName);
960
+ if (cached && Date.now() - cached.fetchedAt < NPM_CACHE_TTL_MS) {
961
+ return cached.weeklyDownloads;
962
+ }
963
+ try {
964
+ const url = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
965
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
966
+ const weeklyDownloads = response.ok
967
+ ? (await response.json()).downloads ?? 0
968
+ : 0;
969
+ NPM_DOWNLOAD_CACHE.set(packageName, { weeklyDownloads, fetchedAt: Date.now() });
970
+ return weeklyDownloads;
971
+ }
972
+ catch {
973
+ NPM_DOWNLOAD_CACHE.set(packageName, { weeklyDownloads: null, fetchedAt: Date.now() });
974
+ return null;
975
+ }
976
+ }
977
+ function levenshteinDistance(a, b) {
978
+ const m = a.length, n = b.length;
979
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
980
+ for (let i = 1; i <= m; i++) {
981
+ for (let j = 1; j <= n; j++) {
982
+ dp[i][j] =
983
+ a[i - 1] === b[j - 1]
984
+ ? dp[i - 1][j - 1]
985
+ : 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
986
+ }
987
+ }
988
+ return dp[m][n];
989
+ }
990
+ function checkTyposquat(packageName) {
991
+ const popularPackages = [
992
+ "lodash", "express", "react", "axios", "moment", "webpack", "babel",
993
+ "typescript", "eslint", "prettier", "jest", "mocha", "bcrypt", "crypto", "request",
994
+ ];
995
+ for (const popular of popularPackages) {
996
+ if (packageName === popular)
997
+ continue;
998
+ if (Math.abs(packageName.length - popular.length) > 3)
999
+ continue;
1000
+ if (levenshteinDistance(packageName, popular) <= 2)
1001
+ return popular;
1002
+ }
1003
+ return null;
1004
+ }
1005
+ function countDependencies(content) {
1006
+ try {
1007
+ const pkg = JSON.parse(content);
1008
+ return (Object.keys(pkg.dependencies || {}).length +
1009
+ Object.keys(pkg.devDependencies || {}).length);
1010
+ }
1011
+ catch {
1012
+ return 0;
1013
+ }
1014
+ }
1015
+ // Export internal helpers for testing
1016
+ export { checkTyposquat, detectObfuscation, isTestFile };
1017
+ //# sourceMappingURL=repo-scanner.js.map