muaddib-scanner 2.10.42 → 2.10.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.42",
3
+ "version": "2.10.43",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -56,7 +56,8 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
56
56
  'systemd_persistence', // writeFile to systemd/ or systemctl enable (CanisterWorm T1543.002)
57
57
  'npm_token_steal', // exec("npm config get _authToken") (CanisterWorm findNpmTokens)
58
58
  'root_filesystem_wipe', // rm -rf / (CanisterWorm kamikaze.sh wiper T1485)
59
- 'proc_mem_scan' // /proc/mem scanning (TeamPCP Trivy credential stealer)
59
+ 'proc_mem_scan', // /proc/mem scanning (TeamPCP Trivy credential stealer)
60
+ 'trusted_new_unknown_dependency' // TRUSTED package added unknown/new (<7d) dependency (account takeover)
60
61
  ]);
61
62
 
62
63
  // Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
@@ -81,6 +81,105 @@ async function getWeeklyDownloads(packageName) {
81
81
  }
82
82
  }
83
83
 
84
+ // --- Trusted dependency diff check ---
85
+
86
+ const TRUSTED_DEP_AGE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
87
+
88
+ /**
89
+ * Check for new dependencies added to a TRUSTED (popular) package.
90
+ * Detects supply-chain attacks where a compromised maintainer account adds a
91
+ * malicious dependency in a patch bump (e.g., axios 1.14.0 → 1.14.1 adding
92
+ * plain-crypto-js, 2026-03-30).
93
+ *
94
+ * @param {string} name - Package name
95
+ * @param {string} newVersion - Newly published version
96
+ * @returns {Array} Array of findings (empty if no new deps or on error)
97
+ */
98
+ async function checkTrustedDepDiff(name, newVersion) {
99
+ const findings = [];
100
+ try {
101
+ // Fetch packument to get version list and dependencies
102
+ const body = await httpsGet(`https://registry.npmjs.org/${encodeURIComponent(name)}`, 10_000);
103
+ const packument = JSON.parse(body);
104
+
105
+ if (!packument.versions || !packument.time) return findings;
106
+
107
+ // Sort versions by publish time (not semver — handles prereleases correctly)
108
+ const timeMap = packument.time;
109
+ const versionKeys = Object.keys(packument.versions)
110
+ .filter(v => timeMap[v])
111
+ .sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]));
112
+
113
+ const newIdx = versionKeys.indexOf(newVersion);
114
+ if (newIdx <= 0) return findings; // First version or not found
115
+
116
+ const prevVersion = versionKeys[newIdx - 1];
117
+
118
+ const prevDeps = (packument.versions[prevVersion] && packument.versions[prevVersion].dependencies) || {};
119
+ const newDeps = (packument.versions[newVersion] && packument.versions[newVersion].dependencies) || {};
120
+
121
+ // Find newly added dependencies (name not present in previous version)
122
+ const addedDeps = Object.keys(newDeps).filter(dep => !(dep in prevDeps));
123
+ if (addedDeps.length === 0) return findings;
124
+
125
+ console.log(`[MONITOR] TRUSTED dep diff: ${name} ${prevVersion} → ${newVersion}: +${addedDeps.length} new dep(s): ${addedDeps.join(', ')}`);
126
+
127
+ for (const dep of addedDeps) {
128
+ let ageMs = null;
129
+ try {
130
+ const depBody = await httpsGet(`https://registry.npmjs.org/${encodeURIComponent(dep)}`, 5_000);
131
+ const depData = JSON.parse(depBody);
132
+ const created = depData.time && depData.time.created;
133
+ if (created) {
134
+ ageMs = Date.now() - new Date(created).getTime();
135
+ }
136
+ } catch (err) {
137
+ console.log(`[MONITOR] WARNING: could not check age of dependency ${dep}: ${err.message}`);
138
+ }
139
+
140
+ if (ageMs === null || ageMs < TRUSTED_DEP_AGE_THRESHOLD_MS) {
141
+ // Unknown or < 7 days old — CRITICAL
142
+ const ageDays = ageMs !== null ? Math.floor(ageMs / 86400000) : 'unknown';
143
+ findings.push({
144
+ type: 'trusted_new_unknown_dependency',
145
+ severity: 'CRITICAL',
146
+ confidence: ageMs === null ? 'medium' : 'high',
147
+ file: 'package.json',
148
+ message: `TRUSTED package ${name} added unknown dependency ${dep} (age: ${ageDays}d) in version ${prevVersion} → ${newVersion}`,
149
+ rule_id: 'MUADDIB-TRUSTED-001',
150
+ mitre: 'T1195.002',
151
+ dep,
152
+ depAgeDays: ageDays,
153
+ prevVersion,
154
+ newVersion
155
+ });
156
+ } else {
157
+ // Known dependency (>= 7 days old) — HIGH
158
+ const ageDays = Math.floor(ageMs / 86400000);
159
+ findings.push({
160
+ type: 'trusted_new_dependency',
161
+ severity: 'HIGH',
162
+ confidence: 'medium',
163
+ file: 'package.json',
164
+ message: `TRUSTED package ${name} added new dependency ${dep} (age: ${ageDays}d) in version ${prevVersion} → ${newVersion}`,
165
+ rule_id: 'MUADDIB-TRUSTED-002',
166
+ mitre: 'T1195.002',
167
+ dep,
168
+ depAgeDays: ageDays,
169
+ prevVersion,
170
+ newVersion
171
+ });
172
+ }
173
+ }
174
+
175
+ return findings;
176
+ } catch (err) {
177
+ // Graceful fallback — log warning, continue as TRUSTED
178
+ console.log(`[MONITOR] WARNING: trusted dep diff check failed for ${name}@${newVersion}: ${err.message}`);
179
+ return findings;
180
+ }
181
+ }
182
+
84
183
  // --- Tarball URL helpers ---
