UNPKG

super-confused

Version:

Super-confused is a next-gen dependency confusion analysis tool that works on local or remote package manifests or SBOMs.

1,033 lines (891 loc) 41.7 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const https = require('https'); const { promisify } = require('util'); const readFile = promisify(fs.readFile); const readdir = promisify(fs.readdir); const stat = promisify(fs.stat); // ANSI color codes const colors = { red: '\x1b[31m', reset: '\x1b[0m' }; function colorize(text, color) { return colors[color] + text + colors.reset; } class SuperConfused { constructor(jsonMode = false) { this.results = []; this.jsonMode = jsonMode; this.supportedFiles = { 'package.json': this.scanPackageJson, 'requirements.txt': this.scanRequirementsTxt, 'pyproject.toml': this.scanPyprojectToml, 'go.mod': this.scanGoMod, 'go.sum': this.scanGoSum, 'Cargo.toml': this.scanCargoToml, 'pom.xml': this.scanPomXml, 'build.gradle': this.scanGradle, 'composer.json': this.scanComposerJson, 'Gemfile': this.scanGemfile, 'yarn.lock': this.scanYarnLock, 'package-lock.json': this.scanPackageLock, 'bom.json': this.scanSbom, 'sbom.json': this.scanSbom, 'bom.xml': this.scanSbomXml, 'sbom.xml': this.scanSbomXml }; } async scan(targetPath) { if (!this.jsonMode) { console.log(`Scanning ${targetPath} for dependency confusion opportunities...`); } if (targetPath.startsWith('http://') || targetPath.startsWith('https://')) { await this.scanUrl(targetPath); } else { const isDirectory = (await stat(targetPath)).isDirectory(); if (isDirectory) { await this.scanDirectory(targetPath); } else { await this.scanFile(targetPath); } } this.printResults(); return this.results; } async scanUrl(url) { try { // Convert GitHub blob URLs to raw URLs let rawUrl = url; if (url.includes('github.com') && url.includes('/blob/')) { rawUrl = url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/'); } else if (url.includes('gitlab.com') && url.includes('/blob/')) { rawUrl = url.replace('/blob/', '/raw/'); } const content = await this.fetchUrl(rawUrl); const fileName = this.getFileNameFromUrl(url); if (this.supportedFiles[fileName]) { const scanner = this.supportedFiles[fileName].bind(this); await scanner(url, content); } else { if (!this.jsonMode) { console.error(`Unsupported file type: ${fileName}`); } } } catch (error) { if (!this.jsonMode) { console.error(`Error fetching URL ${url}: ${error.message}`); } } } getFileNameFromUrl(url) { const pathname = new URL(url).pathname; const fileName = pathname.split('/').pop(); return fileName; } fetchUrl(url) { return new Promise((resolve, reject) => { const request = https.get(url, { timeout: 10000 }, (response) => { if (response.statusCode === 200) { let data = ''; response.on('data', chunk => { data += chunk; }); response.on('end', () => { resolve(data); }); } else if (response.statusCode === 302 || response.statusCode === 301) { // Handle redirects this.fetchUrl(response.headers.location).then(resolve).catch(reject); } else { reject(new Error(`HTTP ${response.statusCode}`)); } }); request.on('error', reject); request.on('timeout', () => { request.destroy(); reject(new Error('Request timeout')); }); }); } async scanDirectory(dirPath) { try { const entries = await readdir(dirPath); for (const entry of entries) { const fullPath = path.join(dirPath, entry); const stats = await stat(fullPath); if (stats.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') { await this.scanDirectory(fullPath); } else if (stats.isFile() && this.supportedFiles[entry]) { await this.scanFile(fullPath); } } } catch (error) { if (!this.jsonMode) { console.error(`Error scanning directory ${dirPath}: ${error.message}`); } } } async scanFile(filePath) { const fileName = path.basename(filePath); if (!this.supportedFiles[fileName]) { return; } try { const content = await readFile(filePath, 'utf8'); const scanner = this.supportedFiles[fileName].bind(this); await scanner(filePath, content); } catch (error) { if (!this.jsonMode) { console.error(`Error reading ${filePath}: ${error.message}`); } } } async scanPackageJson(filePath, content) { try { const packageData = JSON.parse(content); const dependencies = { ...packageData.dependencies, ...packageData.devDependencies, ...packageData.peerDependencies, ...packageData.optionalDependencies }; for (const [name, version] of Object.entries(dependencies || {})) { if (this.isPotentiallyVulnerable(name)) { const exists = await this.checkNpmPackageExists(name); this.addResult(filePath, 'npm', name, version, exists); } } } catch (error) { if (!this.jsonMode) { console.error(`Error parsing package.json: ${error.message}`); } } } async scanPackageLock(filePath, content) { try { const lockData = JSON.parse(content); const dependencies = lockData.dependencies || {}; for (const [name, info] of Object.entries(dependencies)) { if (this.isPotentiallyVulnerable(name)) { const exists = await this.checkNpmPackageExists(name); this.addResult(filePath, 'npm', name, info.version, exists); } } } catch (error) { if (!this.jsonMode) { console.error(`Error parsing package-lock.json: ${error.message}`); } } } async scanYarnLock(filePath, content) { const lines = content.split('\n'); const packages = new Set(); for (const line of lines) { const match = line.match(/^"?([^@\s]+)@/); if (match && this.isPotentiallyVulnerable(match[1])) { packages.add(match[1]); } } for (const packageName of packages) { const exists = await this.checkNpmPackageExists(packageName); this.addResult(filePath, 'npm', packageName, 'unknown', exists); } } async scanRequirementsTxt(filePath, content) { const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('-')) { // Match various requirements.txt formats: // package==1.0.0 // package>=1.0.0 // package~=1.0.0 // package!=1.0.0 // package<=1.0.0 // package>1.0.0 // package<1.0.0 // package[extra]==1.0.0 // package let packageName, version = 'unknown'; // Try to match package with version specifiers const versionMatch = trimmed.match(/^([a-zA-Z0-9\-_\.]+(?:\[[^\]]*\])?)\s*([><=!~]+)\s*([^;,\s]+)/); if (versionMatch) { packageName = versionMatch[1].replace(/\[.*\]/, ''); // Remove extras like [dev] const operator = versionMatch[2]; const versionNumber = versionMatch[3]; version = `${operator}${versionNumber}`; } else { // Try to match package without version const packageMatch = trimmed.match(/^([a-zA-Z0-9\-_\.]+)/); if (packageMatch) { packageName = packageMatch[1]; version = 'unknown'; } else { continue; } } if (packageName && this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkPyPiPackageExists(packageName); this.addResult(filePath, 'pypi', packageName, version, exists); } } } } async scanPyprojectToml(filePath, content) { const lines = content.split('\n'); let inDependencies = false; let inOptionalDependencies = false; for (const line of lines) { const trimmed = line.trim(); // Check for dependencies section if (trimmed === 'dependencies = [') { inDependencies = true; inOptionalDependencies = false; continue; } // Check for optional-dependencies section if (trimmed.includes('optional-dependencies') && trimmed.includes('[')) { inOptionalDependencies = true; inDependencies = false; continue; } // Check for end of section if (trimmed === ']' && (inDependencies || inOptionalDependencies)) { inDependencies = false; inOptionalDependencies = false; continue; } // Parse dependencies with version extraction if ((inDependencies || inOptionalDependencies) && trimmed.includes('"')) { // Match patterns like: // "requests>=2.25.1" // "flask==2.0.0" // "django~=4.0" // "requests[security]>=2.25.1" // "requests" let packageName, version = 'unknown'; // Try to match dependency with version const depWithVersionMatch = trimmed.match(/"([a-zA-Z0-9\-_\.]+)(?:\[[^\]]*\])?\s*([><=!~]+)\s*([^"]+)"/); if (depWithVersionMatch) { packageName = depWithVersionMatch[1]; const operator = depWithVersionMatch[2]; const versionNumber = depWithVersionMatch[3]; version = `${operator}${versionNumber}`; } else { // Try to match dependency without version const depMatch = trimmed.match(/"([a-zA-Z0-9\-_\.]+)(?:\[[^\]]*\])?"/); if (depMatch) { packageName = depMatch[1]; version = 'unknown'; } } if (packageName && this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkPyPiPackageExists(packageName); this.addResult(filePath, 'pypi', packageName, version, exists); } } // Also check for [project] dependencies format (single line) if (trimmed.startsWith('dependencies') && trimmed.includes('=') && trimmed.includes('[')) { // Handle single-line dependencies array const depsMatch = trimmed.match(/dependencies\s*=\s*\[(.*)\]/); if (depsMatch) { const depsString = depsMatch[1]; const deps = depsString.split(','); for (const dep of deps) { const cleanDep = dep.trim().replace(/['"]/g, ''); let packageName, version = 'unknown'; // Try to extract version from single-line format const versionMatch = cleanDep.match(/^([a-zA-Z0-9\-_\.]+)(?:\[[^\]]*\])?\s*([><=!~]+)\s*(.+)/); if (versionMatch) { packageName = versionMatch[1]; const operator = versionMatch[2]; const versionNumber = versionMatch[3]; version = `${operator}${versionNumber}`; } else { const packageMatch = cleanDep.match(/^([a-zA-Z0-9\-_\.]+)/); if (packageMatch) { packageName = packageMatch[1]; version = 'unknown'; } } if (packageName && this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkPyPiPackageExists(packageName); this.addResult(filePath, 'pypi', packageName, version, exists); } } } } } } async scanSbom(filePath, content) { try { const sbomData = JSON.parse(content); // Detect SBOM format and extract components if (sbomData.bomFormat === 'CycloneDX') { await this.scanCycloneDx(filePath, sbomData); } else if (sbomData.spdxVersion) { await this.scanSpdx(filePath, sbomData); } else { // Try to auto-detect based on structure if (sbomData.components) { await this.scanCycloneDx(filePath, sbomData); } else if (sbomData.packages) { await this.scanSpdx(filePath, sbomData); } } } catch (error) { if (!this.jsonMode) { console.error(`Error parsing SBOM file: ${error.message}`); } } } async scanCycloneDx(filePath, sbomData) { const components = sbomData.components || []; for (const component of components) { if (component.name && component.type === 'library') { const packageName = component.name; const version = component.version || 'unknown'; const ecosystem = this.detectEcosystemFromPurl(component.purl) || 'unknown'; if (this.isPotentiallyVulnerable(packageName)) { let exists = 'unknown'; // Check package existence based on ecosystem if (ecosystem === 'npm') { exists = await this.checkNpmPackageExists(packageName); } else if (ecosystem === 'pypi') { exists = await this.checkPyPiPackageExists(packageName); } else if (ecosystem === 'cargo') { exists = await this.checkCratesIoPackageExists(packageName); } else if (ecosystem === 'packagist') { exists = await this.checkPackagistPackageExists(packageName); } else if (ecosystem === 'gem') { exists = await this.checkRubyGemsPackageExists(packageName); } else if (ecosystem === 'maven') { exists = await this.checkMavenPackageExists(packageName); } this.addResult(filePath, ecosystem, packageName, version, exists); } } } } async scanSpdx(filePath, sbomData) { const packages = sbomData.packages || []; for (const pkg of packages) { if (pkg.name && pkg.name !== sbomData.name) { const packageName = pkg.name; const version = pkg.versionInfo || 'unknown'; const ecosystem = this.detectEcosystemFromSpdx(pkg) || 'unknown'; if (this.isPotentiallyVulnerable(packageName)) { let exists = 'unknown'; // Check package existence based on ecosystem if (ecosystem === 'npm') { exists = await this.checkNpmPackageExists(packageName); } else if (ecosystem === 'pypi') { exists = await this.checkPyPiPackageExists(packageName); } else if (ecosystem === 'cargo') { exists = await this.checkCratesIoPackageExists(packageName); } else if (ecosystem === 'packagist') { exists = await this.checkPackagistPackageExists(packageName); } else if (ecosystem === 'gem') { exists = await this.checkRubyGemsPackageExists(packageName); } else if (ecosystem === 'maven') { exists = await this.checkMavenPackageExists(packageName); } this.addResult(filePath, ecosystem, packageName, version, exists); } } } } async scanSbomXml(filePath, content) { // Basic XML parsing for CycloneDX XML format const componentRegex = /<component[^>]*type="library"[^>]*>[\s\S]*?<name>([^<]+)<\/name>[\s\S]*?(?:<version>([^<]+)<\/version>)?[\s\S]*?(?:<purl>([^<]+)<\/purl>)?[\s\S]*?<\/component>/g; let match; while ((match = componentRegex.exec(content)) !== null) { const packageName = match[1]; const version = match[2] || 'unknown'; const purl = match[3]; const ecosystem = this.detectEcosystemFromPurl(purl) || 'unknown'; if (this.isPotentiallyVulnerable(packageName)) { let exists = 'unknown'; // Check package existence based on ecosystem if (ecosystem === 'npm') { exists = await this.checkNpmPackageExists(packageName); } else if (ecosystem === 'pypi') { exists = await this.checkPyPiPackageExists(packageName); } else if (ecosystem === 'cargo') { exists = await this.checkCratesIoPackageExists(packageName); } else if (ecosystem === 'packagist') { exists = await this.checkPackagistPackageExists(packageName); } else if (ecosystem === 'gem') { exists = await this.checkRubyGemsPackageExists(packageName); } else if (ecosystem === 'maven') { exists = await this.checkMavenPackageExists(packageName); } this.addResult(filePath, ecosystem, packageName, version, exists); } } } detectEcosystemFromPurl(purl) { if (!purl) return null; if (purl.startsWith('pkg:npm/')) return 'npm'; if (purl.startsWith('pkg:pypi/')) return 'pypi'; if (purl.startsWith('pkg:cargo/')) return 'cargo'; if (purl.startsWith('pkg:composer/')) return 'packagist'; if (purl.startsWith('pkg:gem/')) return 'gem'; if (purl.startsWith('pkg:maven/')) return 'maven'; if (purl.startsWith('pkg:golang/')) return 'go'; return null; } detectEcosystemFromSpdx(pkg) { // Try to detect ecosystem from SPDX package info const downloadLocation = pkg.downloadLocation || ''; const packageFileName = pkg.packageFileName || ''; const homepage = pkg.homepage || ''; if (downloadLocation.includes('npmjs.org') || packageFileName.includes('.tgz')) { return 'npm'; } if (downloadLocation.includes('pypi.org') || downloadLocation.includes('files.pythonhosted.org')) { return 'pypi'; } if (downloadLocation.includes('crates.io')) { return 'cargo'; } if (downloadLocation.includes('packagist.org')) { return 'packagist'; } if (downloadLocation.includes('rubygems.org')) { return 'gem'; } if (downloadLocation.includes('maven') || packageFileName.includes('.jar')) { return 'maven'; } return null; } async scanGoMod(filePath, content) { const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); const requireMatch = trimmed.match(/^\s*([^\s]+)\s+v/); if (requireMatch) { const moduleName = requireMatch[1]; if (this.isPotentiallyVulnerable(moduleName)) { this.addResult(filePath, 'go', moduleName, 'unknown', 'unknown'); } } } } async scanGoSum(filePath, content) { const lines = content.split('\n'); const modules = new Set(); for (const line of lines) { const parts = line.split(' '); if (parts.length >= 2) { const modulePath = parts[0].split('/v')[0]; if (this.isPotentiallyVulnerable(modulePath)) { modules.add(modulePath); } } } for (const moduleName of modules) { this.addResult(filePath, 'go', moduleName, 'unknown', 'unknown'); } } async scanCargoToml(filePath, content) { const lines = content.split('\n'); let inDependencies = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed === '[dependencies]' || trimmed === '[dev-dependencies]') { inDependencies = true; continue; } if (trimmed.startsWith('[') && trimmed !== '[dependencies]' && trimmed !== '[dev-dependencies]') { inDependencies = false; continue; } if (inDependencies && trimmed.includes('=')) { const packageMatch = trimmed.match(/^([a-zA-Z0-9\-_]+)\s*=/); if (packageMatch && this.isPotentiallyVulnerable(packageMatch[1])) { const exists = await this.checkCratesIoPackageExists(packageMatch[1]); this.addResult(filePath, 'crates.io', packageMatch[1], 'unknown', exists); } } } } async scanComposerJson(filePath, content) { try { const composerData = JSON.parse(content); const dependencies = { ...composerData.require, ...composerData['require-dev'] }; for (const [name, version] of Object.entries(dependencies || {})) { if (name !== 'php' && this.isPotentiallyVulnerable(name)) { const exists = await this.checkPackagistPackageExists(name); this.addResult(filePath, 'packagist', name, version, exists); } } } catch (error) { if (!this.jsonMode) { console.error(`Error parsing composer.json: ${error.message}`); } } } async scanGemfile(filePath, content) { const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith('#')) { continue; } // Match various gem declaration formats and capture version: // gem 'name' // gem "name" // gem 'name', 'version' // gem 'name', version: 'x.x.x' // gem 'name', '~> x.x.x' let gemMatch, version = 'unknown'; // Try to match gem with quoted version: gem 'name', 'version' gemMatch = trimmed.match(/gem\s+['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/); if (gemMatch) { const packageName = gemMatch[1]; version = gemMatch[2]; if (this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkRubyGemsPackageExists(packageName); this.addResult(filePath, 'rubygems', packageName, version, exists); } continue; } // Try to match gem with version: syntax: gem 'name', version: 'x.x.x' gemMatch = trimmed.match(/gem\s+['"]([^'"]+)['"]\s*,\s*version:\s*['"]([^'"]+)['"]/); if (gemMatch) { const packageName = gemMatch[1]; version = gemMatch[2]; if (this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkRubyGemsPackageExists(packageName); this.addResult(filePath, 'rubygems', packageName, version, exists); } continue; } // Try to match gem with hash syntax: gem 'name', '~> x.x.x', require: false gemMatch = trimmed.match(/gem\s+['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"](?:\s*,.*)?/); if (gemMatch) { const packageName = gemMatch[1]; version = gemMatch[2]; if (this.isPotentiallyVulnerable(packageName)) { const exists = await this.checkRubyGemsPackageExists(packageName); this.addResult(filePath, 'rubygems', packageName, version, exists); } continue; } // Fall back to simple gem declaration without version gemMatch = trimmed.match(/gem\s+['"]([^'"]+)['"](?:\s*$|\s*,\s*(?!['"]))/); if (gemMatch && this.isPotentiallyVulnerable(gemMatch[1])) { const exists = await this.checkRubyGemsPackageExists(gemMatch[1]); this.addResult(filePath, 'rubygems', gemMatch[1], 'unknown', exists); } } } async scanPomXml(filePath, content) { // First, find all dependency blocks const dependencyBlocks = content.match(/<dependency[^>]*>[\s\S]*?<\/dependency>/g) || []; for (const block of dependencyBlocks) { // Extract groupId, artifactId, and version from each block const groupIdMatch = block.match(/<groupId>([^<]+)<\/groupId>/); const artifactIdMatch = block.match(/<artifactId>([^<]+)<\/artifactId>/); const versionMatch = block.match(/<version>([^<]+)<\/version>/); if (groupIdMatch && artifactIdMatch) { const groupId = groupIdMatch[1].trim(); const artifactId = artifactIdMatch[1].trim(); const version = versionMatch ? versionMatch[1].trim() : 'unknown'; const fullName = `${groupId}:${artifactId}`; if (this.isPotentiallyVulnerable(artifactId)) { const exists = await this.checkMavenPackageExists(fullName); this.addResult(filePath, 'maven', fullName, version, exists); } } } } async scanGradle(filePath, content) { const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Match various Gradle dependency formats with versions: // implementation 'group:artifact:version' // implementation "group:artifact:version" // implementation group: 'group', name: 'artifact', version: 'version' let depMatch, groupId, artifactId, version = 'unknown'; // Try to match standard format: implementation 'group:artifact:version' depMatch = trimmed.match(/(?:implementation|compile|api|testImplementation|runtimeOnly|compileOnly|testCompileOnly|testRuntimeOnly)\s+['"]([^'"]+)['"]/); if (depMatch) { const parts = depMatch[1].split(':'); if (parts.length >= 2) { groupId = parts[0]; artifactId = parts[1]; version = parts[2] || 'unknown'; const fullName = parts.length >= 3 ? `${groupId}:${artifactId}` : depMatch[1]; if (this.isPotentiallyVulnerable(artifactId)) { const exists = await this.checkMavenPackageExists(fullName); this.addResult(filePath, 'maven', fullName, version, exists); } } continue; } // Try to match map syntax: implementation group: 'group', name: 'artifact', version: 'version' const mapMatch = trimmed.match(/(?:implementation|compile|api|testImplementation|runtimeOnly|compileOnly|testCompileOnly|testRuntimeOnly)\s+group:\s*['"]([^'"]+)['"],?\s*name:\s*['"]([^'"]+)['"](?:,?\s*version:\s*['"]([^'"]+)['"])?/); if (mapMatch) { groupId = mapMatch[1]; artifactId = mapMatch[2]; version = mapMatch[3] || 'unknown'; const fullName = `${groupId}:${artifactId}`; if (this.isPotentiallyVulnerable(artifactId)) { const exists = await this.checkMavenPackageExists(fullName); this.addResult(filePath, 'maven', fullName, version, exists); } continue; } // Try to match alternative map syntax with different order const altMapMatch = trimmed.match(/(?:implementation|compile|api|testImplementation|runtimeOnly|compileOnly|testCompileOnly|testRuntimeOnly)\s+name:\s*['"]([^'"]+)['"],?\s*group:\s*['"]([^'"]+)['"](?:,?\s*version:\s*['"]([^'"]+)['"])?/); if (altMapMatch) { artifactId = altMapMatch[1]; groupId = altMapMatch[2]; version = altMapMatch[3] || 'unknown'; const fullName = `${groupId}:${artifactId}`; if (this.isPotentiallyVulnerable(artifactId)) { const exists = await this.checkMavenPackageExists(fullName); this.addResult(filePath, 'maven', fullName, version, exists); } } } } isPotentiallyVulnerable(packageName) { if (packageName.includes('://') || packageName.startsWith('git+')) { return false; } // For scoped packages, we still want to check them, but with different logic if (packageName.startsWith('@')) { // Only check scoped packages that look suspicious const scopedSuspiciousPatterns = [ /^@[a-z0-9]+\/[a-z0-9]{3,8}$/i, // short package names in scope /^@[a-z0-9]+\/(lib|utils?|helper|common|core|base|tools?|sdk)$/i, // generic names /^@[a-z0-9]+\/(test|demo|example|sample)[-_]?/i, // test/demo packages ]; return scopedSuspiciousPatterns.some(pattern => pattern.test(packageName)); } const suspiciousPatterns = [ /^[a-z0-9]+[-_][a-z0-9]+$/i, // patterns like "6mile-test", "test-utils", etc. /^[a-z0-9]{3,8}$/i, // short alphanumeric names /^(lib|utils?|helper|common|core|base|tools?|sdk)$/i, // generic names /^(test|demo|example|sample)[-_]?/i, // test/demo packages /^[a-z0-9]*[-_]?(test|demo|example|sample)$/i, // packages ending with test/demo /^[0-9]+[a-z]+[-_]?[a-z0-9]*$/i, // packages starting with numbers like "6mile" ]; const wellKnownPackages = [ 'react', 'vue', 'angular', 'lodash', 'express', 'axios', 'moment', 'jquery', 'bootstrap', 'webpack', 'babel', 'eslint', 'jest', 'mocha', 'typescript', 'commander', 'chalk', 'inquirer', 'yargs', 'fs-extra', 'rimraf', 'glob', 'mkdirp', 'debug', 'semver', 'uuid', 'cors', 'dotenv', 'nodemon', 'concurrently', 'cross-env', 'husky', 'lint-staged', 'rails', 'sqlite3', 'pg', 'mysql2', 'puma', 'sidekiq', 'devise', 'rspec' ]; if (wellKnownPackages.includes(packageName.toLowerCase())) { return false; } return suspiciousPatterns.some(pattern => pattern.test(packageName)) || packageName.length <= 4; } async checkNpmPackageExists(packageName) { const encodedPackageName = packageName.startsWith('@') ? packageName.replace('@', '%40') : packageName; return this.makeHttpRequest(`https://registry.npmjs.org/${encodedPackageName}`) .then(() => true) .catch(() => false); } async checkPyPiPackageExists(packageName) { return this.makeHttpRequest(`https://pypi.org/pypi/${packageName}/json`) .then(() => true) .catch(() => false); } async checkCratesIoPackageExists(packageName) { return this.makeHttpRequest(`https://crates.io/api/v1/crates/${packageName}`) .then(() => true) .catch(() => false); } async checkPackagistPackageExists(packageName) { return this.makeHttpRequest(`https://packagist.org/packages/${packageName}.json`) .then(() => true) .catch(() => false); } async checkRubyGemsPackageExists(packageName) { // Clean the package name - remove any version constraints or other characters const cleanPackageName = packageName.replace(/[<>=!~\s]/g, '').split(',')[0].trim(); try { await this.makeHttpRequest(`https://rubygems.org/api/v1/gems/${cleanPackageName}.json`); return true; } catch (error) { // Try alternative endpoint format try { await this.makeHttpRequest(`https://rubygems.org/api/v1/versions/${cleanPackageName}.json`); return true; } catch (alternativeError) { return false; } } } async checkMavenPackageExists(packageName) { // Parse groupId:artifactId format let groupId, artifactId; if (packageName.includes(':')) { const parts = packageName.split(':'); groupId = parts[0]; artifactId = parts[1]; } else { // If only artifact name is provided, we can't reliably check Maven Central // Return 'unknown' for single artifact names without group return 'unknown'; } // Convert groupId dots to slashes for Maven Central API const groupPath = groupId.replace(/\./g, '/'); // Try Maven Central Search API first (more reliable) try { const searchUrl = `https://search.maven.org/solrsearch/select?q=g:"${groupId}"+AND+a:"${artifactId}"&rows=1&wt=json`; const response = await this.makeHttpRequest(searchUrl); // If we get here, the request succeeded, but we need to check the response return new Promise((resolve) => { let data = ''; response.on('data', chunk => { data += chunk; }); response.on('end', () => { try { const searchResult = JSON.parse(data); const found = searchResult.response && searchResult.response.numFound && searchResult.response.numFound > 0; resolve(found); } catch (e) { resolve(false); } }); }); } catch (searchError) { // Fall back to direct Maven Central repository check try { const repoUrl = `https://repo1.maven.org/maven2/${groupPath}/${artifactId}/maven-metadata.xml`; await this.makeHttpRequest(repoUrl); return true; } catch (repoError) { // Try alternative: check if group directory exists try { const groupUrl = `https://repo1.maven.org/maven2/${groupPath}/`; await this.makeHttpRequest(groupUrl); // Group exists, but artifact might not - return false for dependency confusion potential return false; } catch (groupError) { // Neither group nor artifact found return false; } } } } makeHttpRequest(url) { return new Promise((resolve, reject) => { const request = https.get(url, { timeout: 5000 }, (response) => { if (response.statusCode === 200) { resolve(response); } else { reject(new Error(`Status: ${response.statusCode}`)); } }); request.on('error', reject); request.on('timeout', () => { request.destroy(); reject(new Error('Request timeout')); }); }); } addResult(filePath, ecosystem, packageName, version, exists) { const risk = exists === false ? 'HIGH' : exists === true ? 'LOW' : 'UNKNOWN'; this.results.push({ file: filePath, ecosystem, package: packageName, version, exists, risk }); } printResults() { if (this.jsonMode) { const vulnerabilities = this.results .filter(r => r.risk === 'HIGH' || r.risk === 'UNKNOWN'); if (vulnerabilities.length > 0) { const packages = vulnerabilities.map(result => ({ [result.package]: result.version })); const output = { "name": "super-confused", "description": "Identify dependeny confusion in your source code", "author": "6mile", "dependency-confused-packages": packages }; console.log(JSON.stringify(output, null, 2)); } return; } const vulnerabilities = this.results.filter(r => r.risk === 'HIGH' || r.risk === 'UNKNOWN'); if (vulnerabilities.length === 0) { console.log('No Dependency Confusion Opportunities found.'); return; } vulnerabilities.forEach(result => { console.log(''); console.log('DEPENDENCY CONFUSION OPPORTUNITY!'); console.log(`${result.package} (${result.ecosystem}) in ${result.file}`); console.log(`Version: ${result.version}`); }); } } async function main() { const args = process.argv.slice(2); let jsonMode = false; let targetPath; if (args.includes('--json')) { jsonMode = true; targetPath = args.find(arg => arg !== '--json'); } else { targetPath = args[0]; } if (!targetPath) { console.log('Usage: super-confused [--json] <path|url>'); console.log(''); console.log('Examples:'); console.log(' super-confused ./package.json'); console.log(' super-confused --json ./my-project'); console.log(' super-confused https://github.com/user/repo/blob/main/package.json'); console.log(' super-confused .'); process.exit(1); } if (!targetPath.startsWith('http://') && !targetPath.startsWith('https://') && !fs.existsSync(targetPath)) { if (!jsonMode) { console.error(`Path does not exist: ${targetPath}`); } process.exit(1); } const scanner = new SuperConfused(jsonMode); try { await scanner.scan(targetPath); } catch (error) { if (!jsonMode) { console.error(`Scan failed: ${error.message}`); } process.exit(1); } } module.exports = SuperConfused; if (require.main === module) { main().catch(console.error); }