muaddib-scanner 2.11.176 → 2.11.178

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.md CHANGED
@@ -345,7 +345,7 @@ npm test
345
345
 
346
346
  ### Testing
347
347
 
348
- - **<!--stat:tests-->4534<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
348
+ - **<!--stat:tests-->4543<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
349
349
  - **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
350
350
  - **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
351
351
  - **Ground truth validation** - 96 real-world attacks (95.74% TPR@3, 88.30% TPR@20 — v2.11.48 full measure on 94 in-scope)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.176",
3
+ "version": "2.11.178",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-07-22T15:08:41.143Z",
3
+ "timestamp": "2026-07-23T07:53:45.520Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -930,65 +930,14 @@ async function scrapeOSVPyPIDataDump() {
930
930
  return packages;
931
931
  }
932
932
 
933
- // ============================================
934
- // SOURCE 4: GitHub Advisory Database (Malware)
935
- // ============================================
936
- async function scrapeGitHubAdvisory() {
937
- console.log('[SCRAPER] GitHub Advisory Database (malware)...');
938
- const packages = [];
939
-
940
- try {
941
- const resp = await fetchJSON('https://api.osv.dev/v1/query', {
942
- method: 'POST',
943
- headers: { 'Content-Type': 'application/json' },
944
- body: { package: { ecosystem: 'npm' } }
945
- });
946
-
947
- if (resp.status === 200 && resp.data && resp.data.vulns) {
948
- for (const vuln of resp.data.vulns) {
949
- // Filter GHSA with malware mention
950
- if (vuln.id && vuln.id.startsWith('GHSA-')) {
951
- const summary = (vuln.summary || '').toLowerCase();
952
- const details = (vuln.details || '').toLowerCase();
953
- const isMalware = summary.includes('malware') ||
954
- summary.includes('malicious') ||
955
- details.includes('malware') ||
956
- details.includes('malicious') ||
957
- summary.includes('backdoor') ||
958
- summary.includes('trojan');
959
-
960
- if (isMalware) {
961
- for (const affected of vuln.affected || []) {
962
- if (affected.package && affected.package.ecosystem === 'npm') {
963
- const versions = extractVersions(affected);
964
- for (const ver of versions) {
965
- packages.push({
966
- id: vuln.id,
967
- name: affected.package.name,
968
- version: ver,
969
- severity: 'critical',
970
- confidence: 'high',
971
- source: 'github-advisory',
972
- description: (vuln.summary || 'Malicious package').slice(0, 200),
973
- references: ['https://github.com/advisories/' + vuln.id],
974
- mitre: 'T1195.002',
975
- freshness: createFreshness('github-advisory', 'high')
976
- });
977
- }
978
- }
979
- }
980
- }
981
- }
982
- }
983
- }
984
-
985
- console.log(`[SCRAPER] ${packages.length} packages`);
986
- } catch (e) {
987
- console.log(`[SCRAPER] Error: ${e.message}`);
988
- }
989
-
990
- return packages;
991
- }
933
+ // SOURCE 4 (GitHub Advisory Database) removed 2026-07 (GAP-2): it POSTed
934
+ // api.osv.dev/v1/query with `{ package: { ecosystem: 'npm' } }` — no package name,
935
+ // which the OSV /v1/query API rejects with HTTP 400 on every call. It therefore
936
+ // returned 0 packages permanently while logging as a normal completion (fictional
937
+ // coverage). Empirically verified that the GHSA-malware it targeted is already
938
+ // 100% covered by the OSV MAL- dump (153/153 pre-snapshot npm GHSA advisories
939
+ // present in the IOC store), so its removal loses no coverage. The GHSA poller
940
+ // (src/ioc/ghsa-poller.js) tracks the coverage denominator independently.
992
941
 
993
942
  // ============================================
994
943
  // MAIN SCRAPER
