visus-mcp 0.6.2 → 0.9.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.
Files changed (203) hide show
  1. package/.claude/settings.local.json +15 -1
  2. package/.env.status +7 -0
  3. package/CHANGELOG.md +110 -0
  4. package/CLAUDE.md +3 -0
  5. package/README.md +29 -19
  6. package/SECURITY.md +2 -0
  7. package/STATUS.md +320 -12
  8. package/dist/browser/playwright-renderer.d.ts.map +1 -1
  9. package/dist/browser/playwright-renderer.js +27 -5
  10. package/dist/browser/playwright-renderer.js.map +1 -1
  11. package/dist/content-handlers/index.d.ts +36 -0
  12. package/dist/content-handlers/index.d.ts.map +1 -0
  13. package/dist/content-handlers/index.js +59 -0
  14. package/dist/content-handlers/index.js.map +1 -0
  15. package/dist/content-handlers/json-handler.d.ts +28 -0
  16. package/dist/content-handlers/json-handler.d.ts.map +1 -0
  17. package/dist/content-handlers/json-handler.js +116 -0
  18. package/dist/content-handlers/json-handler.js.map +1 -0
  19. package/dist/content-handlers/pdf-handler.d.ts +29 -0
  20. package/dist/content-handlers/pdf-handler.d.ts.map +1 -0
  21. package/dist/content-handlers/pdf-handler.js +77 -0
  22. package/dist/content-handlers/pdf-handler.js.map +1 -0
  23. package/dist/content-handlers/svg-handler.d.ts +35 -0
  24. package/dist/content-handlers/svg-handler.d.ts.map +1 -0
  25. package/dist/content-handlers/svg-handler.js +206 -0
  26. package/dist/content-handlers/svg-handler.js.map +1 -0
  27. package/dist/content-handlers/types.d.ts +42 -0
  28. package/dist/content-handlers/types.d.ts.map +1 -0
  29. package/dist/content-handlers/types.js +7 -0
  30. package/dist/content-handlers/types.js.map +1 -0
  31. package/dist/sanitizer/framework-mapper.d.ts +4 -0
  32. package/dist/sanitizer/framework-mapper.d.ts.map +1 -1
  33. package/dist/sanitizer/framework-mapper.js +92 -0
  34. package/dist/sanitizer/framework-mapper.js.map +1 -1
  35. package/dist/sanitizer/threat-reporter.d.ts +5 -0
  36. package/dist/sanitizer/threat-reporter.d.ts.map +1 -1
  37. package/dist/sanitizer/threat-reporter.js +15 -6
  38. package/dist/sanitizer/threat-reporter.js.map +1 -1
  39. package/dist/tools/fetch-structured.d.ts.map +1 -1
  40. package/dist/tools/fetch-structured.js +4 -0
  41. package/dist/tools/fetch-structured.js.map +1 -1
  42. package/dist/tools/fetch.d.ts.map +1 -1
  43. package/dist/tools/fetch.js +68 -4
  44. package/dist/tools/fetch.js.map +1 -1
  45. package/dist/tools/read.d.ts.map +1 -1
  46. package/dist/tools/read.js +4 -0
  47. package/dist/tools/read.js.map +1 -1
  48. package/dist/types.d.ts +9 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/types.js.map +1 -1
  51. package/package.json +2 -1
  52. package/server.json +25 -14
  53. package/src/browser/playwright-renderer.ts +29 -6
  54. package/src/content-handlers/index.ts +72 -0
  55. package/src/content-handlers/json-handler.ts +137 -0
  56. package/src/content-handlers/pdf-handler.ts +91 -0
  57. package/src/content-handlers/svg-handler.ts +243 -0
  58. package/src/content-handlers/types.ts +44 -0
  59. package/src/sanitizer/framework-mapper.ts +94 -0
  60. package/src/sanitizer/threat-reporter.ts +17 -6
  61. package/src/tools/fetch-structured.ts +5 -0
  62. package/src/tools/fetch.ts +76 -4
  63. package/src/tools/read.ts +5 -0
  64. package/src/types.ts +9 -1
  65. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
  66. package/.github/ISSUE_TEMPLATE/false_positive.md +0 -43
  67. package/.github/ISSUE_TEMPLATE/new_pattern.md +0 -49
  68. package/.github/ISSUE_TEMPLATE/security_report.md +0 -31
  69. package/.github/PULL_REQUEST_TEMPLATE.md +0 -39
  70. package/.mcpregistry_github_token +0 -1
  71. package/.mcpregistry_registry_token +0 -1
  72. package/CONTRIBUTING.md +0 -329
  73. package/LINKEDIN-STRATEGY.md +0 -367
  74. package/ROADMAP.md +0 -221
  75. package/SECURITY-AUDIT-v1.md +0 -277
  76. package/SUBMISSION.md +0 -66
  77. package/TROUBLESHOOT-AUTH-20260322-2019.md +0 -291
  78. package/TROUBLESHOOT-BUILD-20260319-1450.md +0 -546
  79. package/TROUBLESHOOT-COGNITO-AUTH-20260324-2029.md +0 -415
  80. package/TROUBLESHOOT-COGNITO-JWT-20260324.md +0 -592
  81. package/TROUBLESHOOT-FETCH-20260320-1150.md +0 -168
  82. package/TROUBLESHOOT-JEST-20260323-1357.md +0 -139
  83. package/TROUBLESHOOT-LAMBDA-20260322-1945.md +0 -183
  84. package/TROUBLESHOOT-PLAYWRIGHT-20260321-1549.md +0 -217
  85. package/TROUBLESHOOT-SSL-20260320-1138.md +0 -171
  86. package/TROUBLESHOOT-STRUCTURED-20260320-1200.md +0 -246
  87. package/TROUBLESHOOT-TEST-20260320-0942.md +0 -281
  88. package/VISUS-CLAUDE-CODE-PROMPT.md +0 -324
  89. package/VISUS-PROJECT-PLAN.md +0 -205
  90. package/cdk.json +0 -73
  91. package/infrastructure/app.ts +0 -39
  92. package/infrastructure/stack.ts +0 -298
  93. package/jest.config.js +0 -33
  94. package/jest.setup.js +0 -9
  95. package/lambda-deploy/index.js +0 -81512
  96. package/lambda-deploy/index.js.map +0 -7
  97. package/lambda-package/browser/__mocks__/playwright-renderer.d.ts +0 -25
  98. package/lambda-package/browser/__mocks__/playwright-renderer.d.ts.map +0 -1
  99. package/lambda-package/browser/__mocks__/playwright-renderer.js +0 -119
  100. package/lambda-package/browser/__mocks__/playwright-renderer.js.map +0 -1
  101. package/lambda-package/browser/playwright-renderer.d.ts +0 -40
  102. package/lambda-package/browser/playwright-renderer.d.ts.map +0 -1
  103. package/lambda-package/browser/playwright-renderer.js +0 -214
  104. package/lambda-package/browser/playwright-renderer.js.map +0 -1
  105. package/lambda-package/browser/reader.d.ts +0 -31
  106. package/lambda-package/browser/reader.d.ts.map +0 -1
  107. package/lambda-package/browser/reader.js +0 -98
  108. package/lambda-package/browser/reader.js.map +0 -1
  109. package/lambda-package/index.d.ts +0 -18
  110. package/lambda-package/index.d.ts.map +0 -1
  111. package/lambda-package/index.js +0 -238
  112. package/lambda-package/index.js.map +0 -1
  113. package/lambda-package/lambda-handler.d.ts +0 -28
  114. package/lambda-package/lambda-handler.d.ts.map +0 -1
  115. package/lambda-package/lambda-handler.js +0 -257
  116. package/lambda-package/lambda-handler.js.map +0 -1
  117. package/lambda-package/package-lock.json +0 -7435
  118. package/lambda-package/package.json +0 -74
  119. package/lambda-package/runtime.d.ts +0 -50
  120. package/lambda-package/runtime.d.ts.map +0 -1
  121. package/lambda-package/runtime.js +0 -86
  122. package/lambda-package/runtime.js.map +0 -1
  123. package/lambda-package/sanitizer/elicit-runner.d.ts +0 -48
  124. package/lambda-package/sanitizer/elicit-runner.d.ts.map +0 -1
  125. package/lambda-package/sanitizer/elicit-runner.js +0 -100
  126. package/lambda-package/sanitizer/elicit-runner.js.map +0 -1
  127. package/lambda-package/sanitizer/framework-mapper.d.ts +0 -24
  128. package/lambda-package/sanitizer/framework-mapper.d.ts.map +0 -1
  129. package/lambda-package/sanitizer/framework-mapper.js +0 -342
  130. package/lambda-package/sanitizer/framework-mapper.js.map +0 -1
  131. package/lambda-package/sanitizer/hitl-gate.d.ts +0 -69
  132. package/lambda-package/sanitizer/hitl-gate.d.ts.map +0 -1
  133. package/lambda-package/sanitizer/hitl-gate.js +0 -101
  134. package/lambda-package/sanitizer/hitl-gate.js.map +0 -1
  135. package/lambda-package/sanitizer/index.d.ts +0 -63
  136. package/lambda-package/sanitizer/index.d.ts.map +0 -1
  137. package/lambda-package/sanitizer/index.js +0 -105
  138. package/lambda-package/sanitizer/index.js.map +0 -1
  139. package/lambda-package/sanitizer/injection-detector.d.ts +0 -34
  140. package/lambda-package/sanitizer/injection-detector.d.ts.map +0 -1
  141. package/lambda-package/sanitizer/injection-detector.js +0 -89
  142. package/lambda-package/sanitizer/injection-detector.js.map +0 -1
  143. package/lambda-package/sanitizer/patterns.d.ts +0 -30
  144. package/lambda-package/sanitizer/patterns.d.ts.map +0 -1
  145. package/lambda-package/sanitizer/patterns.js +0 -372
  146. package/lambda-package/sanitizer/patterns.js.map +0 -1
  147. package/lambda-package/sanitizer/pii-allowlist.d.ts +0 -49
  148. package/lambda-package/sanitizer/pii-allowlist.d.ts.map +0 -1
  149. package/lambda-package/sanitizer/pii-allowlist.js +0 -231
  150. package/lambda-package/sanitizer/pii-allowlist.js.map +0 -1
  151. package/lambda-package/sanitizer/pii-redactor.d.ts +0 -41
  152. package/lambda-package/sanitizer/pii-redactor.d.ts.map +0 -1
  153. package/lambda-package/sanitizer/pii-redactor.js +0 -213
  154. package/lambda-package/sanitizer/pii-redactor.js.map +0 -1
  155. package/lambda-package/sanitizer/severity-classifier.d.ts +0 -33
  156. package/lambda-package/sanitizer/severity-classifier.d.ts.map +0 -1
  157. package/lambda-package/sanitizer/severity-classifier.js +0 -113
  158. package/lambda-package/sanitizer/severity-classifier.js.map +0 -1
  159. package/lambda-package/sanitizer/threat-reporter.d.ts +0 -66
  160. package/lambda-package/sanitizer/threat-reporter.d.ts.map +0 -1
  161. package/lambda-package/sanitizer/threat-reporter.js +0 -163
  162. package/lambda-package/sanitizer/threat-reporter.js.map +0 -1
  163. package/lambda-package/tools/fetch-structured.d.ts +0 -51
  164. package/lambda-package/tools/fetch-structured.d.ts.map +0 -1
  165. package/lambda-package/tools/fetch-structured.js +0 -237
  166. package/lambda-package/tools/fetch-structured.js.map +0 -1
  167. package/lambda-package/tools/fetch.d.ts +0 -49
  168. package/lambda-package/tools/fetch.d.ts.map +0 -1
  169. package/lambda-package/tools/fetch.js +0 -131
  170. package/lambda-package/tools/fetch.js.map +0 -1
  171. package/lambda-package/tools/read.d.ts +0 -51
  172. package/lambda-package/tools/read.d.ts.map +0 -1
  173. package/lambda-package/tools/read.js +0 -127
  174. package/lambda-package/tools/read.js.map +0 -1
  175. package/lambda-package/tools/search.d.ts +0 -45
  176. package/lambda-package/tools/search.d.ts.map +0 -1
  177. package/lambda-package/tools/search.js +0 -220
  178. package/lambda-package/tools/search.js.map +0 -1
  179. package/lambda-package/types.d.ts +0 -167
  180. package/lambda-package/types.d.ts.map +0 -1
  181. package/lambda-package/types.js +0 -16
  182. package/lambda-package/types.js.map +0 -1
  183. package/lambda-package/utils/format-converter.d.ts +0 -39
  184. package/lambda-package/utils/format-converter.d.ts.map +0 -1
  185. package/lambda-package/utils/format-converter.js +0 -191
  186. package/lambda-package/utils/format-converter.js.map +0 -1
  187. package/lambda-package/utils/truncate.d.ts +0 -26
  188. package/lambda-package/utils/truncate.d.ts.map +0 -1
  189. package/lambda-package/utils/truncate.js +0 -54
  190. package/lambda-package/utils/truncate.js.map +0 -1
  191. package/lambda.zip +0 -0
  192. package/test-output.txt +0 -4
  193. package/tests/auth-smoke.test.ts +0 -480
  194. package/tests/elicit-runner.test.ts +0 -232
  195. package/tests/fetch-tool.test.ts +0 -922
  196. package/tests/hitl-gate.test.ts +0 -267
  197. package/tests/injection-corpus.ts +0 -338
  198. package/tests/pii-allowlist.test.ts +0 -282
  199. package/tests/reader.test.ts +0 -353
  200. package/tests/sanitizer.test.ts +0 -358
  201. package/tests/search.test.ts +0 -456
  202. package/tests/threat-reporter.test.ts +0 -334
  203. package/tsconfig.cdk.json +0 -35
