muaddib-scanner 2.11.22 → 2.11.24
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 +3 -3
- package/package.json +1 -1
- package/src/ml/feature-extractor.js +238 -1
- package/src/scoring.js +10 -0
package/README.md
CHANGED
|
@@ -275,7 +275,7 @@ With pre-commit framework:
|
|
|
275
275
|
```yaml
|
|
276
276
|
repos:
|
|
277
277
|
- repo: https://github.com/DNSZLSK/muad-dib
|
|
278
|
-
rev: v2.11.
|
|
278
|
+
rev: v2.11.24
|
|
279
279
|
hooks:
|
|
280
280
|
- id: muaddib-scan
|
|
281
281
|
```
|
|
@@ -296,7 +296,7 @@ repos:
|
|
|
296
296
|
| **FPR** (Benign random, v2.10.95 measure) | **7.0%** (14/200) | 200 random npm packages, stratified sampling |
|
|
297
297
|
| **ADR** (Adversarial + Holdout) | **96.3%** (103/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
|
|
298
298
|
|
|
299
|
-
**
|
|
299
|
+
**3602 tests** across 93 files. **234 rules** (229 RULES + 5 PARANOID).
|
|
300
300
|
|
|
301
301
|
> **ML retrain methodology (v2.10.51):**
|
|
302
302
|
> - Ground truth: 377 confirmed_malicious via auto-labeler (OSSF malicious-packages, GitHub Advisory Database, npm registry takedown correlation)
|
|
@@ -344,7 +344,7 @@ npm test
|
|
|
344
344
|
|
|
345
345
|
### Testing
|
|
346
346
|
|
|
347
|
-
- **
|
|
347
|
+
- **3602 tests** across 93 modular test files
|
|
348
348
|
- **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
|
|
349
349
|
- **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
|
|
350
350
|
- **Ground truth validation** - 67 real-world attacks (93.85% TPR@3, 86.2% TPR@20 — v2.10.95 measure)
|
package/package.json
CHANGED
|
@@ -703,6 +703,237 @@ function mcpServerEnvAccess(result, meta) {
|
|
|
703
703
|
return true;
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
// ============================================================================
|
|
707
|
+
// Feature 10 — vendor_cli_sdk (v2.11.23, audit week3 cluster, 96 FP)
|
|
708
|
+
// ============================================================================
|
|
709
|
+
//
|
|
710
|
+
// Targets the largest residual FP cluster from the audit 2026-05-week3
|
|
711
|
+
// (96 entries, 33.6% of FP): legitimate vendor / community CLIs and SDKs
|
|
712
|
+
// that fire `credential_regex_harvest` + `env_access` on their OWN
|
|
713
|
+
// in-package credential handling (Stripe checkout, OAuth-PKCE, bearer
|
|
714
|
+
// tokens to vendor APIs, .env template scaffolding). Examples observed:
|
|
715
|
+
// @nocobase/cli-v1, @posterly/cli, @super-hands/cli, codeapp-js-cli
|
|
716
|
+
// (Microsoft Power Apps), nodebb-plugin-flawless-donations (Stripe),
|
|
717
|
+
// @aiyiran/myclaw (Chinese OpenClaw wrapper), usegrain (scaffolder),
|
|
718
|
+
// @tapestry-mud/cli, db-model-router, etc.
|
|
719
|
+
//
|
|
720
|
+
// Discriminator vs vendor-impersonating malware: SANDWORM_MODE droppers
|
|
721
|
+
// (a) typically have no `bin` entry (they install via lifecycle hook,
|
|
722
|
+
// not user-invoked CLI), (b) emit `mcp_config_injection` (F9 catches
|
|
723
|
+
// those), (c) cite credential file paths (.npmrc / .ssh / .aws), (d)
|
|
724
|
+
// emit third-party exfil threats. F10's conjunction requires NONE of
|
|
725
|
+
// these and additionally requires a vendor identity hint (homepage or
|
|
726
|
+
// scoped name).
|
|
727
|
+
|
|
728
|
+
function _f10HasBinEntry(meta) {
|
|
729
|
+
const bin = meta && meta.registryMeta && meta.registryMeta.bin;
|
|
730
|
+
if (!bin) return false;
|
|
731
|
+
if (typeof bin === 'string' && bin.trim().length > 0) return true;
|
|
732
|
+
if (typeof bin === 'object' && Object.keys(bin).length > 0) return true;
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function _f10HasVendorIdentity(meta) {
|
|
737
|
+
if (!meta) return false;
|
|
738
|
+
if (getHomepageHost(meta)) return true;
|
|
739
|
+
const name = meta.name && String(meta.name);
|
|
740
|
+
if (name && name.startsWith('@') && name.includes('/')) return true;
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Feature 10 — TRUE iff the package looks structurally like a legitimate
|
|
746
|
+
* vendor / community CLI / SDK whose credential-handling threats are
|
|
747
|
+
* intrinsic to its functionality, not an exfil vector.
|
|
748
|
+
*
|
|
749
|
+
* Conjunction of 7 conditions (see file header for SANDWORM_MODE
|
|
750
|
+
* discriminator rationale):
|
|
751
|
+
*
|
|
752
|
+
* C1 has `bin` entry — CLI signal
|
|
753
|
+
* C2 credential_regex_harvest OR env_access fires
|
|
754
|
+
* C3 no `mcp_config_injection` — F9 catches MCP installers
|
|
755
|
+
* C4 no install lifecycle hook — legit CLIs are opt-in
|
|
756
|
+
* C5 no third-party exfil threat (15 types)
|
|
757
|
+
* C6 no credential file path (.npmrc/.ssh/.aws) in any threat message
|
|
758
|
+
* C7 vendor identity present (homepage host OR scoped @vendor/name)
|
|
759
|
+
*
|
|
760
|
+
* Cap value 35 (CRITICAL → MEDIUM-HIGH boundary). Reuses the F9 constants
|
|
761
|
+
* F9_EXFIL_TYPES and F9_CREDENTIAL_FILE_RE for C5/C6.
|
|
762
|
+
*
|
|
763
|
+
* Covers up to 96 FP (33.6% of audit week3 FP corpus). Estimated effective
|
|
764
|
+
* coverage 60-75 after the conjunction filters (some week3 entries lack
|
|
765
|
+
* a bin field, e.g. design-system asset packages — those fall under F1
|
|
766
|
+
* `bundle_without_install_scripts` instead).
|
|
767
|
+
*/
|
|
768
|
+
function vendorCliSdk(result, meta) {
|
|
769
|
+
// C1 — has bin entry
|
|
770
|
+
if (!_f10HasBinEntry(meta)) return false;
|
|
771
|
+
const threats = (result && result.threats) || [];
|
|
772
|
+
if (threats.length === 0) return false;
|
|
773
|
+
// C2 — at least one credential-noise threat (the FP source)
|
|
774
|
+
const hasCredentialNoise = threats.some(t =>
|
|
775
|
+
t.type === 'credential_regex_harvest' ||
|
|
776
|
+
t.type === 'env_access' ||
|
|
777
|
+
t.type === 'env_charcode_reconstruction' ||
|
|
778
|
+
t.type === 'credential_tampering'
|
|
779
|
+
);
|
|
780
|
+
if (!hasCredentialNoise) return false;
|
|
781
|
+
// C3 — no mcp_config_injection (F9 territory)
|
|
782
|
+
if (threats.some(t => t.type === 'mcp_config_injection')) return false;
|
|
783
|
+
// C4 — no install lifecycle hook
|
|
784
|
+
if (hasLifecycleScripts(meta)) return false;
|
|
785
|
+
// C5 + C6 — scan threats for exfil signal and credential-file mentions
|
|
786
|
+
for (const t of threats) {
|
|
787
|
+
if (F9_EXFIL_TYPES.has(t.type)) return false; // C5
|
|
788
|
+
if (F9_CREDENTIAL_FILE_RE.test(String(t.message || ''))) return false; // C6
|
|
789
|
+
}
|
|
790
|
+
// C7 — vendor identity
|
|
791
|
+
if (!_f10HasVendorIdentity(meta)) return false;
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ============================================================================
|
|
796
|
+
// Feature 11 — ai_agent_bot (v2.11.24, audit week3 cluster, 54 FP)
|
|
797
|
+
// ============================================================================
|
|
798
|
+
//
|
|
799
|
+
// Targets the third cluster from the audit 2026-05-week3 (54 entries,
|
|
800
|
+
// 18.9 % of FP): packages that ARE themselves multi-provider AI agents,
|
|
801
|
+
// orchestrators, chatbots, or IM⇄AI bridges. Examples: gm-skill (AI coding
|
|
802
|
+
// harness), codexmate (multi-provider orchestrator), lazyclaw (terminal
|
|
803
|
+
// multi-LLM CLI), linco-connect (WeChat→Claude bridge), natureco-cli
|
|
804
|
+
// (WhatsApp+Telegram bot), multis (Telegram chatbot), @aitne-sh/aitne
|
|
805
|
+
// (personal AI daemon), @jhizzard/termdeck (browser term mux with AI),
|
|
806
|
+
// triflux (Claude Code router), opuscode (Claude config wizard).
|
|
807
|
+
//
|
|
808
|
+
// These packages legitimately fire `dangerous_call_eval` (LLM tool-use
|
|
809
|
+
// execute_code feature), `remote_code_load` (bun x pkg@latest fetching),
|
|
810
|
+
// `detached_credential_exfil` (local session token storage), and lots
|
|
811
|
+
// of `env_access` + `suspicious_dataflow`. F11 cannot blacklist these —
|
|
812
|
+
// they ARE the core capabilities. Instead the conjunction requires:
|
|
813
|
+
//
|
|
814
|
+
// - Positive AI agent identity (name/desc/keywords/deps signal)
|
|
815
|
+
// - Evidence the package operates on agent runtime data (touches paths
|
|
816
|
+
// like ~/.claude/, ~/.codex/, ~/.cursor/, etc.)
|
|
817
|
+
// - Absence of SANDWORM_MODE signatures: no preinstall, no
|
|
818
|
+
// mcp_config_injection (F9 priority), no third-party suspicious_domain,
|
|
819
|
+
// no credential file harvest, no binary dropper (F2 priority).
|
|
820
|
+
//
|
|
821
|
+
// Cap 35 (aligned with F10 — broader conjunction than F9).
|
|
822
|
+
|
|
823
|
+
// Agent runtime directory regex — matches references in threat messages to
|
|
824
|
+
// AI tool runtime paths. Both '~/.X/' and 'os.homedir() + "/.X"' patterns
|
|
825
|
+
// surface as substrings here.
|
|
826
|
+
const AGENT_RUNTIME_PATHS_RE = /[~/\\\\]\.(?:claude|codex|cursor|windsurf|continue|openclaude|openclaudia|hermes|aiflow|tdpilot|aitne|kimi|opuscode|freddie|gm-?log|gm-?skill|termdeck|relaydesk|natureco|grok|gemini|copilot|cline|aider|cody|tabnine|cursor-ai|cursorrules|claude-?desktop|claude-?code|llm[- ]?cache)\b/i;
|
|
827
|
+
|
|
828
|
+
// AI agent name regex — package name signals identity.
|
|
829
|
+
const AGENT_NAME_RE = /(?:^|[/_-])(?:agent|bot|chat|chatbot|claw|codex|coder|swarm|harness|brain|orchestr|orchestrator|claude|llm|hermes|aider|kimi|cline|cody|aitne|opuscode|relaydesk|termdeck|gm-skill|gm-hermes|gm-qwen|gm-thebird|gm-plugkit|relipa|triflux|protocol-proxy|codexmate|lazy?claw|natureco)(?:[_-]|$)/i;
|
|
830
|
+
|
|
831
|
+
// Keywords that signal AI agent purpose (case-insensitive).
|
|
832
|
+
const AGENT_KEYWORDS_SET = new Set([
|
|
833
|
+
'agent', 'ai', 'llm', 'chatbot', 'bot', 'claude', 'codex',
|
|
834
|
+
'cursor', 'copilot', 'ollama', 'openai', 'anthropic', 'gemini',
|
|
835
|
+
'multi-llm', 'multi-provider', 'orchestrator', 'coding-agent',
|
|
836
|
+
'ai-agent', 'llm-agent', 'mcp-agent'
|
|
837
|
+
]);
|
|
838
|
+
|
|
839
|
+
// Description regex — matches agent purpose phrases.
|
|
840
|
+
const AGENT_DESC_RE = /\b(?:ai|llm|claude|codex|gemini|openai|anthropic|ollama)[ -]?(?:agent|bot|chatbot|orchestrator|harness|cli|assistant|coding[ -]?agent|gateway|relay|router|harness|workspace)\b|\bmulti[ -]?provider\b|\bcoding[ -]?agent\b|\bagent[ -]?(?:bridge|router|orchestrator)\b|telegram[ -]?(?:bot|bridge)|whatsapp[ -]?(?:bot|bridge)|wechat[ -]?(?:bot|bridge)/i;
|
|
841
|
+
|
|
842
|
+
// Dependency names that signal AI agent / bot framework usage.
|
|
843
|
+
const AGENT_DEPS = new Set([
|
|
844
|
+
'@anthropic-ai/sdk', '@anthropic-ai/claude-code', '@openai/agents', 'openai',
|
|
845
|
+
'@google/genai', '@google/generative-ai', 'ai', 'ollama', 'groq-sdk',
|
|
846
|
+
'telegraf', 'node-telegram-bot-api', '@whiskeysockets/baileys',
|
|
847
|
+
'whatsapp-web.js', 'discord.js', 'eventsource', 'node-pty',
|
|
848
|
+
'@anthropic-ai/bedrock-sdk', '@openai/realtime-api-beta'
|
|
849
|
+
]);
|
|
850
|
+
|
|
851
|
+
function _f11HasAgentIdentity(meta) {
|
|
852
|
+
if (!meta) return false;
|
|
853
|
+
const name = String(meta.name || '');
|
|
854
|
+
if (AGENT_NAME_RE.test(name)) return true;
|
|
855
|
+
const r = (meta.registryMeta || {});
|
|
856
|
+
const desc = r.description || meta.description || '';
|
|
857
|
+
if (AGENT_DESC_RE.test(desc)) return true;
|
|
858
|
+
if (Array.isArray(r.keywords)) {
|
|
859
|
+
for (const k of r.keywords) {
|
|
860
|
+
if (AGENT_KEYWORDS_SET.has(String(k).toLowerCase())) return true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const deps = r.dependencies || meta.dependencies;
|
|
864
|
+
if (deps && typeof deps === 'object') {
|
|
865
|
+
for (const d of Object.keys(deps)) {
|
|
866
|
+
if (AGENT_DEPS.has(d)) return true;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function _f11HasAgentPathReference(threats) {
|
|
873
|
+
for (const t of threats) {
|
|
874
|
+
const msg = String(t.message || '');
|
|
875
|
+
if (AGENT_RUNTIME_PATHS_RE.test(msg)) return true;
|
|
876
|
+
// Also accept the threat's file field — sometimes the path leaks via the
|
|
877
|
+
// file location rather than the message body.
|
|
878
|
+
const file = String(t.file || '');
|
|
879
|
+
if (AGENT_RUNTIME_PATHS_RE.test(file)) return true;
|
|
880
|
+
}
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Feature 11 — TRUE iff the package self-identifies as an AI agent / bot /
|
|
886
|
+
* multi-LLM orchestrator AND demonstrably operates on AI tool runtime
|
|
887
|
+
* data (~/.claude/, ~/.codex/, ~/.cursor/, etc.) AND lacks the
|
|
888
|
+
* SANDWORM_MODE / vendor-impersonation signatures.
|
|
889
|
+
*
|
|
890
|
+
* Conjunction of 7 conditions:
|
|
891
|
+
*
|
|
892
|
+
* C1 AI agent identity (name|desc|keywords|deps signal)
|
|
893
|
+
* C2 no install lifecycle hook
|
|
894
|
+
* C3 no `mcp_config_injection` (F9 priority)
|
|
895
|
+
* C4 no `suspicious_domain` threat (third-party exfil discriminator)
|
|
896
|
+
* C5 no credential file path in any threat message (reuse F9 regex)
|
|
897
|
+
* C6 >=1 threat references an agent runtime path (positive operating signal)
|
|
898
|
+
* C7 no `binary_dropper` / `download_exec_binary` (F2 priority)
|
|
899
|
+
*
|
|
900
|
+
* Cap 35. Same cap as F10 (broader conjunction than F9). Reuses
|
|
901
|
+
* `F9_CREDENTIAL_FILE_RE` from v2.11.22.
|
|
902
|
+
*
|
|
903
|
+
* Discriminator vs malware:
|
|
904
|
+
* - SANDWORM droppers use preinstall/postinstall (C2 blocks).
|
|
905
|
+
* - MCP-impersonating malware emits mcp_config_injection (C3 → F9).
|
|
906
|
+
* - Exfilers have suspicious_domain (C4 blocks).
|
|
907
|
+
* - Binary droppers (C7 → F2 territory).
|
|
908
|
+
* - Credential file harvesters (C5 blocks).
|
|
909
|
+
*
|
|
910
|
+
* Covers up to 54 FP (18.9% of audit week3). Effective estimated coverage
|
|
911
|
+
* 30-40 (55-75%): the rest lack agent runtime path references or fire on
|
|
912
|
+
* suspicious_domain due to Chinese model rerouting (yingclaw pattern).
|
|
913
|
+
*/
|
|
914
|
+
function aiAgentBot(result, meta) {
|
|
915
|
+
// C1 — identity
|
|
916
|
+
if (!_f11HasAgentIdentity(meta)) return false;
|
|
917
|
+
const threats = (result && result.threats) || [];
|
|
918
|
+
if (threats.length === 0) return false;
|
|
919
|
+
// C2 — no install lifecycle hook
|
|
920
|
+
if (hasLifecycleScripts(meta)) return false;
|
|
921
|
+
// C3, C4, C7 — fast threat-type checks
|
|
922
|
+
for (const t of threats) {
|
|
923
|
+
if (t.type === 'mcp_config_injection') return false; // C3
|
|
924
|
+
if (t.type === 'suspicious_domain') return false; // C4
|
|
925
|
+
if (t.type === 'binary_dropper') return false; // C7
|
|
926
|
+
if (t.type === 'download_exec_binary') return false; // C7
|
|
927
|
+
}
|
|
928
|
+
// C5 — no credential file path in any message
|
|
929
|
+
for (const t of threats) {
|
|
930
|
+
if (F9_CREDENTIAL_FILE_RE.test(String(t.message || ''))) return false;
|
|
931
|
+
}
|
|
932
|
+
// C6 — at least one threat references an agent runtime path
|
|
933
|
+
if (!_f11HasAgentPathReference(threats)) return false;
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
|
|
706
937
|
/**
|
|
707
938
|
* Feature 8 — TRUE iff the package declares at least one install
|
|
708
939
|
* lifecycle script AND the scan shows no network egress capability
|
|
@@ -855,6 +1086,10 @@ function extractFeatures(result, meta) {
|
|
|
855
1086
|
|
|
856
1087
|
// --- v2.11.22 Feature 9 (audit week3 cluster — 25 FP) ---
|
|
857
1088
|
features.mcp_server_env_access = mcpServerEnvAccess(result, meta) ? 1 : 0;
|
|
1089
|
+
// --- v2.11.23 Feature 10 (audit week3 cluster — up to 96 FP) ---
|
|
1090
|
+
features.vendor_cli_sdk = vendorCliSdk(result, meta) ? 1 : 0;
|
|
1091
|
+
// --- v2.11.24 Feature 11 (audit week3 cluster — up to 54 FP) ---
|
|
1092
|
+
features.ai_agent_bot = aiAgentBot(result, meta) ? 1 : 0;
|
|
858
1093
|
|
|
859
1094
|
return features;
|
|
860
1095
|
}
|
|
@@ -934,5 +1169,7 @@ module.exports = {
|
|
|
934
1169
|
obfuscationWithoutVector,
|
|
935
1170
|
placeholderAntiDepConfusion,
|
|
936
1171
|
installScriptNoNetworkEgress,
|
|
937
|
-
mcpServerEnvAccess
|
|
1172
|
+
mcpServerEnvAccess,
|
|
1173
|
+
vendorCliSdk,
|
|
1174
|
+
aiAgentBot
|
|
938
1175
|
};
|
package/src/scoring.js
CHANGED
|
@@ -1484,6 +1484,8 @@ const {
|
|
|
1484
1484
|
obfuscationWithoutVector,
|
|
1485
1485
|
placeholderAntiDepConfusion,
|
|
1486
1486
|
mcpServerEnvAccess,
|
|
1487
|
+
vendorCliSdk,
|
|
1488
|
+
aiAgentBot,
|
|
1487
1489
|
} = require('./ml/feature-extractor.js');
|
|
1488
1490
|
|
|
1489
1491
|
/**
|
|
@@ -1538,6 +1540,14 @@ function applyContextualFPCaps(result, pkgMeta) {
|
|
|
1538
1540
|
if (obfuscationWithoutVector(result)) {
|
|
1539
1541
|
applied.push({ feature: 'obfuscation_without_vector', cap: 35 });
|
|
1540
1542
|
}
|
|
1543
|
+
// F10: legit vendor CLI/SDK with intrinsic credential handling → MAX 35
|
|
1544
|
+
if (vendorCliSdk(result, meta)) {
|
|
1545
|
+
applied.push({ feature: 'vendor_cli_sdk', cap: 35 });
|
|
1546
|
+
}
|
|
1547
|
+
// F11: legit AI agent / bot / multi-LLM orchestrator → MAX 35
|
|
1548
|
+
if (aiAgentBot(result, meta)) {
|
|
1549
|
+
applied.push({ feature: 'ai_agent_bot', cap: 35 });
|
|
1550
|
+
}
|
|
1541
1551
|
// F5: typosquat on scoped package → suppress typosquat points
|
|
1542
1552
|
if (typosquatScopedPackage(result, meta)) {
|
|
1543
1553
|
applied.push({ feature: 'typosquat_scoped_package', cap: -1 });
|