muaddib-scanner 2.2.17 → 2.2.18

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.2.17",
3
+ "version": "2.2.18",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -213,19 +213,20 @@ async function run(targetPath, options = {}) {
213
213
  }
214
214
  }
215
215
 
216
- // Parallel execution of all independent scanners
217
- // Sync scanners use yieldThen() to yield to event loop (keeps spinner animating)
216
+ // Sequential execution of scanners with event loop yields between each.
217
+ // All scanners (even "async" ones) are effectively synchronous (readFileSync, readdirSync).
218
+ // Running them via yieldThen ensures the spinner animates between each scanner.
218
219
  let scanResult;
219
220
  try {
220
221
  scanResult = await Promise.all([
221
- scanPackageJson(targetPath),
222
- scanShellScripts(targetPath),
223
- analyzeAST(targetPath, { deobfuscate: deobfuscateFn }),
222
+ yieldThen(() => scanPackageJson(targetPath)),
223
+ yieldThen(() => scanShellScripts(targetPath)),
224
+ yieldThen(() => analyzeAST(targetPath, { deobfuscate: deobfuscateFn })),
224
225
  yieldThen(() => detectObfuscation(targetPath)),
225
- scanDependencies(targetPath),
226
- scanHashes(targetPath),
227
- analyzeDataFlow(targetPath, { deobfuscate: deobfuscateFn }),
228
- scanTyposquatting(targetPath),
226
+ yieldThen(() => scanDependencies(targetPath)),
227
+ yieldThen(() => scanHashes(targetPath)),
228
+ yieldThen(() => analyzeDataFlow(targetPath, { deobfuscate: deobfuscateFn })),
229
+ yieldThen(() => scanTyposquatting(targetPath)),
229
230
  yieldThen(() => scanGitHubActions(targetPath)),
230
231
  yieldThen(() => matchPythonIOCs(pythonDeps, targetPath)),
231
232
  yieldThen(() => checkPyPITyposquatting(pythonDeps, targetPath)),
package/src/monitor.js CHANGED
@@ -1061,18 +1061,26 @@ function isDailyReportDue() {
1061
1061
  }
1062
1062
 
1063
1063
  function buildDailyReportEmbed() {
1064
- const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
1064
+ // Use disk-based daily entries filtered by lastDailyReportDate for accurate delta
1065
+ const { agg, top3: diskTop3 } = buildReportFromDisk();
1065
1066
 
1066
- // Top 3 suspects sorted by findings count descending
1067
- const top3 = dailyAlerts
1068
- .slice()
1069
- .sort((a, b) => b.findingsCount - a.findingsCount)
1070
- .slice(0, 3);
1067
+ // Prefer in-memory dailyAlerts for top suspects (richer data), fallback to disk
1068
+ const top3 = dailyAlerts.length > 0
1069
+ ? dailyAlerts.slice().sort((a, b) => b.findingsCount - a.findingsCount).slice(0, 3)
1070
+ : diskTop3;
1071
1071
 
1072
1072
  const top3Text = top3.length > 0
1073
- ? top3.map((a, i) => `${i + 1}. **${a.ecosystem}/${a.name}@${a.version}** — ${a.findingsCount} finding(s)`).join('\n')
1073
+ ? top3.map((a, i) => {
1074
+ const name = a.ecosystem ? `${a.ecosystem}/${a.name || a.package}` : (a.name || a.package);
1075
+ const version = a.version || 'N/A';
1076
+ const count = a.findingsCount || (a.findings ? a.findings.length : 0);
1077
+ return `${i + 1}. **${name}@${version}** — ${count} finding(s)`;
1078
+ }).join('\n')
1074
1079
  : 'None';
1075
1080
 
1081
+ // Avg scan time from in-memory stats (not available on disk)
1082
+ const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
1083
+
1076
1084
  const now = new Date();
1077
1085
  const readableTime = now.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
1078
1086
 
@@ -1081,9 +1089,9 @@ function buildDailyReportEmbed() {
1081
1089
  title: '\uD83D\uDCCA MUAD\'DIB Daily Report',
1082
1090
  color: 0x3498db,
1083
1091
  fields: [
1084
- { name: 'Packages Scanned', value: `${stats.scanned}`, inline: true },
1085
- { name: 'Clean', value: `${stats.clean}`, inline: true },
1086
- { name: 'Suspects', value: `${stats.suspect}`, inline: true },
1092
+ { name: 'Packages Scanned', value: `${agg.scanned}`, inline: true },
1093
+ { name: 'Clean', value: `${agg.clean}`, inline: true },
1094
+ { name: 'Suspects', value: `${agg.suspect}`, inline: true },
1087
1095
  { name: 'Errors', value: `${stats.errors}`, inline: true },
1088
1096
  { name: 'Avg Scan Time', value: `${avg}s/pkg`, inline: true },
1089
1097
  { name: 'Top Suspects', value: top3Text, inline: false }
@@ -1140,12 +1148,11 @@ function loadStateRaw() {
1140
1148
  function buildReportFromDisk() {
1141
1149
  const scanData = loadScanStats();
1142
1150
  const stateRaw = loadStateRaw();
1143
- const lastDate = stateRaw.lastDailyReportDate || null;
1151
+ // Default to today if no report ever sent (avoids summing entire history)
1152
+ const lastDate = stateRaw.lastDailyReportDate || getParisDateString();
1144
1153
 
1145
- // Filter daily entries since last report
1146
- const sinceDays = lastDate
1147
- ? scanData.daily.filter(d => d.date > lastDate)
1148
- : scanData.daily;
1154
+ // Filter daily entries strictly AFTER last report date
1155
+ const sinceDays = scanData.daily.filter(d => d.date > lastDate);
1149
1156
 
1150
1157
  // Aggregate counters
1151
1158
  const agg = { scanned: 0, clean: 0, suspect: 0 };
@@ -1157,9 +1164,9 @@ function buildReportFromDisk() {
1157
1164
 
1158
1165
  // Load detections since last report for top suspects
1159
1166
  const detections = loadDetections();
1160
- const recentDetections = lastDate
1161
- ? detections.detections.filter(d => d.first_seen_at && d.first_seen_at.slice(0, 10) > lastDate)
1162
- : detections.detections;
1167
+ const recentDetections = detections.detections.filter(
1168
+ d => d.first_seen_at && d.first_seen_at.slice(0, 10) > lastDate
1169
+ );
1163
1170
 
1164
1171
  const top3 = recentDetections
1165
1172
  .slice()
@@ -1241,11 +1248,10 @@ function getReportStatus() {
1241
1248
  const stateRaw = loadStateRaw();
1242
1249
  const lastDate = stateRaw.lastDailyReportDate || null;
1243
1250
 
1244
- // Count packages scanned since last report
1251
+ // Count packages scanned since last report (default to today if never sent)
1245
1252
  const scanData = loadScanStats();
1246
- const sinceDays = lastDate
1247
- ? scanData.daily.filter(d => d.date > lastDate)
1248
- : scanData.daily;
1253
+ const sinceDate = lastDate || getParisDateString();
1254
+ const sinceDays = scanData.daily.filter(d => d.date > sinceDate);
1249
1255
 
1250
1256
  let scannedSince = 0;
1251
1257
  for (const d of sinceDays) {
@@ -121,6 +121,8 @@ async function scanPackageJson(targetPath) {
121
121
 
122
122
  for (const [depName, depVersion] of Object.entries(allDeps)) {
123
123
  if (DANGEROUS_KEYS.has(depName)) continue;
124
+ // Skip local dependencies (link:, file:, workspace:) — they're local code, not npm packages
125
+ if (typeof depVersion === 'string' && /^(link:|file:|workspace:)/.test(depVersion)) continue;
124
126
  let malicious = null;
125
127
 
126
128
  // Use optimized Map for O(1) lookup if available
@@ -0,0 +1 @@
1
+ {"name":"test-link","dependencies":{"eslint-plugin-react-internal":"link:./scripts/eslint-rules","lodash":"^4.17.21"}}