[AAE-46514] - Improved code

This commit is contained in:
VitoAlbano
2026-06-04 09:26:32 +01:00
parent 90673df4f7
commit 4b4f2390c1
7 changed files with 1081 additions and 792 deletions
+336 -561
View File
File diff suppressed because it is too large Load Diff
+65 -60
View File
@@ -30,23 +30,19 @@
* 3. Sets up husky
*/
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'node:child_process';
import { statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
// Use npm_execpath from environment (set by npm during lifecycle scripts)
// Falls back to 'npm' if not available (e.g., running script directly)
const NPM_PATH = process.env.npm_execpath || 'npm';
const NPX_CMD = NPM_PATH.endsWith('npm-cli.js')
? `"${process.execPath}" "${NPM_PATH.replace('npm-cli.js', 'npx-cli.js')}"`
: 'npx';
// Packages that are trusted to run postinstall/install scripts
// These typically need to compile native bindings or setup tooling
const TRUSTED_PACKAGES = [
'esbuild',
'sharp',
@@ -66,7 +62,6 @@ const TRUSTED_PACKAGES = [
'core-js-pure'
];
// Scoped packages that need rebuild (full package names)
const TRUSTED_SCOPED_PACKAGES = [
'@esbuild/darwin-arm64',
'@esbuild/darwin-x64',
@@ -81,6 +76,14 @@ const TRUSTED_SCOPED_PACKAGES = [
'@swc/core'
];
function directoryExists(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
function run(command, options = {}) {
try {
execSync(command, {
@@ -96,28 +99,59 @@ function run(command, options = {}) {
function getInstalledTrustedPackages() {
const nodeModulesPath = join(ROOT_DIR, 'node_modules');
if (!existsSync(nodeModulesPath)) return [];
const installed = [];
// Check non-scoped packages
for (const pkg of TRUSTED_PACKAGES) {
const pkgPath = join(nodeModulesPath, pkg);
if (existsSync(pkgPath)) {
installed.push(pkg);
}
if (!directoryExists(nodeModulesPath)) {
return [];
}
// Check scoped packages
for (const pkg of TRUSTED_SCOPED_PACKAGES) {
const [scope, name] = pkg.split('/');
const pkgPath = join(nodeModulesPath, scope, name);
if (existsSync(pkgPath)) {
installed.push(pkg);
}
const nonScopedInstalled = TRUSTED_PACKAGES
.filter(pkg => directoryExists(join(nodeModulesPath, pkg)));
const scopedInstalled = TRUSTED_SCOPED_PACKAGES
.filter(pkg => {
const [scope, name] = pkg.split('/');
return directoryExists(join(nodeModulesPath, scope, name));
});
return [...new Set([...nonScopedInstalled, ...scopedInstalled])];
}
function runSecurityCheck() {
console.log('Step 1/3: Running security check...\n');
const securityCheckPath = join(__dirname, 'check-security.mjs');
const securityPassed = run(`"${process.execPath}" "${securityCheckPath}"`);
if (!securityPassed) {
console.error('\n❌ Security check failed - installation aborted\n');
process.exit(1);
}
}
function rebuildTrustedPackages() {
console.log('\nStep 2/3: Rebuilding trusted packages...\n');
const trustedInstalled = getInstalledTrustedPackages();
if (!trustedInstalled.length) {
console.log('No trusted packages require rebuilding.\n');
return;
}
return [...new Set(installed)];
console.log('Trusted packages to rebuild:');
for (const pkg of trustedInstalled) {
console.log(`${pkg}`);
}
const npmCmd = NPM_PATH === 'npm' ? 'npm' : `"${process.execPath}" "${NPM_PATH}"`;
run(`${npmCmd} rebuild --ignore-scripts=false ${trustedInstalled.join(' ')}`);
}
function setupHusky() {
console.log('\nStep 3/3: Setting up husky...\n');
const huskyPath = join(ROOT_DIR, 'node_modules', 'husky');
if (directoryExists(huskyPath)) {
run(`${NPX_CMD} husky`);
}
}
async function main() {
@@ -125,45 +159,16 @@ async function main() {
console.log('🔒 ADF POST-INSTALL SECURITY');
console.log('='.repeat(70) + '\n');
// Step 1: Run security check
console.log('Step 1/3: Running security check...\n');
const securityCheckPath = join(__dirname, 'check-security.mjs');
// Run security check as subprocess using process.execPath to avoid PATH-based attacks
const securityPassed = run(`"${process.execPath}" "${securityCheckPath}"`);
if (!securityPassed) {
console.error('\n❌ Security check failed - installation aborted\n');
process.exit(1);
}
// Step 2: Rebuild trusted packages
console.log('\nStep 2/3: Rebuilding trusted packages...\n');
const trustedInstalled = getInstalledTrustedPackages();
if (trustedInstalled.length > 0) {
console.log('Trusted packages to rebuild:');
trustedInstalled.forEach(pkg => console.log(`${pkg}`));
console.log('');
const npmCmd = NPM_PATH === 'npm' ? 'npm' : `"${process.execPath}" "${NPM_PATH}"`;
run(`${npmCmd} rebuild --ignore-scripts=false ${trustedInstalled.join(' ')}`);
} else {
console.log('No trusted packages require rebuilding.\n');
}
// Step 3: Setup husky
console.log('Step 3/3: Setting up husky...\n');
const huskyPath = join(ROOT_DIR, 'node_modules', 'husky');
if (existsSync(huskyPath)) {
run(`${NPX_CMD} husky`);
}
runSecurityCheck();
rebuildTrustedPackages();
setupHusky();
console.log('='.repeat(70));
console.log('✅ Post-install security complete');
console.log('='.repeat(70) + '\n');
}
main().catch(err => {
console.error('Post-install failed:', err.message);
main().catch(error => {
console.error('Post-install failed:', error.message);
process.exit(1);
});
+236 -171
View File
@@ -28,16 +28,15 @@
* (e.g., nx migrate) before the lockfile is updated.
*/
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const CACHE_DIR = join(ROOT_DIR, 'node_modules', '.cache', 'security-check');
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
const CACHE_TTL = 24 * 60 * 60 * 1000;
// Known supply chain attack packages (fallback if APIs fail)
const KNOWN_MALICIOUS = new Set([
'event-stream@3.3.6',
'flatmap-stream@0.1.1',
@@ -52,47 +51,85 @@ const KNOWN_MALICIOUS = new Set([
'@primevue/themes@4.3.0', '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3'
]);
function readCache() {
const cachePath = join(CACHE_DIR, 'threats.json');
if (!existsSync(cachePath)) return null;
// ============================================================================
// FILE UTILITIES
// ============================================================================
function readJsonFile(filePath) {
try {
const data = JSON.parse(readFileSync(cachePath, 'utf8'));
if (Date.now() - data.timestamp < CACHE_TTL) {
return {
threats: new Set(data.threats),
ranges: data.ranges || []
};
}
} catch { /* ignore */ }
return null;
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function writeJsonFile(filePath, data) {
try {
writeFileSync(filePath, JSON.stringify(data));
return true;
} catch {
return false;
}
}
function ensureDirectory(path) {
try {
mkdirSync(path, { recursive: true });
return true;
} catch {
return false;
}
}
// ============================================================================
// CACHE MANAGEMENT
// ============================================================================
function readCache() {
const data = readJsonFile(join(CACHE_DIR, 'threats.json'));
if (!data || Date.now() - data.timestamp >= CACHE_TTL) {
return null;
}
return {
threats: new Set(data.threats),
ranges: data.ranges || []
};
}
function writeCache(threats, ranges) {
try {
if (!existsSync(CACHE_DIR)) {
mkdirSync(CACHE_DIR, { recursive: true });
}
writeFileSync(join(CACHE_DIR, 'threats.json'), JSON.stringify({
timestamp: Date.now(),
threats: [...threats],
ranges: ranges
}));
} catch { /* ignore */ }
ensureDirectory(CACHE_DIR);
writeJsonFile(join(CACHE_DIR, 'threats.json'), {
timestamp: Date.now(),
threats: [...threats],
ranges
});
}
// ============================================================================
// SECURITY PROVIDERS
// ============================================================================
function splitIntoBatches(items, batchSize) {
const batches = [];
for (let index = 0; index < items.length; index += batchSize) {
batches.push(items.slice(index, index + batchSize));
}
return batches;
}
async function fetchOSV(projectDependencies) {
// Query OSV batch API for the project's actual dependencies
if (!projectDependencies || projectDependencies.length === 0) {
if (!projectDependencies?.length) {
return new Set();
}
const malicious = new Set();
try {
// Process in batches of 1000
const batchSize = 1000;
for (let i = 0; i < projectDependencies.length; i += batchSize) {
const batch = projectDependencies.slice(i, i + batchSize);
const batches = splitIntoBatches(projectDependencies, 1000);
for (const batch of batches) {
const response = await fetch('https://api.osv.dev/v1/querybatch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -111,18 +148,14 @@ async function fetchOSV(projectDependencies) {
for (const result of data.results || []) {
for (const vuln of result.vulns || []) {
const summary = [vuln.summary || '', vuln.details || ''].join(' ').toLowerCase();
const isMalware = summary.includes('malware') ||
summary.includes('malicious') ||
summary.includes('compromised') ||
summary.includes('supply chain') ||
summary.includes('backdoor');
const isMalware = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor']
.some(keyword => summary.includes(keyword));
if (isMalware && vuln.affected) {
for (const affected of vuln.affected) {
if (affected.package?.ecosystem === 'npm' && affected.package?.name) {
const versions = affected.versions || [];
for (const v of versions) {
malicious.add(`${affected.package.name}@${v}`);
for (const version of affected.versions || []) {
malicious.add(`${affected.package.name}@${version}`);
}
}
}
@@ -131,7 +164,7 @@ async function fetchOSV(projectDependencies) {
}
}
} catch {
// Ignore errors, return what we have
// Ignore errors
}
return malicious;
@@ -143,126 +176,143 @@ async function fetchGitHubAdvisory() {
headers: { 'Accept': 'application/vnd.github+json' },
signal: AbortSignal.timeout(10000)
});
if (!response.ok) return new Set();
if (!response.ok) {
return new Set();
}
const advisories = await response.json();
const malicious = new Set();
for (const advisory of advisories) {
if (advisory.vulnerabilities) {
for (const vuln of advisory.vulnerabilities) {
if (vuln.package?.ecosystem === 'npm' && vuln.package?.name) {
// GitHub uses version ranges, we'll mark the package name
// and check ranges in the scan
if (vuln.vulnerable_version_range) {
malicious.add(`${vuln.package.name}:${vuln.vulnerable_version_range}`);
}
}
for (const vuln of advisory.vulnerabilities || []) {
if (vuln.package?.ecosystem === 'npm' && vuln.package?.name && vuln.vulnerable_version_range) {
malicious.add(`${vuln.package.name}:${vuln.vulnerable_version_range}`);
}
}
}
return malicious;
} catch {
return new Set();
}
}
// ============================================================================
// VERSION COMPARISON
// ============================================================================
function compareVersions(versionA, versionB) {
const partsA = versionA.split('.').map(Number);
const partsB = versionB.split('.').map(Number);
const maxLength = Math.max(partsA.length, partsB.length);
let result = 0;
for (let index = 0; index < maxLength && result === 0; index++) {
const numA = partsA[index] || 0;
const numB = partsB[index] || 0;
result = Math.sign(numA - numB);
}
return result;
}
function parseVersionRange(range, version) {
// Simple version range parser for GitHub advisory format
// Handles: "= 1.0.0", "< 1.0.0", "<= 1.0.0", "> 1.0.0", ">= 1.0.0"
if (!range || !version) return false;
const parts = range.split(',').map(p => p.trim());
const parts = range.split(',').map(part => part.trim());
for (const part of parts) {
const match = part.match(/^([<>=]+)\s*(\S+)$/);
if (!match) {
if (part === version) return true;
continue;
}
const [, op, rangeVer] = match;
const cmp = compareVersions(version, rangeVer);
if (op === '=' && cmp !== 0) return false;
if (op === '<' && cmp >= 0) return false;
if (op === '<=' && cmp > 0) return false;
if (op === '>' && cmp <= 0) return false;
if (op === '>=' && cmp < 0) return false;
const [, operator, rangeVersion] = match;
const comparison = compareVersions(version, rangeVersion);
const checks = {
'=': comparison === 0,
'<': comparison < 0,
'<=': comparison <= 0,
'>': comparison > 0,
'>=': comparison >= 0
};
if (!checks[operator]) return false;
}
return true;
}
function compareVersions(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na > nb) return 1;
if (na < nb) return -1;
}
return 0;
}
// ============================================================================
// PACKAGE EXTRACTION
// ============================================================================
function extractVersionNumber(versionSpec) {
if (!versionSpec) return null;
// Remove ^, ~, >=, <=, >, <, = prefixes
// Use atomic pattern to prevent backtracking: match version then optional prerelease
const match = versionSpec.match(/(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*))?/);
return match ? match[0] : null;
}
function getPackagesFromPackageJson() {
const packageJsonPath = join(ROOT_DIR, 'package.json');
if (!existsSync(packageJsonPath)) return [];
const packageJson = readJsonFile(join(ROOT_DIR, 'package.json'));
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const packages = [];
if (!packageJson) {
return [];
}
const depTypes = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
for (const depType of depTypes) {
const deps = packageJson[depType] || {};
for (const [name, versionSpec] of Object.entries(deps)) {
// Skip file: and link: dependencies
if (typeof versionSpec === 'string' && !versionSpec.startsWith('file:') && !versionSpec.startsWith('link:')) {
const version = extractVersionNumber(versionSpec);
if (version) {
packages.push({ name, version, source: 'package.json' });
}
}
}
}
return packages;
return depTypes
.flatMap(depType => Object.entries(packageJson[depType] || {}))
.filter(([, versionSpec]) =>
typeof versionSpec === 'string' &&
!versionSpec.startsWith('file:') &&
!versionSpec.startsWith('link:')
)
.map(([name, versionSpec]) => ({
name,
version: extractVersionNumber(versionSpec),
source: 'package.json'
}))
.filter(pkg => pkg.version);
}
function getPackagesFromLockfile() {
const lockfilePath = join(ROOT_DIR, 'package-lock.json');
if (!existsSync(lockfilePath)) {
const lockfile = readJsonFile(join(ROOT_DIR, 'package-lock.json'));
if (!lockfile) {
return [];
}
const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf8'));
const packages = [];
// npm v2+ lockfile format
if (lockfile.packages) {
for (const [path, info] of Object.entries(lockfile.packages)) {
if (!path || path === '') continue; // skip root
const name = path.replace(/^node_modules\//, '').replace(/\/node_modules\//g, '/');
if (info.version) {
packages.push({ name, version: info.version, source: 'lockfile' });
}
if (!path || path === '' || !info.version) continue;
packages.push({
name: path.replace(/^node_modules\//, '').replace(/\/node_modules\//g, '/'),
version: info.version,
source: 'lockfile'
});
}
}
// npm v1 lockfile format
if (lockfile.dependencies) {
const extractDeps = (deps, prefix = '') => {
for (const [name, info] of Object.entries(deps)) {
const fullName = prefix ? `${prefix}/${name}` : name;
if (info.version) {
packages.push({ name: fullName, version: info.version, source: 'lockfile' });
}
if (info.dependencies) {
extractDeps(info.dependencies, fullName);
}
@@ -278,11 +328,12 @@ function getAllPackages() {
const packageJsonPkgs = getPackagesFromPackageJson();
const lockfilePkgs = getPackagesFromLockfile();
// Combine and dedupe (lockfile takes precedence for same name)
const seen = new Map();
for (const pkg of lockfilePkgs) {
seen.set(`${pkg.name}@${pkg.version}`, pkg);
}
for (const pkg of packageJsonPkgs) {
const key = `${pkg.name}@${pkg.version}`;
if (!seen.has(key)) {
@@ -293,96 +344,110 @@ function getAllPackages() {
return [...seen.values()];
}
async function main() {
console.log('\n🔒 ADF SECURITY CHECK');
console.log('='.repeat(70));
console.log('Scanning for known supply chain attacks and compromised packages...\n');
// ============================================================================
// VIOLATION DETECTION
// ============================================================================
// Get all packages from both package.json and lockfile
const packages = getAllPackages();
if (packages.length === 0) {
console.log(' ⚠️ No packages found to check');
console.log('='.repeat(70) + '\n');
process.exit(0);
}
const fromPackageJson = packages.filter(p => p.source === 'package.json').length;
const fromLockfile = packages.filter(p => p.source === 'lockfile').length;
console.log(` Checking ${packages.length} packages (${fromPackageJson} from package.json, ${fromLockfile} from lockfile)\n`);
// Try to use cache first
const cache = readCache();
let threats;
let ghRanges;
if (!cache) {
console.log(' Fetching latest security databases...\n');
const [osvThreats, ghThreats] = await Promise.all([
fetchOSV(packages).then(r => { console.log(` 📡 OSV: ${r.size} malware entries`); return r; }),
fetchGitHubAdvisory().then(r => { console.log(` 📡 GitHub Advisory: ${r.size} malware entries`); return r; })
]);
threats = new Set([...KNOWN_MALICIOUS, ...osvThreats]);
ghRanges = [...ghThreats];
// Cache the results including ranges
writeCache(threats, ghRanges);
} else {
threats = cache.threats;
ghRanges = cache.ranges;
console.log(` Using cached security database (${threats.size} exact + ${ghRanges.length} ranges)\n`);
}
// Check packages against exact matches and ranges
const found = [];
function findViolations(packages, threats, ghRanges) {
const violations = [];
for (const pkg of packages) {
const exact = `${pkg.name}@${pkg.version}`;
const exactKey = `${pkg.name}@${pkg.version}`;
// Check exact match
if (threats.has(exact)) {
found.push({ ...pkg, source: 'exact match' });
if (threats.has(exactKey)) {
violations.push({ ...pkg, detectionSource: 'exact match' });
continue;
}
// Check GitHub Advisory ranges
for (const entry of ghRanges) {
const [name, range] = entry.split(':');
if (pkg.name === name && parseVersionRange(range, pkg.version)) {
found.push({ ...pkg, source: 'GitHub Advisory' });
violations.push({ ...pkg, detectionSource: 'GitHub Advisory' });
break;
}
}
}
if (found.length > 0) {
console.log('\n' + '!'.repeat(70));
console.log('🚨 MALICIOUS PACKAGES DETECTED - BLOCKING INSTALLATION');
console.log('!'.repeat(70) + '\n');
return violations;
}
for (const pkg of found) {
console.log(`${pkg.name}@${pkg.version} (${pkg.source})`);
}
// ============================================================================
// REPORTING
// ============================================================================
console.log('\nThese packages are known to contain malware or malicious code.');
console.log('Installation has been blocked to protect your system.\n');
console.log('Actions:');
console.log(' 1. Remove these packages from package.json');
console.log(' 2. Find safe alternatives');
console.log(' 3. Run npm install again\n');
console.log('='.repeat(70) + '\n');
function reportViolations(violations) {
console.log('\n' + '!'.repeat(70));
console.log('🚨 MALICIOUS PACKAGES DETECTED - BLOCKING INSTALLATION');
console.log('!'.repeat(70) + '\n');
process.exit(1);
for (const pkg of violations) {
console.log(`${pkg.name}@${pkg.version} (${pkg.detectionSource})`);
}
console.log(`\n✅ Security check passed (${threats.size} exact + ${ghRanges.length} ranges checked)`)
console.log('\nThese packages are known to contain malware or malicious code.');
console.log('Installation has been blocked to protect your system.\n');
console.log('Actions:');
console.log(' 1. Remove these packages from package.json');
console.log(' 2. Find safe alternatives');
console.log(' 3. Run npm install again\n');
console.log('='.repeat(70) + '\n');
}
main().catch(err => {
console.error('Security check error:', err.message);
// Don't block on errors - allow install to proceed
// ============================================================================
// MAIN
// ============================================================================
async function main() {
console.log('\n🔒 ADF SECURITY CHECK');
console.log('='.repeat(70));
console.log('Scanning for known supply chain attacks and compromised packages...\n');
const packages = getAllPackages();
if (!packages.length) {
console.log(' ⚠️ No packages found to check');
console.log('='.repeat(70) + '\n');
process.exit(0);
}
const fromPackageJson = packages.filter(pkg => pkg.source === 'package.json').length;
const fromLockfile = packages.filter(pkg => pkg.source === 'lockfile').length;
console.log(` Checking ${packages.length} packages (${fromPackageJson} from package.json, ${fromLockfile} from lockfile)\n`);
const cache = readCache();
let threats;
let ghRanges;
if (cache) {
threats = cache.threats;
ghRanges = cache.ranges;
console.log(` Using cached security database (${threats.size} exact + ${ghRanges.length} ranges)\n`);
} else {
console.log(' Fetching latest security databases...\n');
const [osvThreats, ghThreats] = await Promise.all([
fetchOSV(packages).then(result => { console.log(` 📡 OSV: ${result.size} malware entries`); return result; }),
fetchGitHubAdvisory().then(result => { console.log(` 📡 GitHub Advisory: ${result.size} malware entries`); return result; })
]);
threats = new Set([...KNOWN_MALICIOUS, ...osvThreats]);
ghRanges = [...ghThreats];
writeCache(threats, ghRanges);
}
const violations = findViolations(packages, threats, ghRanges);
if (violations.length) {
reportViolations(violations);
process.exit(1);
}
console.log(`\n✅ Security check passed (${threats.size} exact + ${ghRanges.length} ranges checked)`);
console.log('='.repeat(70) + '\n');
}
main().catch(error => {
console.error('Security check error:', error.message);
process.exit(0);
});
@@ -0,0 +1,60 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* 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.
*/
/**
* Fallback list of known malicious packages.
* Used when external APIs (OSV, GitHub) are unreachable.
* These are confirmed supply chain attacks (not regular CVEs).
*/
export const FALLBACK_BLOCKED_PACKAGES = {
'@solana/web3.js': {
versions: ['1.95.6', '1.95.7'],
reason: 'Compromised - steals private keys (Dec 2024)'
},
'@lottiefiles/lottie-player': {
versions: ['2.0.5', '2.0.6', '2.0.7'],
reason: 'Compromised - crypto wallet drainer (Oct 2024)'
},
'node-ipc': {
versions: ['9.2.2', '10.1.1', '10.1.2', '10.1.3', '11.0.0', '11.1.0'],
reason: 'Protestware - overwrites files (Mar 2022)'
},
'ua-parser-js': {
versions: ['0.7.29', '0.8.0', '1.0.0'],
reason: 'Compromised - cryptominer and password stealer (Oct 2021)'
},
'coa': {
versions: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.0.1', '3.1.3'],
reason: 'Compromised - password stealer (Nov 2021)'
},
'rc': {
versions: ['1.2.9', '1.3.9', '2.3.9'],
reason: 'Compromised - exfiltrates environment variables (Nov 2021)'
},
'colors': {
versions: ['1.4.1', '1.4.44-liberty-2'],
reason: 'Sabotage - infinite loop (Jan 2022)'
},
'faker': {
versions: ['6.6.6'],
reason: 'Sabotage - no functionality (Jan 2022)'
},
'event-stream': {
versions: ['3.3.6'],
reason: 'Compromised - bitcoin wallet theft (Nov 2018)'
}
};
@@ -0,0 +1,91 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* 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.
*/
const GITHUB_ADVISORY_API = 'https://api.github.com/advisories';
const REQUEST_TIMEOUT_MS = 15000;
function extractExactVersion(versionRange) {
const exactMatch = versionRange.match(/^=\s*(.+)$/);
return exactMatch ? exactMatch[1].trim() : null;
}
function processAdvisoryVulnerability(vulnerability, advisory, registry) {
const packageName = vulnerability.package?.name;
if (!packageName || vulnerability.package?.ecosystem !== 'npm') {
return;
}
if (!registry[packageName]) {
registry[packageName] = { versions: [], versionRanges: [], reason: '', source: 'GitHub' };
}
if (vulnerability.vulnerable_version_range) {
registry[packageName].versionRanges.push(vulnerability.vulnerable_version_range);
const exactVersion = extractExactVersion(vulnerability.vulnerable_version_range);
if (exactVersion) {
registry[packageName].versions.push(exactVersion);
}
}
registry[packageName].reason = advisory.summary || 'Malware detected by GitHub Advisory';
}
function processAdvisory(advisory, registry) {
const vulnerabilities = advisory.vulnerabilities || [];
for (const vulnerability of vulnerabilities) {
processAdvisoryVulnerability(vulnerability, advisory, registry);
}
}
export async function fetchMalwareData() {
console.log(' 📡 Fetching from GitHub Advisory...');
const malwareRegistry = {};
try {
const response = await fetch(
`${GITHUB_ADVISORY_API}?ecosystem=npm&type=malware&per_page=100`,
{
headers: {
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
}
);
if (response.ok) {
const advisories = await response.json();
for (const advisory of advisories) {
processAdvisory(advisory, malwareRegistry);
}
console.log(` ✓ GitHub: Found ${Object.keys(malwareRegistry).length} malware entries`);
} else if (response.status === 403) {
console.log(' ⚠ GitHub: Rate limited (will use cached/fallback data)');
} else {
console.log(` ⚠ GitHub: Unexpected status ${response.status}`);
}
} catch (error) {
console.log(` ⚠ GitHub: Could not fetch (${error.message})`);
}
return malwareRegistry;
}
@@ -0,0 +1,142 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* 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.
*/
import { readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const CLI_TIMEOUT_MS = 60000;
const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
function isDirectory(path) {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
function listDirectories(path) {
try {
return readdirSync(path);
} catch {
return [];
}
}
function findVSCodeExtensionCli(homeDir) {
const extensionsDir = join(homeDir, '.vscode', 'extensions');
const extensions = listDirectories(extensionsDir)
.filter(dir => /^meterian\.meterian-heidi-\d+\.\d+\.\d+$/.test(dir))
.sort()
.reverse();
for (const extension of extensions) {
const cliPath = join(extensionsDir, extension, 'packages', 'meterian-cli');
if (isDirectory(cliPath)) {
return cliPath;
}
}
return null;
}
function findGlobalCli(homeDir, rootDir) {
const possibleLocations = [
join(rootDir, 'node_modules', '@meterian', 'cli'),
join(homeDir, '.npm-global', 'lib', 'node_modules', '@meterian', 'cli'),
'/usr/local/lib/node_modules/@meterian/cli'
];
for (const location of possibleLocations) {
if (isDirectory(location)) {
return location;
}
}
return null;
}
export function findCliPath(rootDir) {
const homeDir = process.env.HOME || process.env.USERPROFILE;
return findVSCodeExtensionCli(homeDir) || findGlobalCli(homeDir, rootDir);
}
export function isAvailable(rootDir) {
return findCliPath(rootDir) !== null;
}
function formatDependenciesForCli(dependencies) {
return dependencies.map(dependency => ({
language: 'nodejs',
name: dependency.name,
version: dependency.version
}));
}
function runCliCheck(cliPath, input) {
const cliScript = join(cliPath, 'src', 'cli.js');
return spawnSync(process.execPath, [cliScript, 'check'], {
input: JSON.stringify(input),
encoding: 'utf-8',
timeout: CLI_TIMEOUT_MS,
maxBuffer: MAX_BUFFER_SIZE
});
}
function parseCliOutput(result) {
if (result.error) {
throw new Error(result.error.message);
}
if (result.status !== 0 && !result.stdout) {
throw new Error(`CLI returned status ${result.status}`);
}
return JSON.parse(result.stdout);
}
export async function checkVulnerabilities(dependencies, rootDir) {
const cliPath = findCliPath(rootDir);
if (!cliPath) {
console.log(' ⏭️ Meterian: Not installed, skipping');
console.log(' Install VSCode extension "Meterian Security" or run: npm i -g @meterian/cli');
return { vulnerable: [], source: 'Meterian' };
}
console.log(' 📡 Checking with Meterian CLI...');
try {
const formattedInput = formatDependenciesForCli(dependencies);
const result = runCliCheck(cliPath, formattedInput);
const output = parseCliOutput(result);
console.log(` ✓ Meterian: Found ${output.vulnerable?.length || 0} vulnerable packages`);
return {
vulnerable: output.vulnerable || [],
summary: output.summary,
source: 'Meterian'
};
} catch (error) {
console.log(` ⚠ Meterian: ${error.message}`);
return { vulnerable: [], source: 'Meterian' };
}
}
+151
View File
@@ -0,0 +1,151 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* 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.
*/
const OSV_BATCH_API = 'https://api.osv.dev/v1/querybatch';
const REQUEST_TIMEOUT_MS = 30000;
const BATCH_SIZE = 1000;
const MALWARE_KEYWORDS = [
'malware',
'malicious',
'compromised',
'supply chain',
'backdoor',
'cryptominer',
'credential stealing',
'data exfiltration',
'typosquat'
];
function isMalwareRelated(text) {
const lowerText = text.toLowerCase();
return MALWARE_KEYWORDS.some(keyword => lowerText.includes(keyword.toLowerCase()));
}
function extractVersionsFromVulnerability(vulnerability) {
const affectedEntries = vulnerability.affected || [];
const introducedVersions = affectedEntries
.flatMap(entry => entry.ranges || [])
.flatMap(range => range.events || [])
.filter(event => event.introduced && event.introduced !== '0')
.map(event => event.introduced);
const explicitVersions = affectedEntries.flatMap(entry => entry.versions || []);
return [...new Set([...introducedVersions, ...explicitVersions])];
}
function splitIntoBatches(items, batchSize) {
const batches = [];
for (let index = 0; index < items.length; index += batchSize) {
batches.push(items.slice(index, index + batchSize));
}
return batches;
}
function buildQueryPayload(dependencies) {
return {
queries: dependencies.map(dependency => ({
package: { name: dependency.name, ecosystem: 'npm' },
version: dependency.version
}))
};
}
async function queryBatch(dependencies) {
const response = await fetch(OSV_BATCH_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildQueryPayload(dependencies)),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
});
if (!response.ok) {
return [];
}
const data = await response.json();
return data.results || [];
}
function extractMalwareInfo(vulnerability) {
const summary = [vulnerability.summary || '', vulnerability.details || ''].join(' ');
if (!isMalwareRelated(summary)) {
return null;
}
const packageName = vulnerability.affected?.[0]?.package?.name;
if (!packageName) {
return null;
}
const versions = extractVersionsFromVulnerability(vulnerability);
if (versions.length === 0) {
return null;
}
return {
packageName,
versions,
reason: vulnerability.summary || 'Malware detected by OSV'
};
}
function processBatchResults(batchResults, malwareRegistry) {
for (const result of batchResults) {
const vulnerabilities = result.vulns || [];
for (const vulnerability of vulnerabilities) {
const malwareInfo = extractMalwareInfo(vulnerability);
if (malwareInfo) {
if (!malwareRegistry[malwareInfo.packageName]) {
malwareRegistry[malwareInfo.packageName] = { versions: [], reason: '', source: 'OSV' };
}
malwareRegistry[malwareInfo.packageName].versions.push(...malwareInfo.versions);
malwareRegistry[malwareInfo.packageName].reason = malwareInfo.reason;
}
}
}
}
export async function fetchMalwareData(projectDependencies) {
console.log(' 📡 Fetching from OSV (Google)...');
const malwareRegistry = {};
if (!projectDependencies || projectDependencies.length === 0) {
console.log(' ⚠ OSV: No dependencies to check');
return malwareRegistry;
}
try {
const dependencyBatches = splitIntoBatches(projectDependencies, BATCH_SIZE);
for (const batch of dependencyBatches) {
const batchResults = await queryBatch(batch);
processBatchResults(batchResults, malwareRegistry);
}
console.log(` ✓ OSV: Found ${Object.keys(malwareRegistry).length} malware entries`);
} catch (error) {
console.log(` ⚠ OSV: Could not fetch (${error.message})`);
}
return malwareRegistry;
}