muaddib-scanner 2.11.177 → 2.11.179

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-->4539<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
348
+ - **<!--stat:tests-->4545<!--/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.177",
3
+ "version": "2.11.179",
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-22T22:32:17.558Z",
3
+ "timestamp": "2026-07-23T09:11:36.373Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -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/src/scoring.js CHANGED
@@ -1683,7 +1683,20 @@ function calculateRiskScore(deduped, intentResult) {
1683
1683
  // confirmed Python install-time malware reaches its true score and separates from the
1684
1684
  // benign 25-35 cluster (which carries no import-time-exec signal).
1685
1685
  const _hasPyPIImportRCE = deduped.some(t => PYPI_IMPORT_TIME_RCE_TYPES.has(t.type));
1686
- if (!_hasLifecycle && !_hasHC && !_hasCompound && !_hasStagedC2 && !_hasPyPIImportRCE) {
1686
+ // Track E (PyPI unblock, phase 2): a CRITICAL string-IOC match is a campaign-unique
1687
+ // artifact (inclusion criteria in iocs/string-iocs.yaml — unique stagers / XOR keys /
1688
+ // C2 markers; single match = decisive by construction). The hash/name IOC matches
1689
+ // already escape this ceiling via applySingleFireCriticalFloor, but ioc_string_match
1690
+ // is not single-fire, so a lifecycle-less package (the PyPI shape: recon/trapdoor
1691
+ // libs with no npm install hook) was buried at 35 even at globalRiskScore 100
1692
+ // (GT-099 eth-security-auditor). Measured 0 ioc_string_match across 869 benign
1693
+ // packages (545 curated + 200 random npm + 124 PyPI) — zero corpus FP cost.
1694
+ // HIGH-severity strings (weaker, shared artifacts) deliberately do NOT void the cap.
1695
+ const _hasCriticalStringIOC = deduped.some(t =>
1696
+ t.type === 'ioc_string_match' && t.severity === 'CRITICAL'
1697
+ );
1698
+ if (!_hasLifecycle && !_hasHC && !_hasCompound && !_hasStagedC2 && !_hasPyPIImportRCE &&
1699
+ !_hasCriticalStringIOC) {
1687
1700
  riskScore = Math.min(riskScore, 35);
1688
1701
  }
1689
1702
 
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.177",
3
+ "version": "2.11.179",
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": 4539
15
+ "tests": 4545
16
16
  }