@@ -1,480 +0,0 @@
1
- /**
2
- * Authentication Enforcement Smoke Tests
3
- *
4
- * These tests verify that authentication is properly enforced across all
5
- * API endpoints and Lambda invocation paths per CLAUDE.md security rules.
6
- *
7
- * Test Categories:
8
- * 1. API Gateway Cognito Authorizer enforcement
9
- * 2. Lambda handler behavior with/without auth context
10
- * 3. Health endpoint bypass (intentional)
11
- * 4. CORS enforcement
12
- * 5. User ID extraction and audit logging
13
- * 6. Direct Lambda invocation (bypass prevention)
14
- */
15
-
16
- import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
17
- import { handler } from '../src/lambda-handler.js';
18
-
19
- /**
20
- * Mock API Gateway event builder
21
- */
22
- function createMockEvent(
23
- path: string,
24
- httpMethod: string,
25
- body: Record<string, unknown> | null,
26
- authContext?: {
27
- sub: string;
28
- email?: string;
29
- }
30
- ): APIGatewayProxyEvent {
31
- const event: Partial<APIGatewayProxyEvent> = {
32
- path,
33
- httpMethod,
34
- headers: {
35
- 'Content-Type': 'application/json',
36
- 'User-Agent': 'jest/smoke-test',
37
- origin: 'https://claude.ai',
38
- },
39
- body: body ? JSON.stringify(body) : null,
40
- requestContext: {
41
- requestId: 'test-request-id',
42
- identity: {
43
- sourceIp: '127.0.0.1',
44
- } as any,
45
- authorizer: authContext ? { claims: authContext } : undefined,
46
- } as any,
47
- };
48
-
49
- return event as APIGatewayProxyEvent;
50
- }
51
-
52
- /**
53
- * Mock Lambda context
54
- */
55
- const mockContext: Context = {
56
- awsRequestId: 'test-request-id',
57
- functionName: 'visus-mcp-test',
58
- functionVersion: '1',
59
- invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:visus-mcp-test',
60
- memoryLimitInMB: '1024',
61
- logGroupName: '/aws/lambda/visus-mcp-test',
62
- logStreamName: 'test-stream',
63
- callbackWaitsForEmptyEventLoop: false,
64
- getRemainingTimeInMillis: () => 30000,
65
- done: () => {},
66
- fail: () => {},
67
- succeed: () => {},
68
- };
69
-
70
- describe('Authentication Enforcement Smoke Tests', () => {
71
- // Mock environment variables
72
- beforeAll(() => {
73
- process.env.AUDIT_TABLE_NAME = 'visus-audit-test';
74
- process.env.ENVIRONMENT = 'test';
75
- process.env.ALLOWED_ORIGINS = 'https://claude.ai,https://app.claude.ai';
76
- });
77
-
78
- afterAll(() => {
79
- delete process.env.AUDIT_TABLE_NAME;
80
- delete process.env.ENVIRONMENT;
81
- delete process.env.ALLOWED_ORIGINS;
82
- });
83
-
84
- describe('1. Health Endpoint (Unauthenticated Access Allowed)', () => {
85
- it('should allow /health with GET without auth context', async () => {
86
- const event = createMockEvent('/health', 'GET', null);
87
-
88
- const response = await handler(event, mockContext);
89
-
90
- expect(response.statusCode).toBe(200);
91
- const body = JSON.parse(response.body);
92
- expect(body.status).toBe('healthy');
93
- expect(body.service).toBe('visus-mcp');
94
- expect(body.version).toBe('0.3.1');
95
- });
96
-
97
- it('should allow /health with POST without auth context', async () => {
98
- const event = createMockEvent('/health', 'POST', {});
99
-
100
- const response = await handler(event, mockContext);
101
-
102
- expect(response.statusCode).toBe(200);
103
- const body = JSON.parse(response.body);
104
- expect(body.status).toBe('healthy');
105
- });
106
-
107
- it('should allow /dev/health without auth context', async () => {
108
- const event = createMockEvent('/dev/health', 'GET', null);
109
-
110
- const response = await handler(event, mockContext);
111
-
112
- expect(response.statusCode).toBe(200);
113
- const body = JSON.parse(response.body);
114
- expect(body.status).toBe('healthy');
115
- });
116
-
117
- it('should allow /prod/health without auth context', async () => {
118
- const event = createMockEvent('/prod/health', 'GET', null);
119
-
120
- const response = await handler(event, mockContext);
121
-
122
- expect(response.statusCode).toBe(200);
123
- const body = JSON.parse(response.body);
124
- expect(body.status).toBe('healthy');
125
- });
126
- });
127
-
128
- describe('2. Protected Endpoints WITHOUT Auth Context', () => {
129
- it('should REJECT /fetch requests without auth (SECURITY FIX - FINDING 1)', async () => {
130
- const event = createMockEvent('/fetch', 'POST', {
131
- url: 'https://example.com',
132
- });
133
-
134
- const response = await handler(event, mockContext);
135
-
136
- // FIXED: Lambda now enforces auth at application level
137
- expect(response.statusCode).toBe(401);
138
- const body = JSON.parse(response.body);
139
- expect(body.error).toContain('Unauthorized');
140
- expect(body.error).toContain('Authentication required');
141
- });
142
-
143
- it('should REJECT /fetch-structured requests without auth (SECURITY FIX - FINDING 1)', async () => {
144
- const event = createMockEvent('/fetch-structured', 'POST', {
145
- url: 'https://example.com',
146
- schema: { title: 'h1' },
147
- });
148
-
149
- const response = await handler(event, mockContext);
150
-
151
- // FIXED: Lambda now enforces auth at application level
152
- expect(response.statusCode).toBe(401);
153
- const body = JSON.parse(response.body);
154
- expect(body.error).toContain('Unauthorized');
155
- expect(body.error).toContain('Authentication required');
156
- });
157
-
158
- it('should log auth_required event when no auth context present', async () => {
159
- const event = createMockEvent('/fetch', 'POST', {
160
- url: 'https://example.com',
161
- });
162
-
163
- // Capture console.error calls to verify auth logging
164
- const originalConsoleError = console.error;
165
- const loggedEvents: string[] = [];
166
- console.error = (message: string) => {
167
- loggedEvents.push(message);
168
- };
169
-
170
- await handler(event, mockContext);
171
-
172
- console.error = originalConsoleError;
173
-
174
- // Verify that auth_required event was logged
175
- const authLog = loggedEvents.find((log) => {
176
- try {
177
- const parsed = JSON.parse(log);
178
- return parsed.event === 'auth_required';
179
- } catch {
180
- return false;
181
- }
182
- });
183
-
184
- expect(authLog).toBeDefined();
185
- if (authLog) {
186
- const parsed = JSON.parse(authLog);
187
- expect(parsed.reason).toContain('Cognito authorizer');
188
- }
189
- });
190
- });
191
-
192
- describe('3. Protected Endpoints WITH Auth Context', () => {
193
- it('should extract user_id from Cognito authorizer claims', async () => {
194
- const authContext = {
195
- sub: 'test-user-123',
196
- email: 'test@example.com',
197
- };
198
-
199
- const event = createMockEvent(
200
- '/fetch',
201
- 'POST',
202
- { url: 'https://example.com' },
203
- authContext
204
- );
205
-
206
- // Capture console.error to verify user_id is extracted
207
- const originalConsoleError = console.error;
208
- const loggedEvents: string[] = [];
209
- console.error = (message: string) => {
210
- loggedEvents.push(message);
211
- };
212
-
213
- await handler(event, mockContext);
214
-
215
- console.error = originalConsoleError;
216
-
217
- // User ID extraction happens at line 132 of lambda-handler.ts
218
- // We can't directly inspect it, but we can verify the handler doesn't crash
219
- // and processes the request normally
220
- expect(loggedEvents.length).toBeGreaterThan(0);
221
- });
222
-
223
- it('should process /fetch with valid auth context', async () => {
224
- const authContext = {
225
- sub: 'test-user-123',
226
- };
227
-
228
- const event = createMockEvent(
229
- '/fetch',
230
- 'POST',
231
- { url: 'https://example.com' },
232
- authContext
233
- );
234
-
235
- const response = await handler(event, mockContext);
236
-
237
- // Should succeed (or fail with a valid error, not 401/403)
238
- expect([200, 400, 500]).toContain(response.statusCode);
239
- });
240
-
241
- it('should process /fetch-structured with valid auth context', async () => {
242
- const authContext = {
243
- sub: 'test-user-456',
244
- };
245
-
246
- const event = createMockEvent(
247
- '/fetch-structured',
248
- 'POST',
249
- { url: 'https://example.com', schema: { title: 'h1' } },
250
- authContext
251
- );
252
-
253
- const response = await handler(event, mockContext);
254
-
255
- // Should succeed (or fail with a valid error, not 401/403)
256
- expect([200, 400, 500]).toContain(response.statusCode);
257
- });
258
- });
259
-
260
- describe('4. CORS Enforcement', () => {
261
- it('should validate origin against allowlist', async () => {
262
- const event = createMockEvent('/health', 'GET', null);
263
- event.headers.origin = 'https://malicious-site.com';
264
-
265
- const response = await handler(event, mockContext);
266
-
267
- // CORS headers should use first allowed origin, not the malicious one
268
- expect(response.headers?.['Access-Control-Allow-Origin']).not.toBe(
269
- 'https://malicious-site.com'
270
- );
271
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://claude.ai');
272
- });
273
-
274
- it('should allow whitelisted origin', async () => {
275
- const event = createMockEvent('/health', 'GET', null);
276
- event.headers.origin = 'https://app.claude.ai';
277
-
278
- const response = await handler(event, mockContext);
279
-
280
- expect(response.headers?.['Access-Control-Allow-Origin']).toBe(
281
- 'https://app.claude.ai'
282
- );
283
- });
284
-
285
- it('should handle OPTIONS preflight request', async () => {
286
- const event = createMockEvent('/fetch', 'OPTIONS', null);
287
-
288
- const response = await handler(event, mockContext);
289
-
290
- expect(response.statusCode).toBe(200);
291
- expect(response.headers?.['Access-Control-Allow-Methods']).toBe('GET, POST, OPTIONS');
292
- expect(response.headers?.['Access-Control-Allow-Headers']).toContain('Authorization');
293
- });
294
- });
295
-
296
- describe('5. Method Enforcement', () => {
297
- it('should reject GET requests to /fetch', async () => {
298
- const event = createMockEvent('/fetch', 'GET', null);
299
-
300
- const response = await handler(event, mockContext);
301
-
302
- expect(response.statusCode).toBe(405);
303
- const body = JSON.parse(response.body);
304
- expect(body.error).toContain('Method not allowed');
305
- });
306
-
307
- it('should reject PUT requests to /fetch-structured', async () => {
308
- const event = createMockEvent('/fetch-structured', 'PUT', null);
309
-
310
- const response = await handler(event, mockContext);
311
-
312
- expect(response.statusCode).toBe(405);
313
- });
314
-
315
- it('should reject DELETE requests', async () => {
316
- const event = createMockEvent('/fetch', 'DELETE', null);
317
-
318
- const response = await handler(event, mockContext);
319
-
320
- expect(response.statusCode).toBe(405);
321
- });
322
- });
323
-
324
- describe('6. Input Validation', () => {
325
- it('should reject /fetch request with missing url', async () => {
326
- const authContext = { sub: 'test-user' };
327
- const event = createMockEvent('/fetch', 'POST', {}, authContext);
328
-
329
- const response = await handler(event, mockContext);
330
-
331
- expect(response.statusCode).toBe(400);
332
- const body = JSON.parse(response.body);
333
- expect(body.error).toContain('url');
334
- });
335
-
336
- it('should reject /fetch-structured request with missing schema', async () => {
337
- const authContext = { sub: 'test-user' };
338
- const event = createMockEvent(
339
- '/fetch-structured',
340
- 'POST',
341
- { url: 'https://example.com' },
342
- authContext
343
- );
344
-
345
- const response = await handler(event, mockContext);
346
-
347
- expect(response.statusCode).toBe(400);
348
- const body = JSON.parse(response.body);
349
- expect(body.error).toContain('schema');
350
- });
351
-
352
- it('should reject invalid JSON body', async () => {
353
- const event = createMockEvent('/fetch', 'POST', null);
354
- event.body = '{invalid json}';
355
-
356
- const response = await handler(event, mockContext);
357
-
358
- expect(response.statusCode).toBe(400);
359
- const body = JSON.parse(response.body);
360
- expect(body.error).toContain('JSON');
361
- });
362
- });
363
-
364
- describe('7. Unknown Endpoint Handling', () => {
365
- it('should return 404 for unknown paths', async () => {
366
- const authContext = { sub: 'test-user' };
367
- const event = createMockEvent('/unknown-endpoint', 'POST', null, authContext);
368
-
369
- const response = await handler(event, mockContext);
370
-
371
- expect(response.statusCode).toBe(404);
372
- const body = JSON.parse(response.body);
373
- expect(body.error).toContain('Not found');
374
- });
375
- });
376
- });
377
-
378
- describe('SECURITY AUDIT FINDINGS - RESOLUTIONS', () => {
379
- it('✅ FINDING 1 RESOLVED: Lambda NOW enforces auth at application level', async () => {
380
- /**
381
- * RESOLUTION VERIFIED (v0.3.1):
382
- * - Lambda handler now validates Cognito authorizer context
383
- * - Returns 401 if userId is missing (lines 188-209 of lambda-handler.ts)
384
- * - Logs 'auth_required' event with details
385
- * - Health check endpoint explicitly excluded from auth requirement
386
- *
387
- * FIXED: Application-level defense-in-depth implemented
388
- */
389
- const event = createMockEvent('/fetch', 'POST', { url: 'https://example.com' });
390
- const response = await handler(event, mockContext);
391
-
392
- // FIXED: Lambda NOW returns 401 when auth is missing
393
- expect(response.statusCode).toBe(401);
394
- const body = JSON.parse(response.body);
395
- expect(body.error).toContain('Unauthorized');
396
- expect(body.error).toContain('Authentication required');
397
- });
398
-
399
- it('✅ FINDING 1 RESOLVED: Auth rejection prevents anonymous audit logs', async () => {
400
- /**
401
- * RESOLUTION VERIFIED (v0.3.1):
402
- * - Unauthenticated requests are rejected before reaching audit logging
403
- * - No more user_id="anonymous" in audit logs
404
- * - auth_required event logged instead for security monitoring
405
- *
406
- * FIXED: No anonymous audit trails possible
407
- */
408
- const event = createMockEvent('/fetch', 'POST', { url: 'https://example.com' });
409
-
410
- // Intercept console.error to verify auth_required logging
411
- const originalConsoleError = console.error;
412
- let authRequiredLogged = false;
413
- let auditLogAttempted = false;
414
- console.error = (message: string) => {
415
- try {
416
- const parsed = JSON.parse(message);
417
- if (parsed.event === 'auth_required') {
418
- authRequiredLogged = true;
419
- }
420
- if (parsed.event === 'audit_logging_failed') {
421
- auditLogAttempted = true;
422
- }
423
- } catch {
424
- // Not JSON
425
- }
426
- };
427
-
428
- await handler(event, mockContext);
429
-
430
- console.error = originalConsoleError;
431
-
432
- // Verify auth_required was logged
433
- expect(authRequiredLogged).toBe(true);
434
- // Verify NO audit logging attempted (rejected before that point)
435
- expect(auditLogAttempted).toBe(false);
436
- });
437
-
438
- it('✅ FINDING 2 RESOLVED: Health check now supports GET method', async () => {
439
- /**
440
- * RESOLUTION VERIFIED (v0.3.1):
441
- * - Health check moved before POST-only validation (lines 152-165 of lambda-handler.ts)
442
- * - Supports both GET and POST methods
443
- * - CORS allows GET, POST, OPTIONS
444
- * - Standard monitoring tools can now use GET /health
445
- *
446
- * FIXED: Standard REST conventions for health checks
447
- */
448
- const getEvent = createMockEvent('/health', 'GET', null);
449
- const getResponse = await handler(getEvent, mockContext);
450
-
451
- expect(getResponse.statusCode).toBe(200);
452
- const body = JSON.parse(getResponse.body);
453
- expect(body.status).toBe('healthy');
454
- expect(body.version).toBe('0.3.1');
455
-
456
- // Also verify POST still works
457
- const postEvent = createMockEvent('/health', 'POST', {});
458
- const postResponse = await handler(postEvent, mockContext);
459
- expect(postResponse.statusCode).toBe(200);
460
- });
461
-
462
- it('✅ CONFIRMED SECURE: Health check remains intentionally unauthenticated', async () => {
463
- /**
464
- * CONFIRMED SECURE (v0.3.1):
465
- * - /health endpoint intentionally bypasses auth (lines 152-165 of lambda-handler.ts)
466
- * - This is standard practice for health checks
467
- * - Only returns non-sensitive metadata (status, version, timestamp)
468
- *
469
- * NO ACTION REQUIRED
470
- */
471
- const event = createMockEvent('/health', 'GET', null);
472
- const response = await handler(event, mockContext);
473
-
474
- expect(response.statusCode).toBe(200);
475
- const body = JSON.parse(response.body);
476
- expect(body).not.toHaveProperty('user_id');
477
- expect(body).not.toHaveProperty('secrets');
478
- expect(body.status).toBe('healthy');
479
- });
480
- });
@@ -1,232 +0,0 @@
1
- /**
2
- * Tests for Elicitation Runner
3
- *
4
- * Validates:
5
- * - User accept/decline/cancel handling
6
- * - Fail-safe behavior on errors
7
- * - Threat report inclusion logic
8
- */
9
-
10
- import { runElicitation } from '../src/sanitizer/elicit-runner.js';
11
- import type { ThreatReport } from '../src/sanitizer/threat-reporter.js';
12
- import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
-
14
- describe('Elicitation Runner', () => {
15
- let mockServer: jest.Mocked<Server>;
16
-
17
- beforeEach(() => {
18
- // Create a mock server with elicitInput method
19
- mockServer = {
20
- elicitInput: jest.fn()
21
- } as any;
22
- });
23
-
24
- const createMockThreatReport = (): ThreatReport => ({
25
- generated: new Date().toISOString(),
26
- source_url: 'https://malicious.example.com',
27
- overall_severity: 'CRITICAL',
28
- total_findings: 5,
29
- by_severity: { CRITICAL: 5, HIGH: 0, MEDIUM: 0, LOW: 0 },
30
- pii_redacted: 0,
31
- sanitization_applied: true,
32
- frameworks: ['OWASP LLM Top 10', 'NIST AI 600-1', 'MITRE ATLAS'],
33
- findings_toon: 'findings[5]{id,pattern_id,category,severity,confidence,owasp_llm,nist_ai_600_1,mitre_atlas,remediation}:\n1,PI-001,role_hijacking,CRITICAL,0.95,LLM01:2025 - Prompt Injection,MS-2.3,AML.T0051.000 - LLM Prompt Injection,Content sanitized',
34
- report_markdown: '# Report'
35
- });
36
-
37
- describe('User response handling', () => {
38
- it('returns proceed:true when user accepts with proceed:true', async () => {
39
- const threatReport = createMockThreatReport();
40
-
41
- mockServer.elicitInput.mockResolvedValue({
42
- action: 'accept',
43
- content: {
44
- proceed: true,
45
- view_report: true
46
- }
47
- });
48
-
49
- const result = await runElicitation(
50
- mockServer,
51
- threatReport,
52
- 'https://malicious.example.com'
53
- );
54
-
55
- expect(result.proceed).toBe(true);
56
- expect(result.includeReport).toBe(true);
57
- });
58
-
59
- it('returns proceed:false when user accepts with proceed:false', async () => {
60
- const threatReport = createMockThreatReport();
61
-
62
- mockServer.elicitInput.mockResolvedValue({
63
- action: 'accept',
64
- content: {
65
- proceed: false,
66
- view_report: true
67
- }
68
- });
69
-
70
- const result = await runElicitation(
71
- mockServer,
72
- threatReport,
73
- 'https://malicious.example.com'
74
- );
75
-
76
- expect(result.proceed).toBe(false);
77
- expect(result.includeReport).toBe(false); // Report not included when not proceeding
78
- });
79
-
80
- it('returns proceed:false on decline action', async () => {
81
- const threatReport = createMockThreatReport();
82
-
83
- mockServer.elicitInput.mockResolvedValue({
84
- action: 'decline',
85
- content: undefined
86
- });
87
-
88
- const result = await runElicitation(
89
- mockServer,
90
- threatReport,
91
- 'https://malicious.example.com'
92
- );
93
-
94
- expect(result.proceed).toBe(false);
95
- expect(result.includeReport).toBe(false);
96
- });
97
-
98
- it('returns proceed:false on cancel action', async () => {
99
- const threatReport = createMockThreatReport();
100
-
101
- mockServer.elicitInput.mockResolvedValue({
102
- action: 'cancel',
103
- content: undefined
104
- });
105
-
106
- const result = await runElicitation(
107
- mockServer,
108
- threatReport,
109
- 'https://malicious.example.com'
110
- );
111
-
112
- expect(result.proceed).toBe(false);
113
- expect(result.includeReport).toBe(false);
114
- });
115
-
116
- it('includes report when user checks view_report', async () => {
117
- const threatReport = createMockThreatReport();
118
-
119
- mockServer.elicitInput.mockResolvedValue({
120
- action: 'accept',
121
- content: {
122
- proceed: true,
123
- view_report: true
124
- }
125
- });
126
-
127
- const result = await runElicitation(
128
- mockServer,
129
- threatReport,
130
- 'https://malicious.example.com'
131
- );
132
-
133
- expect(result.proceed).toBe(true);
134
- expect(result.includeReport).toBe(true);
135
- });
136
-
137
- it('excludes report when user unchecks view_report', async () => {
138
- const threatReport = createMockThreatReport();
139
-
140
- mockServer.elicitInput.mockResolvedValue({
141
- action: 'accept',
142
- content: {
143
- proceed: true,
144
- view_report: false
145
- }
146
- });
147
-
148
- const result = await runElicitation(
149
- mockServer,
150
- threatReport,
151
- 'https://malicious.example.com'
152
- );
153
-
154
- expect(result.proceed).toBe(true);
155
- expect(result.includeReport).toBe(false);
156
- });
157
-
158
- it('defaults to including report when view_report is undefined', async () => {
159
- const threatReport = createMockThreatReport();
160
-
161
- mockServer.elicitInput.mockResolvedValue({
162
- action: 'accept',
163
- content: {
164
- proceed: true
165
- }
166
- });
167
-
168
- const result = await runElicitation(
169
- mockServer,
170
- threatReport,
171
- 'https://malicious.example.com'
172
- );
173
-
174
- expect(result.proceed).toBe(true);
175
- expect(result.includeReport).toBe(true);
176
- });
177
- });
178
-
179
- describe('Fail-safe behavior', () => {
180
- it('proceeds with sanitized content on elicitation error (fail-safe)', async () => {
181
- const threatReport = createMockThreatReport();
182
-
183
- mockServer.elicitInput.mockRejectedValue(
184
- new Error('Elicitation not supported')
185
- );
186
-
187
- const result = await runElicitation(
188
- mockServer,
189
- threatReport,
190
- 'https://malicious.example.com'
191
- );
192
-
193
- expect(result.proceed).toBe(true);
194
- expect(result.includeReport).toBe(true);
195
- });
196
-
197
- it('proceeds with sanitized content on timeout (fail-safe)', async () => {
198
- const threatReport = createMockThreatReport();
199
-
200
- mockServer.elicitInput.mockRejectedValue(
201
- new Error('Request timeout')
202
- );
203
-
204
- const result = await runElicitation(
205
- mockServer,
206
- threatReport,
207
- 'https://malicious.example.com'
208
- );
209
-
210
- expect(result.proceed).toBe(true);
211
- expect(result.includeReport).toBe(true);
212
- });
213
-
214
- it('proceeds with sanitized content on unknown action (fail-safe)', async () => {
215
- const threatReport = createMockThreatReport();
216
-
217
- mockServer.elicitInput.mockResolvedValue({
218
- action: 'unknown_action' as any,
219
- content: undefined
220
- });
221
-
222
- const result = await runElicitation(
223
- mockServer,
224
- threatReport,
225
- 'https://malicious.example.com'
226
- );
227
-
228
- expect(result.proceed).toBe(true);
229
- expect(result.includeReport).toBe(true);
230
- });
231
- });
232
- });