UNPKG

am-i-secure

Version:

A CLI tool to detect malicious npm packages in your project dependencies

193 lines (169 loc) 7.27 kB
const fs = require('fs'); const path = require('path'); /** * Scan node_modules directory recursively to find all packages * @param {string} projectDir - The project directory containing node_modules * @param {boolean} recursive - Whether to scan node_modules in subdirectories too * @returns {Object[]} - Array of package objects with name, version, and path info */ function scanNodeModules(projectDir, recursive = false) { const packages = []; const scannedPackages = new Set(); // Avoid duplicates if (!recursive) { // Original behavior - scan only immediate node_modules const nodeModulesPath = path.join(projectDir, 'node_modules'); if (fs.existsSync(nodeModulesPath)) { scanNodeModulesDirectory(nodeModulesPath, packages, scannedPackages, projectDir); } return packages; } // Recursive behavior - scan node_modules in all subdirectories function findAndScanNodeModules(dir) { try { const nodeModulesPath = path.join(dir, 'node_modules'); if (fs.existsSync(nodeModulesPath)) { scanNodeModulesDirectory(nodeModulesPath, packages, scannedPackages, projectDir); } // Recursively check subdirectories const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { const subDir = path.join(dir, entry.name); findAndScanNodeModules(subDir); } } } catch (error) { // Skip directories that can't be read return; } } findAndScanNodeModules(projectDir); return packages; } /** * Scan a specific node_modules directory * @param {string} nodeModulesPath - Path to node_modules directory * @param {Array} packages - Array to collect packages * @param {Set} scannedPackages - Set to track scanned packages * @param {string} projectDir - Original project directory for relative path calculations */ function scanNodeModulesDirectory(nodeModulesPath, packages, scannedPackages, projectDir) { function scanDirectory(dirPath, parentPackage = null) { try { const entries = fs.readdirSync(dirPath); for (const entry of entries) { const entryPath = path.join(dirPath, entry); try { const stat = fs.statSync(entryPath); if (stat.isDirectory()) { // Handle scoped packages if (entry.startsWith('@')) { const scopedEntries = fs.readdirSync(entryPath); for (const scopedEntry of scopedEntries) { const scopedPath = path.join(entryPath, scopedEntry); const scopedStat = fs.statSync(scopedPath); if (scopedStat.isDirectory()) { const packageName = `${entry}/${scopedEntry}`; scanPackage(scopedPath, packageName, parentPackage); } } } else { // Regular package scanPackage(entryPath, entry, parentPackage); } } } catch (error) { // Skip entries that can't be read (permissions, etc.) continue; } } } catch (error) { // Skip directories that can't be read return; } } function scanPackage(packagePath, packageName, parentPackage) { const packageJsonPath = path.join(packagePath, 'package.json'); const packageKey = `${packageName}@${packagePath}`; // Avoid scanning the same package multiple times if (scannedPackages.has(packageKey)) { return; } scannedPackages.add(packageKey); try { if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packages.push({ name: packageJson.name || packageName, version: packageJson.version || 'unknown', path: packagePath, packageJsonPath: packageJsonPath, parentPackage: parentPackage, isDirect: isDirectDependency(packagePath, projectDir) }); // Recursively scan nested node_modules const nestedNodeModules = path.join(packagePath, 'node_modules'); if (fs.existsSync(nestedNodeModules)) { scanDirectory(nestedNodeModules, packageJson.name || packageName); } } } catch (error) { // Skip packages with invalid package.json return; } } // Start scanning from the main node_modules directory scanDirectory(nodeModulesPath); } /** * Determine if a package is a direct dependency (in root node_modules) * @param {string} packagePath - Path to the package * @param {string} projectDir - Project root directory * @returns {boolean} - True if it's a direct dependency */ function isDirectDependency(packagePath, projectDir) { const nodeModulesPath = path.join(projectDir, 'node_modules'); const relativePath = path.relative(nodeModulesPath, packagePath); // Direct dependency if the path doesn't contain additional node_modules return !relativePath.includes('node_modules'); } /** * Get package information from a specific path * @param {string} packagePath - Path to the package directory * @returns {Object|null} - Package info or null if not found */ function getPackageInfo(packagePath) { const packageJsonPath = path.join(packagePath, 'package.json'); try { if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); return { name: packageJson.name, version: packageJson.version, description: packageJson.description, dependencies: packageJson.dependencies || {}, devDependencies: packageJson.devDependencies || {} }; } } catch (error) { return null; } return null; } /** * Find all versions of a specific package in node_modules * @param {string} projectDir - Project directory * @param {string} packageName - Package name to search for * @param {boolean} recursive - Whether to scan node_modules in subdirectories too * @returns {Object[]} - Array of package instances with versions and paths */ function findPackageVersions(projectDir, packageName, recursive = false) { const packages = scanNodeModules(projectDir, recursive); return packages.filter(pkg => pkg.name === packageName); } module.exports = { scanNodeModules, getPackageInfo, findPackageVersions, isDirectDependency };