🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

worm-sign

Package Overview
Dependencies
Maintainers
1
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

worm-sign - npm Package Compare versions

Comparing version
3.0.0
to
3.0.1
+11
-4
dist/bin/scan.js

@@ -273,6 +273,13 @@ #!/usr/bin/env node

if (matches.length > 0) {
console.log(chalk.red(`\n🚫 FOUND ${matches.length} COMPROMISED PACKAGES:`));
matches.forEach((m) => {
console.log(chalk.red(` - ${m.name}@${m.version}`) + chalk.dim(` (found in ${m.section})`));
});
const title = chalk.bold.red(`🚫 FOUND ${matches.length} COMPROMISED PACKAGES`);
const list = matches
.map((m) => chalk.red(` - ${m.name}@${m.version}`) + chalk.dim(` (found in ${m.section})`))
.join('\n');
console.log(boxen(`${title}\n\n${list}`, {
padding: 1,
borderStyle: 'double',
borderColor: 'red',
title: 'CRITICAL SECURITY ALERT',
titleAlignment: 'center',
}));
}

@@ -279,0 +286,0 @@ }

{
"name": "worm-sign",
"version": "3.0.0",
"version": "3.0.1",
"description": "A prescient scanner to detect and banish Shai Hulud malware from your dependencies.",

@@ -5,0 +5,0 @@ "main": "dist/src/index.js",

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EntropyCalculator = void 0;
exports.calculateEntropy = calculateEntropy;
exports.calculateEntropyStream = calculateEntropyStream;
exports.isHighEntropy = isHighEntropy;
/**

@@ -9,5 +14,25 @@ * Shannon Entropy Analysis

*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateEntropy = calculateEntropy;
exports.isHighEntropy = isHighEntropy;
class EntropyCalculator {
frequencies = {};
totalBytes = 0;
update(chunk) {
const len = chunk.length;
this.totalBytes += len;
for (let i = 0; i < len; i++) {
const byte = typeof chunk === 'string' ? chunk.charCodeAt(i) : chunk[i];
this.frequencies[byte] = (this.frequencies[byte] || 0) + 1;
}
}
digest() {
if (this.totalBytes === 0)
return 0;
let entropy = 0;
for (const count of Object.values(this.frequencies)) {
const p = count / this.totalBytes;
entropy -= p * Math.log2(p);
}
return entropy;
}
}
exports.EntropyCalculator = EntropyCalculator;
/**

@@ -21,19 +46,18 @@ * Calculates the Shannon entropy of a string.

function calculateEntropy(input) {
if (!input || input.length === 0) {
return 0;
}
const frequencies = {};
const len = input.length;
for (let i = 0; i < len; i++) {
const byte = typeof input === 'string' ? input.charCodeAt(i) : input[i];
frequencies[byte] = (frequencies[byte] || 0) + 1;
}
let entropy = 0;
for (const count of Object.values(frequencies)) {
const p = count / len;
entropy -= p * Math.log2(p);
}
return entropy;
const calculator = new EntropyCalculator();
calculator.update(input);
return calculator.digest();
}
/**
* Calculates entropy from a readable stream.
*/
function calculateEntropyStream(stream) {
return new Promise((resolve, reject) => {
const calculator = new EntropyCalculator();
stream.on('data', (chunk) => calculator.update(chunk));
stream.on('error', reject);
stream.on('end', () => resolve(calculator.digest()));
});
}
/**
* Checks if a string has suspiciously high entropy.

@@ -40,0 +64,0 @@ *

@@ -58,5 +58,7 @@ "use strict";

Object.defineProperty(exports, "parseCsv", { enumerable: true, get: function () { return csv_1.parseCsv; } });
const yarn_1 = __importDefault(require("./package-managers/yarn"));
const pnpm_1 = __importDefault(require("./package-managers/pnpm"));
function loadJson(filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
try {
const raw = fs.readFileSync(filePath, 'utf8');
const json = JSON.parse(raw);

@@ -72,3 +74,2 @@ if (Array.isArray(json)) {

return [];
return [];
}

@@ -104,4 +105,5 @@ catch (e) {

const fetchUrl = async (targetUrl, attempt = 1) => {
// SSRF Protection
await (0, validators_1.validateUrl)(targetUrl);
// SSRF Protection: Resolve and validate IP first
const resolvedIp = await (0, validators_1.validateUrl)(targetUrl);
const urlObj = new URL(targetUrl);
return new Promise((resolve, reject) => {

@@ -113,3 +115,8 @@ if (attempt > 5) {

const options = {
hostname: resolvedIp,
port: urlObj.port || 443,
path: urlObj.pathname + urlObj.search,
servername: urlObj.hostname, // SNI
headers: {
Host: urlObj.hostname, // Host header
Accept: type === 'json' ? 'application/json' : 'text/csv',

@@ -120,3 +127,3 @@ 'User-Agent': 'worm-sign',

};
const req = https.get(targetUrl, options, (res) => {
const req = https.get(options, (res) => {
if (res.statusCode &&

@@ -291,36 +298,50 @@ res.statusCode >= 300 &&

try {
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
if (KNOWN_MALWARE_HASHES.has(hash)) {
allWarnings.push(`CONFIRMED MALWARE file detected: '${file}' (Hash match: ${hash})`);
const stats = fs.statSync(filePath);
const isLarge = stats.size > 5 * 1024 * 1024;
const hash = crypto.createHash('sha256');
// Only calculate entropy if file is large (optimization)
const entropyCalc = isLarge ? new entropy_1.EntropyCalculator() : null;
const stream = fs.createReadStream(filePath);
await new Promise((resolve, reject) => {
stream.on('data', (chunk) => {
hash.update(chunk);
if (entropyCalc) {
entropyCalc.update(chunk);
}
});
stream.on('error', reject);
stream.on('end', resolve);
});
const finalHash = hash.digest('hex');
if (KNOWN_MALWARE_HASHES.has(finalHash)) {
allWarnings.push(`CONFIRMED MALWARE file detected: '${file}' (Hash match: ${finalHash})`);
}
else {
// Check for high entropy (obfuscation) if file is large (> 5MB)
const stats = fs.statSync(filePath);
if (stats.size > 5 * 1024 * 1024) {
const entropy = (0, entropy_1.calculateEntropy)(fileBuffer);
// Threshold 7.5 for binary/compressed data is conservative;
// but for text files (js), > 5.2 is suspicious.
// The test expects "High Entropy" warning.
// Let's use 7.0 as a safe bet for "packed malware" in JS context if it's huge.
if (entropy > 7.0) {
allWarnings.push(`HIGH RISK file detected: '${file}' (High Entropy: ${entropy.toFixed(2)}, Size: ${stats.size} bytes)`);
}
else if (isLarge && entropyCalc) {
const entropy = entropyCalc.digest();
// Threshold 7.0 for packed malware in JS context
if (entropy > 7.0) {
allWarnings.push(`HIGH RISK file detected: '${file}' (High Entropy: ${entropy.toFixed(2)}, Size: ${stats.size} bytes)`);
}
allWarnings.push(`Suspicious file detected: '${file}' (associated with Shai Hulud)`);
}
else {
allWarnings.push(`Suspicious file detected: '${file}' (associated with Shai Hulud)`);
}
}
catch {
allWarnings.push(`Suspicious file detected: '${file}' (associated with Shai Hulud) - could not read hash`);
catch (e) {
const msg = e instanceof Error ? e.message : String(e);
allWarnings.push(`Suspicious file detected: '${file}' (associated with Shai Hulud) - could not read: ${msg}`);
}
}
}
// 3. Scan Dependencies using Arborist
// 3. Scan Dependencies
const compromisedMap = buildCompromisedMap(compromisedEntries);
const matches = [];
let treeLoaded = false;
// Try npm (Arborist) first
try {
const arb = new arborist_1.default({ path: resolvedRoot });
// loadVirtual() reads the lockfile and builds the tree without checking node_modules
const tree = await arb.loadVirtual();
debug(`Loaded dependency tree with ${tree.inventory.size} nodes.`);
treeLoaded = true;
for (const node of tree.inventory.values()) {

@@ -330,3 +351,2 @@ const { name, version, integrity } = node;

if (shouldFlag(info, version, integrity)) {
// Determine section (dev/prod) - Arborist nodes have 'dev' property
const section = node.dev ? 'devDependencies' : 'dependencies';

@@ -339,9 +359,50 @@ matches.push({ name, version, section });

const msg = e instanceof Error ? e.message : String(e);
// Fallback or error reporting
if (msg.includes('ENOENT') && msg.includes('lock')) {
throw new Error('No lockfile found. Please run npm install/yarn install to generate one.');
debug(`Arborist failed: ${msg}`);
// If it's not a missing lockfile error, rethrow
if (!msg.includes('ENOENT') && !msg.includes('lockfile') && !msg.includes('shrinkwrap')) {
throw new Error(`Failed to load dependency tree: ${msg}`);
}
throw new Error(`Failed to load dependency tree: ${msg}`);
}
// Fallback to Yarn or pnpm if npm failed
if (!treeLoaded) {
debug('Checking for Yarn or pnpm lockfiles...');
const handlers = [yarn_1.default, pnpm_1.default];
let handlerFound = false;
for (const handler of handlers) {
const lockFile = handler.findLockFile(resolvedRoot);
if (lockFile) {
debug(`Found ${handler.label} lockfile: ${lockFile}`);
handlerFound = true;
const result = handler.loadLockPackages(lockFile);
if (!result.success) {
result.warnings.forEach((w) => allWarnings.push(w));
continue;
}
// Iterate over loaded packages
for (const [name, versions] of result.packages.entries()) {
const info = compromisedMap.get(name);
if (!info)
continue;
for (const version of versions) {
// Get integrity if available
let integrity;
if (result.packageIntegrity) {
const pkgIntegrity = result.packageIntegrity.get(name);
if (pkgIntegrity) {
integrity = pkgIntegrity.get(version);
}
}
if (shouldFlag(info, version, integrity)) {
matches.push({ name, version, section: 'locked' });
}
}
}
break; // Stop after first successful handler
}
}
if (!handlerFound) {
throw new Error('No lockfile found (package-lock.json, yarn.lock, pnpm-lock.yaml). Please install dependencies.');
}
}
return { matches, warnings: allWarnings };
}

@@ -50,12 +50,23 @@ "use strict";

trim: true,
relax_column_count: true
relax_column_count: true,
});
return parsed.map((record) => {
return parsed
.map((record) => {
// Try to find name and version fields
const name = record['package name'] || record['name'] || record['Package Name'] || record['package_name'] || Object.values(record)[0];
const version = record['package version'] || record['version'] || record['Package Version'] || record['package_version'] || Object.values(record)[1] || '';
const name = record['package name'] ||
record['name'] ||
record['Package Name'] ||
record['package_name'] ||
Object.values(record)[0];
const version = record['package version'] ||
record['version'] ||
record['Package Version'] ||
record['package_version'] ||
Object.values(record)[1] ||
'';
const reason = record['MSC ID'] || record['reason'] || '';
const integrity = record['integrity'] || record['hash'] || record['shasum'] || undefined;
return { name, version, reason, integrity };
}).filter((p) => !!p.name);
})
.filter((p) => !!p.name);
}

@@ -62,0 +73,0 @@ catch (e) {

@@ -99,3 +99,3 @@ "use strict";

}
return;
return hostname;
}

@@ -113,5 +113,5 @@ // Resolve hostname

}
resolve();
resolve(address);
});
});
}

@@ -15,3 +15,3 @@ "use strict";

// This matches the key in scripts/encrypt-signatures.ts
const CIPHER_KEY = 0x5F;
const CIPHER_KEY = 0x5f;
/**

@@ -18,0 +18,0 @@ * Decrypts a buffer of XOR-encoded bytes back into a string.

{
"name": "worm-sign",
"version": "3.0.0",
"version": "3.0.1",
"description": "A prescient scanner to detect and banish Shai Hulud malware from your dependencies.",

@@ -5,0 +5,0 @@ "main": "dist/src/index.js",

# Worm Sign 🪱🚫
[![CI Status](https://github.com/BranLang/worm-sign/actions/workflows/ci.yml/badge.svg)](https://github.com/BranLang/worm-sign/actions/workflows/ci.yml)
[![npm version](https://badge.fury.io/js/worm-sign.svg)](https://badge.fury.io/js/worm-sign)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm provenance](https://img.shields.io/badge/provenance-verified-green)](https://docs.npmjs.com/generating-provenance-statements)

@@ -4,0 +7,0 @@