pkgradar 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/checks.js +29 -19
- package/lib/scan.js +15 -2
- package/package.json +1 -1
package/lib/checks.js
CHANGED
|
@@ -95,14 +95,11 @@ function checkWormFiles(pkg) {
|
|
|
95
95
|
out.push({ sev: SEV.CRITICAL, code: 'worm-ioc-file', msg: `known worm artifact file: ${e.name}`, evidence: path.relative(pkg.dir, full) });
|
|
96
96
|
}
|
|
97
97
|
if (/(^|[\\/])\.github[\\/]workflows([\\/]|$)/.test(d) && /\.ya?ml$/i.test(low)) {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
out.push(nasty
|
|
104
|
-
? { sev: SEV.CRITICAL, code: 'malicious-gh-workflow', msg: `bundled GitHub Actions workflow runs a piped shell download / exfiltrates secrets`, evidence: path.relative(pkg.dir, full) }
|
|
105
|
-
: { sev: SEV.INFO, code: 'embedded-gh-workflow', msg: `package ships a GitHub Actions workflow (${e.name})`, evidence: path.relative(pkg.dir, full) });
|
|
98
|
+
// Lots of legit packages publish their whole repo, .github included, and plenty
|
|
99
|
+
// of normal CI workflows pipe an installer into a shell. On its own this is not a
|
|
100
|
+
// signal, so it is INFO only. A genuinely malicious workflow file with a known
|
|
101
|
+
// name is already caught by worm-ioc-file.
|
|
102
|
+
out.push({ sev: SEV.INFO, code: 'embedded-gh-workflow', msg: `package ships a GitHub Actions workflow (${e.name})`, evidence: path.relative(pkg.dir, full) });
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
}
|
|
@@ -158,21 +155,28 @@ function checkObfuscation(pkg) {
|
|
|
158
155
|
const weak = []; // weak reasons only matter in combination
|
|
159
156
|
let critical = false;
|
|
160
157
|
|
|
161
|
-
// ---
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
if (softHits.length >= 3) { strong.push(`co-occurring worm terms: ${softHits.slice(0, 6).join(', ')}`); }
|
|
166
|
-
else if (softHits.length) weak.push(`worm term(s): ${softHits.join(', ')}`);
|
|
167
|
-
|
|
168
|
-
// --- javascript-obfuscator signature ---
|
|
158
|
+
// --- "executable payload" context: dynamic code execution over decoded data, or
|
|
159
|
+
// heavy identifier mangling. Worm IOC strings are only damning when they sit
|
|
160
|
+
// alongside one of these; on their own they also appear in scanners, advisories,
|
|
161
|
+
// and writeups (this file included). ---
|
|
169
162
|
const hexIds = (t.match(/_0x[a-f0-9]{4,6}\b/g) || []).length;
|
|
163
|
+
const evalOverDecoded = /(eval|new Function|Function\()\s*\(?\s*(atob|Buffer\.from\s*\([^)]*base64|unescape|decodeURIComponent)/.test(t);
|
|
164
|
+
const procFromDecoded = /\bchild_process\b[\s\S]{0,400}\b(atob|Buffer\.from\s*\([^)]*base64)/.test(t);
|
|
165
|
+
const execContext = evalOverDecoded || procFromDecoded || hexIds > 80;
|
|
166
|
+
if (evalOverDecoded) strong.push('constructs & executes decoded payload (eval/Function over atob/Buffer.from)');
|
|
167
|
+
if (procFromDecoded) strong.push('spawns a process from decoded data');
|
|
170
168
|
if (hexIds > 80) strong.push(`${hexIds} _0x… mangled identifiers (javascript-obfuscator)`);
|
|
171
169
|
else if (hexIds > 20) weak.push(`${hexIds} _0x… identifiers`);
|
|
172
170
|
|
|
173
|
-
// ---
|
|
174
|
-
|
|
175
|
-
if (
|
|
171
|
+
// --- worm IOC strings ---
|
|
172
|
+
const hardHits = WORM_HARD_MARKERS.filter((m) => t.includes(m));
|
|
173
|
+
if (hardHits.length) {
|
|
174
|
+
if (execContext) { strong.push(`worm IOC string(s) in an executing payload: ${hardHits.join(', ')}`); critical = true; }
|
|
175
|
+
else weak.push(`contains known IOC string(s): ${hardHits.join(', ')} (also found in security tooling / advisories / writeups)`);
|
|
176
|
+
}
|
|
177
|
+
const softHits = WORM_SOFT_MARKERS.filter((m) => t.includes(m));
|
|
178
|
+
if (softHits.length >= 3 && execContext) strong.push(`co-occurring worm terms with an executable payload: ${softHits.slice(0, 6).join(', ')}`);
|
|
179
|
+
else if (softHits.length) weak.push(`worm term(s): ${softHits.slice(0, 6).join(', ')}`);
|
|
176
180
|
|
|
177
181
|
// --- weak structural signals (modern bundlers do these constantly) ---
|
|
178
182
|
const longest = t.split('\n').reduce((m, l) => Math.max(m, l.length), 0);
|
|
@@ -217,7 +221,13 @@ function checkManifestAnomalies(pkg) {
|
|
|
217
221
|
return out;
|
|
218
222
|
}
|
|
219
223
|
|
|
224
|
+
let SELF = null;
|
|
225
|
+
try { SELF = require('../package.json').name; } catch { /* ignore */ }
|
|
226
|
+
|
|
220
227
|
function runAll(pkg) {
|
|
228
|
+
// Don't scan our own package: this file necessarily contains the IOC strings it
|
|
229
|
+
// searches for, so scanning a cached copy of pkgradar would flag pkgradar.
|
|
230
|
+
if (SELF && pkg && pkg.manifest && pkg.manifest.name === SELF) return [];
|
|
221
231
|
const findings = [];
|
|
222
232
|
for (const fn of [checkLifecycleHooks, checkWormFiles, checkManifestAnomalies, checkObfuscation]) {
|
|
223
233
|
try { findings.push(...fn(pkg)); } catch (e) { /* ignore per-check failure */ }
|
package/lib/scan.js
CHANGED
|
@@ -76,9 +76,22 @@ async function scan(opts = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
findings
|
|
79
|
+
// Collapse identical findings that show up in several stores (e.g. the same package
|
|
80
|
+
// cached under multiple npx hashes) into one, noting how many places it was seen.
|
|
81
|
+
const byKey = new Map();
|
|
82
|
+
for (const f of findings) {
|
|
83
|
+
const k = `${f.package}@${f.version}|${f.code}|${f.evidence || ''}|${f.message}`;
|
|
84
|
+
const prev = byKey.get(k);
|
|
85
|
+
if (prev) { prev._copies = (prev._copies || 1) + 1; if (!prev._stores.includes(f.store)) prev._stores.push(f.store); }
|
|
86
|
+
else { f._copies = 1; f._stores = [f.store]; byKey.set(k, f); }
|
|
87
|
+
}
|
|
88
|
+
const deduped = [...byKey.values()].map((f) => {
|
|
89
|
+
if (f._copies > 1) { f.store = `${f._stores.join(', ')} (${f._copies}x)`; }
|
|
90
|
+
delete f._copies; delete f._stores; return f;
|
|
91
|
+
});
|
|
92
|
+
deduped.sort((a, b) => b.sevNum - a.sevNum || a.package.localeCompare(b.package));
|
|
80
93
|
const totalPackages = [...new Set(allPkgList.map((p) => `${p.name}@${p.version}`))].length;
|
|
81
|
-
return { cwd, stores: storeStats, totalPackages, totalScanned: seen.size, findings, osv };
|
|
94
|
+
return { cwd, stores: storeStats, totalPackages, totalScanned: seen.size, findings: deduped, osv };
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
function splitNV(nv) { const i = nv.lastIndexOf('@'); return [nv.slice(0, i), nv.slice(i + 1)]; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkgradar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Content-based supply-chain scanner for npm/pnpm/yarn/bun: inspects the bytes you actually installed (lifecycle hooks, obfuscated payloads, worm IOCs) instead of just matching package names against an advisory list.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pkgradar": "bin/pkgradar.js"
|