muaddib-scanner 2.5.9 → 2.5.11

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "npm/evil-pkg@1.0.0",
3
- "timestamp": "2026-03-06T19:26:22.192Z",
3
+ "timestamp": "2026-03-06T20:50:14.544Z",
4
4
  "ecosystem": "npm",
5
5
  "summary": {
6
6
  "critical": 1,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "npm/evil-pkg@1.0.0",
3
- "timestamp": "2026-03-06T19:26:22.193Z",
3
+ "timestamp": "2026-03-06T20:50:14.545Z",
4
4
  "ecosystem": "npm",
5
5
  "summary": {
6
6
  "critical": 1,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "npm/suspect-pkg@1.0",
3
- "timestamp": "2026-03-06T19:26:22.192Z",
3
+ "timestamp": "2026-03-06T20:50:14.545Z",
4
4
  "ecosystem": "npm",
5
5
  "summary": {
6
6
  "critical": 0,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "npm/evil-pkg@2.0.0",
3
- "timestamp": "2026-03-06T19:26:22.572Z",
3
+ "timestamp": "2026-03-06T20:50:15.036Z",
4
4
  "ecosystem": "npm",
5
5
  "summary": {
6
6
  "critical": 1,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "date": "2026-03-06",
3
- "timestamp": "2026-03-06T19:26:22.664Z",
3
+ "timestamp": "2026-03-06T20:50:15.113Z",
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-1772825182189@1.0.0** — 1 finding(s)",
37
+ "value": "1. **npm/test-dedup-detection-1772830214542@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 19:26:22 UTC"
42
+ "text": "MUAD'DIB - Daily summary | 2026-03-06 20:50:15 UTC"
43
43
  },
44
- "timestamp": "2026-03-06T19:26:22.664Z"
44
+ "timestamp": "2026-03-06T20:50:15.113Z"
45
45
  }
46
46
  ]
47
47
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.5.9",
3
+ "version": "2.5.11",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ # Fix EROFS/EACCES on /opt/muaddib/logs/ directories
3
+ # Run on VPS: sudo bash scripts/fix-permissions.sh
4
+
5
+ set -e
6
+
7
+ MUADDIB_DIR="${MUADDIB_DIR:-/opt/muaddib}"
8
+ LOG_DIR="$MUADDIB_DIR/logs"
9
+ OWNER="${SUDO_USER:-ubuntu}"
10
+
11
+ echo "[fix-permissions] Fixing log directory permissions..."
12
+
13
+ sudo mkdir -p "$LOG_DIR/alerts"
14
+ sudo mkdir -p "$LOG_DIR/daily-reports"
15
+ sudo chown -R "$OWNER:$OWNER" "$LOG_DIR"
16
+ sudo chmod -R 755 "$LOG_DIR"
17
+
18
+ echo "[fix-permissions] Done. Verifying..."
19
+ ls -la "$LOG_DIR/"
20
+ echo "[fix-permissions] Owner: $(stat -c '%U:%G' "$LOG_DIR")"
21
+
22
+ # Verify writability
23
+ PROBE="$LOG_DIR/alerts/.write-test"
24
+ touch "$PROBE" && rm "$PROBE" && echo "[fix-permissions] Write test OK" || echo "[fix-permissions] ERROR: Still not writable!"
@@ -1,31 +1,51 @@
1
1
  const crypto = require('crypto');
2
2
 
3
3
  /**
4
- * Canary token definitions.
5
- * Each key is an env var name, value is a prefix.
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
- const CANARY_PREFIXES = {
9
- GITHUB_TOKEN: 'ghp_MUADDIB_CANARY_',
10
- NPM_TOKEN: 'npm_MUADDIB_CANARY_',
11
- AWS_ACCESS_KEY_ID: 'AKIA_MUADDIB_CANARY_',
12
- AWS_SECRET_ACCESS_KEY: 'MUADDIB_CANARY_SECRET_',
13
- GITLAB_TOKEN: 'glpat-MUADDIB_CANARY_',
14
- DOCKER_PASSWORD: 'dckr_MUADDIB_CANARY_',
15
- NPM_AUTH_TOKEN: 'npm_MUADDIB_CANARY_AUTH_',
16
- GH_TOKEN: 'ghp_MUADDIB_CANARY_GH_'
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 random suffixes.
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, prefix] of Object.entries(CANARY_PREFIXES)) {
27
- tokens[key] = prefix + suffix;
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
- CANARY_PREFIXES,
198
+ CANARY_GENERATORS,
179
199
  generateCanaryTokens,
180
200
  createCanaryEnvFile,
181
201
  createCanaryNpmrc,
@@ -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 suffixes per session).
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: 'MUADDIB_CANARY_GITHUB_f8k3t0k3n',
57
- NPM_TOKEN: 'MUADDIB_CANARY_NPM_s3cr3tt0k3n',
58
- AWS_ACCESS_KEY_ID: 'MUADDIB_CANARY_AKIAIOSFODNN7EXAMPLE',
59
- AWS_SECRET_ACCESS_KEY: 'MUADDIB_CANARY_wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
60
- SLACK_WEBHOOK_URL: 'MUADDIB_CANARY_SLACK',
61
- DISCORD_WEBHOOK_URL: 'MUADDIB_CANARY_DISCORD'
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 = `muaddib-sandbox-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
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', `MUADDIB_TIME_OFFSET_MS=${timeOffset}`);
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: 'MUADDIB_CANARY_GITHUB_f8k3t0k3n',
45
- NPM_TOKEN: 'MUADDIB_CANARY_NPM_s3cr3tt0k3n',
46
- AWS_ACCESS_KEY_ID: 'MUADDIB_CANARY_AKIAIOSFODNN7EXAMPLE',
47
- AWS_SECRET_ACCESS_KEY: 'MUADDIB_CANARY_wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
48
- SLACK_WEBHOOK_URL: 'MUADDIB_CANARY_SLACK',
49
- DISCORD_WEBHOOK_URL: 'MUADDIB_CANARY_DISCORD'
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
  };