Skip to content

Instantly share code, notes, and snippets.

@Lp-Francois
Last active November 24, 2025 14:40
Show Gist options
  • Select an option

  • Save Lp-Francois/9e70eb7488bcc8ec76e0e27ed1c738e5 to your computer and use it in GitHub Desktop.

Select an option

Save Lp-Francois/9e70eb7488bcc8ec76e0e27ed1c738e5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/**
* SHA1Hulud Vulnerability Checker
*
* Checks if the workspace is affected by the SHA1Hulud vulnerability
* as described in https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24
*
* Usage: node tools/check-sha1hulud-vuln.js
*/
const { execSync } = require('child_process');
const { readFileSync, existsSync } = require('fs');
const { join } = require('path');
// Debug mode - set DEBUG=true to enable verbose logging
const DEBUG = process.env.DEBUG === 'true';
/**
* Debug logging function
*/
function debugLog(...args) {
if (DEBUG) {
console.log('[DEBUG]', ...args);
}
}
/**
* Detect which package manager is being used
* Returns: 'pnpm', 'yarn', 'npm', or null
*/
function detectPackageManager() {
const cwd = process.cwd();
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
debugLog('Detected package manager: pnpm (found pnpm-lock.yaml)');
return 'pnpm';
}
if (existsSync(join(cwd, 'yarn.lock'))) {
debugLog('Detected package manager: yarn (found yarn.lock)');
return 'yarn';
}
if (existsSync(join(cwd, 'package-lock.json'))) {
debugLog('Detected package manager: npm (found package-lock.json)');
return 'npm';
}
// Check package.json for packageManager field
try {
const packageJsonPath = join(cwd, 'package.json');
if (existsSync(packageJsonPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
if (packageJson.packageManager) {
const pm = packageJson.packageManager.split('@')[0];
debugLog(`Detected package manager from package.json: ${pm}`);
return pm;
}
}
} catch (error) {
debugLog('Error reading package.json:', error.message);
}
debugLog('No lock file found, defaulting to package.json fallback');
return null;
}
// Vulnerable packages and versions from the blog post
const VULNERABLE_PACKAGES = {
'@zapier/zapier-sdk': ['0.15.5', '0.15.7'],
'@posthog/core': ['1.5.6'],
'posthog-node': ['5.11.3', '5.13.3', '4.18.1'],
'@asyncapi/specs': ['6.10.1', '6.8.2', '6.9.1', '6.8.3'],
'@postman/tunnel-agent': ['0.6.6', '0.6.5'],
'posthog-react-native': ['4.12.5', '4.11.1'],
'@asyncapi/parser': ['3.4.1', '3.4.2'],
'@asyncapi/openapi-schema-parser': ['3.0.25'],
'@asyncapi/avro-schema-parser': ['3.0.25', '3.0.26'],
'@asyncapi/protobuf-schema-parser': ['3.6.1', '3.5.3'],
'@asyncapi/react-component': ['2.6.6'],
'@asyncapi/generator': ['2.8.5'],
'@posthog/ai': ['7.1.2'],
'@asyncapi/modelina': ['5.10.2', '5.10.3'],
'@asyncapi/generator-react-sdk': ['1.1.4', '1.1.5'],
'@postman/csv-parse': ['4.0.3', '4.0.4', '4.0.5'],
'posthog-react-native-session-replay': ['1.2.2'],
'@asyncapi/converter': ['1.6.3'],
'@asyncapi/multi-parser': ['2.2.1', '2.2.2'],
'@posthog/cli': ['0.5.15'],
'@zapier/secret-scrubber': ['1.1.3', '1.1.4', '1.1.5'],
'zapier-platform-schema': ['18.0.2'],
'zapier-platform-core': ['18.0.2', '18.0.3'],
'@ensdomains/address-encoder': ['1.1.5'],
'@ensdomains/content-hash': ['3.0.1'],
'crypto-addr-codec': ['0.1.9'],
'@asyncapi/nunjucks-filters': ['2.1.1', '2.1.2'],
'@asyncapi/bundler': ['0.6.5', '0.6.6'],
'@posthog/nextjs-config': ['1.5.1'],
'@asyncapi/html-template': ['3.3.2', '3.3.3'],
'@asyncapi/diff': ['0.5.1', '0.5.2'],
'@asyncapi/cli': ['4.1.2'],
'@asyncapi/optimizer': ['1.0.5', '1.0.6'],
'@asyncapi/modelina-cli': ['5.10.2', '5.10.3'],
'@postman/aether-icons': ['2.23.2', '2.23.4'],
'@asyncapi/generator-components': ['0.3.2'],
'@asyncapi/generator-helpers': ['0.2.1', '0.2.2'],
'zapier-platform-cli': ['18.0.3'],
'@posthog/rrweb': ['0.0.31'],
'ethereum-ens': ['0.8.1'],
'@posthog/rrweb-utils': ['0.0.31'],
'@posthog/rrweb-snapshot': ['0.0.31'],
'@posthog/rrdom': ['0.0.31'],
'@asyncapi/problem': ['1.0.1', '1.0.2'],
'@postman/secret-scanner-wasm': ['2.1.3', '2.1.2', '2.1.4'],
'@ensdomains/eth-ens-namehash': ['2.0.16'],
'posthog-docusaurus': ['2.0.6'],
'@postman/pretty-ms': ['6.1.1', '6.1.3', '6.1.2'],
'web-types-lit': ['0.1.1'],
'mcp-use': ['1.4.2', '1.4.3'],
'@posthog/react-rrweb-player': ['1.1.4'],
'@asyncapi/markdown-template': ['1.6.8', '1.6.9'],
'@ensdomains/buffer': ['0.1.2'],
'@postman/node-keytar': ['7.9.4', '7.9.5', '7.9.6'],
'@mcp-use/inspector': ['0.6.2', '0.6.3'],
'@mcp-use/cli': ['2.2.6'],
'@zapier/spectral-api-ruleset': ['1.9.1', '1.9.2', '1.9.3'],
'@posthog/geoip-plugin': ['0.0.8'],
'@ensdomains/dnsprovejs': ['0.5.3'],
'@ensdomains/solsha1': ['0.0.4'],
'@asyncapi/web-component': ['2.6.6', '2.6.7'],
'@posthog/nuxt': ['1.2.9'],
'@zapier/browserslist-config-zapier': ['1.0.3', '1.0.5'],
'@posthog/wizard': ['1.18.1'],
'react-native-use-modal': ['1.0.3'],
'@asyncapi/java-spring-template': ['1.6.1', '1.6.2'],
'@posthog/rrweb-record': ['0.0.31'],
'@posthog/siphash': ['1.1.2'],
'@posthog/piscina': ['3.2.1'],
'@ensdomains/ens-validation': ['0.1.1'],
'@posthog/plugin-contrib': ['0.0.6'],
'@posthog/agent': ['1.24.1'],
'@postman/postman-mcp-server': ['2.4.11', '2.4.10'],
'@asyncapi/nodejs-ws-template': ['0.10.1', '0.10.2'],
'@actbase/react-daum-postcode': ['1.0.5'],
'token.js-fork': ['0.7.32'],
'@postman/pm-bin-windows-x64': ['1.24.5', '1.24.4'],
'@ensdomains/ens-avatar': ['1.0.4'],
'@postman/pm-bin-linux-x64': ['1.24.3', '1.24.4', '1.24.5'],
'@posthog/hedgehog-mode': ['0.0.42'],
'create-mcp-use-app': ['0.5.3', '0.5.4'],
'@postman/pm-bin-macos-arm64': ['1.24.5', '1.24.3', '1.24.4'],
'@posthog/nextjs': ['0.0.3'],
'@postman/pm-bin-macos-x64': ['1.24.3', '1.24.5'],
'redux-router-kit': ['1.2.2', '1.2.3', '1.2.4'],
'@ensdomains/dnssecoraclejs': ['0.2.9'],
'@postman/mcp-ui-client': ['5.5.1', '5.5.2'],
'@postman/postman-mcp-cli': ['1.0.5', '1.0.4'],
'@zapier/babel-preset-zapier': ['6.4.1', '6.4.3'],
'@ensdomains/thorin': ['0.6.51'],
'@postman/postman-collection-fork': ['4.3.3', '4.3.4', '4.3.5'],
'@asyncapi/nodejs-template': ['3.0.5'],
'@postman/wdio-allure-reporter': ['0.0.9'],
'@postman/wdio-junit-reporter': ['0.0.4', '0.0.6'],
'@postman/final-node-keytar': ['7.9.1', '7.9.2'],
'zapier-async-storage': ['1.0.1', '1.0.2', '1.0.3'],
'@ensdomains/test-utils': ['1.3.1'],
'@ensdomains/hardhat-chai-matchers-viem': ['0.1.15'],
'@asyncapi/java-spring-cloud-stream-template': ['0.13.5', '0.13.6'],
'@zapier/eslint-plugin-zapier': ['11.0.3', '11.0.4', '11.0.5'],
'devstart-cli': ['1.0.6'],
'@asyncapi/java-template': ['0.3.5', '0.3.6'],
'@asyncapi/go-watermill-template': ['0.2.76', '0.2.77'],
'@asyncapi/python-paho-template': ['0.2.14', '0.2.15'],
'@ensdomains/hardhat-toolbox-viem-extended': ['0.0.6'],
'@ensdomains/vite-plugin-i18next-loader': ['4.0.4'],
'zapier-platform-legacy-scripting-runner': ['4.0.3', '4.0.4'],
'@asyncapi/server-api': ['0.16.25'],
'@ensdomains/offchain-resolver-contracts': ['0.2.2'],
'@zapier/ai-actions': ['0.1.18', '0.1.19', '0.1.20'],
'@zapier/mcp-integration': ['3.0.1', '3.0.3'],
'@ensdomains/ens-archived-contracts': ['0.0.3'],
'@ensdomains/dnssec-oracle-anchors': ['0.0.2'],
'@ensdomains/mock': ['2.1.52'],
'zapier-scripts': ['7.8.3', '7.8.4'],
'@quick-start-soft/quick-task-refine': ['1.4.2511142126'],
'@zapier/ai-actions-react': ['0.1.13', '0.1.14'],
'@quick-start-soft/quick-git-clean-markdown': ['1.4.2511142126'],
'@ensdomains/ui': ['3.4.6'],
'@quick-start-soft/quick-markdown': ['1.4.2511142126'],
'@zapier/stubtree': ['0.1.3'],
'@ensdomains/unruggable-gateways': ['0.0.3'],
'@posthog/rrweb-player': ['0.0.31'],
'@asyncapi/dotnet-rabbitmq-template': ['1.0.1', '1.0.2'],
'@ensdomains/react-ens-address': ['0.0.32'],
'@asyncapi/php-template': ['0.1.1'],
'@quick-start-soft/quick-document-translator': ['1.4.2511142126'],
'@quick-start-soft/quick-markdown-image': ['1.4.2511142126'],
'@strapbuild/react-native-date-time-picker': ['2.0.4'],
'github-action-for-generator': ['2.1.28'],
'@actbase/react-kakaosdk': ['0.9.27'],
'bytecode-checker-cli': ['1.0.8', '1.0.9', '1.0.10'],
'@markvivanco/app-version-checker': ['1.0.1', '1.0.2'],
'@louisle2/cortex-js': ['0.1.6'],
'orbit-boxicons': ['2.1.3'],
'react-native-worklet-functions': ['3.3.3'],
'poper-react-sdk': ['0.1.2'],
'@ensdomains/web3modal': ['1.10.2'],
'gate-evm-tools-test': ['1.0.5', '1.0.6', '1.0.7'],
'n8n-nodes-tmdb': ['0.5.1'],
'capacitor-plugin-purchase': ['0.1.1'],
'expo-audio-session': ['0.2.1'],
'capacitor-plugin-apptrackingios': ['0.0.21'],
'asyncapi-preview': ['1.0.1', '1.0.2'],
'@actbase/react-absolute': ['0.8.3'],
'@actbase/react-native-devtools': ['0.1.3'],
'@posthog/variance-plugin': ['0.0.8'],
'@posthog/twitter-followers-plugin': ['0.0.8'],
'medusa-plugin-momo': ['0.0.68'],
'scgs-capacitor-subscribe': ['1.0.11'],
'gate-evm-check-code2': ['2.0.3', '2.0.4', '2.0.5'],
'lite-serper-mcp-server': ['0.2.2'],
'@asyncapi/edavisualiser': ['1.2.1', '1.2.2'],
'esbuild-plugin-eta': ['0.1.1'],
'@ensdomains/server-analytics': ['0.0.2'],
'zuper-stream': ['2.0.9'],
'@quick-start-soft/quick-markdown-compose': ['1.4.2506300029'],
'@posthog/snowflake-export-plugin': ['0.0.8'],
'@actbase/react-native-kakao-channel': ['1.0.2'],
'@posthog/sendgrid-plugin': ['0.0.8'],
'evm-checkcode-cli': ['1.0.12', '1.0.13', '1.0.14'],
'@ensdomains/subdomain-registrar': ['0.2.4'],
'claude-token-updater': ['1.0.3'],
'@trigo/atrix-pubsub': ['4.0.3'],
'@trigo/hapi-auth-signedlink': ['1.3.1'],
'@strapbuild/react-native-perspective-image-cropper-poojan31': ['0.4.6'],
'axios-builder': ['1.2.1'],
'calc-loan-interest': ['1.0.4'],
'medusa-plugin-announcement': ['0.0.3'],
open2internet: ['0.1.1'],
'@ensdomains/cypress-metamask': ['1.2.1'],
'@ensdomains/renewal': ['0.0.13'],
'cpu-instructions': ['0.0.14'],
'orbit-soap': ['0.43.13'],
'@asyncapi/keeper': ['0.0.2', '0.0.3'],
'@strapbuild/react-native-perspective-image-cropper-2': ['0.4.7'],
'@actbase/react-native-actionsheet': ['1.0.3'],
'@posthog/ingestion-alert-plugin': ['0.0.8'],
'@actbase/react-native-simple-video': ['1.0.13'],
'@actbase/react-native-kakao-navi': ['2.0.4'],
'medusa-plugin-zalopay': ['0.0.40'],
'@kvytech/medusa-plugin-newsletter': ['0.0.5'],
'@posthog/databricks-plugin': ['0.0.8'],
'capacitor-voice-recorder-wav': ['6.0.3'],
'create-hardhat3-app': ['1.1.1', '1.1.2'],
'rollup-plugin-httpfile': ['0.2.1'],
'@ensdomains/name-wrapper': ['1.0.1'],
'test-foundry-app': ['1.0.3'],
'jan-browser': ['0.13.1'],
'@mparpaillon/page': ['1.0.1'],
'go-template': ['0.1.8'],
'@strapbuild/react-native-perspective-image-cropper': ['0.4.15'],
'manual-billing-system-miniapp-api': ['1.3.1'],
'korea-administrative-area-geo-json-util': ['1.0.7'],
'@posthog/currency-normalization-plugin': ['0.0.8'],
'@posthog/web-dev-server': ['1.0.5'],
'@posthog/pagerduty-plugin': ['0.0.8'],
'@posthog/event-sequence-timer-plugin': ['0.0.8'],
'@posthog/automatic-cohorts-plugin': ['0.0.8'],
'@posthog/first-time-event-tracker': ['0.0.8'],
'@actbase/css-to-react-native-transform': ['1.0.3'],
'@posthog/url-normalizer-plugin': ['0.0.8'],
'@posthog/twilio-plugin': ['0.0.8'],
'@actbase/node-server': ['1.1.19'],
'@posthog/gitub-star-sync-plugin': ['0.0.8'],
'@seung-ju/react-native-action-sheet': ['0.2.1'],
'@posthog/maxmind-plugin': ['0.1.6'],
'@posthog/github-release-tracking-plugin': ['0.0.8'],
'@actbase/react-native-fast-image': ['8.5.13'],
'@posthog/customerio-plugin': ['0.0.8'],
'@posthog/kinesis-plugin': ['0.0.8'],
'@actbase/react-native-less-transformer': ['1.0.6'],
'@posthog/taxonomy-plugin': ['0.0.8'],
'medusa-plugin-product-reviews-kvy': ['0.0.4'],
'@aryanhussain/my-angular-lib': ['0.0.23'],
'dotnet-template': ['0.0.4'],
'capacitor-plugin-scgssigninwithgoogle': ['0.0.5'],
'capacitor-purchase-history': ['0.0.10'],
'@posthog/plugin-unduplicates': ['0.0.8'],
'posthog-plugin-hello-world': ['1.0.1'],
'esbuild-plugin-httpfile': ['0.4.1'],
'@ensdomains/blacklist': ['1.0.1'],
'@ensdomains/renewal-widget': ['0.1.10'],
'@ensdomains/hackathon-registrar': ['1.0.5'],
'@ensdomains/ccip-read-router': ['0.0.7'],
'@mcp-use/mcp-use': ['1.0.1'],
'test-hardhat-app': ['1.0.3'],
'zuper-cli': ['1.0.1'],
'skills-use': ['0.1.2'],
'typeorm-orbit': ['0.2.27'],
'orbit-nebula-editor': ['1.0.2'],
'@trigo/atrix-elasticsearch': ['2.0.1'],
'@trigo/atrix-soap': ['1.0.2'],
'eslint-config-zeallat-base': ['1.0.4'],
'iron-shield-miniapp': ['0.0.2'],
'shinhan-limit-scrap': ['1.0.3'],
'create-glee-app': ['0.2.3'],
'@seung-ju/next': ['0.0.2'],
'@actbase/react-native-tiktok': ['1.1.3'],
'discord-bot-server': ['0.1.2'],
'@seung-ju/openapi-generator': ['0.0.4'],
'@seung-ju/react-hooks': ['0.0.2'],
'@actbase/react-native-naver-login': ['1.0.1'],
'@kvytech/medusa-plugin-announcement': ['0.0.8'],
'@kvytech/components': ['0.0.2'],
'@kvytech/cli': ['0.0.7'],
'@kvytech/medusa-plugin-management': ['0.0.5'],
'@kvytech/medusa-plugin-product-reviews': ['0.0.9'],
'@kvytech/web': ['0.0.2'],
scgsffcreator: ['1.0.5'],
'vite-plugin-httpfile': ['0.2.1'],
'@ensdomains/curvearithmetics': ['1.0.1'],
'@ensdomains/reverse-records': ['1.0.1'],
'@ensdomains/ccip-read-dns-gateway': ['0.1.1'],
'@ensdomains/unicode-confusables': ['0.1.1'],
'@ensdomains/durin-middleware': ['0.0.2'],
'@ensdomains/ccip-read-worker-viem': ['0.0.4'],
atrix: ['1.0.1'],
'@caretive/caret-cli': ['0.0.2'],
'exact-ticker': ['0.3.5'],
'@orbitgtbelgium/orbit-components': ['1.2.9'],
'react-library-setup': ['0.0.6'],
'@orbitgtbelgium/mapbox-gl-draw-scale-rotate-mode': ['1.1.1'],
'orbit-nebula-draw-tools': ['1.0.10'],
'@orbitgtbelgium/time-slider': ['1.0.187'],
'react-element-prompt-inspector': ['0.1.18'],
'@trigo/pathfinder-ui-css': ['0.1.1'],
'eslint-config-trigo': ['22.0.2'],
'@trigo/fsm': ['3.4.2'],
'@trigo/atrix': ['7.0.1'],
'@trigo/atrix-postgres': ['1.0.3'],
'trigo-react-app': ['4.1.2'],
'@trigo/eslint-config-trigo': ['3.3.1'],
'@trigo/bool-expressions': ['4.1.3'],
'@trigo/trigo-hapijs': ['5.0.1'],
'@trigo/node-soap': ['0.5.4'],
'@trigo/jsdt': ['0.2.1'],
'bool-expressions': ['0.1.2'],
'@trigo/atrix-redis': ['1.0.2'],
'@trigo/atrix-acl': ['4.0.2'],
'@trigo/atrix-orientdb': ['1.0.2'],
'@trigo/atrix-mongoose': ['1.0.2'],
'atrix-mongoose': ['1.0.1'],
'redux-forge': ['2.5.3'],
'@trigo/keycloak-api': ['1.3.1'],
'@mparpaillon/connector-parse': ['1.0.1'],
'@mparpaillon/imagesloaded': ['4.1.2'],
'@alaan/s2s-auth': ['2.0.3'],
};
/**
* Normalize version string by removing leading '^', '~', or '=' characters
*/
function normalizeVersion(version) {
return version.replace(/^[~^=]/, '');
}
/**
* Check if a version matches any of the vulnerable versions
*/
function isVulnerableVersion(packageName, version) {
const vulnerableVersions = VULNERABLE_PACKAGES[packageName];
if (!vulnerableVersions) {
debugLog(`Package ${packageName} not in vulnerable list`);
return false;
}
const normalizedVersion = normalizeVersion(version);
debugLog(
`Checking ${packageName} version ${version} (normalized: ${normalizedVersion}) against vulnerable versions:`,
vulnerableVersions
);
const isVulnerable = vulnerableVersions.some((vulnVersion) => {
// Exact match
if (normalizedVersion === vulnVersion) {
debugLog(
` βœ“ Exact match found: ${normalizedVersion} === ${vulnVersion}`
);
return true;
}
// Handle version ranges (e.g., "1.2.3" matches "1.2.3" even if installed as "^1.2.3")
const startsWithMatch = normalizedVersion.startsWith(vulnVersion);
if (startsWithMatch) {
debugLog(
` βœ“ Prefix match found: ${normalizedVersion} starts with ${vulnVersion}`
);
}
return startsWithMatch;
});
if (isVulnerable) {
debugLog(` ❌ VULNERABLE: ${packageName}@${version}`);
} else {
debugLog(` βœ“ Safe: ${packageName}@${version}`);
}
return isVulnerable;
}
/**
* Check for vulnerable packages in pnpm-lock.yaml
*/
function checkVulnerablePackagesInPnpmLock() {
const lockFilePath = join(process.cwd(), 'pnpm-lock.yaml');
const packages = [];
debugLog(`Reading pnpm lock file: ${lockFilePath}`);
try {
// Read file once (lock files are typically manageable in size)
const fileContent = readFileSync(lockFilePath, 'utf-8');
const allLines = fileContent.split('\n');
debugLog(
`Lock file read: ${allLines.length} lines, ${fileContent.length} bytes`
);
debugLog(
`Checking ${Object.keys(VULNERABLE_PACKAGES).length} vulnerable packages`
);
// For each vulnerable package, search in the lock file
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) {
debugLog(`\nSearching for package: ${packageName}`);
// Escape special regex characters in package name
const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Pattern 1: Search for package entry in dependencies section
// Format: 'package-name': or "package-name": or package-name:
const packagePattern = new RegExp(`^\\s+['"]?${escapedName}['"]?:\\s*$`);
debugLog(` Pattern 1 (dependencies): ${packagePattern}`);
// Pattern 2: Search in packages section
// Format: /package-name/version:
const packagePathPattern = new RegExp(
`^\\s+/${escapedName}/([0-9]+\\.[0-9]+\\.[0-9]+[^:]*):`
);
debugLog(` Pattern 2 (packages section): ${packagePathPattern}`);
let found = false;
for (let i = 0; i < allLines.length; i++) {
// Check for package entry in dependencies section
if (packagePattern.test(allLines[i])) {
debugLog(
` Found package entry at line ${i + 1}: ${allLines[i].trim()}`
);
// Look for version in the next few lines (usually 1-3 lines after)
for (let j = i + 1; j < Math.min(i + 5, allLines.length); j++) {
const versionMatch = allLines[j].match(
/^\s+version:\s*([0-9]+\.[0-9]+\.[0-9]+[^(\s]*)/
);
if (versionMatch) {
const version = versionMatch[1];
debugLog(` Found version at line ${j + 1}: ${version}`);
packages.push({
name: packageName,
version: version,
path: packageName,
});
found = true;
break; // Found, move to next package
}
}
if (found) break; // Found package entry, move to next package
}
// Check for package in packages section
const pathMatch = allLines[i].match(packagePathPattern);
if (pathMatch) {
const version = pathMatch[1];
debugLog(
` Found package in packages section at line ${i + 1}: version ${version}`
);
packages.push({
name: packageName,
version: version,
path: packageName,
});
found = true;
break; // Found, move to next package
}
}
if (!found) {
debugLog(` Package ${packageName} not found in lock file`);
}
}
debugLog(`\nTotal packages found in lock file: ${packages.length}`);
return packages;
} catch (error) {
console.error('Error reading pnpm-lock.yaml:', error.message);
debugLog('Error details:', error);
return [];
}
}
/**
* Check for vulnerable packages in yarn.lock
*/
function checkVulnerablePackagesInYarnLock() {
const lockFilePath = join(process.cwd(), 'yarn.lock');
const packages = [];
debugLog(`Reading yarn lock file: ${lockFilePath}`);
try {
const fileContent = readFileSync(lockFilePath, 'utf-8');
const allLines = fileContent.split('\n');
debugLog(
`Yarn lock file read: ${allLines.length} lines, ${fileContent.length} bytes`
);
debugLog(
`Checking ${Object.keys(VULNERABLE_PACKAGES).length} vulnerable packages`
);
// Yarn lock format:
// package-name@version: or "package-name@version": or "@scope/package-name@version":
// version "x.y.z"
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) {
debugLog(`\nSearching for package: ${packageName}`);
// Escape special regex characters
const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Pattern 1: package-name@version: (for unscoped packages)
// Pattern 2: "@scope/package-name@version": (for scoped packages)
const packageHeaderPattern = new RegExp(
`^["']?${escapedName}@([0-9]+\\.[0-9]+\\.[0-9]+[^"':\\s]*):`
);
debugLog(` Header pattern: ${packageHeaderPattern}`);
let found = false;
for (let i = 0; i < allLines.length; i++) {
// Check for package header line
const headerMatch = allLines[i].match(packageHeaderPattern);
if (headerMatch) {
debugLog(
` Found package header at line ${i + 1}: ${allLines[i].trim()}`
);
// Look for version in the next few lines (usually 1-2 lines after)
for (let j = i + 1; j < Math.min(i + 10, allLines.length); j++) {
// Yarn format: version "x.y.z"
const versionMatch = allLines[j].match(
/^\s+version\s+"([0-9]+\.[0-9]+\.[0-9]+[^"]*)"/
);
if (versionMatch) {
const version = versionMatch[1];
debugLog(` Found version at line ${j + 1}: ${version}`);
packages.push({
name: packageName,
version: version,
path: packageName,
});
found = true;
break;
}
// Stop if we hit another package entry (starts with package name or empty line followed by package)
if (allLines[j].match(/^[^#\s]/) && !allLines[j].match(/^\s/)) {
break;
}
}
if (found) break;
}
}
if (!found) {
debugLog(` Package ${packageName} not found in yarn.lock`);
}
}
debugLog(`\nTotal packages found in yarn.lock: ${packages.length}`);
return packages;
} catch (error) {
console.error('Error reading yarn.lock:', error.message);
debugLog('Error details:', error);
return [];
}
}
/**
* Check for vulnerable packages in package-lock.json
*/
function checkVulnerablePackagesInNpmLock() {
const lockFilePath = join(process.cwd(), 'package-lock.json');
const packages = [];
debugLog(`Reading npm lock file: ${lockFilePath}`);
try {
const fileContent = readFileSync(lockFilePath, 'utf-8');
const lockData = JSON.parse(fileContent);
debugLog(`NPM lock file parsed successfully`);
debugLog(
`Checking ${Object.keys(VULNERABLE_PACKAGES).length} vulnerable packages`
);
// NPM lock format (v2+): packages["node_modules/package-name"]
// NPM lock format (v1): dependencies["package-name"]
const packagesObj = lockData.packages || lockData.dependencies || {};
debugLog(`Found ${Object.keys(packagesObj).length} packages in lock file`);
debugLog(`Lock file version: ${lockData.lockfileVersion || '1'}`);
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) {
debugLog(`\nSearching for package: ${packageName}`);
// Build possible paths for package-lock.json
const possiblePaths = [];
// For scoped packages like @scope/package
if (packageName.startsWith('@')) {
const [scope, name] = packageName.split('/');
possiblePaths.push(
`node_modules/${packageName}`,
packageName,
`node_modules/${scope}/node_modules/${name}`
);
} else {
// For unscoped packages
possiblePaths.push(`node_modules/${packageName}`, packageName);
}
let found = false;
for (const path of possiblePaths) {
const pkg = packagesObj[path];
if (pkg && pkg.version) {
const version = pkg.version;
debugLog(` Found at path "${path}": version ${version}`);
packages.push({
name: packageName,
version: version,
path: packageName,
});
found = true;
break;
}
}
// Also check nested dependencies (for older lock file versions)
if (!found && lockData.dependencies) {
function searchDependencies(deps) {
for (const [name, pkg] of Object.entries(deps)) {
// Check if this matches our package name (handle scoped packages)
const matches =
name === packageName ||
name === packageName.split('/').pop() ||
(packageName.startsWith('@') && name.startsWith(packageName));
if (matches && pkg.version) {
debugLog(
` Found in dependencies at "${name}": version ${pkg.version}`
);
packages.push({
name: packageName,
version: pkg.version,
path: packageName,
});
return true;
}
// Recursively search nested dependencies
if (pkg.dependencies && searchDependencies(pkg.dependencies)) {
return true;
}
}
return false;
}
if (searchDependencies(lockData.dependencies)) {
found = true;
}
}
if (!found) {
debugLog(` Package ${packageName} not found in package-lock.json`);
}
}
debugLog(`\nTotal packages found in package-lock.json: ${packages.length}`);
return packages;
} catch (error) {
console.error('Error reading package-lock.json:', error.message);
debugLog('Error details:', error);
return [];
}
}
/**
* Get installed packages by checking lock file for vulnerable packages only
*/
function getInstalledPackages() {
debugLog('Getting installed packages...');
const packageManager = detectPackageManager();
let lockFilePackages = [];
// Check appropriate lock file based on package manager
if (packageManager === 'pnpm') {
lockFilePackages = checkVulnerablePackagesInPnpmLock();
} else if (packageManager === 'yarn') {
lockFilePackages = checkVulnerablePackagesInYarnLock();
} else if (packageManager === 'npm') {
lockFilePackages = checkVulnerablePackagesInNpmLock();
}
debugLog(`Found ${lockFilePackages.length} packages in lock file`);
if (lockFilePackages.length > 0) {
debugLog('Using packages from lock file');
return lockFilePackages;
}
// Fallback: check package.json (only direct dependencies)
debugLog('No packages found in lock file, falling back to package.json');
const packageJsonPackages = getPackagesFromPackageJson();
debugLog(`Found ${packageJsonPackages.length} packages in package.json`);
return packageJsonPackages;
}
/**
* Fallback: Get packages from root package.json
*/
function getPackagesFromPackageJson() {
try {
const packageJsonPath = join(process.cwd(), 'package.json');
debugLog(`Reading package.json: ${packageJsonPath}`);
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const packages = [];
if (packageJson.dependencies) {
const depCount = Object.keys(packageJson.dependencies).length;
debugLog(`Found ${depCount} dependencies`);
Object.entries(packageJson.dependencies).forEach(([name, version]) => {
debugLog(` ${name}: ${version}`);
packages.push({
name,
version: version,
path: name,
});
});
}
if (packageJson.devDependencies) {
const devDepCount = Object.keys(packageJson.devDependencies).length;
debugLog(`Found ${devDepCount} devDependencies`);
Object.entries(packageJson.devDependencies).forEach(([name, version]) => {
debugLog(` ${name}: ${version}`);
packages.push({
name,
version: version,
path: name,
});
});
}
return packages;
} catch (error) {
console.error('Error reading package.json:', error);
debugLog('Error details:', error);
return [];
}
}
/**
* Main function
*/
function main() {
if (DEBUG) {
console.log('πŸ› DEBUG MODE ENABLED\n');
}
const packageManager = detectPackageManager();
const pmDisplay = packageManager ? ` (${packageManager})` : '';
console.log('πŸ” Checking for SHA1Hulud vulnerability...\n');
console.log(
'Reference: https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24'
);
if (packageManager) {
console.log(`Package manager: ${packageManager}\n`);
} else {
console.log(
'Package manager: auto-detected (using package.json fallback)\n'
);
}
debugLog(
`Vulnerable packages to check: ${Object.keys(VULNERABLE_PACKAGES).length}`
);
debugLog(
`Vulnerable packages list:`,
Object.keys(VULNERABLE_PACKAGES).slice(0, 10),
'...'
);
const installedPackages = getInstalledPackages();
debugLog(
`\nChecking ${installedPackages.length} installed packages for vulnerabilities`
);
const vulnerablePackages = [];
for (const pkg of installedPackages) {
debugLog(`\nChecking: ${pkg.name}@${pkg.version}`);
if (isVulnerableVersion(pkg.name, pkg.version)) {
debugLog(` β†’ Added to vulnerable list`);
vulnerablePackages.push(pkg);
}
}
debugLog(`\n\n=== SUMMARY ===`);
debugLog(`Total installed packages checked: ${installedPackages.length}`);
debugLog(`Vulnerable packages found: ${vulnerablePackages.length}`);
if (vulnerablePackages.length === 0) {
console.log(
'βœ… No vulnerable packages found. Your workspace appears to be safe.\n'
);
process.exit(0);
} else {
console.log(
`❌ Found ${vulnerablePackages.length} vulnerable package(s):\n`
);
vulnerablePackages.forEach((pkg) => {
const vulnerableVersions = VULNERABLE_PACKAGES[pkg.name];
console.log(` πŸ“¦ ${pkg.name}`);
console.log(` Installed version: ${pkg.version}`);
console.log(` Vulnerable versions: ${vulnerableVersions.join(', ')}`);
if (pkg.path && pkg.path !== pkg.name) {
console.log(` Dependency path: ${pkg.path}`);
}
console.log();
});
console.log('\n⚠️ ACTION REQUIRED:');
console.log(
' Please update the vulnerable packages to non-vulnerable versions.'
);
console.log(
' For more information, visit: https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24\n'
);
process.exit(1);
}
}
main();
@Lp-Francois
Copy link
Author

To run the script:

curl -s https://gist.githubusercontent.com/Lp-Francois/9e70eb7488bcc8ec76e0e27ed1c738e5/raw/ca9b7e31797fb1421938aaead54ad2381572890f/check-sha1hulud-vuln.js | node

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment