|
#!/usr/bin/env node |
|
|
|
/** |
|
* GitHub Organization SHA1Hulud Vulnerability Checker |
|
* |
|
* Checks all repositories in a GitHub organization or user account for SHA1Hulud vulnerabilities |
|
* by fetching lock files directly via GitHub API (no cloning required) |
|
* |
|
* Usage: GITHUB_ACCOUNT=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js |
|
* GITHUB_ACCOUNT=myuser GITHUB_ACCOUNT_TYPE=user 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_ACCOUNT - GitHub organization or user name (required) |
|
* GITHUB_ORG - Alias for GITHUB_ACCOUNT (for backwards compatibility) |
|
* GITHUB_ACCOUNT_TYPE - Type of account: 'org' (default) or 'user' |
|
* 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 for an organization or user account (with pagination and caching) |
|
*/ |
|
async function fetchAccountRepos(account, accountType, token, cache) { |
|
const cacheKey = `__account_repos_${accountType}_${account}`; |
|
|
|
// Check cache first |
|
if (cache[cacheKey]) { |
|
const cached = cache[cacheKey]; |
|
debugLog( |
|
`Using cached repository list for ${accountType}: ${account} (${cached.result.length} repos)`, |
|
); |
|
return cached.result; |
|
} |
|
|
|
const repos = []; |
|
let page = 1; |
|
let hasMore = true; |
|
|
|
debugLog(`Fetching repositories for ${accountType}: ${account}`); |
|
|
|
while (hasMore) { |
|
try { |
|
// Use different API endpoints for orgs vs users |
|
const basePath = |
|
accountType === "user" |
|
? `/users/${account}/repos` |
|
: `/orgs/${account}/repos`; |
|
const path = `${basePath}?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"); |
|
} |
|
|
|
// Support both GITHUB_ACCOUNT and GITHUB_ORG (for backwards compatibility) |
|
const account = process.env.GITHUB_ACCOUNT || process.env.GITHUB_ORG; |
|
const accountType = process.env.GITHUB_ACCOUNT_TYPE || "org"; |
|
const token = process.env.GITHUB_TOKEN; |
|
|
|
if (!account) { |
|
console.error( |
|
"β Error: GITHUB_ACCOUNT (or GITHUB_ORG) environment variable is required", |
|
); |
|
console.error( |
|
"Usage: GITHUB_ACCOUNT=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js", |
|
); |
|
console.error( |
|
" GITHUB_ACCOUNT=myuser GITHUB_ACCOUNT_TYPE=user 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_ACCOUNT=myorg GITHUB_TOKEN=ghp_xxx node tools/check-github-org-sha1hulud.js", |
|
); |
|
console.error( |
|
" GITHUB_ACCOUNT=myuser GITHUB_ACCOUNT_TYPE=user 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 (accountType !== "org" && accountType !== "user") { |
|
console.error( |
|
"β Error: GITHUB_ACCOUNT_TYPE must be 'org' or 'user' (default: 'org')", |
|
); |
|
process.exit(1); |
|
} |
|
|
|
const accountTypeLabel = accountType === "user" ? "User" : "Organization"; |
|
console.log( |
|
`π Checking SHA1Hulud vulnerability across GitHub ${accountTypeLabel.toLowerCase()}...\n`, |
|
); |
|
console.log(`${accountTypeLabel}: ${account}`); |
|
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 accountCacheKey = `__account_repos_${accountType}_${account}`; |
|
const hasAccountCache = cache[accountCacheKey] !== undefined; |
|
|
|
if (cacheSize > 0) { |
|
console.log( |
|
`π¦ Loaded ${cacheSize} cached entries${hasAccountCache ? " (including repo list)" : ""}\n`, |
|
); |
|
} |
|
|
|
// Fetch all repos (with caching) |
|
if (hasAccountCache) { |
|
console.log(`π¦ Using cached repository list...`); |
|
} else { |
|
console.log("Fetching list of repositories..."); |
|
} |
|
const repos = await fetchAccountRepos(account, accountType, 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(); |