agent-security-scanner-mcp 1.0.2 → 1.1.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/README.md +188 -2
- package/index.js +229 -0
- package/package.json +8 -4
- package/packages/dart.txt +64721 -0
- package/packages/perl.txt +50202 -0
- package/packages/raku.txt +2626 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# agent-security-scanner-mcp
|
|
2
2
|
|
|
3
|
-
A powerful MCP (Model Context Protocol) server for real-time security vulnerability scanning. Integrates with Claude Desktop
|
|
3
|
+
A powerful MCP (Model Context Protocol) server for real-time security vulnerability scanning. Integrates with Claude Desktop, Claude Code, OpenCode.ai, Kilo Code, and any MCP-compatible client to automatically detect and fix security issues as you code.
|
|
4
4
|
|
|
5
|
-
**165 Semgrep-aligned security rules | 105 auto-fix templates | 100% fix coverage**
|
|
5
|
+
**165 Semgrep-aligned security rules | 105 auto-fix templates | 100% fix coverage | Package hallucination detection**
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -11,6 +11,7 @@ A powerful MCP (Model Context Protocol) server for real-time security vulnerabil
|
|
|
11
11
|
- **Multi-language support** - JavaScript, TypeScript, Python, Java, Go, Dockerfile
|
|
12
12
|
- **Semgrep-compatible** - Rules aligned with Semgrep registry format
|
|
13
13
|
- **CWE & OWASP mapped** - Every rule includes CWE and OWASP references
|
|
14
|
+
- **Hallucination detection** - Detect AI-invented package names (Dart, Perl, Raku)
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -65,6 +66,82 @@ Add to your MCP settings (`~/.claude/settings.json`):
|
|
|
65
66
|
}
|
|
66
67
|
```
|
|
67
68
|
|
|
69
|
+
### OpenCode.ai
|
|
70
|
+
|
|
71
|
+
Add to your `opencode.jsonc` configuration file:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"$schema": "https://opencode.ai/config.json",
|
|
76
|
+
"mcp": {
|
|
77
|
+
"security-scanner": {
|
|
78
|
+
"type": "local",
|
|
79
|
+
"command": ["npx", "-y", "agent-security-scanner-mcp"],
|
|
80
|
+
"enabled": true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or if installed globally:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcp": {
|
|
91
|
+
"security-scanner": {
|
|
92
|
+
"type": "local",
|
|
93
|
+
"command": ["agent-security-scanner-mcp"],
|
|
94
|
+
"enabled": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Kilo Code
|
|
101
|
+
|
|
102
|
+
**Global configuration** - Add to VS Code settings `mcp_settings.json`:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"security-scanner": {
|
|
108
|
+
"command": "npx",
|
|
109
|
+
"args": ["-y", "agent-security-scanner-mcp"],
|
|
110
|
+
"alwaysAllow": [],
|
|
111
|
+
"disabled": false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Project-level configuration** - Create `.kilocode/mcp.json` in your project root:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"security-scanner": {
|
|
123
|
+
"command": "npx",
|
|
124
|
+
"args": ["-y", "agent-security-scanner-mcp"],
|
|
125
|
+
"alwaysAllow": ["scan_security", "list_security_rules"],
|
|
126
|
+
"disabled": false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Windows users** - Use cmd wrapper:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"security-scanner": {
|
|
138
|
+
"command": "cmd",
|
|
139
|
+
"args": ["/c", "npx", "-y", "agent-security-scanner-mcp"]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
68
145
|
## Available Tools
|
|
69
146
|
|
|
70
147
|
### `scan_security`
|
|
@@ -127,6 +204,115 @@ Returns:
|
|
|
127
204
|
|
|
128
205
|
List all 105 available auto-fix templates.
|
|
129
206
|
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Package Hallucination Detection
|
|
210
|
+
|
|
211
|
+
Detect AI-hallucinated package names that don't exist in official registries. Prevents supply chain attacks where attackers register fake package names suggested by AI.
|
|
212
|
+
|
|
213
|
+
### `check_package`
|
|
214
|
+
|
|
215
|
+
Check if a single package name is legitimate or potentially hallucinated.
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
Parameters:
|
|
219
|
+
package_name (string): The package name to verify
|
|
220
|
+
ecosystem (enum): "dart", "perl", "raku", "npm", "pypi"
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
- legitimate: true/false
|
|
224
|
+
- hallucinated: true/false
|
|
225
|
+
- confidence: "high"
|
|
226
|
+
- recommendation: Action to take
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Example:**
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"package": "flutter_animations",
|
|
233
|
+
"ecosystem": "dart",
|
|
234
|
+
"legitimate": true,
|
|
235
|
+
"hallucinated": false,
|
|
236
|
+
"confidence": "high",
|
|
237
|
+
"total_known_packages": 64721,
|
|
238
|
+
"recommendation": "Package exists in registry - safe to use"
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### `scan_packages`
|
|
243
|
+
|
|
244
|
+
Scan a code file and detect all potentially hallucinated package imports.
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Parameters:
|
|
248
|
+
file_path (string): Path to the file to scan
|
|
249
|
+
ecosystem (enum): "dart", "perl", "raku", "npm", "pypi"
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
- List of all packages found
|
|
253
|
+
- Which are legitimate vs hallucinated
|
|
254
|
+
- Recommendation
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Example output:**
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"file": "/path/to/main.dart",
|
|
261
|
+
"ecosystem": "dart",
|
|
262
|
+
"total_packages_found": 5,
|
|
263
|
+
"legitimate_count": 4,
|
|
264
|
+
"hallucinated_count": 1,
|
|
265
|
+
"hallucinated_packages": ["fake_flutter_pkg"],
|
|
266
|
+
"legitimate_packages": ["flutter", "http", "provider", "shared_preferences"],
|
|
267
|
+
"recommendation": "⚠️ Found 1 potentially hallucinated package(s): fake_flutter_pkg"
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `list_package_stats`
|
|
272
|
+
|
|
273
|
+
Show statistics about loaded package lists.
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"package_lists": [
|
|
278
|
+
{ "ecosystem": "dart", "packages_loaded": 64721, "status": "ready" },
|
|
279
|
+
{ "ecosystem": "perl", "packages_loaded": 1000, "status": "ready" },
|
|
280
|
+
{ "ecosystem": "raku", "packages_loaded": 2626, "status": "ready" }
|
|
281
|
+
],
|
|
282
|
+
"total_packages": 68347
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Adding Custom Package Lists
|
|
287
|
+
|
|
288
|
+
Add your own package lists to `packages/` directory:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
# Format: one package name per line
|
|
292
|
+
packages/
|
|
293
|
+
├── dart.txt # 64,721 packages
|
|
294
|
+
├── perl.txt # 1,000 packages
|
|
295
|
+
├── raku.txt # 2,626 packages
|
|
296
|
+
├── npm.txt # Add your own
|
|
297
|
+
└── pypi.txt # Add your own
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Fetching Package Lists
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Using the included script
|
|
304
|
+
cd mcp-server
|
|
305
|
+
pip install datasets
|
|
306
|
+
python scripts/fetch-packages.py
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Package lists are sourced from:
|
|
310
|
+
- **Dart:** [dchitimalla1/dart-20250529](https://huggingface.co/datasets/dchitimalla1/dart-20250529)
|
|
311
|
+
- **Perl:** [dchitimalla1/perl-20250530](https://huggingface.co/datasets/dchitimalla1/perl-20250530)
|
|
312
|
+
- **Raku:** [dchitimalla1/raku-20250523](https://huggingface.co/datasets/dchitimalla1/raku-20250523)
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
130
316
|
## Security Rules (165 total)
|
|
131
317
|
|
|
132
318
|
### By Language
|
package/index.js
CHANGED
|
@@ -824,6 +824,235 @@ server.tool(
|
|
|
824
824
|
}
|
|
825
825
|
);
|
|
826
826
|
|
|
827
|
+
// ===========================================
|
|
828
|
+
// PACKAGE HALLUCINATION DETECTION
|
|
829
|
+
// ===========================================
|
|
830
|
+
|
|
831
|
+
// Load legitimate package lists into memory (hash sets for O(1) lookup)
|
|
832
|
+
const LEGITIMATE_PACKAGES = {
|
|
833
|
+
dart: new Set(),
|
|
834
|
+
perl: new Set(),
|
|
835
|
+
raku: new Set(),
|
|
836
|
+
npm: new Set(),
|
|
837
|
+
pypi: new Set()
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// Package import patterns by ecosystem
|
|
841
|
+
const IMPORT_PATTERNS = {
|
|
842
|
+
dart: [
|
|
843
|
+
/import\s+['"]package:([^\/'"]+)/g,
|
|
844
|
+
/dependencies:\s*\n(?:\s+(\w+):\s*[\^~]?[\d.]+\n)+/g
|
|
845
|
+
],
|
|
846
|
+
perl: [
|
|
847
|
+
/use\s+([\w:]+)/g,
|
|
848
|
+
/require\s+([\w:]+)/g
|
|
849
|
+
],
|
|
850
|
+
raku: [
|
|
851
|
+
/use\s+([\w:]+)/g,
|
|
852
|
+
/need\s+([\w:]+)/g
|
|
853
|
+
],
|
|
854
|
+
npm: [
|
|
855
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
856
|
+
/from\s+['"]([^'"]+)['"]/g,
|
|
857
|
+
/import\s+['"]([^'"]+)['"]/g
|
|
858
|
+
],
|
|
859
|
+
pypi: [
|
|
860
|
+
/^import\s+([\w]+)/gm,
|
|
861
|
+
/^from\s+([\w]+)/gm
|
|
862
|
+
]
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// Load package lists on startup
|
|
866
|
+
function loadPackageLists() {
|
|
867
|
+
const packagesDir = join(__dirname, 'packages');
|
|
868
|
+
|
|
869
|
+
for (const ecosystem of Object.keys(LEGITIMATE_PACKAGES)) {
|
|
870
|
+
const filePath = join(packagesDir, `${ecosystem}.txt`);
|
|
871
|
+
try {
|
|
872
|
+
if (existsSync(filePath)) {
|
|
873
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
874
|
+
const packages = content.split('\n').filter(p => p.trim());
|
|
875
|
+
LEGITIMATE_PACKAGES[ecosystem] = new Set(packages);
|
|
876
|
+
console.error(`Loaded ${packages.length} ${ecosystem} packages`);
|
|
877
|
+
}
|
|
878
|
+
} catch (error) {
|
|
879
|
+
console.error(`Warning: Could not load ${ecosystem} packages: ${error.message}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Extract package names from code
|
|
885
|
+
function extractPackages(code, ecosystem) {
|
|
886
|
+
const packages = new Set();
|
|
887
|
+
const patterns = IMPORT_PATTERNS[ecosystem] || [];
|
|
888
|
+
|
|
889
|
+
for (const pattern of patterns) {
|
|
890
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
891
|
+
let match;
|
|
892
|
+
while ((match = regex.exec(code)) !== null) {
|
|
893
|
+
const pkg = match[1];
|
|
894
|
+
if (pkg && !pkg.startsWith('.') && !pkg.startsWith('/')) {
|
|
895
|
+
// Normalize package name (handle scoped packages, subpaths)
|
|
896
|
+
const basePkg = pkg.split('/')[0].replace(/^@/, '');
|
|
897
|
+
packages.add(basePkg);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return Array.from(packages);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Check if a package is hallucinated
|
|
906
|
+
function isHallucinated(packageName, ecosystem) {
|
|
907
|
+
const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
|
|
908
|
+
if (!legitPackages || legitPackages.size === 0) {
|
|
909
|
+
return { unknown: true, reason: `No package list loaded for ${ecosystem}` };
|
|
910
|
+
}
|
|
911
|
+
return { hallucinated: !legitPackages.has(packageName) };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Register check_package tool
|
|
915
|
+
server.tool(
|
|
916
|
+
"check_package",
|
|
917
|
+
"Check if a package name is legitimate or potentially hallucinated (AI-invented)",
|
|
918
|
+
{
|
|
919
|
+
package_name: z.string().describe("The package name to verify"),
|
|
920
|
+
ecosystem: z.enum(["dart", "perl", "raku", "npm", "pypi"]).describe("The package ecosystem")
|
|
921
|
+
},
|
|
922
|
+
async ({ package_name, ecosystem }) => {
|
|
923
|
+
const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
|
|
924
|
+
const totalPackages = legitPackages?.size || 0;
|
|
925
|
+
|
|
926
|
+
if (totalPackages === 0) {
|
|
927
|
+
return {
|
|
928
|
+
content: [{
|
|
929
|
+
type: "text",
|
|
930
|
+
text: JSON.stringify({
|
|
931
|
+
package: package_name,
|
|
932
|
+
ecosystem,
|
|
933
|
+
status: "unknown",
|
|
934
|
+
reason: `No package list loaded for ${ecosystem}. Add packages/${ecosystem}.txt`,
|
|
935
|
+
suggestion: "Load package list or verify manually at the package registry"
|
|
936
|
+
}, null, 2)
|
|
937
|
+
}]
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const exists = legitPackages.has(package_name);
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
content: [{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: JSON.stringify({
|
|
947
|
+
package: package_name,
|
|
948
|
+
ecosystem,
|
|
949
|
+
legitimate: exists,
|
|
950
|
+
hallucinated: !exists,
|
|
951
|
+
confidence: "high",
|
|
952
|
+
total_known_packages: totalPackages,
|
|
953
|
+
recommendation: exists
|
|
954
|
+
? "Package exists in registry - safe to use"
|
|
955
|
+
: "⚠️ POTENTIAL HALLUCINATION - Package not found in registry. Verify before using!"
|
|
956
|
+
}, null, 2)
|
|
957
|
+
}]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
// Register scan_packages tool
|
|
963
|
+
server.tool(
|
|
964
|
+
"scan_packages",
|
|
965
|
+
"Scan code for package imports and check for hallucinated (AI-invented) packages",
|
|
966
|
+
{
|
|
967
|
+
file_path: z.string().describe("Path to the file to scan"),
|
|
968
|
+
ecosystem: z.enum(["dart", "perl", "raku", "npm", "pypi"]).describe("The package ecosystem")
|
|
969
|
+
},
|
|
970
|
+
async ({ file_path, ecosystem }) => {
|
|
971
|
+
if (!existsSync(file_path)) {
|
|
972
|
+
return {
|
|
973
|
+
content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const code = readFileSync(file_path, 'utf-8');
|
|
978
|
+
const packages = extractPackages(code, ecosystem);
|
|
979
|
+
const legitPackages = LEGITIMATE_PACKAGES[ecosystem];
|
|
980
|
+
const totalKnown = legitPackages?.size || 0;
|
|
981
|
+
|
|
982
|
+
if (totalKnown === 0) {
|
|
983
|
+
return {
|
|
984
|
+
content: [{
|
|
985
|
+
type: "text",
|
|
986
|
+
text: JSON.stringify({
|
|
987
|
+
file: file_path,
|
|
988
|
+
ecosystem,
|
|
989
|
+
packages_found: packages,
|
|
990
|
+
status: "unknown",
|
|
991
|
+
reason: `No package list loaded for ${ecosystem}`
|
|
992
|
+
}, null, 2)
|
|
993
|
+
}]
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const results = packages.map(pkg => ({
|
|
998
|
+
package: pkg,
|
|
999
|
+
legitimate: legitPackages.has(pkg),
|
|
1000
|
+
hallucinated: !legitPackages.has(pkg)
|
|
1001
|
+
}));
|
|
1002
|
+
|
|
1003
|
+
const hallucinated = results.filter(r => r.hallucinated);
|
|
1004
|
+
const legitimate = results.filter(r => r.legitimate);
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: JSON.stringify({
|
|
1010
|
+
file: file_path,
|
|
1011
|
+
ecosystem,
|
|
1012
|
+
total_packages_found: packages.length,
|
|
1013
|
+
legitimate_count: legitimate.length,
|
|
1014
|
+
hallucinated_count: hallucinated.length,
|
|
1015
|
+
known_packages_in_registry: totalKnown,
|
|
1016
|
+
hallucinated_packages: hallucinated.map(r => r.package),
|
|
1017
|
+
legitimate_packages: legitimate.map(r => r.package),
|
|
1018
|
+
all_results: results,
|
|
1019
|
+
recommendation: hallucinated.length > 0
|
|
1020
|
+
? `⚠️ Found ${hallucinated.length} potentially hallucinated package(s): ${hallucinated.map(r => r.package).join(', ')}`
|
|
1021
|
+
: "✅ All packages verified as legitimate"
|
|
1022
|
+
}, null, 2)
|
|
1023
|
+
}]
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
// Register list_package_stats tool
|
|
1029
|
+
server.tool(
|
|
1030
|
+
"list_package_stats",
|
|
1031
|
+
"List statistics about loaded package lists for hallucination detection",
|
|
1032
|
+
{},
|
|
1033
|
+
async () => {
|
|
1034
|
+
const stats = Object.entries(LEGITIMATE_PACKAGES).map(([ecosystem, packages]) => ({
|
|
1035
|
+
ecosystem,
|
|
1036
|
+
packages_loaded: packages.size,
|
|
1037
|
+
status: packages.size > 0 ? "ready" : "not loaded"
|
|
1038
|
+
}));
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
content: [{
|
|
1042
|
+
type: "text",
|
|
1043
|
+
text: JSON.stringify({
|
|
1044
|
+
package_lists: stats,
|
|
1045
|
+
total_packages: stats.reduce((sum, s) => sum + s.packages_loaded, 0),
|
|
1046
|
+
usage: "Use check_package or scan_packages to detect hallucinated packages"
|
|
1047
|
+
}, null, 2)
|
|
1048
|
+
}]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
// Load package lists on module initialization
|
|
1054
|
+
loadPackageLists();
|
|
1055
|
+
|
|
827
1056
|
// Start the server with stdio transport
|
|
828
1057
|
async function main() {
|
|
829
1058
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for security
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "MCP server for security scanning & package hallucination detection - SQL injection, XSS, secrets, and AI-invented package detection",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"code-analysis",
|
|
22
22
|
"sql-injection",
|
|
23
23
|
"xss",
|
|
24
|
-
"secrets-detection"
|
|
24
|
+
"secrets-detection",
|
|
25
|
+
"hallucination-detection",
|
|
26
|
+
"package-verification",
|
|
27
|
+
"supply-chain-security"
|
|
25
28
|
],
|
|
26
29
|
"author": "",
|
|
27
30
|
"license": "MIT",
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
"files": [
|
|
44
47
|
"index.js",
|
|
45
48
|
"analyzer.py",
|
|
46
|
-
"rules/**"
|
|
49
|
+
"rules/**",
|
|
50
|
+
"packages/**"
|
|
47
51
|
]
|
|
48
52
|
}
|