muaddib-scanner 1.4.2 → 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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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-09T23:17:51.587Z",
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
  }