muaddib-scanner 2.6.9 → 2.7.1

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.6.9",
3
+ "version": "2.7.1",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@ const IOC_FILE = path.join(__dirname, 'data/iocs.json');
8
8
  const COMPACT_IOC_FILE = path.join(__dirname, 'data/iocs-compact.json');
9
9
  const HOME_IOC_FILE = path.join(os.homedir(), '.muaddib', 'data', 'iocs.json');
10
10
  const STATIC_IOCS_FILE = path.join(__dirname, '../../data/static-iocs.json');
11
- const { generateCompactIOCs } = require('./updater.js');
11
+ const { generateCompactIOCs, NEVER_WILDCARD } = require('./updater.js');
12
12
  const { Spinner } = require('../utils.js');
13
13
  const { NPM_PACKAGE_REGEX } = require('../shared/constants.js');
14
14
 
@@ -1146,11 +1146,17 @@ async function runScraper() {
1146
1146
  let addedPackages = 0;
1147
1147
  let upgradedPackages = 0;
1148
1148
  let skippedInvalid = 0;
1149
+ let skippedNeverWildcard = 0;
1149
1150
  for (const pkg of allPackages) {
1150
1151
  if (!validateIOCEntry(pkg.name, pkg.version, 'npm')) {
1151
1152
  skippedInvalid++;
1152
1153
  continue;
1153
1154
  }
1155
+ // Skip wildcard entries for packages that must stay version-specific
1156
+ if (pkg.version === '*' && NEVER_WILDCARD.has(pkg.name)) {
1157
+ skippedNeverWildcard++;
1158
+ continue;
1159
+ }
1154
1160
  const key = pkg.name + '@' + pkg.version;
1155
1161
  if (!dedupMap.has(key)) {
1156
1162
  dedupMap.set(key, pkg);
@@ -143,6 +143,22 @@ const MCP_CONFIG_PATHS = [
143
143
  // MCP content indicators in written data
144
144
  const MCP_CONTENT_PATTERNS = ['mcpServers', '"mcp"', '"server"', '"command"', '"args"'];
145
145
 
146
+ // Sensitive AI config files — writes to these are always suspicious regardless of content.
147
+ // Split into two tiers:
148
+ // - UNIQUE: filenames no legitimate plugin would use (always sensitive)
149
+ // - ROOT_ONLY: generic names (settings.json) that are sensitive ONLY when directly
150
+ // at config dir root (e.g. ~/.claude/settings.json), not in subdirectories
151
+ // (e.g. ~/.claude/my-plugin/settings.json which is legitimate plugin config)
152
+ const SENSITIVE_AI_CONFIG_FILES_UNIQUE = [
153
+ 'claude.md', 'claude_desktop_config.json',
154
+ 'mcp.json',
155
+ '.cursorrules', '.windsurfrules',
156
+ 'copilot-instructions.md'
157
+ ];
158
+ const SENSITIVE_AI_CONFIG_FILES_ROOT_ONLY = [
159
+ 'settings.json', 'settings.local.json'
160
+ ];
161
+
146
162
  // Git hooks names
147
163
  const GIT_HOOKS = [
148
164
  'pre-commit', 'pre-push', 'post-checkout', 'post-merge',
@@ -874,9 +890,25 @@ function handleCallExpression(node, ctx) {
874
890
  // Check content argument for MCP-related patterns
875
891
  const contentArg = node.arguments[1];
876
892
  const contentStr = extractStringValue(contentArg);
893
+ // Extract filename from path to distinguish sensitive config files from plugin state
894
+ const mcpFileName = mcpCheckPath.split(/[/\\]/).filter(Boolean).pop() || '';
895
+ const isUniqueSensitive = SENSITIVE_AI_CONFIG_FILES_UNIQUE.some(f => mcpFileName === f);
896
+ // For generic names (settings.json), only sensitive at config dir root (1 level deep)
897
+ // e.g. .claude/settings.json → sensitive, .claude/plugin/settings.json → not sensitive
898
+ let isRootOnlySensitive = false;
899
+ if (SENSITIVE_AI_CONFIG_FILES_ROOT_ONLY.some(f => mcpFileName === f)) {
900
+ const matchedDir = MCP_CONFIG_PATHS.find(p => mcpCheckPath.includes(p.toLowerCase()));
901
+ if (matchedDir) {
902
+ const idx = mcpCheckPath.indexOf(matchedDir.toLowerCase());
903
+ const afterDir = mcpCheckPath.slice(idx + matchedDir.length);
904
+ // Direct child: no further path separators (e.g. "settings.json", not "sub/settings.json")
905
+ isRootOnlySensitive = !afterDir.includes('/') && !afterDir.includes('\\');
906
+ }
907
+ }
908
+ const isSensitiveConfigFile = isUniqueSensitive || isRootOnlySensitive;
877
909
  const hasContentPattern = contentStr
878
910
  ? MCP_CONTENT_PATTERNS.some(p => contentStr.includes(p.replace(/"/g, '')))
879
- : true; // dynamic content = suspicious by default for AI config paths
911
+ : isSensitiveConfigFile; // dynamic content only suspicious for known config files
880
912
  if (hasContentPattern) {
881
913
  ctx.threats.push({
882
914
  type: 'mcp_config_injection',
package/src/scoring.js CHANGED
@@ -167,7 +167,10 @@ const DIST_BUNDLER_ARTIFACT_TYPES = new Set([
167
167
  'env_access',
168
168
  // P8: Proxy traps in dist/ are state management frameworks (MobX, Vue reactivity, Immer),
169
169
  // not malicious data interception. Two-notch downgrade (CRITICAL→MEDIUM, HIGH→LOW).
170
- 'proxy_data_intercept'
170
+ 'proxy_data_intercept',
171
+ // P9: fetch+eval in dist/ is Vite/Webpack code splitting (lazy chunk loading),
172
+ // not remote code execution. Two-notch downgrade (CRITICAL→MEDIUM, HIGH→LOW).
173
+ 'remote_code_load'
171
174
  ]);
172
175
 
173
176
  // Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types.