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 +24 -4
- package/README.md +21 -1
- package/bin/muaddib.js +6 -1
- package/eslint.config.mjs +17 -2
- package/package.json +2 -1
- package/src/index.js +31 -15
- package/src/ioc/data/iocs.json +1 -1
- package/src/scanner/ast.js +30 -7
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.
|
|
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
|
-
###
|
|
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
|
|
469
|
-
- [
|
|
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.
|
|
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 (
|
|
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.
|
|
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 =
|
|
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 =
|
|
169
|
-
const highCount =
|
|
170
|
-
const mediumCount =
|
|
171
|
-
const lowCount =
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
263
|
+
if (deduped.length === 0) {
|
|
249
264
|
console.log('[OK] No threats detected.\n');
|
|
250
265
|
} else {
|
|
251
|
-
console.log(`[ALERT] ${
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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 =
|
|
289
|
-
|
|
304
|
+
const failingThreats = deduped.filter(t => levelsToCheck.includes(t.severity));
|
|
305
|
+
|
|
290
306
|
return failingThreats.length;
|
|
291
307
|
}
|
|
292
308
|
|
package/src/ioc/data/iocs.json
CHANGED
|
@@ -17564,7 +17564,7 @@
|
|
|
17564
17564
|
"pigS3cr3ts.json"
|
|
17565
17565
|
],
|
|
17566
17566
|
"files": [],
|
|
17567
|
-
"updated": "2026-02-
|
|
17567
|
+
"updated": "2026-02-10T18:17:09.546Z",
|
|
17568
17568
|
"sources": [
|
|
17569
17569
|
"shai-hulud-detector",
|
|
17570
17570
|
"datadog-consolidated",
|
package/src/scanner/ast.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
138
|
-
if (
|
|
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: '
|
|
142
|
-
message:
|
|
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
|
}
|