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 +1 -1
- package/package.json +1 -1
- package/{self-scan-v2.11.177.json → self-scan-v2.11.179.json} +1 -1
- package/src/scanner/dataflow.js +23 -0
- package/src/scoring.js +14 -1
- package/stats.json +2 -2
package/README.md
CHANGED
|
@@ -345,7 +345,7 @@ npm test
|
|
|
345
345
|
|
|
346
346
|
### Testing
|
|
347
347
|
|
|
348
|
-
- **<!--stat:tests-->
|
|
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
package/src/scanner/dataflow.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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":
|
|
15
|
+
"tests": 4545
|
|
16
16
|
}
|