Skip to content

Instantly share code, notes, and snippets.

@JuanFelix88
Last active May 13, 2026 12:24
Show Gist options
  • Select an option

  • Save JuanFelix88/ffaa62e8073dc2f70a3917e7b489e522 to your computer and use it in GitHub Desktop.

Select an option

Save JuanFelix88/ffaa62e8073dc2f70a3917e7b489e522 to your computer and use it in GitHub Desktop.
Check for leaked npm packages @TanStack and related packages.
#!/usr/bin/env node
/**
* @example node verify-npm-libraries.mjs --root localfolder --csv csv-file.csv --only-likely
*
* Plan with libraries affecteds:
* @link https://docs.google.com/spreadsheets/d/157SIcf-VSpb4VF_DlpppsG9GanNk6LorakSQgUEW99c/edit?usp=sharing
*/
import { access, readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const DEFAULT_IGNORED_DIRS = new Set([
'.git',
'.hg',
'.svn',
'node_modules',
'bower_components',
'dist',
'build',
'out',
'coverage',
'.next',
'.nuxt',
'.svelte-kit',
'.turbo',
'.cache',
'.vite',
'vendor',
]);
const DEPENDENCY_SECTIONS = [
'dependencies',
'devDependencies',
'optionalDependencies',
'peerDependencies',
];
const LOCKFILE_NAMES = new Set([
'package-lock.json',
'npm-shrinkwrap.json',
'pnpm-lock.yaml',
'yarn.lock',
]);
const args = parseArgs(process.argv.slice(2));
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const baseDir = path.resolve(args.root ?? scriptDir);
const csvPath = path.resolve(args.csv ?? path.join(scriptDir, 'pacotes-comprometidos.csv'));
if (args.help) {
printHelp();
process.exit(0);
}
const progress = createProgress({ enabled: !args.noProgress && !args.json });
try {
progress.start('Carregando CSV de pacotes comprometidos');
const compromisedPackages = await loadCompromisedPackages(csvPath);
progress.update('Mapeando package.json e lockfiles');
const projectFiles = await findProjectFiles(baseDir, progress);
const reports = [];
const allPackages = [];
let packageJsonProcessed = 0;
for (const packageJsonPath of projectFiles.packageJsonPaths) {
packageJsonProcessed += 1;
progress.update(
`Analisando package.json ${packageJsonProcessed}/${projectFiles.packageJsonPaths.length}: ${relativePath(packageJsonPath, baseDir)}`,
);
const packageJson = await readPackageJson(packageJsonPath);
if (!packageJson) {
continue;
}
const repoPath = await findRepositoryRoot(path.dirname(packageJsonPath), baseDir);
const dependencies = collectDependencies(packageJson, packageJsonPath, repoPath);
allPackages.push(...dependencies);
reports.push(...matchCompromisedDependencies(dependencies, compromisedPackages));
}
let lockfilesProcessed = 0;
for (const lockfilePath of projectFiles.lockfilePaths) {
lockfilesProcessed += 1;
progress.update(
`Analisando lockfile ${lockfilesProcessed}/${projectFiles.lockfilePaths.length}: ${relativePath(lockfilePath, baseDir)}`,
);
const repoPath = await findRepositoryRoot(path.dirname(lockfilePath), baseDir);
const directNames = await collectSiblingPackageJsonDependencyNames(lockfilePath);
const dependencies = await collectLockfileDependencies(lockfilePath, repoPath, directNames);
allPackages.push(...dependencies);
reports.push(...matchCompromisedDependencies(dependencies, compromisedPackages));
}
progress.stop();
printResult({
baseDir,
csvPath,
compromisedPackages,
packageJsonPaths: projectFiles.packageJsonPaths,
lockfilePaths: projectFiles.lockfilePaths,
allPackages,
reports,
listPackages: args.listPackages,
json: args.json,
onlyLikely: args.onlyLikely,
});
} catch (error) {
progress.stop();
console.error(`Erro: ${error.message}`);
process.exit(1);
}
function parseArgs(argv) {
const parsed = {
root: undefined,
csv: undefined,
help: false,
listPackages: false,
json: false,
onlyLikely: false,
noProgress: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === '--help' || arg === '-h') {
parsed.help = true;
continue;
}
if (arg === '--root') {
parsed.root = readRequiredArg(argv, index, arg);
index += 1;
continue;
}
if (arg.startsWith('--root=')) {
parsed.root = arg.slice('--root='.length);
continue;
}
if (arg === '--csv') {
parsed.csv = readRequiredArg(argv, index, arg);
index += 1;
continue;
}
if (arg.startsWith('--csv=')) {
parsed.csv = arg.slice('--csv='.length);
continue;
}
if (arg === '--list-packages') {
parsed.listPackages = true;
continue;
}
if (arg === '--json') {
parsed.json = true;
continue;
}
if (arg === '--only-likely') {
parsed.onlyLikely = true;
continue;
}
if (arg === '--no-progress') {
parsed.noProgress = true;
continue;
}
throw new Error(`Argumento desconhecido: ${arg}`);
}
return parsed;
}
function readRequiredArg(argv, index, flag) {
const value = argv[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(`Informe um valor para ${flag}`);
}
return value;
}
function createProgress({ enabled }) {
const frames = [
'[> ]',
'[>> ]',
'[>>> ]',
'[ >>> ]',
'[ >>>]',
'[ >>]',
'[ >]',
'[ <<]',
'[ <<<]',
'[ <<< ]',
'[<<< ]',
'[<< ]',
];
let frameIndex = 0;
let message = '';
let timer = null;
const canRender = enabled && process.stderr.isTTY;
function render() {
if (!canRender) {
return;
}
const frame = frames[frameIndex % frames.length];
frameIndex += 1;
process.stderr.write(`\r\x1b[2K${frame} ${message}`);
}
return {
start(initialMessage) {
message = initialMessage;
render();
if (canRender && !timer) {
timer = setInterval(render, 120);
}
},
update(nextMessage) {
message = nextMessage;
render();
},
stop() {
if (timer) {
clearInterval(timer);
timer = null;
}
if (canRender) {
process.stderr.write('\r\x1b[2K');
}
},
};
}
function printHelp() {
console.log(`Uso:
node verificar-pacotes-comprometidos.mjs [opcoes]
Opcoes:
--root <pasta> Pasta base para procurar package.json. Padrao: pasta do script.
--csv <arquivo> Caminho do CSV de pacotes comprometidos. Padrao: pacotes-comprometidos.csv ao lado do script.
--list-packages Lista todos os pacotes encontrados nos package.json e lockfiles.
--only-likely Oculta matches somente por nome quando a versao nao parece afetada.
--no-progress Desativa a animacao de progresso.
--json Imprime o resultado completo em JSON.
--help Mostra esta ajuda.
Exemplos:
node verificar-pacotes-comprometidos.mjs
node verificar-pacotes-comprometidos.mjs --root Juan
node verificar-pacotes-comprometidos.mjs --root Juan --list-packages
node verificar-pacotes-comprometidos.mjs --root Juan --only-likely
`);
}
async function loadCompromisedPackages(filePath) {
const content = await readFile(filePath, 'utf8');
const rows = parseCsv(content);
if (rows.length === 0) {
throw new Error(`CSV vazio: ${filePath}`);
}
const header = rows[0].map(normalizeHeader);
const indexes = {
ecosystem: findHeaderIndex(header, ['ecossistema', 'ecosystem']),
namespace: findHeaderIndex(header, ['namespace', 'escopo', 'scope']),
packageName: findHeaderIndex(header, ['nomedopacote', 'nomepacote', 'package', 'packagename', 'name']),
versions: findHeaderIndex(header, ['versoesidentificadas', 'versoes', 'versions', 'version']),
notes: findHeaderIndex(header, ['notas', 'notes', 'observacoes']),
};
if (indexes.packageName === -1) {
throw new Error('Nao encontrei a coluna com o nome do pacote no CSV.');
}
const compromised = new Map();
for (const row of rows.slice(1)) {
const ecosystem = readCell(row, indexes.ecosystem).trim().toLowerCase();
if (ecosystem && ecosystem !== 'npm') {
continue;
}
const packageName = readCell(row, indexes.packageName).trim();
const namespace = readCell(row, indexes.namespace).trim();
const fullName = buildPackageName(namespace, packageName);
if (!fullName) {
continue;
}
const notes = readCell(row, indexes.notes).trim();
const identifiedVersions = splitVersions(readCell(row, indexes.versions));
const versions = extractCompromisedVersions(notes, identifiedVersions);
const existing = compromised.get(fullName);
if (existing) {
existing.versions = unique([...existing.versions, ...versions]);
existing.notes = unique([existing.notes, notes].filter(Boolean)).join(' | ');
continue;
}
compromised.set(fullName, { versions, notes });
}
return compromised;
}
function parseCsv(content) {
const normalizedContent = content.replace(/^\uFEFF/, '');
const delimiter = detectDelimiter(normalizedContent);
const rows = [];
let row = [];
let cell = '';
let inQuotes = false;
for (let index = 0; index < normalizedContent.length; index += 1) {
const char = normalizedContent[index];
const next = normalizedContent[index + 1];
if (char === '"') {
if (inQuotes && next === '"') {
cell += '"';
index += 1;
} else {
inQuotes = !inQuotes;
}
continue;
}
if (char === delimiter && !inQuotes) {
row.push(cell);
cell = '';
continue;
}
if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && next === '\n') {
index += 1;
}
row.push(cell);
rows.push(row);
row = [];
cell = '';
continue;
}
cell += char;
}
if (cell.length > 0 || row.length > 0) {
row.push(cell);
rows.push(row);
}
return rows.filter((currentRow) => currentRow.some((currentCell) => currentCell.trim() !== ''));
}
function detectDelimiter(content) {
const firstLine = content.split(/\r?\n/, 1)[0] ?? '';
const commaCount = countOccurrences(firstLine, ',');
const semicolonCount = countOccurrences(firstLine, ';');
return semicolonCount > commaCount ? ';' : ',';
}
function countOccurrences(value, token) {
return [...value].filter((char) => char === token).length;
}
function normalizeHeader(value) {
return value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]/g, '');
}
function findHeaderIndex(header, candidates) {
return header.findIndex((value) => candidates.includes(value));
}
function readCell(row, index) {
if (index === -1) {
return '';
}
return row[index] ?? '';
}
function buildPackageName(namespace, packageName) {
if (!packageName) {
return '';
}
if (packageName.startsWith('@') || !namespace || isGlobalNamespace(namespace)) {
return packageName;
}
if (namespace.startsWith('@')) {
return `${namespace}/${packageName}`;
}
return packageName;
}
function isGlobalNamespace(namespace) {
return /^(global|sem namespace|global\/sem namespace|none|n\/a)$/i.test(namespace.trim());
}
function splitVersions(value) {
return unique(
value
.split(',')
.map((version) => normalizeVersion(version))
.filter(Boolean),
);
}
function extractCompromisedVersions(notes, identifiedVersions) {
if (!/comprometid/i.test(removeDiacritics(notes))) {
return identifiedVersions;
}
const versions = [];
const rangePattern = /v?(\d+(?:\.\d+){1,2}(?:[-+][0-9A-Za-z.-]+)?)\s+(?:a|ate|to|-)\s+v?(\d+(?:\.\d+){1,2}(?:[-+][0-9A-Za-z.-]+)?)/gi;
for (const match of notes.matchAll(rangePattern)) {
const start = normalizeVersion(match[1]);
const end = normalizeVersion(match[2]);
const versionsInRange = identifiedVersions.filter(
(version) => compareVersions(version, start) >= 0 && compareVersions(version, end) <= 0,
);
versions.push(...(versionsInRange.length > 0 ? versionsInRange : [start, end]));
}
const exactPattern = /(?:^|[^\w.-])v?(\d+(?:\.\d+){1,2}(?:[-+][0-9A-Za-z.-]+)?)(?=$|[^\w.-])/g;
for (const match of notes.matchAll(exactPattern)) {
versions.push(normalizeVersion(match[1]));
}
const compromisedVersions = unique(versions.filter(Boolean));
return compromisedVersions.length > 0 ? compromisedVersions : identifiedVersions;
}
function removeDiacritics(value) {
return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function normalizeVersion(value) {
return value
.trim()
.replace(/^v/i, '')
.replace(/^=+/, '')
.trim();
}
async function findProjectFiles(root, progress) {
const results = {
packageJsonPaths: [],
lockfilePaths: [],
};
const state = {
directoryCount: 0,
};
await walk(root, results, state, progress);
return results;
}
async function walk(currentDir, results, state, progress) {
let entries;
try {
entries = await readdir(currentDir, { withFileTypes: true });
} catch (error) {
console.warn(`Aviso: nao foi possivel ler ${currentDir}: ${error.message}`);
return;
}
state.directoryCount += 1;
if (state.directoryCount % 25 === 0) {
progress?.update(`Mapeando arquivos... ${state.directoryCount} pastas lidas`);
}
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (!DEFAULT_IGNORED_DIRS.has(entry.name)) {
await walk(fullPath, results, state, progress);
}
continue;
}
if (entry.isFile() && entry.name === 'package.json') {
results.packageJsonPaths.push(fullPath);
continue;
}
if (entry.isFile() && LOCKFILE_NAMES.has(entry.name)) {
results.lockfilePaths.push(fullPath);
}
}
}
async function readPackageJson(filePath) {
try {
return JSON.parse(await readFile(filePath, 'utf8'));
} catch (error) {
console.warn(`Aviso: package.json invalido em ${filePath}: ${error.message}`);
return null;
}
}
async function findRepositoryRoot(startDir, fallbackRoot) {
let currentDir = startDir;
while (currentDir.startsWith(fallbackRoot)) {
if (await exists(path.join(currentDir, '.git'))) {
return currentDir;
}
const parent = path.dirname(currentDir);
if (parent === currentDir) {
break;
}
currentDir = parent;
}
return inferProjectRoot(startDir, fallbackRoot);
}
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
function inferProjectRoot(startDir, fallbackRoot) {
const relative = path.relative(fallbackRoot, startDir);
if (!relative || relative.startsWith('..')) {
return startDir;
}
const segments = relative.split(path.sep).filter(Boolean);
if (segments[0]?.toLowerCase() === 'juan' && segments.length > 1) {
return path.join(fallbackRoot, segments[0], segments[1]);
}
return path.join(fallbackRoot, segments[0]);
}
function relativePath(filePath, root) {
return path.relative(root, filePath) || path.basename(filePath);
}
function collectDependencies(packageJson, packageJsonPath, repoPath) {
const dependencies = [];
for (const section of DEPENDENCY_SECTIONS) {
const values = packageJson[section];
if (!values || typeof values !== 'object' || Array.isArray(values)) {
continue;
}
for (const [name, versionSpec] of Object.entries(values)) {
dependencies.push({
repoPath,
sourceFilePath: packageJsonPath,
packageJsonPath,
lockfilePath: '',
sourceType: 'package.json',
dependencyType: 'direct',
section,
name,
versionSpec: String(versionSpec),
installedVersion: '',
});
}
}
return dependencies;
}
async function collectSiblingPackageJsonDependencyNames(lockfilePath) {
const packageJsonPath = path.join(path.dirname(lockfilePath), 'package.json');
const packageJson = await readPackageJsonIfExists(packageJsonPath);
if (!packageJson) {
return new Set();
}
return collectPackageJsonDependencyNames(packageJson);
}
async function readPackageJsonIfExists(packageJsonPath) {
if (!(await exists(packageJsonPath))) {
return null;
}
return readPackageJson(packageJsonPath);
}
function collectPackageJsonDependencyNames(packageJson) {
const names = new Set();
for (const section of DEPENDENCY_SECTIONS) {
const values = packageJson[section];
if (!values || typeof values !== 'object' || Array.isArray(values)) {
continue;
}
for (const name of Object.keys(values)) {
names.add(name);
}
}
return names;
}
async function collectLockfileDependencies(lockfilePath, repoPath, directNames) {
const fileName = path.basename(lockfilePath);
if (fileName === 'package-lock.json' || fileName === 'npm-shrinkwrap.json') {
return collectPackageLockDependencies(lockfilePath, repoPath, directNames);
}
if (fileName === 'pnpm-lock.yaml') {
return collectPnpmLockDependencies(lockfilePath, repoPath, directNames);
}
if (fileName === 'yarn.lock') {
return collectYarnLockDependencies(lockfilePath, repoPath, directNames);
}
return [];
}
async function collectPackageLockDependencies(lockfilePath, repoPath, directNamesFromPackageJson) {
let lockfile;
try {
lockfile = JSON.parse(await readFile(lockfilePath, 'utf8'));
} catch (error) {
console.warn(`Aviso: lockfile JSON invalido em ${lockfilePath}: ${error.message}`);
return [];
}
const sourceType = path.basename(lockfilePath);
const rootPackage = lockfile.packages?.[''];
const directNames = new Set([
...directNamesFromPackageJson,
...collectPackageJsonDependencyNames(rootPackage ?? {}),
]);
if (lockfile.packages && typeof lockfile.packages === 'object') {
return Object.entries(lockfile.packages)
.filter(([packagePath]) => packagePath !== '')
.map(([packagePath, packageInfo]) =>
createLockfileDependency({
repoPath,
lockfilePath,
sourceType,
name: packageInfo?.name || extractPackageNameFromNodeModulesPath(packagePath),
installedVersion: packageInfo?.version,
directNames,
section: detectLockfileSection(packageInfo),
}),
)
.filter(Boolean);
}
if (lockfile.dependencies && typeof lockfile.dependencies === 'object') {
const dependencies = [];
collectNpmLockV1Dependencies(lockfile.dependencies, {
repoPath,
lockfilePath,
sourceType,
directNames,
dependencies,
depth: 0,
});
return dependencies;
}
return [];
}
function collectNpmLockV1Dependencies(dependenciesByName, context) {
for (const [name, packageInfo] of Object.entries(dependenciesByName)) {
const dependency = createLockfileDependency({
repoPath: context.repoPath,
lockfilePath: context.lockfilePath,
sourceType: context.sourceType,
name,
installedVersion: packageInfo?.version,
directNames: context.depth === 0 ? new Set([...context.directNames, name]) : context.directNames,
section: detectLockfileSection(packageInfo),
forceDependencyType: context.depth === 0 ? 'direct' : undefined,
});
if (dependency) {
context.dependencies.push(dependency);
}
if (packageInfo?.dependencies && typeof packageInfo.dependencies === 'object') {
collectNpmLockV1Dependencies(packageInfo.dependencies, {
...context,
depth: context.depth + 1,
});
}
}
}
async function collectPnpmLockDependencies(lockfilePath, repoPath, directNamesFromPackageJson) {
const content = await readFile(lockfilePath, 'utf8');
const directNames = new Set([...directNamesFromPackageJson, ...collectPnpmImporterDependencyNames(content)]);
const dependencies = [];
for (const key of collectPnpmPackageKeys(content)) {
const parsed = parsePnpmPackageKey(key);
if (!parsed) {
continue;
}
const dependency = createLockfileDependency({
repoPath,
lockfilePath,
sourceType: 'pnpm-lock.yaml',
name: parsed.name,
installedVersion: parsed.version,
directNames,
section: 'packages',
});
if (dependency) {
dependencies.push(dependency);
}
}
return uniqueDependencies(dependencies);
}
function collectPnpmImporterDependencyNames(content) {
const names = new Set();
let inImporters = false;
let dependencySectionIndent = -1;
let dependencyNameIndent = -1;
for (const line of content.split(/\r?\n/)) {
if (!line.trim() || line.trimStart().startsWith('#')) {
continue;
}
const indent = countLeadingSpaces(line);
const trimmed = line.trim();
if (indent === 0) {
inImporters = trimmed === 'importers:';
dependencySectionIndent = -1;
dependencyNameIndent = -1;
continue;
}
if (!inImporters) {
continue;
}
const sectionMatch = trimmed.match(/^(dependencies|devDependencies|optionalDependencies|peerDependencies):$/);
if (sectionMatch) {
dependencySectionIndent = indent;
dependencyNameIndent = indent + 2;
continue;
}
if (dependencySectionIndent !== -1 && indent <= dependencySectionIndent) {
dependencySectionIndent = -1;
dependencyNameIndent = -1;
continue;
}
if (dependencyNameIndent !== -1 && indent === dependencyNameIndent) {
const key = parseYamlKeyFromLine(trimmed);
if (key) {
names.add(key);
}
}
}
return names;
}
function collectPnpmPackageKeys(content) {
const keys = [];
let inPackages = false;
for (const line of content.split(/\r?\n/)) {
if (!line.trim() || line.trimStart().startsWith('#')) {
continue;
}
const indent = countLeadingSpaces(line);
const trimmed = line.trim();
if (indent === 0) {
inPackages = trimmed === 'packages:';
continue;
}
if (!inPackages || indent !== 2) {
continue;
}
const key = parseYamlKeyFromLine(trimmed);
if (key) {
keys.push(key);
}
}
return keys;
}
async function collectYarnLockDependencies(lockfilePath, repoPath, directNames) {
const content = await readFile(lockfilePath, 'utf8');
const dependencies = [];
let currentNames = [];
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
if (!line.startsWith(' ') && trimmed.endsWith(':')) {
currentNames = splitYarnDescriptors(trimmed.slice(0, -1))
.map(parseYarnDescriptorName)
.filter(Boolean);
continue;
}
const version = parseYarnVersionLine(trimmed);
if (!version || currentNames.length === 0) {
continue;
}
for (const name of unique(currentNames)) {
const dependency = createLockfileDependency({
repoPath,
lockfilePath,
sourceType: 'yarn.lock',
name,
installedVersion: version,
directNames,
section: 'lockfile',
});
if (dependency) {
dependencies.push(dependency);
}
}
}
return uniqueDependencies(dependencies);
}
function createLockfileDependency({
repoPath,
lockfilePath,
sourceType,
name,
installedVersion,
directNames,
section,
forceDependencyType,
}) {
const version = normalizeVersion(String(installedVersion ?? ''));
if (!name || !version) {
return null;
}
return {
repoPath,
sourceFilePath: lockfilePath,
packageJsonPath: '',
lockfilePath,
sourceType,
dependencyType: forceDependencyType ?? (directNames.has(name) ? 'direct' : 'transitive'),
section,
name,
versionSpec: version,
installedVersion: version,
};
}
function detectLockfileSection(packageInfo) {
if (!packageInfo || typeof packageInfo !== 'object') {
return 'lockfile';
}
if (packageInfo.optional) {
return 'optional';
}
if (packageInfo.dev) {
return 'dev';
}
return 'prod';
}
function extractPackageNameFromNodeModulesPath(packagePath) {
const parts = packagePath.replace(/\\/g, '/').split('/').filter(Boolean);
let packageName = '';
for (let index = 0; index < parts.length; index += 1) {
if (parts[index] !== 'node_modules') {
continue;
}
const next = parts[index + 1];
if (!next) {
continue;
}
packageName = next.startsWith('@') && parts[index + 2] ? `${next}/${parts[index + 2]}` : next;
}
return packageName;
}
function parsePnpmPackageKey(rawKey) {
let key = unquote(rawKey)
.replace(/^\//, '')
.replace(/\(.+$/, '')
.trim();
if (!key || key.includes('link:') || key.includes('file:')) {
return null;
}
if (key.includes('/')) {
const slashParts = key.split('/');
const lastPart = slashParts.at(-1);
if (/^\d/.test(lastPart) && slashParts.length >= 2) {
const name = slashParts[0].startsWith('@') ? `${slashParts[0]}/${slashParts[1]}` : slashParts[0];
return { name, version: lastPart };
}
}
const versionSeparator = key.lastIndexOf('@');
if (versionSeparator <= 0) {
return null;
}
const name = key.slice(0, versionSeparator);
let version = key.slice(versionSeparator + 1);
if (version.startsWith('npm:')) {
const aliasSeparator = version.lastIndexOf('@');
version = aliasSeparator === -1 ? '' : version.slice(aliasSeparator + 1);
}
version = version.split('_')[0];
if (!name || !version || !/^\d/.test(version)) {
return null;
}
return { name, version };
}
function parseYamlKeyFromLine(trimmedLine) {
const match = trimmedLine.match(/^(.+?):(?:\s|$)/);
if (!match) {
return '';
}
return unquote(match[1].trim());
}
function splitYarnDescriptors(header) {
const descriptors = [];
let current = '';
let quote = '';
for (let index = 0; index < header.length; index += 1) {
const char = header[index];
if ((char === '"' || char === "'") && header[index - 1] !== '\\') {
quote = quote === char ? '' : char;
current += char;
continue;
}
if (char === ',' && !quote) {
descriptors.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.trim()) {
descriptors.push(current.trim());
}
return descriptors.map(unquote);
}
function parseYarnDescriptorName(descriptor) {
const value = unquote(descriptor).trim();
if (!value || value === '__metadata') {
return '';
}
const npmProtocolIndex = value.indexOf('@npm:');
const dependencyPart = npmProtocolIndex === -1 ? value : value.slice(0, npmProtocolIndex);
const separator = dependencyPart.startsWith('@')
? dependencyPart.indexOf('@', dependencyPart.indexOf('/') + 1)
: dependencyPart.indexOf('@');
return separator === -1 ? dependencyPart : dependencyPart.slice(0, separator);
}
function parseYarnVersionLine(trimmedLine) {
const quotedMatch = trimmedLine.match(/^version\s+"([^"]+)"/);
if (quotedMatch) {
return quotedMatch[1];
}
const colonMatch = trimmedLine.match(/^version:\s*"?([^"]+?)"?$/);
return colonMatch ? colonMatch[1] : '';
}
function countLeadingSpaces(value) {
return value.length - value.trimStart().length;
}
function unquote(value) {
const trimmed = value.trim();
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function uniqueDependencies(dependencies) {
const seen = new Set();
const result = [];
for (const dependency of dependencies) {
const key = `${dependency.sourceFilePath}\0${dependency.name}\0${dependency.installedVersion}\0${dependency.dependencyType}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(dependency);
}
return result;
}
function matchCompromisedDependencies(dependencies, compromisedPackages) {
const reports = [];
for (const dependency of dependencies) {
const compromised = compromisedPackages.get(dependency.name);
if (!compromised) {
continue;
}
const match = dependency.installedVersion
? classifyInstalledVersionMatch(dependency.installedVersion, compromised.versions)
: classifyVersionMatch(dependency.versionSpec, compromised.versions);
reports.push({
...dependency,
compromisedVersions: compromised.versions,
notes: compromised.notes,
matchType: match.type,
matchedVersions: match.versions,
});
}
return reports;
}
function classifyVersionMatch(versionSpec, compromisedVersions) {
const normalizedSpec = normalizeDependencySpec(versionSpec);
if (compromisedVersions.length === 0) {
return { type: 'nome encontrado; CSV sem versao', versions: [] };
}
if (!normalizedSpec) {
return { type: 'nome encontrado; versao local nao comparavel', versions: [] };
}
const matchedVersions = compromisedVersions.filter((version) => satisfiesVersionSpec(version, normalizedSpec));
if (matchedVersions.length > 0) {
return { type: 'versao possivelmente afetada', versions: matchedVersions };
}
return { type: 'nome encontrado; versao nao parece afetada', versions: [] };
}
function classifyInstalledVersionMatch(installedVersion, compromisedVersions) {
const normalizedInstalledVersion = normalizeVersion(installedVersion);
if (compromisedVersions.length === 0) {
return { type: 'nome encontrado; CSV sem versao', versions: [] };
}
if (!normalizedInstalledVersion) {
return { type: 'nome encontrado; versao instalada nao comparavel', versions: [] };
}
const matchedVersions = compromisedVersions.filter(
(version) => normalizeVersion(version) === normalizedInstalledVersion,
);
if (matchedVersions.length > 0) {
return { type: 'versao instalada afetada', versions: matchedVersions };
}
return { type: 'nome encontrado; versao instalada nao afetada', versions: [] };
}
function normalizeDependencySpec(spec) {
const value = spec.trim();
if (!value || value === '*' || value.toLowerCase() === 'latest') {
return value;
}
if (/^(workspace:|file:|link:|portal:|git\+|https?:|github:|gitlab:|bitbucket:)/i.test(value)) {
return '';
}
if (value.startsWith('npm:')) {
const aliasVersion = value.split('@').at(-1);
return aliasVersion && aliasVersion !== value ? aliasVersion : '';
}
return value.replace(/^v/i, '');
}
function satisfiesVersionSpec(version, spec) {
const normalizedVersion = normalizeVersion(version);
if (!normalizedVersion || !spec) {
return false;
}
if (spec === '*' || spec.toLowerCase() === 'latest') {
return true;
}
return spec
.split('||')
.some((part) => satisfiesSingleRange(normalizedVersion, part.trim()));
}
function satisfiesSingleRange(version, range) {
if (!range) {
return false;
}
const hyphenMatch = range.match(/^(.+)\s+-\s+(.+)$/);
if (hyphenMatch) {
return compareVersions(version, hyphenMatch[1].trim()) >= 0 && compareVersions(version, hyphenMatch[2].trim()) <= 0;
}
const comparators = range.match(/(?:<=|>=|<|>|=|\^|~)?\s*v?\d+(?:\.\d+|\.x|\.\*)?(?:\.\d+|\.x|\.\*)?(?:[-+][0-9A-Za-z.-]+)?|[xX*]/g);
if (!comparators) {
return normalizeVersion(range) === version;
}
return comparators.every((comparator) => satisfiesComparator(version, comparator.trim()));
}
function satisfiesComparator(version, comparator) {
if (!comparator || comparator === '*' || /^[xX]$/.test(comparator)) {
return true;
}
const operatorMatch = comparator.match(/^(<=|>=|<|>|=|\^|~)?\s*(.+)$/);
const operator = operatorMatch?.[1] ?? '';
const target = normalizeVersion(operatorMatch?.[2] ?? comparator);
if (/[xX*]/.test(target)) {
return satisfiesWildcard(version, target);
}
if (operator === '^') {
return satisfiesCaret(version, target);
}
if (operator === '~') {
return satisfiesTilde(version, target);
}
const comparison = compareVersions(version, target);
if (operator === '>') {
return comparison > 0;
}
if (operator === '>=') {
return comparison >= 0;
}
if (operator === '<') {
return comparison < 0;
}
if (operator === '<=') {
return comparison <= 0;
}
return comparison === 0;
}
function satisfiesWildcard(version, target) {
const versionParts = parseVersion(version);
const targetParts = target.split('.');
return targetParts.every((part, index) => {
if (part === '*' || /^x$/i.test(part)) {
return true;
}
return Number(part) === versionParts[index];
});
}
function satisfiesCaret(version, target) {
const lower = parseVersion(target);
const upper = [...lower];
if (lower[0] > 0) {
upper[0] += 1;
upper[1] = 0;
upper[2] = 0;
} else if (lower[1] > 0) {
upper[1] += 1;
upper[2] = 0;
} else {
upper[2] += 1;
}
return compareVersionParts(parseVersion(version), lower) >= 0 && compareVersionParts(parseVersion(version), upper) < 0;
}
function satisfiesTilde(version, target) {
const lower = parseVersion(target);
const upper = [...lower];
upper[1] += 1;
upper[2] = 0;
return compareVersionParts(parseVersion(version), lower) >= 0 && compareVersionParts(parseVersion(version), upper) < 0;
}
function compareVersions(left, right) {
return compareVersionParts(parseVersion(left), parseVersion(right));
}
function compareVersionParts(left, right) {
for (let index = 0; index < 3; index += 1) {
if (left[index] > right[index]) {
return 1;
}
if (left[index] < right[index]) {
return -1;
}
}
return 0;
}
function parseVersion(version) {
const [major = '0', minor = '0', patch = '0'] = normalizeVersion(version).split(/[.-]/);
return [major, minor, patch].map((part) => {
const value = Number.parseInt(part, 10);
return Number.isFinite(value) ? value : 0;
});
}
function printResult({
baseDir,
csvPath,
compromisedPackages,
packageJsonPaths,
lockfilePaths,
allPackages,
reports,
listPackages,
json,
onlyLikely,
}) {
const visibleReports = onlyLikely ? reports.filter(isLikelyAffectedReport) : reports;
const directDependencyCount = allPackages.filter((dependency) => dependency.sourceType === 'package.json').length;
const lockfileDependencyCount = allPackages.length - directDependencyCount;
if (json) {
console.log(
JSON.stringify(
{
baseDir,
csvPath,
packageJsonCount: packageJsonPaths.length,
lockfileCount: lockfilePaths.length,
dependencyCount: allPackages.length,
directDependencyCount,
lockfileDependencyCount,
compromisedPackageCount: compromisedPackages.size,
affectedRepositoryCount: new Set(visibleReports.map((report) => report.repoPath)).size,
matches: visibleReports,
packages: listPackages ? allPackages : undefined,
},
null,
2,
),
);
return;
}
console.log(`Pasta analisada: ${baseDir}`);
console.log(`CSV analisado: ${csvPath}`);
console.log(`Pacotes comprometidos no CSV: ${compromisedPackages.size}`);
console.log(`package.json analisados: ${packageJsonPaths.length}`);
console.log(`lockfiles analisados: ${lockfilePaths.length}`);
console.log(`Dependencias diretas encontradas: ${directDependencyCount}`);
console.log(`Dependencias resolvidas em lockfiles: ${lockfileDependencyCount}`);
console.log(
`${onlyLikely ? 'Repositorios com versao possivelmente afetada' : 'Repositorios com pacote citado no CSV'}: ${
new Set(visibleReports.map((report) => report.repoPath)).size
}`,
);
console.log('');
if (listPackages) {
console.log('Pacotes encontrados:');
for (const dependency of allPackages) {
console.log(`- ${dependency.name}@${dependency.installedVersion || dependency.versionSpec}`);
console.log(` Repositorio: ${dependency.repoPath}`);
console.log(` Origem: ${dependency.sourceType}`);
console.log(` Arquivo: ${dependency.sourceFilePath}`);
console.log(` Tipo: ${dependency.dependencyType}`);
console.log(` Secao: ${dependency.section}`);
}
console.log('');
}
if (visibleReports.length === 0) {
console.log(
onlyLikely
? 'Nenhuma versao possivelmente comprometida foi encontrada nos package.json/lockfiles analisados.'
: 'Nenhum pacote comprometido foi encontrado nos package.json/lockfiles analisados.',
);
return;
}
console.log('Possiveis repositorios comprometidos:');
for (const report of visibleReports) {
const matchedVersions = report.matchedVersions.length > 0 ? report.matchedVersions.join(', ') : 'sem match exato';
const compromisedVersions = report.compromisedVersions.length > 0 ? report.compromisedVersions.join(', ') : 'nao informado';
console.log(`- Repositorio: ${report.repoPath}`);
console.log(` Arquivo: ${report.sourceFilePath}`);
console.log(` Origem: ${report.sourceType}`);
console.log(` Tipo: ${report.dependencyType}`);
console.log(` Pacote: ${report.name}`);
console.log(` Secao: ${report.section}`);
console.log(
report.installedVersion
? ` Versao instalada/resolvida: ${report.installedVersion}`
: ` Versao declarada no package.json: ${report.versionSpec}`,
);
console.log(` Versoes comprometidas no CSV: ${compromisedVersions}`);
console.log(` Resultado: ${report.matchType}`);
console.log(` Versoes que bateram: ${matchedVersions}`);
if (report.notes) {
console.log(` Notas do CSV: ${report.notes}`);
}
}
}
function isLikelyAffectedReport(report) {
return (
report.matchType === 'versao possivelmente afetada' ||
report.matchType === 'versao instalada afetada' ||
report.matchType === 'nome encontrado; CSV sem versao' ||
report.matchType === 'nome encontrado; versao local nao comparavel' ||
report.matchType === 'nome encontrado; versao instalada nao comparavel'
);
}
function unique(values) {
return [...new Set(values)];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment