agent-security-scanner-mcp 1.4.7 → 1.4.8

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.
Files changed (2) hide show
  1. package/index.js +48 -123
  2. package/package.json +3 -3
package/index.js CHANGED
@@ -941,26 +941,19 @@ const LEGITIMATE_PACKAGES = {
941
941
  dart: new Set(),
942
942
  perl: new Set(),
943
943
  raku: new Set(),
944
- npm: null, // Uses Bloom Filter
945
- pypi: null, // Uses Bloom Filter
946
- rubygems: null, // Uses Bloom Filter
944
+ npm: new Set(),
945
+ pypi: new Set(),
946
+ rubygems: new Set(),
947
947
  crates: new Set()
948
948
  };
949
949
 
950
- // Bloom Filters for large ecosystems (npm, pypi, rubygems)
950
+ // Bloom filters for large package lists (memory-efficient probabilistic lookup)
951
951
  const BLOOM_FILTERS = {
952
952
  npm: null,
953
953
  pypi: null,
954
954
  rubygems: null
955
955
  };
956
956
 
957
- // Package counts for bloom filter ecosystems (filter doesn't store count)
958
- const BLOOM_COUNTS = {
959
- npm: 3329177,
960
- pypi: 554762,
961
- rubygems: 180693
962
- };
963
-
964
957
  // Package import patterns by ecosystem
965
958
  const IMPORT_PATTERNS = {
966
959
  dart: [
@@ -1000,24 +993,7 @@ const IMPORT_PATTERNS = {
1000
993
  function loadPackageLists() {
1001
994
  const packagesDir = join(__dirname, 'packages');
1002
995
 
1003
- // Load Bloom Filter ecosystems (npm, pypi, rubygems)
1004
- for (const ecosystem of Object.keys(BLOOM_FILTERS)) {
1005
- try {
1006
- const bloomPath = join(packagesDir, `${ecosystem}-bloom.json`);
1007
- if (existsSync(bloomPath)) {
1008
- const bloomData = JSON.parse(readFileSync(bloomPath, 'utf-8'));
1009
- BLOOM_FILTERS[ecosystem] = BloomFilter.fromJSON(bloomData);
1010
- console.error(`Loaded ${ecosystem} Bloom Filter (${BLOOM_COUNTS[ecosystem].toLocaleString()} packages)`);
1011
- }
1012
- } catch (error) {
1013
- console.error(`Warning: Could not load ${ecosystem} Bloom Filter: ${error.message}`);
1014
- }
1015
- }
1016
-
1017
- // Load other ecosystems using regular Set (smaller lists)
1018
996
  for (const ecosystem of Object.keys(LEGITIMATE_PACKAGES)) {
1019
- if (BLOOM_FILTERS.hasOwnProperty(ecosystem)) continue; // Uses Bloom Filter
1020
-
1021
997
  const filePath = join(packagesDir, `${ecosystem}.txt`);
1022
998
  try {
1023
999
  if (existsSync(filePath)) {
@@ -1031,9 +1007,18 @@ function loadPackageLists() {
1031
1007
  }
1032
1008
  }
1033
1009
 
1034
- // Note about npm if not loaded
1035
- if (!BLOOM_FILTERS.npm) {
1036
- console.error(`npm: not included (use agent-security-scanner-mcp-full for npm support)`);
1010
+ // Load bloom filters for large ecosystems (npm, pypi, rubygems)
1011
+ for (const ecosystem of Object.keys(BLOOM_FILTERS)) {
1012
+ const bloomPath = join(packagesDir, `${ecosystem}-bloom.json`);
1013
+ try {
1014
+ if (existsSync(bloomPath)) {
1015
+ const bloomData = JSON.parse(readFileSync(bloomPath, 'utf-8'));
1016
+ BLOOM_FILTERS[ecosystem] = BloomFilter.fromJSON(bloomData);
1017
+ console.error(`Loaded ${ecosystem} bloom filter (${bloomData._size} bits)`);
1018
+ }
1019
+ } catch (error) {
1020
+ console.error(`Warning: Could not load ${ecosystem} bloom filter: ${error.message}`);
1021
+ }
1037
1022
  }
1038
1023
  }
1039
1024
 
@@ -1058,45 +1043,28 @@ function extractPackages(code, ecosystem) {
1058
1043
  return Array.from(packages);
1059
1044
  }
1060
1045
 
1061
- // Check if a package exists in the package list
1062
- function packageExists(packageName, ecosystem) {
1063
- // Bloom Filter ecosystems
1064
- if (BLOOM_FILTERS.hasOwnProperty(ecosystem)) {
1065
- return BLOOM_FILTERS[ecosystem] ? BLOOM_FILTERS[ecosystem].has(packageName) : false;
1066
- }
1067
- // Set-based ecosystems
1046
+ // Check if a package is hallucinated
1047
+ function isHallucinated(packageName, ecosystem) {
1068
1048
  const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
1069
- return legitPackages ? legitPackages.has(packageName) : false;
1070
- }
1071
1049
 
1072
- // Get package count for an ecosystem
1073
- function getPackageCount(ecosystem) {
1074
- // Bloom Filter ecosystems
1075
- if (BLOOM_FILTERS.hasOwnProperty(ecosystem)) {
1076
- return BLOOM_FILTERS[ecosystem] ? BLOOM_COUNTS[ecosystem] : 0;
1050
+ // First check Set-based lookup (exact match)
1051
+ if (legitPackages && legitPackages.size > 0) {
1052
+ return { hallucinated: !legitPackages.has(packageName) };
1077
1053
  }
1078
- // Set-based ecosystems
1079
- const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
1080
- return legitPackages ? legitPackages.size : 0;
1081
- }
1082
1054
 
1083
- // Check if ecosystem is loaded
1084
- function isEcosystemLoaded(ecosystem) {
1085
- // Bloom Filter ecosystems
1086
- if (BLOOM_FILTERS.hasOwnProperty(ecosystem)) {
1087
- return BLOOM_FILTERS[ecosystem] !== null;
1055
+ // Fall back to bloom filter for large ecosystems (npm, pypi, rubygems)
1056
+ const bloomFilter = BLOOM_FILTERS[ecosystem];
1057
+ if (bloomFilter) {
1058
+ // Bloom filter: false = definitely not in set, true = probably in set
1059
+ const mightExist = bloomFilter.has(packageName);
1060
+ return {
1061
+ hallucinated: !mightExist,
1062
+ bloomFilter: true,
1063
+ note: mightExist ? "Package likely exists (bloom filter match)" : "Package not found in bloom filter"
1064
+ };
1088
1065
  }
1089
- // Set-based ecosystems
1090
- const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
1091
- return legitPackages && legitPackages.size > 0;
1092
- }
1093
1066
 
1094
- // Check if a package is hallucinated
1095
- function isHallucinated(packageName, ecosystem) {
1096
- if (!isEcosystemLoaded(ecosystem)) {
1097
- return { unknown: true, reason: `No package list loaded for ${ecosystem}` };
1098
- }
1099
- return { hallucinated: !packageExists(packageName, ecosystem) };
1067
+ return { unknown: true, reason: `No package list loaded for ${ecosystem}` };
1100
1068
  }
1101
1069
 
1102
1070
  // Register check_package tool
@@ -1108,25 +1076,10 @@ server.tool(
1108
1076
  ecosystem: z.enum(["dart", "perl", "raku", "npm", "pypi", "rubygems", "crates"]).describe("The package ecosystem (dart=pub.dev, perl=CPAN, raku=raku.land, npm=npmjs, pypi=PyPI, rubygems=RubyGems, crates=crates.io)")
1109
1077
  },
1110
1078
  async ({ package_name, ecosystem }) => {
1111
- // Check if npm is requested but not available
1112
- if (ecosystem === 'npm' && !BLOOM_FILTERS.npm) {
1113
- return {
1114
- content: [{
1115
- type: "text",
1116
- text: JSON.stringify({
1117
- package: package_name,
1118
- ecosystem,
1119
- status: "unavailable",
1120
- reason: "npm hallucination detection not included in default package (saves 7.6 MB)",
1121
- suggestion: "Use 'agent-security-scanner-mcp-full' for npm support, or verify manually at npmjs.com"
1122
- }, null, 2)
1123
- }]
1124
- };
1125
- }
1126
-
1127
- const totalPackages = getPackageCount(ecosystem);
1079
+ const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
1080
+ const totalPackages = legitPackages?.size || 0;
1128
1081
 
1129
- if (!isEcosystemLoaded(ecosystem)) {
1082
+ if (totalPackages === 0) {
1130
1083
  return {
1131
1084
  content: [{
1132
1085
  type: "text",
@@ -1134,14 +1087,14 @@ server.tool(
1134
1087
  package: package_name,
1135
1088
  ecosystem,
1136
1089
  status: "unknown",
1137
- reason: `No package list loaded for ${ecosystem}`,
1138
- suggestion: "Verify manually at the package registry"
1090
+ reason: `No package list loaded for ${ecosystem}. Add packages/${ecosystem}.txt`,
1091
+ suggestion: "Load package list or verify manually at the package registry"
1139
1092
  }, null, 2)
1140
1093
  }]
1141
1094
  };
1142
1095
  }
1143
1096
 
1144
- const exists = packageExists(package_name, ecosystem);
1097
+ const exists = legitPackages.has(package_name);
1145
1098
 
1146
1099
  return {
1147
1100
  content: [{
@@ -1177,27 +1130,12 @@ server.tool(
1177
1130
  };
1178
1131
  }
1179
1132
 
1180
- // Check if npm is requested but not available
1181
- if (ecosystem === 'npm' && !BLOOM_FILTERS.npm) {
1182
- return {
1183
- content: [{
1184
- type: "text",
1185
- text: JSON.stringify({
1186
- file: file_path,
1187
- ecosystem,
1188
- status: "unavailable",
1189
- reason: "npm hallucination detection not included in default package (saves 7.6 MB)",
1190
- suggestion: "Use 'agent-security-scanner-mcp-full' for npm support, or verify manually at npmjs.com"
1191
- }, null, 2)
1192
- }]
1193
- };
1194
- }
1195
-
1196
1133
  const code = readFileSync(file_path, 'utf-8');
1197
1134
  const packages = extractPackages(code, ecosystem);
1198
- const totalKnown = getPackageCount(ecosystem);
1135
+ const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
1136
+ const totalKnown = legitPackages?.size || 0;
1199
1137
 
1200
- if (!isEcosystemLoaded(ecosystem)) {
1138
+ if (totalKnown === 0) {
1201
1139
  return {
1202
1140
  content: [{
1203
1141
  type: "text",
@@ -1214,8 +1152,8 @@ server.tool(
1214
1152
 
1215
1153
  const results = packages.map(pkg => ({
1216
1154
  package: pkg,
1217
- legitimate: packageExists(pkg, ecosystem),
1218
- hallucinated: !packageExists(pkg, ecosystem)
1155
+ legitimate: legitPackages.has(pkg),
1156
+ hallucinated: !legitPackages.has(pkg)
1219
1157
  }));
1220
1158
 
1221
1159
  const hallucinated = results.filter(r => r.hallucinated);
@@ -1249,24 +1187,11 @@ server.tool(
1249
1187
  "List statistics about loaded package lists for hallucination detection",
1250
1188
  {},
1251
1189
  async () => {
1252
- const ecosystems = ['npm', 'pypi', 'rubygems', 'crates', 'dart', 'perl', 'raku'];
1253
- const stats = ecosystems.map(ecosystem => {
1254
- const loaded = isEcosystemLoaded(ecosystem);
1255
- let status = loaded ? "ready" : "not loaded";
1256
- let storage = BLOOM_FILTERS.hasOwnProperty(ecosystem) ? "bloom filter" : "hash set";
1257
-
1258
- // npm is not included in default package
1259
- if (ecosystem === 'npm' && !loaded) {
1260
- status = "not included (use -full package)";
1261
- }
1262
-
1263
- return {
1264
- ecosystem,
1265
- packages_loaded: loaded ? getPackageCount(ecosystem) : 0,
1266
- status,
1267
- storage
1268
- };
1269
- });
1190
+ const stats = Object.entries(LEGITIMATE_PACKAGES).map(([ecosystem, packages]) => ({
1191
+ ecosystem,
1192
+ packages_loaded: packages.size,
1193
+ status: packages.size > 0 ? "ready" : "not loaded"
1194
+ }));
1270
1195
 
1271
1196
  return {
1272
1197
  content: [{
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "agent-security-scanner-mcp",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
+ "mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
4
5
  "description": "MCP server for security scanning, AI agent prompt security & package hallucination detection. Works with Claude Desktop, Claude Code, OpenCode, Kilo Code. Detects SQL injection, XSS, secrets, prompt attacks, and AI-invented packages.",
5
6
  "main": "index.js",
6
7
  "type": "module",
@@ -66,7 +67,6 @@
66
67
  "index.js",
67
68
  "analyzer.py",
68
69
  "rules/**",
69
- "packages/**",
70
- "README.md"
70
+ "packages/**"
71
71
  ]
72
72
  }