muaddib-scanner 1.4.1 → 1.4.3

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/README.fr.md CHANGED
@@ -246,13 +246,21 @@ Ajoutez a `.pre-commit-config.yaml`:
246
246
  ```yaml
247
247
  repos:
248
248
  - repo: https://github.com/DNSZLSK/muad-dib
249
- rev: v1.2.7
249
+ rev: v1.4.1
250
250
  hooks:
251
251
  - id: muaddib-scan # Scanner toutes les menaces
252
252
  # - id: muaddib-diff # Ou: seulement les nouvelles
253
253
  # - id: muaddib-paranoid # Ou: mode ultra-strict
254
254
  ```
255
255
 
256
+ #### Supprimer les hooks
257
+
258
+ ```bash
259
+ muaddib remove-hooks [path]
260
+ ```
261
+
262
+ Supprime tous les hooks MUAD'DIB (husky et git natif).
263
+
256
264
  #### Avec husky
257
265
 
258
266
  ```bash
@@ -261,6 +269,10 @@ npx husky add .husky/pre-commit "npx muaddib scan . --fail-on high"
261
269
  npx husky add .husky/pre-commit "npx muaddib diff HEAD --fail-on high"
262
270
  ```
263
271
 
272
+ ### Version check
273
+
274
+ MUAD'DIB verifie automatiquement les nouvelles versions au demarrage et vous notifie si une mise a jour est disponible.
275
+
264
276
  ---
265
277
 
266
278
  ## Features
@@ -446,7 +458,7 @@ Editez les fichiers YAML dans `iocs/` :
446
458
  mitre: T1195.002
447
459
  ```
448
460
 
449
- ### Développer
461
+ ### Developper
450
462
 
451
463
  ```bash
452
464
  git clone https://github.com/DNSZLSK/muad-dib
@@ -455,6 +467,13 @@ npm install
455
467
  npm test
456
468
  ```
457
469
 
470
+ ### Tests
471
+
472
+ - **145 tests unitaires/integration** — 80% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
473
+ - **56 tests de fuzzing** — YAML malformé, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
474
+ - **15 tests adversariaux** — Packages malveillants simulés, taux de détection 15/15
475
+ - **Audit ESLint securité** — `eslint-plugin-security` avec 14 règles activées
476
+
458
477
  ---
459
478
 
460
479
  ## Communauté
@@ -465,8 +484,9 @@ npm test
465
484
 
466
485
  ## Documentation
467
486
 
468
- - [Threat Model](docs/threat-model.md) - Ce que MUAD'DIB détecte et ne détecte pas
469
- - [IOCs YAML](iocs/) - Base de données des menaces
487
+ - [Threat Model](docs/threat-model.md) - Ce que MUAD'DIB detecte et ne detecte pas
488
+ - [Rapport d'audit securité v1.4.1](docs/MUADDIB_Security_Audit_Report_v1.4.1.pdf) - Audit complet (58 issues corrigees)
489
+ - [IOCs YAML](iocs/) - Base de donnees des menaces
470
490
 
471
491
  ---
472
492
 
package/README.md CHANGED
@@ -264,7 +264,7 @@ Add to `.pre-commit-config.yaml`:
264
264
  ```yaml
265
265
  repos:
266
266
  - repo: https://github.com/DNSZLSK/muad-dib
267
- rev: v1.2.7
267
+ rev: v1.4.1
268
268
  hooks:
269
269
  - id: muaddib-scan # Scan all threats
270
270
  # - id: muaddib-diff # Or: only new threats
@@ -279,6 +279,14 @@ npx husky add .husky/pre-commit "npx muaddib scan . --fail-on high"
279
279
  npx husky add .husky/pre-commit "npx muaddib diff HEAD --fail-on high"
280
280
  ```
281
281
 
282
+ #### Remove hooks
283
+
284
+ ```bash
285
+ muaddib remove-hooks [path]
286
+ ```
287
+
288
+ Removes all MUAD'DIB hooks (husky and git native).
289
+
282
290
  #### Native git hooks
283
291
 
284
292
  ```bash
