@bryan-thompson/inspector-assessment-client 1.15.0 → 1.16.0
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/dist/assets/{OAuthCallback-BleN4Jjs.js → OAuthCallback-KwMiy-L3.js} +1 -1
- package/dist/assets/{OAuthDebugCallback-C__lzEyx.js → OAuthDebugCallback-hckdJlo3.js} +1 -1
- package/dist/assets/{index-CPXmfP9b.js → index-C89umkGV.js} +745 -4350
- package/dist/index.html +1 -1
- package/lib/lib/assessmentTypes.d.ts +123 -0
- package/lib/lib/assessmentTypes.d.ts.map +1 -1
- package/lib/lib/assessmentTypes.js +20 -0
- package/lib/lib/securityPatterns.d.ts +2 -2
- package/lib/lib/securityPatterns.d.ts.map +1 -1
- package/lib/lib/securityPatterns.js +290 -15
- package/lib/services/assessment/AssessmentOrchestrator.d.ts +67 -0
- package/lib/services/assessment/AssessmentOrchestrator.d.ts.map +1 -1
- package/lib/services/assessment/AssessmentOrchestrator.js +91 -1
- package/lib/services/assessment/ResponseValidator.d.ts +7 -34
- package/lib/services/assessment/ResponseValidator.d.ts.map +1 -1
- package/lib/services/assessment/ResponseValidator.js +100 -704
- package/lib/services/assessment/config/annotationPatterns.js +1 -1
- package/lib/services/assessment/lib/RequestHistoryAnalyzer.d.ts +67 -0
- package/lib/services/assessment/lib/RequestHistoryAnalyzer.d.ts.map +1 -0
- package/lib/services/assessment/lib/RequestHistoryAnalyzer.js +191 -0
- package/lib/services/assessment/lib/claudeCodeBridge.d.ts +1 -0
- package/lib/services/assessment/lib/claudeCodeBridge.d.ts.map +1 -1
- package/lib/services/assessment/lib/claudeCodeBridge.js +5 -4
- package/lib/services/assessment/modules/AuthenticationAssessor.d.ts +4 -0
- package/lib/services/assessment/modules/AuthenticationAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/AuthenticationAssessor.js +97 -1
- package/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.d.ts +39 -0
- package/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.d.ts.map +1 -0
- package/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.js +330 -0
- package/lib/services/assessment/modules/FunctionalityAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/FunctionalityAssessor.js +46 -13
- package/lib/services/assessment/modules/MCPSpecComplianceAssessor.d.ts +5 -0
- package/lib/services/assessment/modules/MCPSpecComplianceAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/MCPSpecComplianceAssessor.js +81 -0
- package/lib/services/assessment/modules/ManifestValidationAssessor.js +1 -1
- package/lib/services/assessment/modules/PromptAssessor.d.ts +30 -0
- package/lib/services/assessment/modules/PromptAssessor.d.ts.map +1 -0
- package/lib/services/assessment/modules/PromptAssessor.js +367 -0
- package/lib/services/assessment/modules/ResourceAssessor.d.ts +28 -0
- package/lib/services/assessment/modules/ResourceAssessor.d.ts.map +1 -0
- package/lib/services/assessment/modules/ResourceAssessor.js +296 -0
- package/lib/services/assessment/modules/SecurityAssessor.d.ts +4 -2
- package/lib/services/assessment/modules/SecurityAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/SecurityAssessor.js +10 -41
- package/lib/services/assessment/modules/TemporalAssessor.d.ts +1 -0
- package/lib/services/assessment/modules/TemporalAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/TemporalAssessor.js +35 -4
- package/lib/utils/jsonUtils.d.ts +68 -0
- package/lib/utils/jsonUtils.d.ts.map +1 -0
- package/lib/utils/jsonUtils.js +141 -0
- package/lib/utils/paramUtils.d.ts +11 -0
- package/lib/utils/paramUtils.d.ts.map +1 -0
- package/lib/utils/paramUtils.js +37 -0
- package/lib/utils/schemaUtils.d.ts +74 -0
- package/lib/utils/schemaUtils.d.ts.map +1 -0
- package/lib/utils/schemaUtils.js +268 -0
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Assessor Module
|
|
3
|
+
* Tests MCP server resources for accessibility, security, and compliance
|
|
4
|
+
*
|
|
5
|
+
* Tests include:
|
|
6
|
+
* - Resource accessibility (can read declared resources)
|
|
7
|
+
* - Path traversal vulnerabilities in resource URIs
|
|
8
|
+
* - Sensitive data exposure detection
|
|
9
|
+
* - URI validation and format compliance
|
|
10
|
+
*/
|
|
11
|
+
import { BaseAssessor } from "./BaseAssessor.js";
|
|
12
|
+
// Sensitive file patterns that shouldn't be exposed
|
|
13
|
+
const SENSITIVE_PATTERNS = [
|
|
14
|
+
/\.env$/i,
|
|
15
|
+
/\.pem$/i,
|
|
16
|
+
/\.key$/i,
|
|
17
|
+
/id_rsa/i,
|
|
18
|
+
/id_dsa/i,
|
|
19
|
+
/\.crt$/i,
|
|
20
|
+
/password/i,
|
|
21
|
+
/secret/i,
|
|
22
|
+
/credential/i,
|
|
23
|
+
/\/etc\/passwd/i,
|
|
24
|
+
/\/etc\/shadow/i,
|
|
25
|
+
/\.ssh\//i,
|
|
26
|
+
/aws_access_key/i,
|
|
27
|
+
/api[_-]?key/i,
|
|
28
|
+
/\.htpasswd/i,
|
|
29
|
+
/\.git\/config/i,
|
|
30
|
+
];
|
|
31
|
+
// Path traversal patterns to test
|
|
32
|
+
const PATH_TRAVERSAL_PAYLOADS = [
|
|
33
|
+
"../../../etc/passwd",
|
|
34
|
+
"..\\..\\..\\windows\\system32\\config\\sam",
|
|
35
|
+
"....//....//....//etc/passwd",
|
|
36
|
+
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
|
|
37
|
+
"..%252f..%252f..%252fetc/passwd",
|
|
38
|
+
];
|
|
39
|
+
// Sensitive content patterns in resource content
|
|
40
|
+
const SENSITIVE_CONTENT_PATTERNS = [
|
|
41
|
+
/-----BEGIN.*PRIVATE KEY-----/i,
|
|
42
|
+
/-----BEGIN RSA PRIVATE KEY-----/i,
|
|
43
|
+
/sk-[a-zA-Z0-9]{32,}/i, // OpenAI-style API keys
|
|
44
|
+
/ghp_[a-zA-Z0-9]{36}/i, // GitHub tokens
|
|
45
|
+
/glpat-[a-zA-Z0-9-_]{20}/i, // GitLab tokens
|
|
46
|
+
/xox[baprs]-[a-zA-Z0-9-]+/i, // Slack tokens
|
|
47
|
+
/AKIA[A-Z0-9]{16}/i, // AWS access keys
|
|
48
|
+
/password\s*[:=]\s*['"][^'"]+['"]/i,
|
|
49
|
+
/secret\s*[:=]\s*['"][^'"]+['"]/i,
|
|
50
|
+
];
|
|
51
|
+
export class ResourceAssessor extends BaseAssessor {
|
|
52
|
+
async assess(context) {
|
|
53
|
+
const results = [];
|
|
54
|
+
// Check if resources are provided
|
|
55
|
+
if (!context.resources && !context.resourceTemplates) {
|
|
56
|
+
return this.createNoResourcesResponse();
|
|
57
|
+
}
|
|
58
|
+
const resources = context.resources || [];
|
|
59
|
+
const templates = context.resourceTemplates || [];
|
|
60
|
+
this.log(`Testing ${resources.length} resources and ${templates.length} resource templates`);
|
|
61
|
+
// Test each resource
|
|
62
|
+
for (const resource of resources) {
|
|
63
|
+
this.testCount++;
|
|
64
|
+
const result = await this.testResource(resource, context);
|
|
65
|
+
results.push(result);
|
|
66
|
+
}
|
|
67
|
+
// Test resource templates with path traversal payloads
|
|
68
|
+
for (const template of templates) {
|
|
69
|
+
this.testCount++;
|
|
70
|
+
const templateResults = await this.testResourceTemplate(template, context);
|
|
71
|
+
results.push(...templateResults);
|
|
72
|
+
}
|
|
73
|
+
// Calculate metrics
|
|
74
|
+
const accessibleResources = results.filter((r) => r.accessible).length;
|
|
75
|
+
const securityIssuesFound = results.filter((r) => r.securityIssues.length > 0).length;
|
|
76
|
+
const pathTraversalVulnerabilities = results.filter((r) => r.pathTraversalVulnerable).length;
|
|
77
|
+
const sensitiveDataExposures = results.filter((r) => r.sensitiveDataExposed).length;
|
|
78
|
+
// Determine status
|
|
79
|
+
const status = this.determineResourceStatus(pathTraversalVulnerabilities, sensitiveDataExposures, securityIssuesFound, results.length);
|
|
80
|
+
// Generate explanation and recommendations
|
|
81
|
+
const explanation = this.generateExplanation(results, pathTraversalVulnerabilities, sensitiveDataExposures);
|
|
82
|
+
const recommendations = this.generateRecommendations(results);
|
|
83
|
+
return {
|
|
84
|
+
resourcesTested: resources.length,
|
|
85
|
+
resourceTemplatesTested: templates.length,
|
|
86
|
+
accessibleResources,
|
|
87
|
+
securityIssuesFound,
|
|
88
|
+
pathTraversalVulnerabilities,
|
|
89
|
+
sensitiveDataExposures,
|
|
90
|
+
results,
|
|
91
|
+
status,
|
|
92
|
+
explanation,
|
|
93
|
+
recommendations,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
createNoResourcesResponse() {
|
|
97
|
+
return {
|
|
98
|
+
resourcesTested: 0,
|
|
99
|
+
resourceTemplatesTested: 0,
|
|
100
|
+
accessibleResources: 0,
|
|
101
|
+
securityIssuesFound: 0,
|
|
102
|
+
pathTraversalVulnerabilities: 0,
|
|
103
|
+
sensitiveDataExposures: 0,
|
|
104
|
+
results: [],
|
|
105
|
+
status: "PASS",
|
|
106
|
+
explanation: "No resources declared by server. Resource assessment skipped.",
|
|
107
|
+
recommendations: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async testResource(resource, context) {
|
|
111
|
+
const result = {
|
|
112
|
+
resourceUri: resource.uri,
|
|
113
|
+
resourceName: resource.name,
|
|
114
|
+
mimeType: resource.mimeType,
|
|
115
|
+
tested: true,
|
|
116
|
+
accessible: false,
|
|
117
|
+
securityIssues: [],
|
|
118
|
+
pathTraversalVulnerable: false,
|
|
119
|
+
sensitiveDataExposed: false,
|
|
120
|
+
validUri: this.isValidUri(resource.uri),
|
|
121
|
+
};
|
|
122
|
+
// Check URI for sensitive patterns
|
|
123
|
+
if (this.isSensitiveUri(resource.uri)) {
|
|
124
|
+
result.securityIssues.push(`Resource URI matches sensitive file pattern: ${resource.uri}`);
|
|
125
|
+
result.sensitiveDataExposed = true;
|
|
126
|
+
}
|
|
127
|
+
// Try to read the resource if readResource function is provided
|
|
128
|
+
if (context.readResource) {
|
|
129
|
+
try {
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
const content = await this.executeWithTimeout(context.readResource(resource.uri), 5000);
|
|
132
|
+
result.readTime = Date.now() - startTime;
|
|
133
|
+
result.accessible = true;
|
|
134
|
+
result.contentSizeBytes = content?.length || 0;
|
|
135
|
+
// Check content for sensitive data
|
|
136
|
+
if (content && this.containsSensitiveContent(content)) {
|
|
137
|
+
result.securityIssues.push("Resource content contains sensitive data patterns (credentials, keys, etc.)");
|
|
138
|
+
result.sensitiveDataExposed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
result.error = this.extractErrorMessage(error);
|
|
143
|
+
result.accessible = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
result.tested = false;
|
|
148
|
+
result.error = "readResource function not provided - skipping read test";
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
async testResourceTemplate(template, context) {
|
|
153
|
+
const results = [];
|
|
154
|
+
// Test the template itself
|
|
155
|
+
const templateResult = {
|
|
156
|
+
resourceUri: template.uriTemplate,
|
|
157
|
+
resourceName: template.name,
|
|
158
|
+
mimeType: template.mimeType,
|
|
159
|
+
tested: true,
|
|
160
|
+
accessible: false,
|
|
161
|
+
securityIssues: [],
|
|
162
|
+
pathTraversalVulnerable: false,
|
|
163
|
+
sensitiveDataExposed: false,
|
|
164
|
+
validUri: this.isValidUriTemplate(template.uriTemplate),
|
|
165
|
+
};
|
|
166
|
+
// Check template for sensitive patterns
|
|
167
|
+
if (this.isSensitiveUri(template.uriTemplate)) {
|
|
168
|
+
templateResult.securityIssues.push(`Resource template matches sensitive file pattern: ${template.uriTemplate}`);
|
|
169
|
+
templateResult.sensitiveDataExposed = true;
|
|
170
|
+
}
|
|
171
|
+
results.push(templateResult);
|
|
172
|
+
// Test path traversal vulnerabilities if readResource is available
|
|
173
|
+
if (context.readResource) {
|
|
174
|
+
for (const payload of PATH_TRAVERSAL_PAYLOADS) {
|
|
175
|
+
this.testCount++;
|
|
176
|
+
const testUri = this.injectPayloadIntoTemplate(template.uriTemplate, payload);
|
|
177
|
+
const traversalResult = {
|
|
178
|
+
resourceUri: testUri,
|
|
179
|
+
resourceName: `${template.name} (path traversal test)`,
|
|
180
|
+
tested: true,
|
|
181
|
+
accessible: false,
|
|
182
|
+
securityIssues: [],
|
|
183
|
+
pathTraversalVulnerable: false,
|
|
184
|
+
sensitiveDataExposed: false,
|
|
185
|
+
validUri: false,
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
const content = await this.executeWithTimeout(context.readResource(testUri), 3000);
|
|
189
|
+
// If we got content with a path traversal payload, it's vulnerable
|
|
190
|
+
if (content &&
|
|
191
|
+
(content.includes("root:") || content.includes("[fonts]"))) {
|
|
192
|
+
traversalResult.pathTraversalVulnerable = true;
|
|
193
|
+
traversalResult.accessible = true;
|
|
194
|
+
traversalResult.securityIssues.push(`Path traversal vulnerability: successfully accessed ${testUri}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Expected - path traversal should be rejected
|
|
199
|
+
traversalResult.accessible = false;
|
|
200
|
+
}
|
|
201
|
+
results.push(traversalResult);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
isValidUri(uri) {
|
|
207
|
+
try {
|
|
208
|
+
// Check for common URI schemes
|
|
209
|
+
if (uri.startsWith("file://") ||
|
|
210
|
+
uri.startsWith("http://") ||
|
|
211
|
+
uri.startsWith("https://") ||
|
|
212
|
+
uri.startsWith("resource://") ||
|
|
213
|
+
uri.match(/^[a-z][a-z0-9+.-]*:/i)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// Allow relative paths
|
|
217
|
+
return !uri.includes("..") || uri.startsWith("/");
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
isValidUriTemplate(template) {
|
|
224
|
+
// URI templates can contain {variable} placeholders
|
|
225
|
+
const withoutPlaceholders = template.replace(/\{[^}]+\}/g, "placeholder");
|
|
226
|
+
return this.isValidUri(withoutPlaceholders);
|
|
227
|
+
}
|
|
228
|
+
isSensitiveUri(uri) {
|
|
229
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(uri));
|
|
230
|
+
}
|
|
231
|
+
containsSensitiveContent(content) {
|
|
232
|
+
return SENSITIVE_CONTENT_PATTERNS.some((pattern) => pattern.test(content));
|
|
233
|
+
}
|
|
234
|
+
injectPayloadIntoTemplate(template, payload) {
|
|
235
|
+
// Replace template variables with payload
|
|
236
|
+
const result = template.replace(/\{[^}]+\}/g, payload);
|
|
237
|
+
// If no variables, append payload
|
|
238
|
+
if (result === template) {
|
|
239
|
+
return template + "/" + payload;
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
determineResourceStatus(pathTraversalVulnerabilities, sensitiveDataExposures, securityIssuesFound, totalResources) {
|
|
244
|
+
// Critical failures
|
|
245
|
+
if (pathTraversalVulnerabilities > 0)
|
|
246
|
+
return "FAIL";
|
|
247
|
+
if (sensitiveDataExposures > 0)
|
|
248
|
+
return "FAIL";
|
|
249
|
+
// Moderate issues
|
|
250
|
+
if (securityIssuesFound > 0)
|
|
251
|
+
return "NEED_MORE_INFO";
|
|
252
|
+
// No resources tested
|
|
253
|
+
if (totalResources === 0)
|
|
254
|
+
return "PASS";
|
|
255
|
+
return "PASS";
|
|
256
|
+
}
|
|
257
|
+
generateExplanation(results, pathTraversalVulnerabilities, sensitiveDataExposures) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
parts.push(`Tested ${results.length} resource(s).`);
|
|
260
|
+
if (pathTraversalVulnerabilities > 0) {
|
|
261
|
+
parts.push(`CRITICAL: ${pathTraversalVulnerabilities} path traversal vulnerability(ies) detected.`);
|
|
262
|
+
}
|
|
263
|
+
if (sensitiveDataExposures > 0) {
|
|
264
|
+
parts.push(`WARNING: ${sensitiveDataExposures} resource(s) may expose sensitive data.`);
|
|
265
|
+
}
|
|
266
|
+
const accessibleCount = results.filter((r) => r.accessible).length;
|
|
267
|
+
if (accessibleCount > 0) {
|
|
268
|
+
parts.push(`${accessibleCount} resource(s) are accessible.`);
|
|
269
|
+
}
|
|
270
|
+
return parts.join(" ");
|
|
271
|
+
}
|
|
272
|
+
generateRecommendations(results) {
|
|
273
|
+
const recommendations = [];
|
|
274
|
+
// Path traversal recommendations
|
|
275
|
+
const pathTraversalResults = results.filter((r) => r.pathTraversalVulnerable);
|
|
276
|
+
if (pathTraversalResults.length > 0) {
|
|
277
|
+
recommendations.push("CRITICAL: Implement path validation to prevent path traversal attacks. Normalize paths and validate against allowed directories.");
|
|
278
|
+
}
|
|
279
|
+
// Sensitive data recommendations
|
|
280
|
+
const sensitiveResults = results.filter((r) => r.sensitiveDataExposed);
|
|
281
|
+
if (sensitiveResults.length > 0) {
|
|
282
|
+
recommendations.push("Review resources for sensitive data exposure. Remove or restrict access to resources containing credentials, keys, or sensitive configuration.");
|
|
283
|
+
}
|
|
284
|
+
// Invalid URI recommendations
|
|
285
|
+
const invalidUriResults = results.filter((r) => !r.validUri);
|
|
286
|
+
if (invalidUriResults.length > 0) {
|
|
287
|
+
recommendations.push("Fix invalid resource URIs to ensure proper URI format compliance.");
|
|
288
|
+
}
|
|
289
|
+
// Inaccessible resource recommendations
|
|
290
|
+
const inaccessibleResults = results.filter((r) => r.tested && !r.accessible && !r.pathTraversalVulnerable);
|
|
291
|
+
if (inaccessibleResults.length > 0) {
|
|
292
|
+
recommendations.push(`${inaccessibleResults.length} declared resource(s) are not accessible. Verify resource paths and permissions.`);
|
|
293
|
+
}
|
|
294
|
+
return recommendations;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Security Assessor Module
|
|
3
|
-
* Tests for backend API security vulnerabilities using
|
|
4
|
-
* - Critical Injection (
|
|
3
|
+
* Tests for backend API security vulnerabilities using 18 focused patterns
|
|
4
|
+
* - Critical Injection (6): Command, Calculator, SQL, Path Traversal, XXE, NoSQL
|
|
5
5
|
* - Input Validation (3): Type Safety, Boundary Testing, Required Fields
|
|
6
6
|
* - Protocol Compliance (2): MCP Error Format, Timeout Handling
|
|
7
|
+
* - Tool-Specific (7): SSRF, Unicode Bypass, Nested Injection, Package Squatting,
|
|
8
|
+
* Data Exfiltration, Configuration Drift, Tool Shadowing
|
|
7
9
|
*/
|
|
8
10
|
import { SecurityAssessment } from "../../../lib/assessmentTypes.js";
|
|
9
11
|
import { BaseAssessor } from "./BaseAssessor.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityAssessor.d.ts","sourceRoot":"","sources":["../../../../src/services/assessment/modules/SecurityAssessor.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SecurityAssessor.d.ts","sourceRoot":"","sources":["../../../../src/services/assessment/modules/SecurityAssessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,kBAAkB,EAInB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAa9D,qBAAa,gBAAiB,SAAQ,YAAY;IAC1C,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuFrE;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkC7B;;;;OAIG;YACW,yBAAyB;IAuKvC;;;;OAIG;YACW,qBAAqB;IA2JnC;;OAEG;YACW,WAAW;IA2HzB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAkDzB;;;OAGG;IACH,OAAO,CAAC,8BAA8B;IAmDtC;;OAEG;IACH,OAAO,CAAC,aAAa;IA+BrB;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAgClC;;;OAGG;IACH,OAAO,CAAC,eAAe;IA6HvB;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAiE7B;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAqC5B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAsB3B;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAkC5B;;OAEG;YACW,+BAA+B;IAiC7C;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAYjC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IA0B/B;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAkEnC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAuI3B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,oBAAoB;IAgK5B;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA8BhC;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IA8BhC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,oBAAoB;IAoE5B;;OAEG;IACH,OAAO,CAAC,YAAY;IASpB;;;OAGG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAiB9B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;CAmB3B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Security Assessor Module
|
|
3
|
-
* Tests for backend API security vulnerabilities using
|
|
4
|
-
* - Critical Injection (
|
|
3
|
+
* Tests for backend API security vulnerabilities using 18 focused patterns
|
|
4
|
+
* - Critical Injection (6): Command, Calculator, SQL, Path Traversal, XXE, NoSQL
|
|
5
5
|
* - Input Validation (3): Type Safety, Boundary Testing, Required Fields
|
|
6
6
|
* - Protocol Compliance (2): MCP Error Format, Timeout Handling
|
|
7
|
+
* - Tool-Specific (7): SSRF, Unicode Bypass, Nested Injection, Package Squatting,
|
|
8
|
+
* Data Exfiltration, Configuration Drift, Tool Shadowing
|
|
7
9
|
*/
|
|
8
10
|
import { BaseAssessor } from "./BaseAssessor.js";
|
|
9
11
|
import { getAllAttackPatterns, getPayloadsForAttack, } from "../../../lib/securityPatterns.js";
|
|
@@ -1071,8 +1073,6 @@ export class SecurityAssessor extends BaseAssessor {
|
|
|
1071
1073
|
* Layer 2: Verify NO execution evidence (defense-in-depth)
|
|
1072
1074
|
*/
|
|
1073
1075
|
isReflectionResponse(responseText) {
|
|
1074
|
-
console.log("[DIAG] isReflectionResponse called");
|
|
1075
|
-
console.log("[DIAG] Response preview:", responseText.substring(0, 200));
|
|
1076
1076
|
// Status message patterns (NEW)
|
|
1077
1077
|
const statusPatterns = [
|
|
1078
1078
|
// "Action executed successfully: <anything>" (generic status message)
|
|
@@ -1152,18 +1152,7 @@ export class SecurityAssessor extends BaseAssessor {
|
|
|
1152
1152
|
/error:.*too (long|short|large)/i,
|
|
1153
1153
|
];
|
|
1154
1154
|
// LAYER 1: Check for reflection/status patterns
|
|
1155
|
-
const
|
|
1156
|
-
const hasReflection = reflectionPatterns.some((pattern) => {
|
|
1157
|
-
const matches = pattern.test(responseText);
|
|
1158
|
-
if (matches) {
|
|
1159
|
-
matchedPatterns.push(pattern.source.substring(0, 50));
|
|
1160
|
-
}
|
|
1161
|
-
return matches;
|
|
1162
|
-
});
|
|
1163
|
-
console.log("[DIAG] Has reflection:", hasReflection);
|
|
1164
|
-
if (matchedPatterns.length > 0) {
|
|
1165
|
-
console.log("[DIAG] Matched reflection patterns:", matchedPatterns.join(", "));
|
|
1166
|
-
}
|
|
1155
|
+
const hasReflection = reflectionPatterns.some((pattern) => pattern.test(responseText));
|
|
1167
1156
|
if (hasReflection) {
|
|
1168
1157
|
// LAYER 2: Defense-in-depth - verify NO execution evidence
|
|
1169
1158
|
// For JSON responses, check execution artifacts only in result/output fields
|
|
@@ -1187,25 +1176,18 @@ export class SecurityAssessor extends BaseAssessor {
|
|
|
1187
1176
|
? this.detectExecutionArtifacts(outputFields) // Skip result, check only output fields
|
|
1188
1177
|
: this.detectExecutionArtifacts(resultText) ||
|
|
1189
1178
|
this.detectExecutionArtifacts(outputFields);
|
|
1190
|
-
console.log("[DIAG] JSON mode - checking execution in result/output fields only");
|
|
1191
|
-
console.log("[DIAG] Has execution in output:", hasExecutionInOutput);
|
|
1192
1179
|
if (hasExecutionInOutput) {
|
|
1193
|
-
|
|
1194
|
-
return false;
|
|
1180
|
+
return false; // Reflection + Execution in output = VULNERABLE
|
|
1195
1181
|
}
|
|
1196
|
-
|
|
1197
|
-
return true;
|
|
1182
|
+
return true; // Reflection + clean output = SAFE
|
|
1198
1183
|
}
|
|
1199
1184
|
catch {
|
|
1200
1185
|
// Not JSON, check entire response for execution
|
|
1201
1186
|
const hasExecution = this.detectExecutionArtifacts(responseText);
|
|
1202
|
-
console.log("[DIAG] Text mode - Has execution artifacts:", hasExecution);
|
|
1203
1187
|
if (hasExecution) {
|
|
1204
|
-
|
|
1205
|
-
return false;
|
|
1188
|
+
return false; // Reflection + Execution = VULNERABLE
|
|
1206
1189
|
}
|
|
1207
|
-
|
|
1208
|
-
return true;
|
|
1190
|
+
return true; // Reflection only = SAFE
|
|
1209
1191
|
}
|
|
1210
1192
|
}
|
|
1211
1193
|
// JSON Structural Analysis with execution verification
|
|
@@ -1238,7 +1220,6 @@ export class SecurityAssessor extends BaseAssessor {
|
|
|
1238
1220
|
* MEDIUM confidence: Contextual patterns (root alone, paths)
|
|
1239
1221
|
*/
|
|
1240
1222
|
detectExecutionArtifacts(responseText) {
|
|
1241
|
-
console.log("[DIAG] detectExecutionArtifacts called");
|
|
1242
1223
|
const executionIndicators = [
|
|
1243
1224
|
// HIGH CONFIDENCE - System files (requires format)
|
|
1244
1225
|
/[a-z]+:x:\d+:\d+:/i, // passwd: "root:x:0:0:"
|
|
@@ -1259,19 +1240,7 @@ export class SecurityAssessor extends BaseAssessor {
|
|
|
1259
1240
|
// MEDIUM CONFIDENCE - Process info
|
|
1260
1241
|
/PID:\s*\d{3,}/i, // Process ID
|
|
1261
1242
|
];
|
|
1262
|
-
|
|
1263
|
-
const found = executionIndicators.some((pattern) => {
|
|
1264
|
-
const matches = pattern.test(responseText);
|
|
1265
|
-
if (matches) {
|
|
1266
|
-
matchedExecutionPatterns.push(pattern.source.substring(0, 50));
|
|
1267
|
-
}
|
|
1268
|
-
return matches;
|
|
1269
|
-
});
|
|
1270
|
-
if (matchedExecutionPatterns.length > 0) {
|
|
1271
|
-
console.log("[DIAG] Matched execution patterns:", matchedExecutionPatterns.join(", "));
|
|
1272
|
-
}
|
|
1273
|
-
console.log("[DIAG] Execution artifacts found:", found);
|
|
1274
|
-
return found;
|
|
1243
|
+
return executionIndicators.some((pattern) => pattern.test(responseText));
|
|
1275
1244
|
}
|
|
1276
1245
|
/**
|
|
1277
1246
|
* Analyze injection response (existing logic)
|
|
@@ -12,6 +12,7 @@ import { BaseAssessor } from "./BaseAssessor.js";
|
|
|
12
12
|
export declare class TemporalAssessor extends BaseAssessor {
|
|
13
13
|
private invocationsPerTool;
|
|
14
14
|
private readonly DESTRUCTIVE_PATTERNS;
|
|
15
|
+
private readonly PER_INVOCATION_TIMEOUT;
|
|
15
16
|
constructor(config: AssessmentConfiguration);
|
|
16
17
|
assess(context: AssessmentContext): Promise<TemporalAssessment>;
|
|
17
18
|
private assessTool;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemporalAssessor.d.ts","sourceRoot":"","sources":["../../../../src/services/assessment/modules/TemporalAssessor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,uBAAuB,EAEvB,kBAAkB,EAEnB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"TemporalAssessor.d.ts","sourceRoot":"","sources":["../../../../src/services/assessment/modules/TemporalAssessor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,uBAAuB,EAEvB,kBAAkB,EAEnB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAY9C,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,OAAO,CAAC,kBAAkB,CAAS;IAGnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAoBnC;IAGF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAU;gBAErC,MAAM,EAAE,uBAAuB;IAKrC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;YA8CvD,UAAU;IAkExB,OAAO,CAAC,gBAAgB;IA0DxB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAoDzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,uBAAuB;IAa/B,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,uBAAuB;CA8BhC"}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* payloads but never call the same tool repeatedly with identical payloads.
|
|
8
8
|
*/
|
|
9
9
|
import { BaseAssessor } from "./BaseAssessor.js";
|
|
10
|
+
// Security: Maximum response size to prevent memory exhaustion attacks
|
|
11
|
+
const MAX_RESPONSE_SIZE = 1_000_000; // 1MB
|
|
10
12
|
export class TemporalAssessor extends BaseAssessor {
|
|
11
13
|
invocationsPerTool;
|
|
12
14
|
// Patterns that suggest a tool may have side effects
|
|
@@ -23,7 +25,16 @@ export class TemporalAssessor extends BaseAssessor {
|
|
|
23
25
|
"submit",
|
|
24
26
|
"execute",
|
|
25
27
|
"run",
|
|
28
|
+
// P2-3: Additional destructive patterns
|
|
29
|
+
"drop",
|
|
30
|
+
"truncate",
|
|
31
|
+
"clear",
|
|
32
|
+
"purge",
|
|
33
|
+
"destroy",
|
|
34
|
+
"reset",
|
|
26
35
|
];
|
|
36
|
+
// P2-2: Per-invocation timeout to prevent long-running tools from blocking
|
|
37
|
+
PER_INVOCATION_TIMEOUT = 10_000; // 10 seconds
|
|
27
38
|
constructor(config) {
|
|
28
39
|
super(config);
|
|
29
40
|
this.invocationsPerTool = config.temporalInvocations ?? 25;
|
|
@@ -72,7 +83,19 @@ export class TemporalAssessor extends BaseAssessor {
|
|
|
72
83
|
for (let i = 1; i <= invocations; i++) {
|
|
73
84
|
this.testCount++;
|
|
74
85
|
try {
|
|
75
|
-
|
|
86
|
+
// P2-2: Use shorter per-invocation timeout (10s vs default 30s)
|
|
87
|
+
const response = await this.executeWithTimeout(context.callTool(tool.name, payload), this.PER_INVOCATION_TIMEOUT);
|
|
88
|
+
// Security: Prevent memory exhaustion from large responses
|
|
89
|
+
const responseSize = JSON.stringify(response).length;
|
|
90
|
+
if (responseSize > MAX_RESPONSE_SIZE) {
|
|
91
|
+
responses.push({
|
|
92
|
+
invocation: i,
|
|
93
|
+
response: null,
|
|
94
|
+
error: `Response exceeded size limit (${responseSize} > ${MAX_RESPONSE_SIZE} bytes)`,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
76
99
|
responses.push({
|
|
77
100
|
invocation: i,
|
|
78
101
|
response,
|
|
@@ -88,6 +111,10 @@ export class TemporalAssessor extends BaseAssessor {
|
|
|
88
111
|
timestamp: Date.now(),
|
|
89
112
|
});
|
|
90
113
|
}
|
|
114
|
+
// P2-4: Small delay between invocations to prevent rate limiting false positives
|
|
115
|
+
if (i < invocations) {
|
|
116
|
+
await this.sleep(50);
|
|
117
|
+
}
|
|
91
118
|
}
|
|
92
119
|
const result = this.analyzeResponses(tool, responses);
|
|
93
120
|
return {
|
|
@@ -191,8 +218,8 @@ export class TemporalAssessor extends BaseAssessor {
|
|
|
191
218
|
normalizeResponse(response) {
|
|
192
219
|
const str = JSON.stringify(response);
|
|
193
220
|
return (str
|
|
194
|
-
// ISO timestamps
|
|
195
|
-
.replace(/"\d{4}-\d{2}-\d{2}T[\d:.]
|
|
221
|
+
// ISO timestamps (bounded quantifier to prevent ReDoS)
|
|
222
|
+
.replace(/"\d{4}-\d{2}-\d{2}T[\d:.]{1,30}Z?"/g, '"<TIMESTAMP>"')
|
|
196
223
|
// Unix timestamps (13 digits)
|
|
197
224
|
.replace(/"\d{13}"/g, '"<TIMESTAMP>"')
|
|
198
225
|
// UUIDs
|
|
@@ -218,7 +245,11 @@ export class TemporalAssessor extends BaseAssessor {
|
|
|
218
245
|
.replace(/"index":\s*\d+/g, '"index": <NUMBER>')
|
|
219
246
|
.replace(/\\"index\\":\s*\d+/g, '\\"index\\": <NUMBER>')
|
|
220
247
|
// String IDs
|
|
221
|
-
.replace(/"id":\s*"[^"]+"/g, '"id": "<ID>"')
|
|
248
|
+
.replace(/"id":\s*"[^"]+"/g, '"id": "<ID>"')
|
|
249
|
+
// P2-1: Additional timestamp fields that vary between calls
|
|
250
|
+
.replace(/"(updated_at|created_at|modified_at)":\s*"[^"]+"/g, '"$1": "<TIMESTAMP>"')
|
|
251
|
+
// P2-1: Dynamic tokens/hashes that change per request
|
|
252
|
+
.replace(/"(nonce|token|hash|etag|session_id|correlation_id)":\s*"[^"]+"/g, '"$1": "<DYNAMIC>"'));
|
|
222
253
|
}
|
|
223
254
|
/**
|
|
224
255
|
* Detect if a tool may have side effects based on naming patterns.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type JsonValue = string | number | boolean | null | undefined | JsonValue[] | {
|
|
2
|
+
[key: string]: JsonValue;
|
|
3
|
+
};
|
|
4
|
+
export type JsonSchemaConst = {
|
|
5
|
+
const: JsonValue;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
export type JsonSchemaType = {
|
|
10
|
+
type?: "string" | "number" | "integer" | "boolean" | "array" | "object" | "null" | ("string" | "number" | "integer" | "boolean" | "array" | "object" | "null")[];
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
required?: string[];
|
|
14
|
+
default?: JsonValue;
|
|
15
|
+
properties?: Record<string, JsonSchemaType>;
|
|
16
|
+
items?: JsonSchemaType;
|
|
17
|
+
minItems?: number;
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
minimum?: number;
|
|
20
|
+
maximum?: number;
|
|
21
|
+
minLength?: number;
|
|
22
|
+
maxLength?: number;
|
|
23
|
+
nullable?: boolean;
|
|
24
|
+
pattern?: string;
|
|
25
|
+
format?: string;
|
|
26
|
+
enum?: string[];
|
|
27
|
+
enumNames?: string[];
|
|
28
|
+
const?: JsonValue;
|
|
29
|
+
oneOf?: (JsonSchemaType | JsonSchemaConst)[];
|
|
30
|
+
anyOf?: (JsonSchemaType | JsonSchemaConst)[];
|
|
31
|
+
$ref?: string;
|
|
32
|
+
};
|
|
33
|
+
export type JsonObject = {
|
|
34
|
+
[key: string]: JsonValue;
|
|
35
|
+
};
|
|
36
|
+
export type DataType = "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | "array" | "null";
|
|
37
|
+
/**
|
|
38
|
+
* Determines the specific data type of a JSON value
|
|
39
|
+
* @param value The JSON value to analyze
|
|
40
|
+
* @returns The specific data type including "array" and "null" as distinct types
|
|
41
|
+
*/
|
|
42
|
+
export declare function getDataType(value: JsonValue): DataType;
|
|
43
|
+
/**
|
|
44
|
+
* Attempts to parse a string as JSON, only for objects and arrays
|
|
45
|
+
* @param str The string to parse
|
|
46
|
+
* @returns Object with success boolean and either parsed data or original string
|
|
47
|
+
*/
|
|
48
|
+
export declare function tryParseJson(str: string): {
|
|
49
|
+
success: boolean;
|
|
50
|
+
data: JsonValue;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Updates a value at a specific path in a nested JSON structure
|
|
54
|
+
* @param obj The original JSON value
|
|
55
|
+
* @param path Array of keys/indices representing the path to the value
|
|
56
|
+
* @param value The new value to set
|
|
57
|
+
* @returns A new JSON value with the updated path
|
|
58
|
+
*/
|
|
59
|
+
export declare function updateValueAtPath(obj: JsonValue, path: string[], value: JsonValue): JsonValue;
|
|
60
|
+
/**
|
|
61
|
+
* Gets a value at a specific path in a nested JSON structure
|
|
62
|
+
* @param obj The JSON value to traverse
|
|
63
|
+
* @param path Array of keys/indices representing the path to the value
|
|
64
|
+
* @param defaultValue Value to return if path doesn't exist
|
|
65
|
+
* @returns The value at the path, or defaultValue if not found
|
|
66
|
+
*/
|
|
67
|
+
export declare function getValueAtPath(obj: JsonValue, path: string[], defaultValue?: JsonValue): JsonValue;
|
|
68
|
+
//# sourceMappingURL=jsonUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonUtils.d.ts","sourceRoot":"","sources":["../../src/utils/jsonUtils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EACD,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,OAAO,GACP,QAAQ,GACR,MAAM,GACN,CACI,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,OAAO,GACP,QAAQ,GACR,MAAM,CACT,EAAE,CAAC;IACR,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC5C,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAEhB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,cAAc,GAAG,eAAe,CAAC,EAAE,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,cAAc,GAAG,eAAe,CAAC,EAAE,CAAC;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAChB,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,UAAU,GACV,OAAO,GACP,MAAM,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAItD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;CACjB,CAcA;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,SAAS,GACf,SAAS,CAkBX;AA+ED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EAAE,EACd,YAAY,GAAE,SAAgB,GAC7B,SAAS,CAyBX"}
|