nullvoid
Version:
Detect malicious code
581 lines • 28.1 kB
JavaScript
"use strict";
/**
* Main scan module for NullVoid
* Migrated from scan.js to TypeScript
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scan = scan;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
// Import configuration
// import { CACHE_CONFIG } from './lib/config';
// Import utilities
const nullvoidDetection_1 = require("./lib/nullvoidDetection");
const secureErrorHandler_1 = require("./lib/secureErrorHandler");
const detection_1 = require("./lib/detection");
const packageAnalysis_1 = require("./lib/packageAnalysis");
const dependencyTree_1 = require("./lib/dependencyTree");
const fileSystemUtils_1 = require("./lib/fileSystemUtils");
// Import missing functions
const missingFunctions_1 = require("./lib/missingFunctions");
const analysisFunctions_1 = require("./lib/analysisFunctions");
/**
* Main scan function that performs heuristic checks on npm packages
*/
async function scan(packageName, options = {}, progressCallback) {
const startTime = Date.now();
// Reset performance metrics
(0, missingFunctions_1.resetPerformanceMetrics)();
(0, missingFunctions_1.updatePerformanceMetrics)({ startTime });
// Validate inputs
try {
if (packageName) {
packageName = secureErrorHandler_1.InputValidator.validatePackageName(packageName);
}
// Validate scan options
const validatedOptions = secureErrorHandler_1.InputValidator.validateScanOptions(options);
options = { ...options, ...validatedOptions };
}
catch (error) {
throw new Error(`Invalid scan parameters: ${error.message}`);
}
const threats = [];
let filesScanned = 0;
let packagesScanned = 0;
let directoryStructure;
try {
// If no package specified, scan current directory
if (!packageName) {
const directoryResult = await scanDirectory(process.cwd(), options, progressCallback);
threats.push(...directoryResult.threats);
filesScanned = directoryResult.filesScanned;
packagesScanned = directoryResult.packagesScanned || 0;
directoryStructure = directoryResult.directoryStructure;
// Also scan any package.json files found in the directory
const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
if (fs_1.default.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
if (Object.keys(dependencies).length > 0) {
const maxDepth = options.maxDepth || 3;
// Use parallel processing if enabled and multiple dependencies
const useParallel = options.parallel !== false && Object.keys(dependencies).length > 1;
let treeResult;
if (useParallel) {
try {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTreeParallel)(dependencies, maxDepth, options, 'root');
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Parallel processing failed, falling back to sequential: ${error}`);
}
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
}
else {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
threats.push(...treeResult.threats);
packagesScanned += treeResult.packagesScanned;
}
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Could not parse package.json: ${error}`);
}
}
}
// Scan node_modules if it exists
const nodeModulesPath = path_1.default.join(process.cwd(), 'node_modules');
if (fs_1.default.existsSync(nodeModulesPath)) {
const nodeModulesThreats = await (0, fileSystemUtils_1.scanNodeModules)(nodeModulesPath, options);
threats.push(...nodeModulesThreats);
}
// Get suspicious files
const suspiciousFiles = await (0, missingFunctions_1.getSuspiciousFiles)(process.cwd());
for (const file of suspiciousFiles) {
threats.push({
type: 'SUSPICIOUS_FILE',
severity: 'HIGH',
package: file,
message: 'Suspicious file detected',
details: `File name or content suggests malicious intent: ${path_1.default.basename(file)}`
});
}
}
else if (fs_1.default.existsSync(packageName) && fs_1.default.statSync(packageName).isDirectory()) {
// Scan directory
const directoryResult = await scanDirectory(packageName, options, progressCallback);
threats.push(...directoryResult.threats);
filesScanned = directoryResult.filesScanned;
packagesScanned = directoryResult.packagesScanned || 0;
directoryStructure = directoryResult.directoryStructure;
// Scan node_modules if it exists
const nodeModulesPath = path_1.default.join(packageName, 'node_modules');
if (fs_1.default.existsSync(nodeModulesPath)) {
const nodeModulesThreats = await (0, fileSystemUtils_1.scanNodeModules)(nodeModulesPath, options);
threats.push(...nodeModulesThreats);
}
// Get suspicious files
const suspiciousFiles = await (0, missingFunctions_1.getSuspiciousFiles)(packageName);
for (const file of suspiciousFiles) {
threats.push({
type: 'SUSPICIOUS_FILE',
severity: 'HIGH',
package: file,
message: 'Suspicious file detected',
details: `File name or content suggests malicious intent: ${path_1.default.basename(file)}`
});
}
}
else if (fs_1.default.existsSync(packageName) && fs_1.default.statSync(packageName).isFile()) {
// Scan individual file
const fileThreats = await scanFile(packageName, options);
threats.push(...fileThreats);
filesScanned = 1;
// Additional analysis for the file
try {
const content = fs_1.default.readFileSync(packageName, 'utf8');
// Only analyze if not NullVoid's own code
if (!(0, nullvoidDetection_1.isNullVoidCode)(packageName) && !(0, nullvoidDetection_1.isTestFile)(packageName)) {
const codeStructureThreats = (0, analysisFunctions_1.analyzeCodeStructure)(content, packageName);
threats.push(...codeStructureThreats);
const astThreats = (0, analysisFunctions_1.analyzeJavaScriptAST)(content, packageName);
threats.push(...astThreats);
const fsThreats = (0, analysisFunctions_1.analyzeFsUsageContext)(content, packageName);
threats.push(...fsThreats);
}
}
catch (error) {
// Skip binary files
}
}
else {
// Try to scan as npm package
const packageThreats = await scanPackage(packageName, 'latest', options);
threats.push(...packageThreats);
packagesScanned = 1;
// Get package metadata for additional analysis
try {
const packageData = await (0, missingFunctions_1.getPackageMetadata)(packageName, 'latest');
if (packageData) {
// Analyze package tarball
const tarballThreats = await (0, analysisFunctions_1.analyzePackageTarball)(packageData);
threats.push(...tarballThreats);
// Download and analyze package files
const packageFiles = await (0, missingFunctions_1.downloadPackageFiles)(packageData);
if (packageFiles) {
const contentThreats = (0, analysisFunctions_1.analyzeCodeStructure)(packageFiles, packageName);
threats.push(...contentThreats);
}
}
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Could not analyze package metadata for ${packageName}: ${error}`);
}
}
// Also check if it's a local package with dependencies
const packageJsonPath = path_1.default.join(packageName, 'package.json');
if (fs_1.default.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
if (Object.keys(dependencies).length > 0) {
const maxDepth = options.maxDepth || 3;
// Use parallel processing if enabled and multiple dependencies
const useParallel = options.parallel !== false && Object.keys(dependencies).length > 1;
let treeResult;
if (useParallel) {
try {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTreeParallel)(dependencies, maxDepth, options, 'root');
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Parallel processing failed, falling back to sequential: ${error}`);
}
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
}
else {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
threats.push(...treeResult.threats);
packagesScanned += treeResult.packagesScanned;
}
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Could not parse package.json: ${error}`);
}
}
}
}
// Deduplicate threats to match original JavaScript behavior
const uniqueThreats = deduplicateThreats(threats);
// Calculate performance metrics
const endTime = Date.now();
const performanceMetricsData = (0, missingFunctions_1.getPerformanceMetrics)();
const performance = {
scanTime: endTime - startTime,
staticAnalysisTime: endTime - startTime,
sandboxAnalysisTime: 0,
memoryUsage: process.memoryUsage().heapUsed,
cpuUsage: 0,
packagesScanned: performanceMetricsData.packagesScanned,
cacheHits: performanceMetricsData.cacheHits,
cacheMisses: performanceMetricsData.cacheMisses,
cacheHitRate: performanceMetricsData.cacheHits / (performanceMetricsData.cacheHits + performanceMetricsData.cacheMisses) || 0,
networkRequests: performanceMetricsData.networkRequests,
errors: performanceMetricsData.errors,
packagesPerSecond: performanceMetricsData.packagesScanned / ((endTime - startTime) / 1000) || 0,
duration: endTime - startTime
};
// Create scan metadata
const metadata = {
startTime: new Date(startTime),
endTime: new Date(endTime),
version: '1.3.18',
nodeVersion: process.version,
platform: process.platform,
target: packageName || process.cwd()
};
return {
threats: uniqueThreats,
filesScanned,
packagesScanned,
directoryStructure: directoryStructure || {
directories: [],
files: [],
totalDirectories: 0,
totalFiles: 0
},
performance,
metadata
};
}
catch (error) {
throw new Error(`Scan failed: ${error.message}`);
}
}
/**
* Scan a directory for JavaScript files and suspicious patterns
*/
async function scanDirectory(dirPath, options, progressCallback) {
const threats = [];
let filesScanned = 0;
const directoryStructure = {
directories: [],
files: [],
totalDirectories: 0,
totalFiles: 0
};
try {
// Get all JavaScript files in the directory
const jsFiles = await getJavaScriptFiles(dirPath);
// Remove duplicates to prevent processing the same file twice
const uniqueFiles = [...new Set(jsFiles)];
// Track processed files to prevent duplicates
const processedFiles = new Set();
for (const filePath of uniqueFiles) {
// Skip if already processed
if (processedFiles.has(filePath)) {
continue;
}
processedFiles.add(filePath);
try {
filesScanned++;
// Update progress callback if provided
if (progressCallback) {
progressCallback(filePath);
}
// Use the sophisticated scanFile function for each file
const fileThreats = await scanFile(filePath, options);
threats.push(...fileThreats);
// Additional analysis for each file
try {
const content = fs_1.default.readFileSync(filePath, 'utf8');
// Only analyze if not NullVoid's own code
if (!(0, nullvoidDetection_1.isNullVoidCode)(filePath) && !(0, nullvoidDetection_1.isTestFile)(filePath)) {
// Code structure analysis
const codeStructureThreats = (0, analysisFunctions_1.analyzeCodeStructure)(content, filePath);
threats.push(...codeStructureThreats);
// AST analysis
const astThreats = (0, analysisFunctions_1.analyzeJavaScriptAST)(content, filePath);
threats.push(...astThreats);
// File system usage analysis
const fsThreats = (0, analysisFunctions_1.analyzeFsUsageContext)(content, filePath);
threats.push(...fsThreats);
}
}
catch (error) {
// Skip binary files
}
// Add to directory structure
directoryStructure.files.push(path_1.default.basename(filePath));
directoryStructure.totalFiles++;
}
catch (error) {
console.warn(`Warning: Could not analyze ${filePath}: ${error.message}`);
}
}
// Scan subdirectories
const items = fs_1.default.readdirSync(dirPath);
for (const item of items) {
const itemPath = path_1.default.join(dirPath, item);
const stats = fs_1.default.statSync(itemPath);
if (stats.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
directoryStructure.directories.push(item);
directoryStructure.totalDirectories++;
// Recursively scan subdirectory
const subResult = await scanDirectory(itemPath, options, progressCallback);
threats.push(...subResult.threats);
filesScanned += subResult.filesScanned;
// Scan node_modules in subdirectory if it exists
const nodeModulesPath = path_1.default.join(itemPath, 'node_modules');
if (fs_1.default.existsSync(nodeModulesPath)) {
const nodeModulesThreats = await (0, fileSystemUtils_1.scanNodeModules)(nodeModulesPath, options);
threats.push(...nodeModulesThreats);
}
// Get suspicious files in subdirectory
const suspiciousFiles = await (0, missingFunctions_1.getSuspiciousFiles)(itemPath);
for (const file of suspiciousFiles) {
threats.push({
type: 'SUSPICIOUS_FILE',
severity: 'HIGH',
package: file,
message: 'Suspicious file detected',
details: `File name or content suggests malicious intent: ${path_1.default.basename(file)}`
});
}
}
}
}
catch (error) {
console.warn(`Warning: Could not scan directory ${dirPath}: ${error.message}`);
}
// Also scan any package.json files found in the directory for dependencies
const packageJsonPath = path_1.default.join(dirPath, 'package.json');
if (fs_1.default.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
if (Object.keys(dependencies).length > 0) {
const maxDepth = options.maxDepth || 3;
// Use parallel processing if enabled and multiple dependencies
const useParallel = options.parallel !== false && Object.keys(dependencies).length > 1;
let treeResult;
if (useParallel) {
try {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTreeParallel)(dependencies, maxDepth, options, 'root');
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Parallel processing failed, falling back to sequential: ${error}`);
}
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
}
else {
treeResult = await (0, dependencyTree_1.buildAndScanDependencyTree)(dependencies, maxDepth, options);
}
threats.push(...treeResult.threats);
// Update packagesScanned in the return value
return {
threats,
filesScanned,
packagesScanned: treeResult.packagesScanned,
directoryStructure
};
}
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Could not parse package.json: ${error}`);
}
}
}
// Check for suspicious files in the directory
const suspiciousFiles = await (0, missingFunctions_1.getSuspiciousFiles)(dirPath);
for (const file of suspiciousFiles) {
threats.push({
type: 'SUSPICIOUS_FILE',
severity: 'HIGH',
package: file,
message: 'Suspicious file detected',
details: `File name or content suggests malicious intent: ${path_1.default.basename(file)}`
});
}
// Also scan node_modules if it exists
const nodeModulesPath = path_1.default.join(dirPath, 'node_modules');
if (fs_1.default.existsSync(nodeModulesPath)) {
try {
const nodeModulesThreats = await (0, fileSystemUtils_1.scanNodeModules)(nodeModulesPath, options);
threats.push(...nodeModulesThreats);
}
catch (error) {
if (options.verbose) {
console.warn(`Warning: Could not scan node_modules: ${error}`);
}
}
}
return {
threats,
filesScanned,
packagesScanned: 0,
directoryStructure
};
}
/**
* Scan a single file for threats
*/
async function scanFile(filePath, _options) {
const threats = [];
try {
const content = fs_1.default.readFileSync(filePath, 'utf8');
const fileName = path_1.default.basename(filePath);
const absolutePath = path_1.default.resolve(filePath); // Use absolute path
// Check if it's a JavaScript file
if (fileName.endsWith('.js') || fileName.endsWith('.mjs') || fileName.endsWith('.ts')) {
try {
// Note: Original JavaScript version doesn't call detectWalletHijacking for directory scans
// Commenting out to match original behavior exactly
// const walletThreats = detectWalletHijacking(content, fileName);
// threats.push(...walletThreats.map(threat => ({
// ...threat,
// type: threat.type as any,
// severity: threat.severity as any,
// package: absolutePath
// })));
// Critical: Add malicious code structure analysis
const codeAnalysis = (0, detection_1.analyzeCodeStructure)(content);
if (codeAnalysis.isMalicious && !(0, nullvoidDetection_1.isNullVoidCode)(absolutePath) && !(0, nullvoidDetection_1.isTestFile)(absolutePath)) {
const threat = {
type: 'MALICIOUS_CODE_STRUCTURE',
message: 'Code structure indicates malicious obfuscated content',
package: absolutePath, // Use absolute path
severity: 'CRITICAL',
details: codeAnalysis.reason
};
if (codeAnalysis.lineNumber !== undefined) {
threat.lineNumber = codeAnalysis.lineNumber;
}
if (codeAnalysis.sampleCode !== undefined) {
threat.sampleCode = codeAnalysis.sampleCode;
}
threats.push(threat);
}
// Note: Original JavaScript version doesn't call detectObfuscatedIoCs for directory scans
// Commenting out to match original behavior exactly
// const iocThreats = detectObfuscatedIoCs(content, absolutePath);
// threats.push(...iocThreats.map(threat => ({
// ...threat,
// type: threat.type as any,
// severity: threat.severity as any,
// package: absolutePath
// })));
// Note: Original JavaScript version doesn't call detectDynamicRequires for directory scans
// Commenting out to match original behavior exactly
// const requireThreats = detectDynamicRequires(content, absolutePath);
// threats.push(...requireThreats.map(threat => ({
// ...threat,
// type: threat.type as any,
// severity: threat.severity as any,
// package: absolutePath
// })));
// Note: Original JavaScript version doesn't call analyzeJavaScriptAST for directory scans
// Commenting out to match original behavior exactly
// const astThreats = analyzeJavaScriptAST(content, absolutePath);
// const filteredAstThreats = astThreats.filter(threat => threat.type !== 'MALICIOUS_CODE_STRUCTURE');
// threats.push(...filteredAstThreats.map(threat => ({
// ...threat,
// type: threat.type as any,
// severity: threat.severity as any,
// package: absolutePath
// })));
// Basic threat detection as fallback
if (content.includes('eval(') && !(0, nullvoidDetection_1.isNullVoidCode)(absolutePath) && !(0, nullvoidDetection_1.isTestFile)(absolutePath)) {
threats.push({
type: 'MALICIOUS_CODE_STRUCTURE',
message: 'Code contains eval() function',
package: absolutePath, // Use absolute path
severity: 'HIGH',
details: 'Code contains eval() which can be used for code injection',
confidence: 0.8
});
}
if (content.includes('require(') && content.includes('fs') && !(0, nullvoidDetection_1.isNullVoidCode)(absolutePath) && !(0, nullvoidDetection_1.isTestFile)(absolutePath)) {
threats.push({
type: 'SUSPICIOUS_MODULE',
message: 'Code requires fs module',
package: absolutePath, // Use absolute path
severity: 'CRITICAL',
details: 'Code requires fs module which can be used for file system access',
confidence: 0.9
});
}
}
catch (error) {
console.warn(`Warning: Could not analyze file ${filePath}: ${error.message}`);
}
}
else {
// Non-JavaScript file - check for suspicious patterns safely
try {
// Check for obfuscated patterns even in non-JS files (placeholder)
// const iocThreats = checkObfuscatedIoCs(content, fileName);
// threats.push(...iocThreats);
}
catch (error) {
console.warn(`Warning: Could not analyze file ${filePath}: ${error.message}`);
}
}
}
catch (error) {
console.warn(`Warning: Could not analyze file ${filePath}: ${error.message}`);
}
return threats;
}
/**
* Scan an npm package
*/
async function scanPackage(packageName, version, options) {
return await (0, packageAnalysis_1.scanPackage)(packageName, version, options);
}
/**
* Get all JavaScript files in a directory recursively
*/
async function getJavaScriptFiles(dirPath) {
return await (0, fileSystemUtils_1.findJavaScriptFiles)(dirPath);
}
/**
* Deduplicate threats to match original JavaScript behavior
* Keeps only unique threats based on type, package, and details
*/
function deduplicateThreats(threats) {
const seen = new Set();
const uniqueThreats = [];
for (const threat of threats) {
// Create a unique key based on type, package, and key details
const key = `${threat.type}:${threat.package}:${threat.message}`;
if (!seen.has(key)) {
seen.add(key);
uniqueThreats.push(threat);
}
}
return uniqueThreats;
}
//# sourceMappingURL=scan.js.map