85
184
 
86
185
  function getNpmTarballUrl(pkgData) {
@@ -583,6 +682,8 @@ module.exports = {
583
682
  // HTTP helpers
584
683
  httpsGet,
585
684
  getWeeklyDownloads,
685
+ checkTrustedDepDiff,
686
+ TRUSTED_DEP_AGE_THRESHOLD_MS,
586
687
 
587
688
  // Tarball URL helpers
588
689
  getNpmTarballUrl,
@@ -98,7 +98,7 @@ const {
98
98
  } = require('./temporal.js');
99
99
 
100
100
  // From ./ingestion.js (will be created — currently in monitor.js)
101
- const { getNpmLatestTarball, getPyPITarballUrl, getWeeklyDownloads } = require('./ingestion.js');
101
+ const { getNpmLatestTarball, getPyPITarballUrl, getWeeklyDownloads, checkTrustedDepDiff } = require('./ingestion.js');
102
102
 
103
103
  // From ./tarball-archive.js
104
104
  const { archiveSuspectTarball } = require('./tarball-archive.js');
@@ -518,14 +518,34 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
518
518
  if (ecosystem === 'npm' && !hasIOCMatch(result) && !hasTyposquat(result) && !hasHighOrCritical(result)) {
519
519
  const downloads = await getWeeklyDownloads(name);
520
520
  if (downloads >= POPULAR_THRESHOLD) {
521
- stats.scanned++;
522
- const elapsed = Date.now() - startTime;
523
- stats.totalTimeMs += elapsed;
524
- stats.clean++;
525
- console.log(`[MONITOR] TRUSTED (popular): ${name}@${version} (${Math.round(downloads / 1000)}k downloads/week, ${counts.join(', ')})`);
526
- updateScanStats('clean');
527
- recordTrainingSample(result, { name, version, ecosystem, label: 'clean', registryMeta: meta, unpackedSize: meta.unpackedSize, npmRegistryMeta, fileCountTotal, hasTests });
528
- return { sandboxResult: null, staticClean: true };
521
+ // Dependency diff check: detect supply-chain injection on TRUSTED packages
522
+ // (e.g., axios 1.14.0 → 1.14.1 adding unknown plain-crypto-js, 2026-03-30)
523
+ const trustedFindings = await checkTrustedDepDiff(name, version);
524
+ const hasCriticalDepFinding = trustedFindings.some(f => f.severity === 'CRITICAL');
525
+
526
+ if (hasCriticalDepFinding) {
527
+ // CRITICAL: unknown/new dependency bypass TRUSTED, route to full scan + sandbox
528
+ console.log(`[MONITOR] TRUSTED BYPASS: ${name}@${version} — new unknown dependency detected, routing to full scan`);
529
+ result.threats.push(...trustedFindings);
530
+ for (const f of trustedFindings) {
531
+ if (f.severity === 'CRITICAL') result.summary.critical = (result.summary.critical || 0) + 1;
532
+ else if (f.severity === 'HIGH') result.summary.high = (result.summary.high || 0) + 1;
533
+ }
534
+ // Fall through to full classification below (do NOT return)
535
+ } else {
536
+ // No CRITICAL dep findings — normal TRUSTED skip (log HIGH findings if any)
537
+ for (const f of trustedFindings) {
538
+ console.log(`[MONITOR] TRUSTED dep change: ${f.message}`);
539
+ }
540
+ stats.scanned++;
541
+ const elapsed = Date.now() - startTime;
542
+ stats.totalTimeMs += elapsed;
543
+ stats.clean++;
544
+ console.log(`[MONITOR] TRUSTED (popular): ${name}@${version} (${Math.round(downloads / 1000)}k downloads/week, ${counts.join(', ')})`);
545
+ updateScanStats('clean');
546
+ recordTrainingSample(result, { name, version, ecosystem, label: 'clean', registryMeta: meta, unpackedSize: meta.unpackedSize, npmRegistryMeta, fileCountTotal, hasTests });
547
+ return { sandboxResult: null, staticClean: true };
548
+ }
529
549
  }
530
550
  }
531
551
 
@@ -829,6 +829,15 @@ const PLAYBOOKS = {
829
829
  lifecycle_missing_script:
830
830
  'CRITIQUE: Script lifecycle reference un fichier inexistant dans le package. Script fantome. ' +
831
831
  'Le payload peut etre injecte dynamiquement ou lors d\'une mise a jour. Installer avec --ignore-scripts. Supprimer le package.',
832
+
833
+ trusted_new_unknown_dependency:
834
+ 'CRITIQUE: Package populaire (TRUSTED) a ajoute une dependance inconnue ou tres recente (<7 jours). ' +
835
+ 'Indicateur de compromission de compte mainteneur (supply-chain attack). Bloquer la mise a jour. ' +
836
+ 'Verifier le changelog, les commits recents, et contacter le mainteneur. Inspecter la nouvelle dependance.',
837
+
838
+ trusted_new_dependency:
839
+ 'HAUTE: Package populaire (TRUSTED) a ajoute une nouvelle dependance connue. ' +
840
+ 'Verifier le changelog et la legitimite de l\'ajout. Pas de blocage immediat mais surveillance renforcee.',
832
841
  };
833
842
 
834
843
  function getPlaybook(threatType) {
@@ -2206,6 +2206,30 @@ const RULES = {
2206
2206
  ],
2207
2207
  mitre: 'T1195.002'
2208
2208
  },
2209
+ // Trusted dependency diff detections (monitor-only)
2210
+ trusted_new_unknown_dependency: {
2211
+ id: 'MUADDIB-TRUSTED-001',
2212
+ name: 'Trusted Package Added Unknown Dependency',
2213
+ severity: 'CRITICAL',
2214
+ confidence: 'high',
2215
+ description: 'Un package TRUSTED (>50k downloads/semaine) a ajoute une nouvelle dependance inconnue ou tres recente (<7 jours) — indicateur de compromission de compte mainteneur (supply-chain attack type axios/plain-crypto-js).',
2216
+ references: [
2217
+ 'https://attack.mitre.org/techniques/T1195.002/',
2218
+ 'https://blog.sonatype.com/malicious-npm-packages-targeting-popular-libraries'
2219
+ ],
2220
+ mitre: 'T1195.002'
2221
+ },
2222
+ trusted_new_dependency: {
2223
+ id: 'MUADDIB-TRUSTED-002',
2224
+ name: 'Trusted Package Added New Dependency',
2225
+ severity: 'HIGH',
2226
+ confidence: 'medium',
2227
+ description: 'Un package TRUSTED (>50k downloads/semaine) a ajoute une nouvelle dependance connue (>7 jours) dans un bump de version — changement de surface d\'attaque a verifier.',
2228
+ references: [
2229
+ 'https://attack.mitre.org/techniques/T1195.002/'
2230
+ ],
2231
+ mitre: 'T1195.002'
2232
+ },
2209
2233
  };
2210
2234
 
2211
2235
  function getRule(type) {