am-i-secure
Version:
A CLI tool to detect malicious npm packages in your project dependencies
193 lines (169 loc) • 7.27 kB
JavaScript
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
};