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.
- package/index.js +48 -123
- 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:
|
|
945
|
-
pypi:
|
|
946
|
-
rubygems:
|
|
944
|
+
npm: new Set(),
|
|
945
|
+
pypi: new Set(),
|
|
946
|
+
rubygems: new Set(),
|
|
947
947
|
crates: new Set()
|
|
948
948
|
};
|
|
949
949
|
|
|
950
|
-
// Bloom
|
|
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
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
|
1062
|
-
function
|
|
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
|
-
//
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
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 (
|
|
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: "
|
|
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 =
|
|
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
|
|
1135
|
+
const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
|
|
1136
|
+
const totalKnown = legitPackages?.size || 0;
|
|
1199
1137
|
|
|
1200
|
-
if (
|
|
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:
|
|
1218
|
-
hallucinated: !
|
|
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
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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.
|
|
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
|
}
|