@@ -286,6 +294,10 @@ muaddib init-hooks --type git
286
294
  # Creates .git/hooks/pre-commit
287
295
  ```
288
296
 
297
+ ### Version check
298
+
299
+ MUAD'DIB automatically checks for new versions on startup and notifies you if an update is available.
300
+
289
301
  ---
290
302
 
291
303
  ## Features
@@ -480,6 +492,13 @@ npm install
480
492
  npm test
481
493
  ```
482
494
 
495
+ ### Testing
496
+
497
+ - **145 unit/integration tests** — 80% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
498
+ - **56 fuzz tests** — Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
499
+ - **15 adversarial tests** — Simulated malicious packages, 15/15 detection rate
500
+ - **ESLint security audit** — `eslint-plugin-security` with 14 rules enabled
501
+
483
502
  ---
484
503
 
485
504
  ## Community
@@ -491,6 +510,7 @@ npm test
491
510
  ## Documentation
492
511
 
493
512
  - [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
513
+ - [Security Audit Report v1.4.1](docs/MUADDIB_Security_Audit_Report_v1.4.1.pdf) - Full security audit (58 issues fixed)
494
514
  - [IOCs YAML](iocs/) - Threat database
495
515
 
496
516
  ---
package/bin/muaddib.js CHANGED
@@ -242,6 +242,7 @@ const helpText = `
242
242
  muaddib update Update IOCs
243
243
  muaddib scrape Scrape new IOCs
244
244
  muaddib sandbox <pkg> Analyze in isolated Docker container
245
+ muaddib version Show version
245
246
 
246
247
  Diff Examples:
247
248
  muaddib diff HEAD~1 Compare with previous commit
@@ -267,7 +268,11 @@ const helpText = `
267
268
  `;
268
269
 
269
270
  // Main
270
- if (!command || command === '--help' || command === '-h') {
271
+ if (command === 'version' || command === '--version' || command === '-v') {
272
+ const pkg = require('../package.json');
273
+ console.log(`muaddib-scanner v${pkg.version}`);
274
+ process.exit(0);
275
+ } else if (!command || command === '--help' || command === '-h') {
271
276
  if (command === '--help' || command === '-h') {
272
277
  console.log(helpText);
273
278
  process.exit(0);
package/eslint.config.mjs CHANGED
@@ -1,17 +1,32 @@
1
1
  import js from "@eslint/js";
2
2
  import globals from "globals";
3
+ import security from "eslint-plugin-security";
3
4
  import { defineConfig } from "eslint/config";
4
5
 
5
6
  export default defineConfig([
6
7
  {
7
8
  files: ["**/*.{js,mjs,cjs}"],
8
- plugins: { js },
9
+ plugins: { js, security },
9
10
  extends: ["js/recommended"],
10
11
  languageOptions: {
11
12
  globals: globals.node
12
13
  },
13
14
  rules: {
14
- "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]
15
+ "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
16
+ "security/detect-buffer-noassert": "warn",
17
+ "security/detect-child-process": "warn",
18
+ "security/detect-disable-mustache-escape": "warn",
19
+ "security/detect-eval-with-expression": "warn",
20
+ "security/detect-new-buffer": "warn",
21
+ "security/detect-no-csrf-before-method-override": "warn",
22
+ "security/detect-non-literal-fs-filename": "warn",
23
+ "security/detect-non-literal-regexp": "warn",
24
+ "security/detect-non-literal-require": "warn",
25
+ "security/detect-object-injection": "warn",
26
+ "security/detect-possible-timing-attacks": "warn",
27
+ "security/detect-pseudoRandomBytes": "warn",
28
+ "security/detect-unsafe-regex": "warn",
29
+ "security/detect-bidi-characters": "warn"
15
30
  }
16
31
  },
17
32
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@eslint/js": "9.39.2",
49
49
  "eslint": "9.39.2",
50
+ "eslint-plugin-security": "^3.0.1",
50
51
  "globals": "17.3.0"
51
52
  }
52
53
  }
package/src/index.js CHANGED
@@ -150,8 +150,22 @@ async function run(targetPath, options = {}) {
150
150
  threats.push(...paranoidThreats);
151
151
  }
152
152
 
153
+ // Deduplicate: same file + same type + same message = show once with count
154
+ const deduped = [];
155
+ const seen = new Map();
156
+ for (const t of threats) {
157
+ const key = `${t.file}::${t.type}::${t.message}`;
158
+ if (seen.has(key)) {
159
+ seen.get(key).count++;
160
+ } else {
161
+ const entry = { ...t, count: 1 };
162
+ seen.set(key, entry);
163
+ deduped.push(entry);
164
+ }
165
+ }
166
+
153
167
  // Enrich each threat with rules
154
- const enrichedThreats = threats.map(t => {
168
+ const enrichedThreats = deduped.map(t => {
155
169
  const rule = getRule(t.type);
156
170
  return {
157
171
  ...t,
@@ -164,11 +178,11 @@ async function run(targetPath, options = {}) {
164
178
  };
165
179
  });
166
180
 
167
- // Calculate risk score (0-100)
168
- const criticalCount = threats.filter(t => t.severity === 'CRITICAL').length;
169
- const highCount = threats.filter(t => t.severity === 'HIGH').length;
170
- const mediumCount = threats.filter(t => t.severity === 'MEDIUM').length;
171
- const lowCount = threats.filter(t => t.severity === 'LOW').length;
181
+ // Calculate risk score (0-100) using deduplicated threats
182
+ const criticalCount = deduped.filter(t => t.severity === 'CRITICAL').length;
183
+ const highCount = deduped.filter(t => t.severity === 'HIGH').length;
184
+ const mediumCount = deduped.filter(t => t.severity === 'MEDIUM').length;
185
+ const lowCount = deduped.filter(t => t.severity === 'LOW').length;
172
186
 
173
187
  let riskScore = 0;
174
188
  riskScore += criticalCount * SEVERITY_WEIGHTS.CRITICAL;
@@ -188,7 +202,7 @@ async function run(targetPath, options = {}) {
188
202
  timestamp: new Date().toISOString(),
189
203
  threats: enrichedThreats,
190
204
  summary: {
191
- total: threats.length,
205
+ total: deduped.length,
192
206
  critical: criticalCount,
193
207
  high: highCount,
194
208
  medium: mediumCount,
@@ -222,7 +236,8 @@ async function run(targetPath, options = {}) {
222
236
  console.log(`[ALERT] ${enrichedThreats.length} threat(s) detected:\n`);
223
237
  enrichedThreats.forEach((t, i) => {
224
238
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
225
- console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}`);
239
+ const countStr = t.count > 1 ? ` (x${t.count})` : '';
240
+ console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}${countStr}`);
226
241
  console.log(` Rule ID: ${t.rule_id}`);
227
242
  console.log(` File: ${t.file}`);
228
243
  if (t.line) console.log(` Line: ${t.line}`);
@@ -245,18 +260,19 @@ async function run(targetPath, options = {}) {
245
260
  const scoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
246
261
  console.log(`[SCORE] ${result.summary.riskScore}/100 [${scoreBar}] ${result.summary.riskLevel}\n`);
247
262
 
248
- if (threats.length === 0) {
263
+ if (deduped.length === 0) {
249
264
  console.log('[OK] No threats detected.\n');
250
265
  } else {
251
- console.log(`[ALERT] ${threats.length} threat(s) detected:\n`);
252
- threats.forEach((t, i) => {
253
- console.log(` ${i + 1}. [${t.severity}] ${t.type}`);
266
+ console.log(`[ALERT] ${deduped.length} threat(s) detected:\n`);
267
+ deduped.forEach((t, i) => {
268
+ const countStr = t.count > 1 ? ` (x${t.count})` : '';
269
+ console.log(` ${i + 1}. [${t.severity}] ${t.type}${countStr}`);
254
270
  console.log(` ${t.message}`);
255
271
  console.log(` File: ${t.file}\n`);
256
272
  });
257
273
 
258
274
  console.log('[RESPONSE] Recommendations:\n');
259
- threats.forEach(t => {
275
+ deduped.forEach(t => {
260
276
  const playbook = getPlaybook(t.type);
261
277
  if (playbook) {
262
278
  console.log(` -> ${playbook}\n`);
@@ -285,8 +301,8 @@ async function run(targetPath, options = {}) {
285
301
  };
286
302
 
287
303
  const levelsToCheck = severityLevels[failLevel] || severityLevels.high;
288
- const failingThreats = threats.filter(t => levelsToCheck.includes(t.severity));
289
-
304
+ const failingThreats = deduped.filter(t => levelsToCheck.includes(t.severity));
305
+
290
306
  return failingThreats.length;
291
307
  }
292
308
 
@@ -17564,7 +17564,7 @@
17564
17564
  "pigS3cr3ts.json"
17565
17565
  ],
17566
17566
  "files": [],
17567
- "updated": "2026-02-09T22:02:04.174Z",
17567
+ "updated": "2026-02-10T18:17:09.546Z",
17568
17568
  "sources": [
17569
17569
  "shai-hulud-detector",
17570
17570
  "datadog-consolidated",
@@ -24,10 +24,15 @@ const SENSITIVE_STRINGS = [
24
24
  'Goldox-T3chs'
25
25
  ];
26
26
 
27
+ // Env vars that are safe and should NOT be flagged (common config/runtime vars)
28
+ const SAFE_ENV_VARS = [
29
+ 'NODE_ENV', 'PORT', 'HOST', 'HOSTNAME', 'PWD', 'HOME', 'PATH',
30
+ 'LANG', 'TERM', 'CI', 'DEBUG', 'VERBOSE', 'LOG_LEVEL'
31
+ ];
32
+
27
33
  // Env var keywords to detect sensitive environment access (separate from SENSITIVE_STRINGS)
28
- const ENV_SENSITIVE_VARS = [
29
- 'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL',
30
- 'AUTH', 'NPM', 'AWS', 'GITHUB', 'SSH', 'NPMRC'
34
+ const ENV_SENSITIVE_KEYWORDS = [
35
+ 'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
31
36
  ];
32
37
 
33
38
  // Strings that are NOT suspicious
@@ -134,14 +139,32 @@ function analyzeFile(content, filePath, basePath) {
134
139
  node.object?.object?.name === 'process' &&
135
140
  node.object?.property?.name === 'env'
136
141
  ) {
137
- const envVar = node.property?.name;
138
- if (envVar && ENV_SENSITIVE_VARS.some(s => envVar.toUpperCase().includes(s))) {
142
+ // Dynamic access: process.env[variable] — always flag as MEDIUM
143
+ if (node.computed) {
139
144
  threats.push({
140
145
  type: 'env_access',
141
- severity: 'HIGH',
142
- message: `Access to sensitive variable process.env.${envVar}.`,
146
+ severity: 'MEDIUM',
147
+ message: 'Dynamic access to process.env (variable key).',
143
148
  file: path.relative(basePath, filePath)
144
149
  });
150
+ return;
151
+ }
152
+
153
+ const envVar = node.property?.name;
154
+ if (envVar) {
155
+ // Skip safe/common env vars
156
+ if (SAFE_ENV_VARS.includes(envVar)) {
157
+ return;
158
+ }
159
+ // Flag only vars containing sensitive keywords
160
+ if (ENV_SENSITIVE_KEYWORDS.some(s => envVar.toUpperCase().includes(s))) {
161
+ threats.push({
162
+ type: 'env_access',
163
+ severity: 'HIGH',
164
+ message: `Access to sensitive variable process.env.${envVar}.`,
165
+ file: path.relative(basePath, filePath)
166
+ });
167
+ }
145
168
  }
146
169
  }
147
170
  }