|
#!/usr/bin/env node |
|
|
|
/** |
|
* GitHub Organization SHA1Hulud Vulnerability Checker |
|
* |
|
* Checks all repositories in a GitHub organization for SHA1Hulud vulnerabilities |
|
* by fetching lock files directly via GitHub API (no cloning required) |
|
* |
|
* Usage: GITHUB_ORG=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js |
|
* |
|
* Command-line flags: |
|
* --clear-cache, -c - Clear the cache before running |
|
* --clear-cache-only, -C - Clear the cache and exit (no check performed) |
|
* |
|
* Environment Variables: |
|
* GITHUB_ORG - GitHub organization name (required) |
|
* GITHUB_TOKEN - GitHub personal access token (required) |
|
* BATCH_SIZE - Number of repositories to check in parallel (default: 10) |
|
* DEBUG - Enable debug logging (set to 'true' to enable) |
|
* CLEAR_CACHE - Clear cache before running (set to 'true') |
|
*/ |
|
|
|
const https = require('https'); |
|
const fs = require('fs'); |
|
const path = require('path'); |
|
const os = require('os'); |
|
|
|
// Debug mode - set DEBUG=true to enable verbose logging |
|
const DEBUG = process.env.DEBUG === 'true'; |
|
|
|
// Cache configuration |
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds |
|
const CACHE_FILE = path.join(os.homedir(), '.sha1hulud-cache.json'); |
|
|
|
/** |
|
* Debug logging function |
|
*/ |
|
function debugLog(...args) { |
|
if (DEBUG) { |
|
console.log('[DEBUG]', ...args); |
|
} |
|
} |
|
|
|
// Vulnerable packages and versions from the blog post |
|
// (Same as check-sha1hulud-vuln.js) |
|
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) { |
|
return false; |
|
} |
|
|
|
const normalizedVersion = normalizeVersion(version); |
|
return vulnerableVersions.some((vulnVersion) => { |
|
// Exact match |
|
if (normalizedVersion === vulnVersion) { |
|
return true; |
|
} |
|
// Handle version ranges (e.g., "1.2.3" matches "1.2.3" even if installed as "^1.2.3") |
|
return normalizedVersion.startsWith(vulnVersion); |
|
}); |
|
} |
|
|
|
/** |
|
* Make HTTPS request to GitHub API |
|
*/ |
|
function githubApiRequest(path, token) { |
|
return new Promise((resolve, reject) => { |
|
const options = { |
|
hostname: 'api.github.com', |
|
path: path, |
|
method: 'GET', |
|
headers: { |
|
'User-Agent': 'SHA1Hulud-Checker', |
|
Accept: 'application/vnd.github.v3+json', |
|
Authorization: `token ${token}`, |
|
}, |
|
}; |
|
|
|
const req = https.request(options, (res) => { |
|
let data = ''; |
|
|
|
res.on('data', (chunk) => { |
|
data += chunk; |
|
}); |
|
|
|
res.on('end', () => { |
|
if (res.statusCode >= 200 && res.statusCode < 300) { |
|
try { |
|
const json = JSON.parse(data); |
|
resolve({ data: json, headers: res.headers }); |
|
} catch (e) { |
|
resolve({ data: data, headers: res.headers }); |
|
} |
|
} else if (res.statusCode === 404) { |
|
resolve(null); // Not found |
|
} else if (res.statusCode === 403) { |
|
const rateLimitRemaining = res.headers['x-ratelimit-remaining']; |
|
const rateLimitReset = res.headers['x-ratelimit-reset']; |
|
if (rateLimitRemaining === '0') { |
|
const resetTime = new Date(parseInt(rateLimitReset) * 1000); |
|
reject( |
|
new Error( |
|
`GitHub API rate limit exceeded. Resets at: ${resetTime.toISOString()}` |
|
) |
|
); |
|
} else { |
|
reject(new Error(`GitHub API error: ${res.statusCode} - ${data}`)); |
|
} |
|
} else { |
|
reject(new Error(`GitHub API error: ${res.statusCode} - ${data}`)); |
|
} |
|
}); |
|
}); |
|
|
|
req.on('error', (error) => { |
|
reject(error); |
|
}); |
|
|
|
req.end(); |
|
}); |
|
} |
|
|
|
/** |
|
* Fetch all repositories in an organization (with pagination and caching) |
|
*/ |
|
async function fetchOrgRepos(org, token, cache) { |
|
const cacheKey = `__org_repos_${org}`; |
|
|
|
// Check cache first |
|
if (cache[cacheKey]) { |
|
const cached = cache[cacheKey]; |
|
debugLog( |
|
`Using cached repository list for org: ${org} (${cached.result.length} repos)` |
|
); |
|
return cached.result; |
|
} |
|
|
|
const repos = []; |
|
let page = 1; |
|
let hasMore = true; |
|
|
|
debugLog(`Fetching repositories for org: ${org}`); |
|
|
|
while (hasMore) { |
|
try { |
|
const path = `/orgs/${org}/repos?per_page=100&page=${page}&type=all`; |
|
debugLog(` Fetching page ${page}...`); |
|
const response = await githubApiRequest(path, token); |
|
|
|
if (!response) { |
|
break; |
|
} |
|
|
|
const pageRepos = Array.isArray(response.data) ? response.data : []; |
|
repos.push(...pageRepos); |
|
|
|
debugLog(` Found ${pageRepos.length} repos on page ${page}`); |
|
|
|
// Check if there are more pages |
|
const linkHeader = response.headers.link || ''; |
|
hasMore = linkHeader.includes('rel="next"'); |
|
|
|
page++; |
|
} catch (error) { |
|
console.error(`Error fetching repos (page ${page}):`, error.message); |
|
throw error; |
|
} |
|
} |
|
|
|
debugLog(`Total repos found: ${repos.length}`); |
|
|
|
// Store in cache |
|
cache[cacheKey] = { |
|
timestamp: Date.now(), |
|
result: repos, |
|
}; |
|
|
|
return repos; |
|
} |
|
|
|
/** |
|
* Fetch file content from GitHub repository |
|
*/ |
|
async function fetchFileContent(owner, repo, filePath, token, branch = 'main') { |
|
try { |
|
// Try main branch first, then master |
|
const branches = [branch, 'main', 'master']; |
|
|
|
for (const b of branches) { |
|
const path = `/repos/${owner}/${repo}/contents/${filePath}?ref=${b}`; |
|
debugLog(` Trying to fetch ${filePath} from ${b} branch...`); |
|
|
|
const response = await githubApiRequest(path, token); |
|
|
|
if (response && response.data && response.data.content) { |
|
// Decode base64 content |
|
const content = Buffer.from(response.data.content, 'base64').toString( |
|
'utf-8' |
|
); |
|
debugLog(` β Found ${filePath} on ${b} branch`); |
|
return content; |
|
} |
|
} |
|
|
|
return null; |
|
} catch (error) { |
|
debugLog(` Error fetching ${filePath}:`, error.message); |
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* Search for lock files in repository using GitHub Search API |
|
*/ |
|
async function searchLockFiles(owner, repo, token) { |
|
const lockFiles = [ |
|
{ name: 'pnpm-lock.yaml', manager: 'pnpm' }, |
|
{ name: 'yarn.lock', manager: 'yarn' }, |
|
{ name: 'package-lock.json', manager: 'npm' }, |
|
]; |
|
|
|
const foundFiles = []; |
|
|
|
for (const lockFile of lockFiles) { |
|
try { |
|
// Use GitHub Search API to find lock files |
|
// URL encode the query parameters |
|
const query = `filename:${lockFile.name} repo:${owner}/${repo}`; |
|
const encodedQuery = encodeURIComponent(query); |
|
const path = `/search/code?q=${encodedQuery}`; |
|
const response = await githubApiRequest(path, token); |
|
|
|
if (response && response.data && response.data.items) { |
|
for (const item of response.data.items) { |
|
foundFiles.push({ |
|
path: item.path, |
|
name: lockFile.name, |
|
manager: lockFile.manager, |
|
}); |
|
} |
|
} |
|
} catch (error) { |
|
debugLog(` Error searching for ${lockFile.name}:`, error.message); |
|
// Continue searching for other lock files |
|
// Note: Search API has rate limits (30 req/min), but we continue anyway |
|
} |
|
} |
|
|
|
return foundFiles; |
|
} |
|
|
|
/** |
|
* Detect package manager and fetch lock file (checks root first, then searches nested) |
|
*/ |
|
async function detectAndFetchLockFile( |
|
owner, |
|
repo, |
|
token, |
|
defaultBranch = 'main' |
|
) { |
|
const lockFiles = [ |
|
{ name: 'pnpm-lock.yaml', manager: 'pnpm' }, |
|
{ name: 'yarn.lock', manager: 'yarn' }, |
|
{ name: 'package-lock.json', manager: 'npm' }, |
|
]; |
|
|
|
// First, check root directory (most common case) |
|
for (const lockFile of lockFiles) { |
|
const content = await fetchFileContent( |
|
owner, |
|
repo, |
|
lockFile.name, |
|
token, |
|
defaultBranch |
|
); |
|
if (content) { |
|
debugLog( |
|
`Detected package manager: ${lockFile.manager} (found ${lockFile.name} at root)` |
|
); |
|
return { content, manager: lockFile.manager, fileName: lockFile.name }; |
|
} |
|
} |
|
|
|
// If not found at root, search for nested lock files |
|
debugLog(` No lock file at root, searching nested directories...`); |
|
const foundFiles = await searchLockFiles(owner, repo, token); |
|
|
|
if (foundFiles.length > 0) { |
|
// Use the first found lock file (prioritize by package manager preference) |
|
const priorityOrder = ['pnpm', 'yarn', 'npm']; |
|
const sortedFiles = foundFiles.sort((a, b) => { |
|
const aIndex = priorityOrder.indexOf(a.manager); |
|
const bIndex = priorityOrder.indexOf(b.manager); |
|
return aIndex - bIndex; |
|
}); |
|
|
|
const selectedFile = sortedFiles[0]; |
|
debugLog(` Found nested lock file: ${selectedFile.path}`); |
|
|
|
const content = await fetchFileContent( |
|
owner, |
|
repo, |
|
selectedFile.path, |
|
token, |
|
defaultBranch |
|
); |
|
|
|
if (content) { |
|
debugLog( |
|
`Detected package manager: ${selectedFile.manager} (found ${selectedFile.name} at ${selectedFile.path})` |
|
); |
|
return { |
|
content, |
|
manager: selectedFile.manager, |
|
fileName: selectedFile.name, |
|
}; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Parse pnpm lock file content and find vulnerable packages |
|
*/ |
|
function parsePnpmLockContent(content) { |
|
const packages = []; |
|
const allLines = content.split('\n'); |
|
|
|
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) { |
|
const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
|
const packagePattern = new RegExp(`^\\s+['"]?${escapedName}['"]?:\\s*$`); |
|
const packagePathPattern = new RegExp( |
|
`^\\s+/${escapedName}/([0-9]+\\.[0-9]+\\.[0-9]+[^:]*):` |
|
); |
|
|
|
let found = false; |
|
for (let i = 0; i < allLines.length; i++) { |
|
if (packagePattern.test(allLines[i])) { |
|
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) { |
|
packages.push({ |
|
name: packageName, |
|
version: versionMatch[1], |
|
path: packageName, |
|
}); |
|
found = true; |
|
break; |
|
} |
|
} |
|
if (found) break; |
|
} |
|
|
|
const pathMatch = allLines[i].match(packagePathPattern); |
|
if (pathMatch) { |
|
packages.push({ |
|
name: packageName, |
|
version: pathMatch[1], |
|
path: packageName, |
|
}); |
|
found = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
return packages; |
|
} |
|
|
|
/** |
|
* Parse yarn lock file content and find vulnerable packages |
|
*/ |
|
function parseYarnLockContent(content) { |
|
const packages = []; |
|
const allLines = content.split('\n'); |
|
|
|
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) { |
|
const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
|
const packageHeaderPattern = new RegExp( |
|
`^["']?${escapedName}@([0-9]+\\.[0-9]+\\.[0-9]+[^"':\\s]*):` |
|
); |
|
|
|
let found = false; |
|
for (let i = 0; i < allLines.length; i++) { |
|
const headerMatch = allLines[i].match(packageHeaderPattern); |
|
if (headerMatch) { |
|
for (let j = i + 1; j < Math.min(i + 10, allLines.length); j++) { |
|
const versionMatch = allLines[j].match( |
|
/^\s+version\s+"([0-9]+\.[0-9]+\.[0-9]+[^"]*)"/ |
|
); |
|
if (versionMatch) { |
|
packages.push({ |
|
name: packageName, |
|
version: versionMatch[1], |
|
path: packageName, |
|
}); |
|
found = true; |
|
break; |
|
} |
|
|
|
if (allLines[j].match(/^[^#\s]/) && !allLines[j].match(/^\s/)) { |
|
break; |
|
} |
|
} |
|
if (found) break; |
|
} |
|
} |
|
} |
|
|
|
return packages; |
|
} |
|
|
|
/** |
|
* Parse npm lock file content and find vulnerable packages |
|
*/ |
|
function parseNpmLockContent(content) { |
|
const packages = []; |
|
|
|
try { |
|
const lockData = JSON.parse(content); |
|
const packagesObj = lockData.packages || lockData.dependencies || {}; |
|
|
|
for (const packageName of Object.keys(VULNERABLE_PACKAGES)) { |
|
const possiblePaths = []; |
|
|
|
if (packageName.startsWith('@')) { |
|
const [scope, name] = packageName.split('/'); |
|
possiblePaths.push( |
|
`node_modules/${packageName}`, |
|
packageName, |
|
`node_modules/${scope}/node_modules/${name}` |
|
); |
|
} else { |
|
possiblePaths.push(`node_modules/${packageName}`, packageName); |
|
} |
|
|
|
let found = false; |
|
for (const path of possiblePaths) { |
|
const pkg = packagesObj[path]; |
|
if (pkg && pkg.version) { |
|
packages.push({ |
|
name: packageName, |
|
version: pkg.version, |
|
path: packageName, |
|
}); |
|
found = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!found && lockData.dependencies) { |
|
function searchDependencies(deps) { |
|
for (const [name, pkg] of Object.entries(deps)) { |
|
const matches = |
|
name === packageName || |
|
name === packageName.split('/').pop() || |
|
(packageName.startsWith('@') && name.startsWith(packageName)); |
|
|
|
if (matches && pkg.version) { |
|
packages.push({ |
|
name: packageName, |
|
version: pkg.version, |
|
path: packageName, |
|
}); |
|
return true; |
|
} |
|
|
|
if (pkg.dependencies && searchDependencies(pkg.dependencies)) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
searchDependencies(lockData.dependencies); |
|
} |
|
} |
|
} catch (error) { |
|
debugLog('Error parsing package-lock.json:', error.message); |
|
} |
|
|
|
return packages; |
|
} |
|
|
|
/** |
|
* Clear cache file |
|
*/ |
|
function clearCache() { |
|
try { |
|
if (fs.existsSync(CACHE_FILE)) { |
|
fs.unlinkSync(CACHE_FILE); |
|
console.log('ποΈ Cache cleared\n'); |
|
return true; |
|
} else { |
|
console.log('π¦ No cache file found\n'); |
|
return false; |
|
} |
|
} catch (error) { |
|
console.error(`β Error clearing cache: ${error.message}\n`); |
|
return false; |
|
} |
|
} |
|
|
|
/** |
|
* Load cache from file |
|
*/ |
|
function loadCache() { |
|
try { |
|
if (fs.existsSync(CACHE_FILE)) { |
|
const cacheData = fs.readFileSync(CACHE_FILE, 'utf-8'); |
|
const cache = JSON.parse(cacheData); |
|
const now = Date.now(); |
|
|
|
// Clean expired entries |
|
const validEntries = {}; |
|
for (const [key, value] of Object.entries(cache)) { |
|
if (value.timestamp && now - value.timestamp < CACHE_TTL_MS) { |
|
validEntries[key] = value; |
|
} |
|
} |
|
|
|
// Write back cleaned cache if entries were removed |
|
if (Object.keys(validEntries).length !== Object.keys(cache).length) { |
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(validEntries, null, 2)); |
|
} |
|
|
|
debugLog( |
|
`Loaded ${Object.keys(validEntries).length} valid cache entries` |
|
); |
|
return validEntries; |
|
} |
|
} catch (error) { |
|
debugLog(`Error loading cache: ${error.message}`); |
|
} |
|
return {}; |
|
} |
|
|
|
/** |
|
* Save cache to file |
|
*/ |
|
function saveCache(cache) { |
|
try { |
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2)); |
|
debugLog(`Saved cache with ${Object.keys(cache).length} entries`); |
|
} catch (error) { |
|
debugLog(`Error saving cache: ${error.message}`); |
|
} |
|
} |
|
|
|
/** |
|
* Get cache key for a repository |
|
*/ |
|
function getCacheKey(repo) { |
|
return `${repo.owner.login}/${repo.name}`; |
|
} |
|
|
|
/** |
|
* Check a single repository for vulnerabilities |
|
*/ |
|
async function checkRepo(repo, token, cache) { |
|
const { owner, name, default_branch } = repo; |
|
const fullName = `${owner.login}/${name}`; |
|
const cacheKey = getCacheKey(repo); |
|
|
|
// Check cache first |
|
if (cache[cacheKey]) { |
|
const cached = cache[cacheKey]; |
|
debugLog(` Using cached result for ${fullName}`); |
|
return cached.result; |
|
} |
|
|
|
console.log(` β Fetching lock files for ${fullName}...`); |
|
debugLog(`\nChecking repo: ${fullName}`); |
|
|
|
// Fetch lock file (use repo's default branch) |
|
const defaultBranch = default_branch || 'main'; |
|
const lockFile = await detectAndFetchLockFile( |
|
owner.login, |
|
name, |
|
token, |
|
defaultBranch |
|
); |
|
|
|
let result; |
|
if (!lockFile) { |
|
debugLog(` No lock file found - skipping`); |
|
result = { |
|
repo: fullName, |
|
status: 'no_lock_file', |
|
vulnerablePackages: [], |
|
}; |
|
} else { |
|
// Parse lock file based on package manager |
|
let installedPackages = []; |
|
if (lockFile.manager === 'pnpm') { |
|
installedPackages = parsePnpmLockContent(lockFile.content); |
|
} else if (lockFile.manager === 'yarn') { |
|
installedPackages = parseYarnLockContent(lockFile.content); |
|
} else if (lockFile.manager === 'npm') { |
|
installedPackages = parseNpmLockContent(lockFile.content); |
|
} |
|
|
|
debugLog( |
|
` Found ${installedPackages.length} potentially vulnerable packages` |
|
); |
|
|
|
// Check for vulnerabilities |
|
const vulnerablePackages = []; |
|
for (const pkg of installedPackages) { |
|
if (isVulnerableVersion(pkg.name, pkg.version)) { |
|
vulnerablePackages.push(pkg); |
|
} |
|
} |
|
|
|
result = { |
|
repo: fullName, |
|
status: vulnerablePackages.length > 0 ? 'vulnerable' : 'safe', |
|
packageManager: lockFile.manager, |
|
vulnerablePackages, |
|
}; |
|
} |
|
|
|
// Store in cache |
|
cache[cacheKey] = { |
|
timestamp: Date.now(), |
|
result: result, |
|
}; |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
* Main function |
|
*/ |
|
async function main() { |
|
// Check for clear cache flag |
|
const shouldClearCache = |
|
process.argv.includes('--clear-cache') || |
|
process.argv.includes('-c') || |
|
process.env.CLEAR_CACHE === 'true'; |
|
|
|
if (shouldClearCache) { |
|
clearCache(); |
|
if ( |
|
process.argv.includes('--clear-cache-only') || |
|
process.argv.includes('-C') |
|
) { |
|
process.exit(0); |
|
} |
|
} |
|
|
|
if (DEBUG) { |
|
console.log('π DEBUG MODE ENABLED\n'); |
|
} |
|
|
|
const org = process.env.GITHUB_ORG; |
|
const token = process.env.GITHUB_TOKEN; |
|
|
|
if (!org) { |
|
console.error('β Error: GITHUB_ORG environment variable is required'); |
|
console.error( |
|
'Usage: GITHUB_ORG=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js' |
|
); |
|
console.error(' Add --clear-cache or -c to clear the cache'); |
|
process.exit(1); |
|
} |
|
|
|
if (!token) { |
|
console.error('β Error: GITHUB_TOKEN environment variable is required'); |
|
console.error( |
|
'Usage: GITHUB_ORG=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js' |
|
); |
|
console.error(' Add --clear-cache or -c to clear the cache'); |
|
process.exit(1); |
|
} |
|
|
|
console.log( |
|
'π Checking SHA1Hulud vulnerability across GitHub organization...\n' |
|
); |
|
console.log(`Organization: ${org}`); |
|
console.log( |
|
'Reference: https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24\n' |
|
); |
|
|
|
// Declare cache outside try block so it's accessible in catch |
|
let cache = {}; |
|
|
|
try { |
|
// Load cache |
|
cache = loadCache(); |
|
const cacheSize = Object.keys(cache).length; |
|
const orgCacheKey = `__org_repos_${org}`; |
|
const hasOrgCache = cache[orgCacheKey] !== undefined; |
|
|
|
if (cacheSize > 0) { |
|
console.log( |
|
`π¦ Loaded ${cacheSize} cached entries${hasOrgCache ? ' (including repo list)' : ''}\n` |
|
); |
|
} |
|
|
|
// Fetch all repos (with caching) |
|
if (hasOrgCache) { |
|
console.log(`π¦ Using cached repository list...`); |
|
} else { |
|
console.log('Fetching list of repositories...'); |
|
} |
|
const repos = await fetchOrgRepos(org, token, cache); |
|
console.log(`Found ${repos.length} repositories\n`); |
|
|
|
if (repos.length === 0) { |
|
console.log('No repositories found.'); |
|
process.exit(0); |
|
} |
|
|
|
// Check each repo in parallel batches (configurable via BATCH_SIZE env var, default 10) |
|
const results = []; |
|
const batchSize = parseInt(process.env.BATCH_SIZE || '10', 10); |
|
let checkedCount = 0; |
|
let cacheHits = 0; |
|
let cacheMisses = 0; |
|
|
|
// Set up signal handlers to save cache on interruption |
|
const saveCacheAndExit = (signal) => { |
|
console.log(`\n\nβ οΈ Received ${signal}, saving cache before exit...`); |
|
saveCache(cache); |
|
process.exit(130); // Standard exit code for SIGINT |
|
}; |
|
|
|
process.on('SIGINT', () => saveCacheAndExit('SIGINT')); |
|
process.on('SIGTERM', () => saveCacheAndExit('SIGTERM')); |
|
process.on('uncaughtException', (error) => { |
|
console.error('\nβ Uncaught exception:', error.message); |
|
saveCache(cache); |
|
process.exit(1); |
|
}); |
|
|
|
for (let i = 0; i < repos.length; i += batchSize) { |
|
const batch = repos.slice(i, i + batchSize); |
|
const batchNumber = Math.floor(i / batchSize) + 1; |
|
const totalBatches = Math.ceil(repos.length / batchSize); |
|
|
|
console.log( |
|
`\nπ¦ Processing batch ${batchNumber}/${totalBatches} (${batch.length} repos)...` |
|
); |
|
|
|
// Process batch in parallel |
|
const batchPromises = batch.map(async (repo) => { |
|
checkedCount++; |
|
const repoIndex = checkedCount; |
|
const cacheKey = getCacheKey(repo); |
|
const isCached = cache[cacheKey] !== undefined; |
|
|
|
if (isCached) { |
|
cacheHits++; |
|
} else { |
|
cacheMisses++; |
|
} |
|
|
|
console.log( |
|
` [${repoIndex}/${repos.length}] ${isCached ? 'π¦' : 'π'} Checking ${repo.full_name}...` |
|
); |
|
|
|
try { |
|
const result = await checkRepo(repo, token, cache); |
|
|
|
if (result.status === 'vulnerable') { |
|
console.log( |
|
` β VULNERABLE: Found ${result.vulnerablePackages.length} vulnerable package(s) in ${repo.full_name}` |
|
); |
|
result.vulnerablePackages.forEach((pkg) => { |
|
console.log(` - ${pkg.name}@${pkg.version}`); |
|
}); |
|
} else if (result.status === 'safe') { |
|
console.log( |
|
` β
Safe (${result.packageManager}): ${repo.full_name}` |
|
); |
|
} else { |
|
console.log(` β οΈ No lock file found: ${repo.full_name}`); |
|
} |
|
|
|
return result; |
|
} catch (error) { |
|
console.error( |
|
` β Error checking ${repo.full_name}: ${error.message}` |
|
); |
|
return { |
|
repo: repo.full_name, |
|
status: 'error', |
|
error: error.message, |
|
vulnerablePackages: [], |
|
}; |
|
} |
|
}); |
|
|
|
// Wait for all repos in batch to complete |
|
const batchResults = await Promise.allSettled(batchPromises); |
|
|
|
// Extract results from settled promises |
|
batchResults.forEach((settled) => { |
|
if (settled.status === 'fulfilled') { |
|
results.push(settled.value); |
|
} else { |
|
console.error(` β Batch error: ${settled.reason}`); |
|
results.push({ |
|
repo: 'unknown', |
|
status: 'error', |
|
error: settled.reason?.message || 'Unknown error', |
|
vulnerablePackages: [], |
|
}); |
|
} |
|
}); |
|
|
|
// Save cache after each batch to ensure progress is saved |
|
saveCache(cache); |
|
|
|
// Small delay between batches to avoid rate limiting |
|
if (i + batchSize < repos.length) { |
|
await new Promise((resolve) => setTimeout(resolve, 200)); |
|
} |
|
} |
|
|
|
// Final cache save (redundant but ensures it's saved) |
|
saveCache(cache); |
|
if (cacheHits > 0 || cacheMisses > 0) { |
|
console.log(`\nπ¦ Cache: ${cacheHits} hits, ${cacheMisses} misses`); |
|
} |
|
|
|
// Summary |
|
console.log( |
|
'\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ' |
|
); |
|
console.log('\nπ SUMMARY'); |
|
console.log( |
|
'ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n' |
|
); |
|
|
|
const vulnerableRepos = results.filter((r) => r.status === 'vulnerable'); |
|
const safeRepos = results.filter((r) => r.status === 'safe'); |
|
const noLockFileRepos = results.filter((r) => r.status === 'no_lock_file'); |
|
const errorRepos = results.filter((r) => r.status === 'error'); |
|
|
|
console.log(`Total repositories checked: ${results.length}`); |
|
console.log(`β
Safe: ${safeRepos.length}`); |
|
console.log(`β Vulnerable: ${vulnerableRepos.length}`); |
|
console.log(`β οΈ No lock file: ${noLockFileRepos.length}`); |
|
console.log(`β Errors: ${errorRepos.length}`); |
|
|
|
if (vulnerableRepos.length > 0) { |
|
console.log('\nβ VULNERABLE REPOSITORIES:\n'); |
|
vulnerableRepos.forEach((result) => { |
|
console.log(` π¦ ${result.repo} (${result.packageManager})`); |
|
result.vulnerablePackages.forEach((pkg) => { |
|
const vulnerableVersions = VULNERABLE_PACKAGES[pkg.name]; |
|
console.log(` - ${pkg.name}@${pkg.version}`); |
|
console.log( |
|
` Vulnerable versions: ${vulnerableVersions.join(', ')}` |
|
); |
|
}); |
|
console.log(); |
|
}); |
|
} |
|
|
|
if (errorRepos.length > 0) { |
|
console.log('\nβ REPOSITORIES WITH ERRORS:\n'); |
|
errorRepos.forEach((result) => { |
|
console.log(` π¦ ${result.repo}: ${result.error}`); |
|
}); |
|
console.log(); |
|
} |
|
|
|
if (vulnerableRepos.length > 0) { |
|
console.log('\nβ οΈ ACTION REQUIRED:'); |
|
console.log( |
|
' Please update vulnerable packages in affected repositories to non-vulnerable versions.' |
|
); |
|
console.log( |
|
' For more information, visit: https://helixguard.ai/blog/malicious-sha1hulud-2025-11-24\n' |
|
); |
|
} |
|
|
|
// Exit with error code if vulnerabilities found |
|
if (vulnerableRepos.length > 0) { |
|
process.exit(1); |
|
} |
|
|
|
process.exit(0); |
|
} catch (error) { |
|
console.error('\nβ Fatal error:', error.message); |
|
if (DEBUG) { |
|
console.error(error); |
|
} |
|
// Try to save cache even on fatal error |
|
try { |
|
if (typeof cache !== 'undefined') { |
|
saveCache(cache); |
|
} |
|
} catch (cacheError) { |
|
debugLog(`Failed to save cache on error: ${cacheError.message}`); |
|
} |
|
process.exit(1); |
|
} |
|
} |
|
|
|
main(); |