UNPKG

@neurolint/cli

Version:

NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support

605 lines (530 loc) 20.9 kB
/** * Copyright (c) 2025 NeuroLint * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Layer 8: Security Forensics - Dependency Differ * * Analyzes package.json and lock files to detect suspicious dependency changes, * potential typosquatting, and supply chain attacks. * * IMPORTANT: Layer 8 is READ-ONLY by default. It detects but does not transform * unless explicitly requested (quarantine mode). This follows the NeuroLint * principle of "never break code". */ 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { SEVERITY_LEVELS, IOC_CATEGORIES } = require('../constants'); class DependencyDiffer { constructor(options = {}) { this.verbose = options.verbose || false; this.checkTyposquatting = options.checkTyposquatting !== false; this.checkIntegrity = options.checkIntegrity !== false; this.knownMalicious = this.loadKnownMaliciousPackages(); this.popularPackages = this.loadPopularPackages(); } loadKnownMaliciousPackages() { return new Set([ 'event-stream', 'flatmap-stream', 'electron-native-notify', 'eslint-scope', 'ua-parser-js', 'coa', 'rc', 'colors', 'faker', 'node-ipc', 'peacenotwar', 'primereact', '@pika/pack', 'okhsa', 'klow', 'klown', 'crossenv', 'cross-env.js', 'crossenv.js', 'mongose', 'moogose', 'lodahs', 'lodasg', 'babelcli', 'bable-cli', 'd3.js', 'fabric-js', 'ffmpeg.js', 'gruntcli', 'http-proxy.js', 'jquery.js', 'mariadb', 'mssql.js', 'mssql-node', 'mysqljs', 'node-fabric', 'node-opencv', 'node-opensl', 'node-openssl', 'node-tkinter', 'nodecaffe', 'nodefabric', 'nodeffmpeg', 'nodemailer-js', 'noderequest', 'nodesass', 'nodesqlite', 'opencv.js', 'openssl.js', 'proxy.js', 'shadowsock', 'smb', 'sqlite.js', 'sqliter', 'sqlserver', 'tkinter' ]); } loadPopularPackages() { return new Set([ 'express', 'react', 'lodash', 'axios', 'moment', 'commander', 'debug', 'chalk', 'request', 'async', 'bluebird', 'underscore', 'uuid', 'body-parser', 'mkdirp', 'glob', 'minimist', 'yargs', 'winston', 'dotenv', 'mongoose', 'socket.io', 'redis', 'pg', 'mysql', 'sequelize', 'passport', 'jsonwebtoken', 'bcrypt', 'cors', 'helmet', 'morgan', 'multer', 'nodemailer', 'puppeteer', 'cheerio', 'rxjs', 'typescript', 'webpack', 'babel', 'eslint', 'jest', 'mocha', 'chai', 'sinon', 'supertest', 'prettier', 'next', 'vue', 'angular', 'svelte', 'nuxt', 'gatsby', 'remix', 'tailwindcss', 'bootstrap', 'material-ui', 'antd', 'styled-components', 'graphql', 'apollo', 'prisma', 'typeorm', 'knex', 'objection' ]); } analyze(targetPath, options = {}) { const findings = []; const packageJsonPath = path.join(targetPath, 'package.json'); const lockFiles = [ { name: 'package-lock.json', type: 'npm' }, { name: 'yarn.lock', type: 'yarn' }, { name: 'pnpm-lock.yaml', type: 'pnpm' }, { name: 'bun.lockb', type: 'bun' } ]; if (!fs.existsSync(packageJsonPath)) { return findings; } let packageJson; try { packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } catch (error) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-001', signatureName: 'Corrupted package.json', severity: SEVERITY_LEVELS.HIGH, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Failed to parse package.json: ${error.message}`, file: packageJsonPath, remediation: 'Restore package.json from version control' })); return findings; } const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.optionalDependencies, ...packageJson.peerDependencies }; findings.push(...this.checkForMaliciousPackages(allDeps, packageJsonPath)); if (this.checkTyposquatting) { findings.push(...this.detectTyposquatting(allDeps, packageJsonPath)); } findings.push(...this.checkSuspiciousVersions(allDeps, packageJsonPath)); findings.push(...this.checkScriptInjection(packageJson, packageJsonPath)); findings.push(...this.checkSuspiciousRepositories(packageJson, packageJsonPath)); for (const lockFile of lockFiles) { const lockPath = path.join(targetPath, lockFile.name); if (fs.existsSync(lockPath)) { if (this.checkIntegrity) { findings.push(...this.checkLockfileIntegrity(lockPath, lockFile.type)); } findings.push(...this.analyzeLockfile(lockPath, lockFile.type)); } } if (options.baseline) { findings.push(...this.compareWithBaseline(allDeps, options.baseline, packageJsonPath)); } return findings; } checkForMaliciousPackages(deps, filePath) { const findings = []; for (const [name, version] of Object.entries(deps || {})) { if (this.knownMalicious.has(name)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-002', signatureName: 'Known Malicious Package', severity: SEVERITY_LEVELS.CRITICAL, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" is known to be malicious or compromised`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: `Remove ${name} immediately and audit your codebase`, references: ['npm advisory', 'MITRE T1195.002'] })); } } return findings; } detectTyposquatting(deps, filePath) { const findings = []; for (const [name, version] of Object.entries(deps || {})) { for (const popular of this.popularPackages) { if (name === popular) continue; const distance = this.levenshteinDistance(name, popular); const similarity = 1 - (distance / Math.max(name.length, popular.length)); if (distance <= 2 && distance > 0 && similarity > 0.7) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-003', signatureName: 'Potential Typosquatting', severity: SEVERITY_LEVELS.HIGH, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" is suspiciously similar to popular package "${popular}"`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: `Verify you intended to install "${name}" and not "${popular}"`, confidence: similarity })); } } const typosquatPatterns = [ { pattern: /-js$/, suggestion: 'without -js suffix' }, { pattern: /^node-/, suggestion: 'without node- prefix' }, { pattern: /\.js$/, suggestion: 'without .js suffix' }, { pattern: /-node$/, suggestion: 'without -node suffix' } ]; for (const { pattern, suggestion } of typosquatPatterns) { if (pattern.test(name)) { const normalizedName = name.replace(pattern, ''); if (this.popularPackages.has(normalizedName)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-004', signatureName: 'Suspicious Package Name Pattern', severity: SEVERITY_LEVELS.MEDIUM, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" may be typosquatting "${normalizedName}" (${suggestion})`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: `Verify this is the intended package` })); } } } } return findings; } checkSuspiciousVersions(deps, filePath) { const findings = []; for (const [name, version] of Object.entries(deps || {})) { if (version.includes('git') || version.includes('github') || version.includes('://')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-005', signatureName: 'Git/URL Dependency', severity: SEVERITY_LEVELS.MEDIUM, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" is installed from git/URL instead of registry`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Prefer installing packages from npm registry for better security' })); if (version.includes('://') && !version.includes('github.com') && !version.includes('gitlab.com')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-006', signatureName: 'Suspicious URL Dependency', severity: SEVERITY_LEVELS.HIGH, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" is installed from untrusted URL`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Use npm registry or trusted git hosts only' })); } } if (version.includes('file:') || version.includes('link:')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-007', signatureName: 'Local File Dependency', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" is linked locally`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Ensure local dependencies are intentional' })); } if (/^[0-9]+\.[0-9]+\.[0-9]+-[a-z]+\.[0-9]+$/i.test(version)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-008', signatureName: 'Pre-release Version', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${name}" uses pre-release version`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Consider using stable versions in production' })); } } return findings; } checkScriptInjection(packageJson, filePath) { const findings = []; const scripts = packageJson.scripts || {}; const dangerousPatterns = [ { pattern: /curl\s+.*\|\s*(?:ba)?sh/i, name: 'Remote Script Execution' }, { pattern: /wget\s+.*\|\s*(?:ba)?sh/i, name: 'Remote Script Execution' }, { pattern: /eval\s*\(/i, name: 'Eval in Script' }, { pattern: /\bnode\s+-e\s+['"].*(?:http|https|fetch)/i, name: 'Inline Network Request' }, { pattern: /base64\s+(?:-d|--decode)/i, name: 'Base64 Decode' }, { pattern: /\\x[0-9a-f]{2}/gi, name: 'Hex Escape Sequence' }, { pattern: /rm\s+-rf\s+[\/~]/i, name: 'Destructive Command' }, { pattern: />\s*\/dev\/tcp\//i, name: 'Network Redirect' } ]; for (const [scriptName, command] of Object.entries(scripts)) { for (const { pattern, name } of dangerousPatterns) { if (pattern.test(command)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-009', signatureName: `Dangerous Script: ${name}`, severity: SEVERITY_LEVELS.CRITICAL, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Script "${scriptName}" contains dangerous pattern: ${name}`, file: filePath, matchedText: command.substring(0, 200), remediation: `Review and remove malicious code from "${scriptName}" script` })); } } const hookScripts = ['preinstall', 'install', 'postinstall', 'preuninstall', 'postuninstall']; if (hookScripts.includes(scriptName)) { if (command.includes('node ') || command.includes('npx ') || command.includes('sh ')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-010', signatureName: 'Lifecycle Script Hook', severity: SEVERITY_LEVELS.MEDIUM, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Lifecycle hook "${scriptName}" executes code during install`, file: filePath, matchedText: command.substring(0, 200), remediation: 'Review lifecycle scripts for malicious behavior' })); } } } return findings; } checkSuspiciousRepositories(packageJson, filePath) { const findings = []; if (packageJson.repository) { const repo = typeof packageJson.repository === 'string' ? packageJson.repository : packageJson.repository.url; if (repo && !repo.includes('github.com') && !repo.includes('gitlab.com') && !repo.includes('bitbucket.org')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-011', signatureName: 'Non-standard Repository', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: 'Package uses non-standard repository hosting', file: filePath, matchedText: repo, remediation: 'Verify the repository is legitimate' })); } } return findings; } checkLockfileIntegrity(lockPath, lockType) { const findings = []; try { const content = fs.readFileSync(lockPath, 'utf8'); const hash = crypto.createHash('sha256').update(content).digest('hex'); if (lockType === 'npm') { const lockJson = JSON.parse(content); const checkPackage = (name, pkg, parentPath = '') => { const fullPath = parentPath ? `${parentPath}/${name}` : name; if (pkg.resolved && !pkg.resolved.includes('registry.npmjs.org') && !pkg.resolved.includes('registry.npm.taobao.org') && !pkg.resolved.includes('registry.npmmirror.com')) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-012', signatureName: 'Non-standard Registry', severity: SEVERITY_LEVELS.MEDIUM, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${fullPath}" resolved from non-standard registry`, file: lockPath, matchedText: pkg.resolved, remediation: 'Verify the package source is trusted' })); } if (pkg.dependencies) { for (const [depName, depPkg] of Object.entries(pkg.dependencies)) { checkPackage(depName, depPkg, fullPath); } } }; if (lockJson.packages) { for (const [pkgPath, pkg] of Object.entries(lockJson.packages)) { if (pkgPath && pkg) { checkPackage(pkgPath, pkg); } } } } } catch (error) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-013', signatureName: 'Corrupted Lockfile', severity: SEVERITY_LEVELS.HIGH, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Failed to parse lockfile: ${error.message}`, file: lockPath, remediation: 'Regenerate lockfile with npm/yarn/pnpm install' })); } return findings; } analyzeLockfile(lockPath, lockType) { const findings = []; try { const content = fs.readFileSync(lockPath, 'utf8'); if (lockType === 'npm') { const lockJson = JSON.parse(content); if (lockJson.packages) { for (const [pkgPath, pkg] of Object.entries(lockJson.packages)) { if (pkg.hasInstallScript) { const pkgName = pkgPath.replace('node_modules/', ''); findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-014', signatureName: 'Package with Install Script', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Package "${pkgName}" has install scripts`, file: lockPath, remediation: 'Review install scripts for security issues' })); } } } } } catch (error) { } return findings; } compareWithBaseline(currentDeps, baselinePath, filePath) { const findings = []; try { const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); const baselineDeps = { ...baseline.dependencies, ...baseline.devDependencies }; for (const [name, version] of Object.entries(currentDeps)) { if (!(name in baselineDeps)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-015', signatureName: 'New Dependency Added', severity: SEVERITY_LEVELS.MEDIUM, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `New dependency "${name}@${version}" not in baseline`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Verify this dependency was intentionally added' })); } else if (baselineDeps[name] !== version) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-016', signatureName: 'Dependency Version Changed', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Dependency "${name}" changed from ${baselineDeps[name]} to ${version}`, file: filePath, matchedText: `"${name}": "${version}"`, remediation: 'Verify this version change was intentional' })); } } for (const name of Object.keys(baselineDeps)) { if (!(name in currentDeps)) { findings.push(this.createFinding({ signatureId: 'NEUROLINT-DEP-017', signatureName: 'Dependency Removed', severity: SEVERITY_LEVELS.LOW, category: IOC_CATEGORIES.SUPPLY_CHAIN, description: `Dependency "${name}" was removed from baseline`, file: filePath, remediation: 'Verify this dependency removal was intentional' })); } } } catch (error) { if (this.verbose) { console.error(`[Layer 8] Failed to load baseline: ${error.message}`); } } return findings; } levenshteinDistance(str1, str2) { const m = str1.length; const n = str2.length; if (m === 0) return n; if (n === 0) return m; const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; dp[i][j] = Math.min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost ); } } return dp[m][n]; } createFinding(data) { return { id: `finding-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, ...data, line: 1, column: 1, confidence: data.confidence || 0.8, timestamp: new Date().toISOString() }; } generateReport(findings) { return { summary: { total: findings.length, critical: findings.filter(f => f.severity === SEVERITY_LEVELS.CRITICAL).length, high: findings.filter(f => f.severity === SEVERITY_LEVELS.HIGH).length, medium: findings.filter(f => f.severity === SEVERITY_LEVELS.MEDIUM).length, low: findings.filter(f => f.severity === SEVERITY_LEVELS.LOW).length }, findings: findings, timestamp: new Date().toISOString() }; } } module.exports = DependencyDiffer;