Skip to content

Instantly share code, notes, and snippets.

@b0tting
Last active September 16, 2025 19:57
Show Gist options
  • Select an option

  • Save b0tting/8f808aa7d140b102924579540450d242 to your computer and use it in GitHub Desktop.

Select an option

Save b0tting/8f808aa7d140b102924579540450d242 to your computer and use it in GitHub Desktop.
Given a list of compromised NPM packages, check to see which have an updated or retracted version, or which are still compromised when pulling the most recent. Run with "node .\checkPackages.js .\packages.txt".
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const https = require('https');
// Cache latest version lookups per package to ensure only one fetch per unique package
/** @type {Map<string, Promise<string>>} */
const latestVersionCache = new Map();
/** Add a version entry for a package into the map */
function addVersion(packageToVersions, name, version) {
if (!packageToVersions.has(name)) packageToVersions.set(name, new Set());
packageToVersions.get(name).add(version);
}
/** Process a single comma-separated segment and update the map; returns updated currentPackage */
function processSegment(segment, currentPackage, packageToVersions, isSemver) {
// Case: "[email protected]" or "@scope/[email protected]"
const atPos = segment.lastIndexOf('@');
const hasExplicitVersionWithAt = atPos > 0 && atPos < segment.length - 1 && isSemver(segment.slice(atPos + 1).trim());
if (hasExplicitVersionWithAt) {
const name = segment.slice(0, atPos).trim();
const version = segment.slice(atPos + 1).trim();
addVersion(packageToVersions, name, version);
return name;
}
// Case: shorthand "@1.2.3" meaning version only (must have currentPackage)
if (segment.startsWith('@')) {
const maybeVersion = segment.slice(1).trim();
if (currentPackage && isSemver(maybeVersion)) {
addVersion(packageToVersions, currentPackage, maybeVersion);
return currentPackage;
}
}
// Case: "name 1.2.3" or "@scope/name 1.2.3"
const spaceIdx = segment.lastIndexOf(' ');
if (spaceIdx > 0) {
const left = segment.slice(0, spaceIdx).trim();
const right = segment.slice(spaceIdx + 1).trim();
if (isSemver(right)) {
addVersion(packageToVersions, left, right);
return left;
}
}
// Case: segment is just a semver (e.g. "1.2.3") using the last seen package
if (isSemver(segment) && currentPackage) {
addVersion(packageToVersions, currentPackage, segment);
return currentPackage;
}
// Case: segment is a bare package name
if (segment) {
if (!packageToVersions.has(segment)) packageToVersions.set(segment, new Set());
return segment;
}
return currentPackage;
}
/**
* Parse the packages.txt file into a map of packageName -> Set of compromised versions
* Supported input patterns per line (commas separate entries):
* - [email protected]
* - @scope/[email protected]
* - [email protected], 1.2.4
* - @scope/[email protected], @1.2.4 (shorthand for same package)
* - @scope/name 1.2.3 (space separator)
*/
function parsePackagesFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
/** @type {Map<string, Set<string>>} */
const packageToVersions = new Map();
const isSemver = (s) => /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(s);
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
let currentPackage = null;
// Split on commas to support multiple versions or entries per line
const segments = line.split(',').map(s => s.trim()).filter(Boolean);
for (const segment of segments) {
if (!segment) continue;
currentPackage = processSegment(segment, currentPackage, packageToVersions, isSemver);
}
}
return packageToVersions;
}
function fetchJson(url) {
return new Promise((resolve, reject) => {
https
.get(url, { headers: { 'Accept': 'application/vnd.npm.install-v1+json, application/json', 'User-Agent': 'npmPackageScanner/1.0 (+node https)'} }, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(new Error(`Failed to parse JSON from ${url}: ${err.message}`));
}
} else {
reject(new Error(`HTTP ${res.statusCode} for ${url}: ${data.slice(0, 200)}`));
}
});
})
.on('error', (err) => reject(err));
});
}
function compareSemver(a, b) {
const parse = (s) => {
const [core, pre] = s.split('-');
const [ma, mi, pa] = core.split('.').map((n) => parseInt(n, 10));
return { ma, mi, pa, pre: pre || '' };
};
const A = parse(a);
const B = parse(b);
if (A.ma !== B.ma) return A.ma - B.ma;
if (A.mi !== B.mi) return A.mi - B.mi;
if (A.pa !== B.pa) return A.pa - B.pa;
// Pre-release: absence > presence (e.g., 1.0.0 > 1.0.0-beta)
if (A.pre === B.pre) return 0;
if (!A.pre) return 1;
if (!B.pre) return -1;
return A.pre.localeCompare(B.pre);
}
async function getLatestVersionFromRegistry(pkgName) {
if (latestVersionCache.has(pkgName)) {
return latestVersionCache.get(pkgName);
}
const promise = (async () => {
const encoded = encodeURIComponent(pkgName);
const url = `https://registry.npmjs.org/${encoded}`;
const json = await fetchJson(url);
if (json && json['dist-tags'] && json['dist-tags'].latest) {
return json['dist-tags'].latest;
}
// Fallback: pick max of versions keys
if (json && json.versions && typeof json.versions === 'object') {
const versions = Object.keys(json.versions);
if (versions.length > 0) {
versions.sort(compareSemver);
return versions[versions.length - 1];
}
}
throw new Error(`No versions found for ${pkgName}`);
})();
latestVersionCache.set(pkgName, promise);
return promise;
}
async function main() {
const argPath = process.argv[2];
if (!argPath) {
console.error('Usage: node checkPackages.js <path-to-packages.txt>');
process.exit(1);
}
const filePath = path.resolve(process.cwd(), argPath);
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}
const packageToVersions = parsePackagesFile(filePath);
const entries = Array.from(packageToVersions.entries());
if (entries.length === 0) {
const raw = fs.readFileSync(filePath, 'utf8');
console.error('No packages parsed from packages.txt. Please check formatting.');
console.error(`packages.txt path: ${filePath}`);
console.error(`packages.txt size: ${raw.length} chars`);
const preview = raw.split(/\r?\n/).slice(0, 5).join('\n');
console.error('First lines preview:\n' + preview);
return;
}
// Limit concurrency to avoid overwhelming the registry
const concurrency = 8;
let index = 0;
const results = [];
async function worker() {
while (true) {
let i;
if (index >= entries.length) return;
i = index++;
const [pkg, compromisedSet] = entries[i];
try {
const latest = await getLatestVersionFromRegistry(pkg);
const stillCompromised = compromisedSet.has(latest);
results[i] = { pkg, compromisedSet, latest, stillCompromised, error: null };
} catch (err) {
results[i] = { pkg, compromisedSet, latest: null, stillCompromised: false, error: String(err.message || err) };
}
}
}
const workers = Array.from({ length: Math.min(concurrency, entries.length) }, () => worker());
await Promise.all(workers);
let compromisedCount = 0;
let notCompromisedCount = 0;
let errorCount = 0;
for (const r of results) {
if (!r) continue;
const versions = Array.from(r.compromisedSet || []).sort(compareSemver).join(', ');
if (r.error) {
console.log(`${r.pkg}@${versions} -> ERROR: ${r.error}`);
errorCount++;
} else if (r.stillCompromised) {
console.log(`${r.pkg}@${versions} -> still compromised`);
compromisedCount++;
} else {
console.log(`${r.pkg}@${versions} -> latest ${r.latest}`);
notCompromisedCount++;
}
}
console.log(`\nScanned ${results.filter(Boolean).length} packages: ${compromisedCount} still compromised, ${notCompromisedCount} not compromised, ${errorCount} errors.`);
}
// Given a file with a list of packages and versions, this script will check if requesting the packages from the npm registry
// gives you either a higher version (meaning there was an update) or a lower version (meaning the compromised version is detracted)
// If neither, the package is still compromised. Version information is retrieved by from the npm registry - see for example
// https://registry.npmjs.org/angulartics2
//
// App was mostly vibecoded with Cursor :)
//
// This works great with the list from https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages
// but I started with other examples from aikido.dev which had a comma separated list of versions per package. Also works.
main().catch((err) => {
console.error(err);
process.exit(1);
});
# List from https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages, 21:39 16-09-2025
@ahmedhfarag/[email protected]
@ahmedhfarag/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@art-ws/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@crowdstrike/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@ctrl/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@hestjs/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nativescript-community/[email protected]
@nexe/[email protected]
@nexe/[email protected]
@nexe/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@nstudio/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@operato/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@teselagen/[email protected]
@thangved/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@things-factory/[email protected]
@tnf-dev/[email protected]
@tnf-dev/[email protected]
@tnf-dev/[email protected]
@tnf-dev/[email protected]
@tnf-dev/[email protected]
@ui-ux-gang/[email protected]
@yoobic/[email protected]
@yoobic/[email protected]
@yoobic/[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment