muaddib-scanner 2.5.9 → 2.5.10
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/logs/alerts/{2026-03-06T19-26-22-192-evil-pkg.json → 2026-03-06T20-13-43-891-evil-pkg.json} +1 -1
- package/logs/alerts/{2026-03-06T19-26-22-193-evil-pkg.json → 2026-03-06T20-13-43-892-evil-pkg.json} +1 -1
- package/logs/alerts/{2026-03-06T19-26-22-192-suspect-pkg.json → 2026-03-06T20-13-43-892-suspect-pkg.json} +1 -1
- package/logs/alerts/{2026-03-06T19-26-22-572-evil-pkg.json → 2026-03-06T20-13-44-264-evil-pkg.json} +1 -1
- package/logs/daily-reports/2026-03-06.json +4 -4
- package/package.json +1 -1
- package/src/canary-tokens.js +37 -17
- package/src/sandbox/index.js +10 -9
- package/src/sandbox.js +6 -6
- package/src/scoring.js +0 -35
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"date": "2026-03-06",
|
|
3
|
-
"timestamp": "2026-03-
|
|
3
|
+
"timestamp": "2026-03-06T20:13:44.379Z",
|
|
4
4
|
"embed": {
|
|
5
5
|
"embeds": [
|
|
6
6
|
{
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"name": "Top Suspects",
|
|
37
|
-
"value": "1. **npm/test-dedup-detection-
|
|
37
|
+
"value": "1. **npm/test-dedup-detection-1772828023889@1.0.0** — 1 finding(s)",
|
|
38
38
|
"inline": false
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"footer": {
|
|
42
|
-
"text": "MUAD'DIB - Daily summary | 2026-03-06
|
|
42
|
+
"text": "MUAD'DIB - Daily summary | 2026-03-06 20:13:44 UTC"
|
|
43
43
|
},
|
|
44
|
-
"timestamp": "2026-03-
|
|
44
|
+
"timestamp": "2026-03-06T20:13:44.379Z"
|
|
45
45
|
}
|
|
46
46
|
]
|
|
47
47
|
},
|
package/package.json
CHANGED
package/src/canary-tokens.js
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* A random suffix is appended at generation time.
|
|
4
|
+
* Generate a base32-encoded string of the given length.
|
|
5
|
+
* Uses characters A-Z and 2-7 (RFC 4648 base32 alphabet).
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
function generateBase32(length) {
|
|
8
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
9
|
+
const bytes = crypto.randomBytes(length);
|
|
10
|
+
return Array.from(bytes).map(b => chars[b % 32]).join('');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canary token generators.
|
|
15
|
+
* Each generator produces a format-valid token that matches the real service format.
|
|
16
|
+
* No common marker (like "MUADDIB_CANARY") — detection relies on exact value matching.
|
|
17
|
+
*/
|
|
18
|
+
const CANARY_GENERATORS = {
|
|
19
|
+
// GitHub PAT: ghp_ + 36 alphanumeric chars
|
|
20
|
+
GITHUB_TOKEN: () => 'ghp_' + crypto.randomBytes(27).toString('base64url').substring(0, 36),
|
|
21
|
+
// npm token: npm_ + 36 hex chars
|
|
22
|
+
NPM_TOKEN: () => 'npm_' + crypto.randomBytes(18).toString('hex'),
|
|
23
|
+
// AWS Access Key: AKIA + 16 chars [A-Z2-7]
|
|
24
|
+
AWS_ACCESS_KEY_ID: () => 'AKIA' + generateBase32(16),
|
|
25
|
+
// AWS Secret: 40 chars base64
|
|
26
|
+
AWS_SECRET_ACCESS_KEY: () => crypto.randomBytes(30).toString('base64').substring(0, 40),
|
|
27
|
+
// GitLab PAT: glpat- + 20 alphanumeric
|
|
28
|
+
GITLAB_TOKEN: () => 'glpat-' + crypto.randomBytes(15).toString('base64url').substring(0, 20),
|
|
29
|
+
// Docker: dckr_pat_ + 56 alphanumeric
|
|
30
|
+
DOCKER_PASSWORD: () => 'dckr_pat_' + crypto.randomBytes(42).toString('base64url').substring(0, 56),
|
|
31
|
+
// npm auth token: same as NPM_TOKEN format
|
|
32
|
+
NPM_AUTH_TOKEN: () => 'npm_' + crypto.randomBytes(18).toString('hex'),
|
|
33
|
+
// GH_TOKEN: same as GITHUB_TOKEN format
|
|
34
|
+
GH_TOKEN: () => 'ghp_' + crypto.randomBytes(27).toString('base64url').substring(0, 36)
|
|
17
35
|
};
|
|
18
36
|
|
|
19
37
|
/**
|
|
20
|
-
* Generate a unique set of canary tokens with
|
|
38
|
+
* Generate a unique set of canary tokens with format-valid values.
|
|
39
|
+
* Each token matches its real service format (ghp_, AKIA, npm_, etc.).
|
|
21
40
|
* @returns {{ tokens: Record<string, string>, suffix: string }}
|
|
22
41
|
*/
|
|
23
42
|
function generateCanaryTokens() {
|
|
24
|
-
const suffix = crypto.randomBytes(8).toString('hex');
|
|
25
43
|
const tokens = {};
|
|
26
|
-
for (const [key,
|
|
27
|
-
tokens[key] =
|
|
44
|
+
for (const [key, generator] of Object.entries(CANARY_GENERATORS)) {
|
|
45
|
+
tokens[key] = generator();
|
|
28
46
|
}
|
|
47
|
+
// Suffix retained for backward compatibility (used in some callers)
|
|
48
|
+
const suffix = crypto.randomBytes(8).toString('hex');
|
|
29
49
|
return { tokens, suffix };
|
|
30
50
|
}
|
|
31
51
|
|
|
@@ -175,7 +195,7 @@ function detectCanaryInOutput(stdout, stderr, tokens) {
|
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
module.exports = {
|
|
178
|
-
|
|
198
|
+
CANARY_GENERATORS,
|
|
179
199
|
generateCanaryTokens,
|
|
180
200
|
createCanaryEnvFile,
|
|
181
201
|
createCanaryNpmrc,
|
package/src/sandbox/index.js
CHANGED
|
@@ -51,14 +51,15 @@ const SAFE_SANDBOX_CMDS = new Set(['timeout', 'node', 'npm', 'npx', 'su', 'env']
|
|
|
51
51
|
|
|
52
52
|
// Static canary tokens injected by sandbox-runner.sh (fallback honeypots).
|
|
53
53
|
// These are searched in the sandbox report as a complement to the dynamic
|
|
54
|
-
// tokens from canary-tokens.js (which use random
|
|
54
|
+
// tokens from canary-tokens.js (which use random values per session).
|
|
55
|
+
// Format-valid: match real service token formats to resist format-based detection.
|
|
55
56
|
const STATIC_CANARY_TOKENS = {
|
|
56
|
-
GITHUB_TOKEN: '
|
|
57
|
-
NPM_TOKEN: '
|
|
58
|
-
AWS_ACCESS_KEY_ID: '
|
|
59
|
-
AWS_SECRET_ACCESS_KEY: '
|
|
60
|
-
SLACK_WEBHOOK_URL: '
|
|
61
|
-
DISCORD_WEBHOOK_URL: '
|
|
57
|
+
GITHUB_TOKEN: 'ghp_R8kLmN2pQ4vW7xY9aB3cD5eF6gH8jK0mN2pQ4vW',
|
|
58
|
+
NPM_TOKEN: 'npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8',
|
|
59
|
+
AWS_ACCESS_KEY_ID: 'AKIAIOSFODNN7EXAMPLE',
|
|
60
|
+
AWS_SECRET_ACCESS_KEY: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
61
|
+
SLACK_WEBHOOK_URL: 'https://hooks.example.com/services/TCANARY/BCANARY/canary-slack-token',
|
|
62
|
+
DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/000000000000000000/abcdefghijklmnopqrstuvwxyz'
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
// Patterns indicating data exfiltration in HTTP bodies
|
|
@@ -151,7 +152,7 @@ async function runSingleSandbox(packageName, options = {}) {
|
|
|
151
152
|
let stdout = '';
|
|
152
153
|
let stderr = '';
|
|
153
154
|
let timedOut = false;
|
|
154
|
-
const containerName = `
|
|
155
|
+
const containerName = `npm-audit-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
155
156
|
|
|
156
157
|
const dockerArgs = [
|
|
157
158
|
'run',
|
|
@@ -175,7 +176,7 @@ async function runSingleSandbox(packageName, options = {}) {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// Inject time offset (preload.js deferred to entry point in sandbox-runner.sh)
|
|
178
|
-
dockerArgs.push('-e', `
|
|
179
|
+
dockerArgs.push('-e', `NODE_TIMING_OFFSET=${timeOffset}`);
|
|
179
180
|
|
|
180
181
|
// Both modes need NET_RAW for tcpdump (runs as root in entrypoint).
|
|
181
182
|
// Strict mode also needs NET_ADMIN for iptables network blocking.
|
package/src/sandbox.js
CHANGED
|
@@ -41,12 +41,12 @@ const DANGEROUS_CMDS = ['curl', 'wget', 'nc', 'netcat', 'python', 'python3', 'ba
|
|
|
41
41
|
// These are searched in the sandbox report as a complement to the dynamic
|
|
42
42
|
// tokens from canary-tokens.js (which use random suffixes per session).
|
|
43
43
|
const STATIC_CANARY_TOKENS = {
|
|
44
|
-
GITHUB_TOKEN: '
|
|
45
|
-
NPM_TOKEN: '
|
|
46
|
-
AWS_ACCESS_KEY_ID: '
|
|
47
|
-
AWS_SECRET_ACCESS_KEY: '
|
|
48
|
-
SLACK_WEBHOOK_URL: '
|
|
49
|
-
DISCORD_WEBHOOK_URL: '
|
|
44
|
+
GITHUB_TOKEN: 'ghp_R8kLmN2pQ4vW7xY9aB3cD5eF6gH8jK0mN2pQ4vW',
|
|
45
|
+
NPM_TOKEN: 'npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8',
|
|
46
|
+
AWS_ACCESS_KEY_ID: 'AKIAIOSFODNN7EXAMPLE',
|
|
47
|
+
AWS_SECRET_ACCESS_KEY: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
48
|
+
SLACK_WEBHOOK_URL: 'https://hooks.example.com/services/TCANARY/BCANARY/canary-slack-token',
|
|
49
|
+
DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/000000000000000000/abcdefghijklmnopqrstuvwxyz'
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// Patterns indicating data exfiltration in HTTP bodies
|
package/src/scoring.js
CHANGED
|
@@ -161,41 +161,7 @@ const FRAMEWORK_PROTO_RE = new RegExp(
|
|
|
161
161
|
'^(' + FRAMEWORK_PROTOTYPES.join('|') + ')\\.prototype\\.'
|
|
162
162
|
);
|
|
163
163
|
|
|
164
|
-
// ============================================
|
|
165
|
-
// BENIGN PACKAGE WHITELIST (v2.3.5)
|
|
166
|
-
// ============================================
|
|
167
|
-
// Well-known npm packages whose legitimate code patterns trigger false positives.
|
|
168
|
-
// For whitelisted packages, non-IOC threats are downgraded to LOW.
|
|
169
|
-
// IOC matches, lifecycle_shell_pipe, and cross_file_dataflow are NEVER downgraded
|
|
170
|
-
// — a compromised version of these packages would still be detected.
|
|
171
|
-
const BENIGN_PACKAGE_WHITELIST = new Set([
|
|
172
|
-
'meteor', // powershell PATH setup in install.js (dangerous_exec FP)
|
|
173
|
-
'blessed', // module._compile for terminal capabilities (module_compile FP)
|
|
174
|
-
'sharp', // native bindings with dynamic require + postinstall (lifecycle FP)
|
|
175
|
-
'forever', // process manager: detached spawn + HOME config access (dataflow FP)
|
|
176
|
-
'start-server-and-test', // curl/wget in test scripts, not install hooks (lifecycle FP)
|
|
177
|
-
'ultra-runner', // aliased fs.readFileSync in pnp.js + dynamic require (taint-tracked dataflow FP)
|
|
178
|
-
'node-gyp', // aliased child_process.spawn in node-gyp.js + env access (taint-tracked dataflow FP)
|
|
179
|
-
'graceful-fs' // aliased fs.readFile/readdir/writeFile monkey-patching (taint-tracked credential_tampering FP)
|
|
180
|
-
]);
|
|
181
|
-
|
|
182
|
-
// Threat types never affected by benign package whitelist (real compromise indicators)
|
|
183
|
-
const WHITELIST_EXEMPT_TYPES = new Set([
|
|
184
|
-
'ioc_match', 'known_malicious_package', 'pypi_malicious_package', 'shai_hulud_marker',
|
|
185
|
-
'lifecycle_shell_pipe',
|
|
186
|
-
'cross_file_dataflow'
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
164
|
function applyFPReductions(threats, reachableFiles, packageName) {
|
|
190
|
-
// Benign package whitelist: downgrade all non-IOC threats to LOW
|
|
191
|
-
if (packageName && BENIGN_PACKAGE_WHITELIST.has(packageName)) {
|
|
192
|
-
for (const t of threats) {
|
|
193
|
-
if (!WHITELIST_EXEMPT_TYPES.has(t.type) && t.severity !== 'LOW') {
|
|
194
|
-
t.severity = 'LOW';
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
165
|
// Count occurrences of each threat type (package-level, across all files)
|
|
200
166
|
const typeCounts = {};
|
|
201
167
|
for (const t of threats) {
|
|
@@ -369,6 +335,5 @@ function calculateRiskScore(deduped) {
|
|
|
369
335
|
|
|
370
336
|
module.exports = {
|
|
371
337
|
SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE,
|
|
372
|
-
BENIGN_PACKAGE_WHITELIST, WHITELIST_EXEMPT_TYPES,
|
|
373
338
|
isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore
|
|
374
339
|
};
|