@@ -1072,7 +1021,6 @@ async function runScraper() {
1072
1021
  scrapeShaiHuludDetector(),
1073
1022
  scrapeDatadogIOCs(),
1074
1023
  scrapeOSSFMaliciousPackages(osvResult.knownIds),
1075
- scrapeGitHubAdvisory(),
1076
1024
  scrapeOSVPyPIDataDump(),
1077
1025
  scrapeAikidoMalwareFeed(),
1078
1026
  scrapeOSMQueryLatest()
@@ -1081,10 +1029,9 @@ async function runScraper() {
1081
1029
  const shaiHuludResult = results[0];
1082
1030
  const datadogResult = results[1];
1083
1031
  const ossfPackages = results[2];
1084
- const githubPackages = results[3];
1085
- const pypiPackages = results[4];
1086
- const aikidoResult = results[5];
1087
- const osmResult = results[6];
1032
+ const pypiPackages = results[3];
1033
+ const aikidoResult = results[4];
1034
+ const osmResult = results[5];
1088
1035
 
1089
1036
  // Log aggregated warnings
1090
1037
  if (_noVersionSkipCount > 0) {
@@ -1109,7 +1056,6 @@ async function runScraper() {
1109
1056
  ...shaiHuludResult.packages,
1110
1057
  ...datadogResult.packages,
1111
1058
  ...ossfPackages,
1112
- ...githubPackages,
1113
1059
  ...aikidoResult.packages,
1114
1060
  ...osmResult.packages
1115
1061
  ];
@@ -1597,43 +1543,14 @@ async function scrapeOSMQueryLatest() {
1597
1543
  return { packages: npmPackages, pypi_packages: pypiPackages };
1598
1544
  }
1599
1545
 
1600
- // ============================================
1601
- // SOURCE 5: OSV.dev Lightweight API
1602
- // Used by `muaddib update` (fast, no zip download)
1603
- // ============================================
1604
-
1605
- /**
1606
- * Lightweight OSV.dev query fetches recent npm MAL-* entries via REST API.
1607
- * Used by `muaddib update` as a fast complement to the full zip scrape.
1608
- * @returns {Promise<Array>} Parsed IOC package entries
1609
- */
1610
- async function scrapeOSVLightweightAPI() {
1611
- console.log('[SCRAPER] OSV.dev lightweight API...');
1612
- const packages = [];
1613
-
1614
- try {
1615
- const resp = await fetchJSON('https://api.osv.dev/v1/query', {
1616
- method: 'POST',
1617
- headers: { 'Content-Type': 'application/json' },
1618
- body: { package: { ecosystem: 'npm' } }
1619
- });
1620
-
1621
- if (resp.status === 200 && resp.data && resp.data.vulns) {
1622
- for (const vuln of resp.data.vulns) {
1623
- if (vuln.id && vuln.id.startsWith('MAL-')) {
1624
- const parsed = parseOSVEntry(vuln, 'osv-api');
1625
- for (const p of parsed) packages.push(p);
1626
- }
1627
- }
1628
- }
1629
-
1630
- console.log('[SCRAPER] ' + packages.length + ' MAL-* packages from OSV API');
1631
- } catch (e) {
1632
- console.log('[SCRAPER] OSV API error: ' + e.message);
1633
- }
1634
-
1635
- return packages;
1636
- }
1546
+ // SOURCE 5 (OSV.dev Lightweight API) removed 2026-07 (GAP-2): same defect as
1547
+ // SOURCE 4 — it POSTed api.osv.dev/v1/query with no package name (`{ package:
1548
+ // { ecosystem: 'npm' } }`), which OSV rejects with HTTP 400, so it returned 0
1549
+ // MAL-* packages permanently while `muaddib update` logged "+0 OSV API" as normal.
1550
+ // It was also 100% redundant with the OSV MAL- zip dump (scrapeOSVDataDump), which
1551
+ // pulls the same MAL- npm set. The `muaddib update` light path keeps its recent-
1552
+ // malware coverage via GenSecAI + DataDog + OSM; the authoritative OSV MAL- set
1553
+ // comes from the deep `muaddib scrape`.
1637
1554
 
1638
1555
  /**
1639
1556
  * Batch query OSV.dev for specific package names.
@@ -1718,7 +1635,7 @@ module.exports = {
1718
1635
  runScraper, scrapeShaiHuludDetector, scrapeDatadogIOCs,
1719
1636
  scrapeAikidoMalwareFeed,
1720
1637
  scrapeOSMQueryLatest,
1721
- scrapeOSVLightweightAPI, queryOSVBatch,
1638
+ queryOSVBatch,
1722
1639
  getSourceConfidence,
1723
1640
  // Pure utility functions (exported for testing)
1724
1641
  parseCSVLine, parseCSV, extractVersions, parseOSVEntry,
@@ -66,28 +66,29 @@ async function updateIOCs() {
66
66
  mergeIOCs(baseIOCs, yamlStandard);
67
67
  console.log('[2/4] YAML IOCs: ' + yamlStandard.packages.length + ' packages, ' + yamlStandard.hashes.length + ' hashes');
68
68
 
69
- // Step 3: Download additional IOCs from GitHub + OSV API (GenSecAI + DataDog + OSV lightweight + OSM)
69
+ // Step 3: Download additional IOCs (GenSecAI + DataDog + OSM)
70
70
  // Light path: JSON/REST only, NO heavy zip dumps. Designed to be safe at 15min cadence.
71
- // For the deep refresh (OSV zip dumps + OSSF + Aikido + GitHub Advisory), use `muaddib scrape` (~5min).
72
- const { scrapeShaiHuludDetector, scrapeDatadogIOCs, scrapeOSVLightweightAPI, scrapeOSMQueryLatest } = require('./scraper.js');
73
- console.log('[3/4] Downloading GitHub + OSV API + OSM IOCs...');
71
+ // The OSV MAL- set comes from the deep refresh (`muaddib scrape`, OSV zip dumps + OSSF +
72
+ // Aikido). The former OSV lightweight source was removed 2026-07 (GAP-2): it POSTed a
73
+ // nameless OSV /v1/query (HTTP 400 always 0) and was redundant with the OSV zip dump.
74
+ const { scrapeShaiHuludDetector, scrapeDatadogIOCs, scrapeOSMQueryLatest } = require('./scraper.js');
75
+ console.log('[3/4] Downloading GenSecAI + DataDog + OSM IOCs...');
74
76
 
75
- const [shaiHulud, datadog, osvApi, osmResult] = await Promise.all([
77
+ const [shaiHulud, datadog, osmResult] = await Promise.all([
76
78
  scrapeShaiHuludDetector(),
77
79
  scrapeDatadogIOCs(),
78
- scrapeOSVLightweightAPI(),
79
80
  scrapeOSMQueryLatest()
80
81
  ]);
81
82
 
82
83
  const githubIOCs = {
83
- packages: [].concat(shaiHulud.packages, datadog.packages, osvApi, osmResult.packages),
84
+ packages: [].concat(shaiHulud.packages, datadog.packages, osmResult.packages),
84
85
  pypi_packages: (osmResult.pypi_packages || []).slice(),
85
86
  hashes: [].concat(shaiHulud.hashes || [], datadog.hashes || []),
86
87
  markers: [],
87
88
  files: []
88
89
  };
89
90
  mergeIOCs(baseIOCs, githubIOCs);
90
- console.log(' +' + shaiHulud.packages.length + ' GenSecAI, +' + datadog.packages.length + ' DataDog, +' + osvApi.length + ' OSV API, +' + osmResult.packages.length + ' OSM npm, +' + (osmResult.pypi_packages || []).length + ' OSM PyPI');
91
+ console.log(' +' + shaiHulud.packages.length + ' GenSecAI, +' + datadog.packages.length + ' DataDog, +' + osmResult.packages.length + ' OSM npm, +' + (osmResult.pypi_packages || []).length + ' OSM PyPI');
91
92
 
92
93
  // Phase 2c (feed health): a feed that previously returned data but now returns 0 is the
93
94
  // silent failure mode that froze the OSM feed and collapsed coverage. Raise a one-shot
@@ -95,13 +96,11 @@ async function updateIOCs() {
95
96
  // Only feeds that were actually ATTEMPTED are health-checked: OSM is token-gated, so without
96
97
  // OSM_API_TOKEN it is SKIPPED (not down) — counting it would raise a false "OSM went dark"
97
98
  // alarm in any no-token context (e.g. an ad-hoc `muaddib update`) against the monitor-seeded
98
- // baseline. OSV-API is public and volatile; its small counts rarely cross MIN_HEALTHY_BASELINE,
99
- // so the engine naturally never establishes an alarm-able baseline for it.
99
+ // baseline.
100
100
  try {
101
101
  const feedCounts = {
102
102
  'GenSecAI': shaiHulud.packages.length,
103
- 'DataDog': datadog.packages.length,
104
- 'OSV-API': osvApi.length
103
+ 'DataDog': datadog.packages.length
105
104
  };
106
105
  if (process.env.OSM_API_TOKEN) {
107
106
  feedCounts['OSM'] = osmResult.packages.length + (osmResult.pypi_packages || []).length;
@@ -474,6 +474,29 @@ function analyzeFile(content, filePath, basePath) {
474
474
  }
475
475
  }
476
476
 
477
+ // Stream-based credential read: fs.createReadStream(<sensitive path>) is the
478
+ // stream equivalent of readFileSync. Registering it as a credential_read source
479
+ // closes the .pipe() exfil gap — createReadStream('/.ssh/id_rsa').pipe(net.connect(...))
480
+ // now scores the same as the readFileSync + socket.write form (the co-occurring
481
+ // network sink from the .pipe() argument — net.connect/http.request — is already
482
+ // detected by the MemberExpression sink handler below). Strictly gated on
483
+ // isCredentialPath: FP-measured 0/745 benign packages (createReadStream of a
484
+ // sensitive path is absent from legitimate code; the benign servers'
485
+ // createReadStream('public/index.html').pipe(res) is not sensitive-sourced, and
486
+ // the .pipe(res)/.pipe(createWriteStream) sinks are not network sinks). Deliberately
487
+ // NOT added to MODULE_SOURCE_METHODS to avoid the alias path, which does not
488
+ // re-check the path and would flag a non-sensitive aliased read.
489
+ if (callName === 'createReadStream' || callName === 'fs.createReadStream') {
490
+ const arg = node.arguments[0];
491
+ if (arg && isCredentialPath(arg, sensitivePathVars)) {
492
+ sources.push({
493
+ type: 'credential_read',
494
+ name: callName,
495
+ line: node.loc?.start?.line
496
+ });
497
+ }
498
+ }
499
+
477
500
  // fs.promises.readFile(path) — 3-level member chain
478
501
  if (node.callee.type === 'MemberExpression' &&
479
502
  node.callee.object?.type === 'MemberExpression') {
package/stats.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "GENERATED by scripts/collect-stats.js — do not edit by hand. Run `npm run docs:stats`.",
3
- "version": "2.11.176",
3
+ "version": "2.11.178",
4
4
  "scanners": 22,
5
5
  "rulesTotal": 277,
6
6
  "rulesCore": 272,
@@ -12,5 +12,5 @@
12
12
  "moduleGraph": 9,
13
13
  "pythonAstDetectors": 6,
14
14
  "testFiles": 152,
15
- "tests": 4534
15
+ "tests": 4543
16
16
  }