@bryan-thompson/inspector-assessment-client 1.26.4 → 1.26.6
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-DRmaIku9.js → OAuthCallback-CCWVtjr7.js} +1 -1
- package/dist/assets/{OAuthDebugCallback-BU8UZdx8.js → OAuthDebugCallback-DqbXfUi4.js} +1 -1
- package/dist/assets/{index-Dd4pL57l.js → index-CsDJSSWq.js} +4 -4
- package/dist/index.html +1 -1
- package/lib/lib/securityPatterns.d.ts.map +1 -1
- package/lib/lib/securityPatterns.js +26 -0
- package/lib/services/assessment/modules/securityTests/ConfidenceScorer.d.ts +57 -0
- package/lib/services/assessment/modules/securityTests/ConfidenceScorer.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ConfidenceScorer.js +199 -0
- package/lib/services/assessment/modules/securityTests/ErrorClassifier.d.ts +57 -0
- package/lib/services/assessment/modules/securityTests/ErrorClassifier.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ErrorClassifier.js +113 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts +49 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.js +74 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts +58 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.js +251 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts +59 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.js +151 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts +229 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.js +566 -0
- package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.js +49 -1
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts +63 -85
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.js +270 -1159
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Analyzer
|
|
3
|
+
* Detects computed math expression results (Calculator Injection)
|
|
4
|
+
*
|
|
5
|
+
* Extracted from SecurityResponseAnalyzer.ts (Issue #53)
|
|
6
|
+
* Handles: math computation detection, coincidental numeric detection
|
|
7
|
+
*/
|
|
8
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Result of computed math analysis with confidence level (Issue #58)
|
|
11
|
+
*/
|
|
12
|
+
export interface MathResultAnalysis {
|
|
13
|
+
isComputed: boolean;
|
|
14
|
+
confidence: "high" | "medium" | "low";
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Analyzes tool responses for math computation evidence (Calculator Injection)
|
|
19
|
+
*/
|
|
20
|
+
export declare class MathAnalyzer {
|
|
21
|
+
/**
|
|
22
|
+
* Enhanced computed math result analysis with tool context (Issue #58)
|
|
23
|
+
*
|
|
24
|
+
* Returns a confidence level indicating how likely this is a real Calculator Injection:
|
|
25
|
+
* - high: Strong evidence of computation (should flag as vulnerable)
|
|
26
|
+
* - medium: Ambiguous (excluded from vulnerability count per user decision)
|
|
27
|
+
* - low: Likely coincidental data (excluded from vulnerability count)
|
|
28
|
+
*/
|
|
29
|
+
analyzeComputedMathResult(payload: string, responseText: string, tool?: Tool): MathResultAnalysis;
|
|
30
|
+
/**
|
|
31
|
+
* Legacy method for backward compatibility
|
|
32
|
+
* @deprecated Use analyzeComputedMathResult instead
|
|
33
|
+
*/
|
|
34
|
+
isComputedMathResult(payload: string, responseText: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Check if numeric value appears in structured data context (not as computation result)
|
|
37
|
+
* Distinguishes {"records": 4} from computed "4" (Issue #58)
|
|
38
|
+
*
|
|
39
|
+
* @param result The computed numeric result to check for
|
|
40
|
+
* @param responseText The response text to analyze
|
|
41
|
+
* @returns true if the number appears to be coincidental data, not a computed result
|
|
42
|
+
*/
|
|
43
|
+
isCoincidentalNumericInStructuredData(result: number, responseText: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Recursively check JSON object for coincidental numeric values
|
|
46
|
+
*/
|
|
47
|
+
private checkObjectForCoincidentalNumeric;
|
|
48
|
+
/**
|
|
49
|
+
* Check text for structured patterns containing coincidental numerics
|
|
50
|
+
*/
|
|
51
|
+
private checkTextForCoincidentalNumeric;
|
|
52
|
+
/**
|
|
53
|
+
* Compute math expression from regex match
|
|
54
|
+
* Returns null if invalid operator
|
|
55
|
+
*/
|
|
56
|
+
private computeExpression;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=MathAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MathAnalyzer.d.ts","sourceRoot":"","sources":["../../../../../src/services/assessment/modules/securityTests/MathAnalyzer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAW1D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;OAOG;IACH,yBAAyB,CACvB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,GACV,kBAAkB;IAkIrB;;;OAGG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAKpE;;;;;;;OAOG;IACH,qCAAqC,CACnC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,OAAO;IAWV;;OAEG;IACH,OAAO,CAAC,iCAAiC;IAyCzC;;OAEG;IACH,OAAO,CAAC,+BAA+B;IAqBvC;;;OAGG;IACH,OAAO,CAAC,iBAAiB;CA+C1B"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Analyzer
|
|
3
|
+
* Detects computed math expression results (Calculator Injection)
|
|
4
|
+
*
|
|
5
|
+
* Extracted from SecurityResponseAnalyzer.ts (Issue #53)
|
|
6
|
+
* Handles: math computation detection, coincidental numeric detection
|
|
7
|
+
*/
|
|
8
|
+
import { ToolClassifier, ToolCategory } from "../../ToolClassifier.js";
|
|
9
|
+
import { SIMPLE_MATH_PATTERN, COMPUTATIONAL_INDICATORS, STRUCTURED_DATA_FIELD_NAMES, READ_ONLY_TOOL_NAME_PATTERN, isHttpError, matchesAny, } from "./SecurityPatternLibrary.js";
|
|
10
|
+
/**
|
|
11
|
+
* Analyzes tool responses for math computation evidence (Calculator Injection)
|
|
12
|
+
*/
|
|
13
|
+
export class MathAnalyzer {
|
|
14
|
+
/**
|
|
15
|
+
* Enhanced computed math result analysis with tool context (Issue #58)
|
|
16
|
+
*
|
|
17
|
+
* Returns a confidence level indicating how likely this is a real Calculator Injection:
|
|
18
|
+
* - high: Strong evidence of computation (should flag as vulnerable)
|
|
19
|
+
* - medium: Ambiguous (excluded from vulnerability count per user decision)
|
|
20
|
+
* - low: Likely coincidental data (excluded from vulnerability count)
|
|
21
|
+
*/
|
|
22
|
+
analyzeComputedMathResult(payload, responseText, tool) {
|
|
23
|
+
// Skip HTTP error responses
|
|
24
|
+
if (isHttpError(responseText)) {
|
|
25
|
+
return {
|
|
26
|
+
isComputed: false,
|
|
27
|
+
confidence: "high",
|
|
28
|
+
reason: "HTTP error response",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Parse math expression
|
|
32
|
+
const match = payload.match(SIMPLE_MATH_PATTERN);
|
|
33
|
+
if (!match) {
|
|
34
|
+
return {
|
|
35
|
+
isComputed: false,
|
|
36
|
+
confidence: "high",
|
|
37
|
+
reason: "Not a math expression",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const result = this.computeExpression(match);
|
|
42
|
+
if (result === null) {
|
|
43
|
+
return {
|
|
44
|
+
isComputed: false,
|
|
45
|
+
confidence: "high",
|
|
46
|
+
reason: "Invalid operator",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const resultStr = result.toString();
|
|
50
|
+
const hasComputedResult = responseText.includes(resultStr);
|
|
51
|
+
const normalizedPayload = payload.replace(/\s+/g, "");
|
|
52
|
+
const hasOriginalExpression = responseText.includes(payload) ||
|
|
53
|
+
responseText.includes(normalizedPayload);
|
|
54
|
+
// Basic detection: result present without original expression
|
|
55
|
+
const basicDetection = hasComputedResult && !hasOriginalExpression;
|
|
56
|
+
if (!basicDetection) {
|
|
57
|
+
return {
|
|
58
|
+
isComputed: false,
|
|
59
|
+
confidence: "high",
|
|
60
|
+
reason: "No computed result found",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Layer 1: Check if numeric appears in structured data context (Issue #58)
|
|
64
|
+
if (this.isCoincidentalNumericInStructuredData(result, responseText)) {
|
|
65
|
+
return {
|
|
66
|
+
isComputed: false,
|
|
67
|
+
confidence: "low",
|
|
68
|
+
reason: "Numeric value appears in structured data field (e.g., count, records)",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Layer 2: Tool classification heuristics (Issue #58)
|
|
72
|
+
if (tool) {
|
|
73
|
+
const classifier = new ToolClassifier();
|
|
74
|
+
const classification = classifier.classify(tool.name, tool.description);
|
|
75
|
+
// Check for read-only/data fetcher categories
|
|
76
|
+
if (classification.categories.includes(ToolCategory.DATA_FETCHER) ||
|
|
77
|
+
classification.categories.includes(ToolCategory.API_WRAPPER) ||
|
|
78
|
+
classification.categories.includes(ToolCategory.SEARCH_RETRIEVAL)) {
|
|
79
|
+
return {
|
|
80
|
+
isComputed: false,
|
|
81
|
+
confidence: "low",
|
|
82
|
+
reason: `Tool classified as ${classification.categories[0]} - unlikely to compute math`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Check for "get_", "list_", "fetch_" patterns in tool name
|
|
86
|
+
if (READ_ONLY_TOOL_NAME_PATTERN.test(tool.name)) {
|
|
87
|
+
return {
|
|
88
|
+
isComputed: false,
|
|
89
|
+
confidence: "low",
|
|
90
|
+
reason: "Tool name indicates read-only operation",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Layer 3: Check for computational language in response
|
|
95
|
+
const hasComputationalContext = matchesAny(COMPUTATIONAL_INDICATORS, responseText);
|
|
96
|
+
if (hasComputationalContext) {
|
|
97
|
+
return {
|
|
98
|
+
isComputed: true,
|
|
99
|
+
confidence: "high",
|
|
100
|
+
reason: "Response contains computational language",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Layer 4: Longer responses without computational language are likely data
|
|
104
|
+
if (responseText.length > 50) {
|
|
105
|
+
return {
|
|
106
|
+
isComputed: false,
|
|
107
|
+
confidence: "medium",
|
|
108
|
+
reason: "Response lacks computational language, likely coincidental data",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Short response with just the number - this is suspicious
|
|
112
|
+
if (responseText.trim() === resultStr) {
|
|
113
|
+
return {
|
|
114
|
+
isComputed: true,
|
|
115
|
+
confidence: "high",
|
|
116
|
+
reason: "Response is exactly the computed result",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Default: medium confidence (excluded per user decision)
|
|
120
|
+
return {
|
|
121
|
+
isComputed: false,
|
|
122
|
+
confidence: "medium",
|
|
123
|
+
reason: "Ambiguous - numeric match without computational context",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { isComputed: false, confidence: "high", reason: "Parse error" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Legacy method for backward compatibility
|
|
132
|
+
* @deprecated Use analyzeComputedMathResult instead
|
|
133
|
+
*/
|
|
134
|
+
isComputedMathResult(payload, responseText) {
|
|
135
|
+
const analysis = this.analyzeComputedMathResult(payload, responseText);
|
|
136
|
+
return analysis.isComputed && analysis.confidence === "high";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if numeric value appears in structured data context (not as computation result)
|
|
140
|
+
* Distinguishes {"records": 4} from computed "4" (Issue #58)
|
|
141
|
+
*
|
|
142
|
+
* @param result The computed numeric result to check for
|
|
143
|
+
* @param responseText The response text to analyze
|
|
144
|
+
* @returns true if the number appears to be coincidental data, not a computed result
|
|
145
|
+
*/
|
|
146
|
+
isCoincidentalNumericInStructuredData(result, responseText) {
|
|
147
|
+
// Try to parse as JSON
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(responseText);
|
|
150
|
+
return this.checkObjectForCoincidentalNumeric(parsed, result);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Not JSON - check for structured text patterns
|
|
154
|
+
return this.checkTextForCoincidentalNumeric(result, responseText);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Recursively check JSON object for coincidental numeric values
|
|
159
|
+
*/
|
|
160
|
+
checkObjectForCoincidentalNumeric(obj, result, depth = 0) {
|
|
161
|
+
if (depth > 5)
|
|
162
|
+
return false; // Prevent deep recursion
|
|
163
|
+
if (typeof obj !== "object" || obj === null)
|
|
164
|
+
return false;
|
|
165
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
166
|
+
// Check if numeric value matches result and key is a data field
|
|
167
|
+
if (value === result) {
|
|
168
|
+
const keyLower = key.toLowerCase();
|
|
169
|
+
if (STRUCTURED_DATA_FIELD_NAMES.some((pattern) => keyLower.includes(pattern))) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Recurse into nested objects
|
|
174
|
+
if (typeof value === "object" && value !== null) {
|
|
175
|
+
if (this.checkObjectForCoincidentalNumeric(value, result, depth + 1)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Check arrays
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
for (const item of value) {
|
|
182
|
+
if (typeof item === "object" &&
|
|
183
|
+
this.checkObjectForCoincidentalNumeric(item, result, depth + 1)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check text for structured patterns containing coincidental numerics
|
|
193
|
+
*/
|
|
194
|
+
checkTextForCoincidentalNumeric(result, responseText) {
|
|
195
|
+
const structuredPatterns = [
|
|
196
|
+
new RegExp(`(records|count|total|page|items|results|employees|entries|rows)[:\\s]+${result}\\b`, "i"),
|
|
197
|
+
new RegExp(`\\b${result}\\s+(records|items|results|entries|employees|rows)\\b`, "i"),
|
|
198
|
+
new RegExp(`page\\s+\\d+\\s+of\\s+${result}\\b`, "i"),
|
|
199
|
+
new RegExp(`total[:\\s]+${result}\\b`, "i"),
|
|
200
|
+
new RegExp(`found\\s+${result}\\s+(results|items|entries)`, "i"),
|
|
201
|
+
];
|
|
202
|
+
return structuredPatterns.some((pattern) => pattern.test(responseText));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Compute math expression from regex match
|
|
206
|
+
* Returns null if invalid operator
|
|
207
|
+
*/
|
|
208
|
+
computeExpression(match) {
|
|
209
|
+
const num1 = parseInt(match[1], 10);
|
|
210
|
+
const op1 = match[2];
|
|
211
|
+
const num2 = parseInt(match[3], 10);
|
|
212
|
+
const op2 = match[4];
|
|
213
|
+
const num3 = match[5] ? parseInt(match[5], 10) : undefined;
|
|
214
|
+
let result;
|
|
215
|
+
switch (op1) {
|
|
216
|
+
case "+":
|
|
217
|
+
result = num1 + num2;
|
|
218
|
+
break;
|
|
219
|
+
case "-":
|
|
220
|
+
result = num1 - num2;
|
|
221
|
+
break;
|
|
222
|
+
case "*":
|
|
223
|
+
result = num1 * num2;
|
|
224
|
+
break;
|
|
225
|
+
case "/":
|
|
226
|
+
result = Math.floor(num1 / num2);
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (op2 && num3 !== undefined) {
|
|
232
|
+
switch (op2) {
|
|
233
|
+
case "+":
|
|
234
|
+
result = result + num3;
|
|
235
|
+
break;
|
|
236
|
+
case "-":
|
|
237
|
+
result = result - num3;
|
|
238
|
+
break;
|
|
239
|
+
case "*":
|
|
240
|
+
result = result * num3;
|
|
241
|
+
break;
|
|
242
|
+
case "/":
|
|
243
|
+
result = Math.floor(result / num3);
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Response Detector
|
|
3
|
+
* Detects safe (non-vulnerable) response patterns
|
|
4
|
+
*
|
|
5
|
+
* Extracted from SecurityResponseAnalyzer.ts (Issue #53)
|
|
6
|
+
* Handles: MCP validation, HTTP errors, reflection detection, validation rejection
|
|
7
|
+
*/
|
|
8
|
+
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Error info extracted from response
|
|
11
|
+
*/
|
|
12
|
+
export interface ErrorInfo {
|
|
13
|
+
code?: string | number;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Result of safe response check
|
|
18
|
+
*/
|
|
19
|
+
export interface SafeResponseResult {
|
|
20
|
+
isSafe: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Detects safe response patterns indicating proper tool behavior
|
|
25
|
+
*/
|
|
26
|
+
export declare class SafeResponseDetector {
|
|
27
|
+
private executionDetector;
|
|
28
|
+
constructor();
|
|
29
|
+
/**
|
|
30
|
+
* Check if response is an MCP validation error (safe rejection)
|
|
31
|
+
*/
|
|
32
|
+
isMCPValidationError(errorInfo: ErrorInfo, responseText: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if response is an HTTP error (Issue #26)
|
|
35
|
+
*/
|
|
36
|
+
isHttpErrorResponse(responseText: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Check if response is just reflection (safe)
|
|
39
|
+
* Two-layer defense: Match reflection patterns, verify NO execution evidence
|
|
40
|
+
*/
|
|
41
|
+
isReflectionResponse(responseText: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Check if response is returning search results
|
|
44
|
+
*/
|
|
45
|
+
isSearchResultResponse(responseText: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Check if response is from a creation/modification operation
|
|
48
|
+
*/
|
|
49
|
+
isCreationResponse(responseText: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Check if tool explicitly rejected input with validation error (SAFE)
|
|
52
|
+
*/
|
|
53
|
+
isValidationRejection(response: CompatibilityCallToolResult): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Extract response content from MCP response
|
|
56
|
+
*/
|
|
57
|
+
extractResponseContent(response: CompatibilityCallToolResult): string;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=SafeResponseDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SafeResponseDetector.d.ts","sourceRoot":"","sources":["../../../../../src/services/assessment/modules/securityTests/SafeResponseDetector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AAcjF;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,iBAAiB,CAA4B;;IAMrD;;OAEG;IACH,oBAAoB,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAQzE;;OAEG;IACH,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIlD;;;OAGG;IACH,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAkEnD;;OAEG;IACH,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIrD;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIjD;;OAEG;IACH,qBAAqB,CAAC,QAAQ,EAAE,2BAA2B,GAAG,OAAO;IA0CrE;;OAEG;IACH,sBAAsB,CAAC,QAAQ,EAAE,2BAA2B,GAAG,MAAM;CAUtE"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Response Detector
|
|
3
|
+
* Detects safe (non-vulnerable) response patterns
|
|
4
|
+
*
|
|
5
|
+
* Extracted from SecurityResponseAnalyzer.ts (Issue #53)
|
|
6
|
+
* Handles: MCP validation, HTTP errors, reflection detection, validation rejection
|
|
7
|
+
*/
|
|
8
|
+
import { VALIDATION_ERROR_PATTERNS, STATUS_PATTERNS, REFLECTION_PATTERNS, SEARCH_RESULT_PATTERNS, CREATION_PATTERNS, TEXT_REJECTION_PATTERNS, RESULT_REJECTION_PATTERNS, isHttpError, matchesAny, } from "./SecurityPatternLibrary.js";
|
|
9
|
+
import { ExecutionArtifactDetector } from "./ExecutionArtifactDetector.js";
|
|
10
|
+
/**
|
|
11
|
+
* Detects safe response patterns indicating proper tool behavior
|
|
12
|
+
*/
|
|
13
|
+
export class SafeResponseDetector {
|
|
14
|
+
executionDetector;
|
|
15
|
+
constructor() {
|
|
16
|
+
this.executionDetector = new ExecutionArtifactDetector();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if response is an MCP validation error (safe rejection)
|
|
20
|
+
*/
|
|
21
|
+
isMCPValidationError(errorInfo, responseText) {
|
|
22
|
+
if (errorInfo.code === -32602 || errorInfo.code === "-32602") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return matchesAny(VALIDATION_ERROR_PATTERNS, responseText);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if response is an HTTP error (Issue #26)
|
|
29
|
+
*/
|
|
30
|
+
isHttpErrorResponse(responseText) {
|
|
31
|
+
return isHttpError(responseText);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if response is just reflection (safe)
|
|
35
|
+
* Two-layer defense: Match reflection patterns, verify NO execution evidence
|
|
36
|
+
*/
|
|
37
|
+
isReflectionResponse(responseText) {
|
|
38
|
+
// Combine status patterns and reflection patterns
|
|
39
|
+
const allReflectionPatterns = [...STATUS_PATTERNS, ...REFLECTION_PATTERNS];
|
|
40
|
+
const hasReflection = matchesAny(allReflectionPatterns, responseText);
|
|
41
|
+
if (hasReflection) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(responseText);
|
|
44
|
+
const resultText = String(parsed.result || "");
|
|
45
|
+
const outputFields = [
|
|
46
|
+
parsed.stdout,
|
|
47
|
+
parsed.stderr,
|
|
48
|
+
parsed.output,
|
|
49
|
+
parsed.contents,
|
|
50
|
+
parsed.execution_log,
|
|
51
|
+
parsed.command_output,
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(" ");
|
|
55
|
+
const resultIsStatusOnly = matchesAny(STATUS_PATTERNS, resultText);
|
|
56
|
+
const hasExecutionInOutput = resultIsStatusOnly
|
|
57
|
+
? this.executionDetector.detectExecutionArtifacts(outputFields)
|
|
58
|
+
: this.executionDetector.detectExecutionArtifacts(resultText) ||
|
|
59
|
+
this.executionDetector.detectExecutionArtifacts(outputFields);
|
|
60
|
+
if (hasExecutionInOutput) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
const hasExecution = this.executionDetector.detectExecutionArtifacts(responseText);
|
|
67
|
+
if (hasExecution) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Check for JSON status patterns
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(responseText);
|
|
76
|
+
if (parsed.action === "test" || parsed.action === "placeholder") {
|
|
77
|
+
const resultText = String(parsed.result || "");
|
|
78
|
+
if (!this.executionDetector.detectExecutionArtifacts(resultText)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (parsed.status && /(completed|success|ok|done)/.test(parsed.status)) {
|
|
83
|
+
if (!this.executionDetector.detectExecutionArtifacts(responseText)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not JSON
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if response is returning search results
|
|
95
|
+
*/
|
|
96
|
+
isSearchResultResponse(responseText) {
|
|
97
|
+
return matchesAny(SEARCH_RESULT_PATTERNS, responseText);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if response is from a creation/modification operation
|
|
101
|
+
*/
|
|
102
|
+
isCreationResponse(responseText) {
|
|
103
|
+
return matchesAny(CREATION_PATTERNS, responseText);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if tool explicitly rejected input with validation error (SAFE)
|
|
107
|
+
*/
|
|
108
|
+
isValidationRejection(response) {
|
|
109
|
+
const responseText = this.extractResponseContent(response);
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(responseText);
|
|
112
|
+
if (parsed.valid === false ||
|
|
113
|
+
parsed.error === true ||
|
|
114
|
+
parsed.error === "true" ||
|
|
115
|
+
(parsed.error && parsed.error !== false) ||
|
|
116
|
+
parsed.status === "rejected" ||
|
|
117
|
+
parsed.status === "invalid" ||
|
|
118
|
+
parsed.status === "failed") {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (parsed.errors &&
|
|
122
|
+
Array.isArray(parsed.errors) &&
|
|
123
|
+
parsed.errors.length > 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (parsed.error && typeof parsed.error === "string") {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (typeof parsed.result === "string") {
|
|
130
|
+
if (matchesAny(RESULT_REJECTION_PATTERNS, parsed.result)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Not JSON, check text patterns
|
|
137
|
+
}
|
|
138
|
+
return matchesAny(TEXT_REJECTION_PATTERNS, responseText);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract response content from MCP response
|
|
142
|
+
*/
|
|
143
|
+
extractResponseContent(response) {
|
|
144
|
+
if (response.content && Array.isArray(response.content)) {
|
|
145
|
+
return response.content
|
|
146
|
+
.map((c) => c.type === "text" ? c.text : "")
|
|
147
|
+
.join(" ");
|
|
148
|
+
}
|
|
149
|
+
return String(response.content || "");
|
|
150
|
+
}
|
|
151
|
+
}
|