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.
- package/.claude/settings.local.json +15 -1
- package/.env.status +7 -0
- package/CHANGELOG.md +110 -0
- package/CLAUDE.md +3 -0
- package/README.md +29 -19
- package/SECURITY.md +2 -0
- package/STATUS.md +320 -12
- package/dist/browser/playwright-renderer.d.ts.map +1 -1
- package/dist/browser/playwright-renderer.js +27 -5
- package/dist/browser/playwright-renderer.js.map +1 -1
- package/dist/content-handlers/index.d.ts +36 -0
- package/dist/content-handlers/index.d.ts.map +1 -0
- package/dist/content-handlers/index.js +59 -0
- package/dist/content-handlers/index.js.map +1 -0
- package/dist/content-handlers/json-handler.d.ts +28 -0
- package/dist/content-handlers/json-handler.d.ts.map +1 -0
- package/dist/content-handlers/json-handler.js +116 -0
- package/dist/content-handlers/json-handler.js.map +1 -0
- package/dist/content-handlers/pdf-handler.d.ts +29 -0
- package/dist/content-handlers/pdf-handler.d.ts.map +1 -0
- package/dist/content-handlers/pdf-handler.js +77 -0
- package/dist/content-handlers/pdf-handler.js.map +1 -0
- package/dist/content-handlers/svg-handler.d.ts +35 -0
- package/dist/content-handlers/svg-handler.d.ts.map +1 -0
- package/dist/content-handlers/svg-handler.js +206 -0
- package/dist/content-handlers/svg-handler.js.map +1 -0
- package/dist/content-handlers/types.d.ts +42 -0
- package/dist/content-handlers/types.d.ts.map +1 -0
- package/dist/content-handlers/types.js +7 -0
- package/dist/content-handlers/types.js.map +1 -0
- package/dist/sanitizer/framework-mapper.d.ts +4 -0
- package/dist/sanitizer/framework-mapper.d.ts.map +1 -1
- package/dist/sanitizer/framework-mapper.js +92 -0
- package/dist/sanitizer/framework-mapper.js.map +1 -1
- package/dist/sanitizer/threat-reporter.d.ts +5 -0
- package/dist/sanitizer/threat-reporter.d.ts.map +1 -1
- package/dist/sanitizer/threat-reporter.js +15 -6
- package/dist/sanitizer/threat-reporter.js.map +1 -1
- package/dist/tools/fetch-structured.d.ts.map +1 -1
- package/dist/tools/fetch-structured.js +4 -0
- package/dist/tools/fetch-structured.js.map +1 -1
- package/dist/tools/fetch.d.ts.map +1 -1
- package/dist/tools/fetch.js +68 -4
- package/dist/tools/fetch.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +4 -0
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
- package/server.json +25 -14
- package/src/browser/playwright-renderer.ts +29 -6
- package/src/content-handlers/index.ts +72 -0
- package/src/content-handlers/json-handler.ts +137 -0
- package/src/content-handlers/pdf-handler.ts +91 -0
- package/src/content-handlers/svg-handler.ts +243 -0
- package/src/content-handlers/types.ts +44 -0
- package/src/sanitizer/framework-mapper.ts +94 -0
- package/src/sanitizer/threat-reporter.ts +17 -6
- package/src/tools/fetch-structured.ts +5 -0
- package/src/tools/fetch.ts +76 -4
- package/src/tools/read.ts +5 -0
- package/src/types.ts +9 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
- package/.github/ISSUE_TEMPLATE/false_positive.md +0 -43
- package/.github/ISSUE_TEMPLATE/new_pattern.md +0 -49
- package/.github/ISSUE_TEMPLATE/security_report.md +0 -31
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -39
- package/.mcpregistry_github_token +0 -1
- package/.mcpregistry_registry_token +0 -1
- package/CONTRIBUTING.md +0 -329
- package/LINKEDIN-STRATEGY.md +0 -367
- package/ROADMAP.md +0 -221
- package/SECURITY-AUDIT-v1.md +0 -277
- package/SUBMISSION.md +0 -66
- package/TROUBLESHOOT-AUTH-20260322-2019.md +0 -291
- package/TROUBLESHOOT-BUILD-20260319-1450.md +0 -546
- package/TROUBLESHOOT-COGNITO-AUTH-20260324-2029.md +0 -415
- package/TROUBLESHOOT-COGNITO-JWT-20260324.md +0 -592
- package/TROUBLESHOOT-FETCH-20260320-1150.md +0 -168
- package/TROUBLESHOOT-JEST-20260323-1357.md +0 -139
- package/TROUBLESHOOT-LAMBDA-20260322-1945.md +0 -183
- package/TROUBLESHOOT-PLAYWRIGHT-20260321-1549.md +0 -217
- package/TROUBLESHOOT-SSL-20260320-1138.md +0 -171
- package/TROUBLESHOOT-STRUCTURED-20260320-1200.md +0 -246
- package/TROUBLESHOOT-TEST-20260320-0942.md +0 -281
- package/VISUS-CLAUDE-CODE-PROMPT.md +0 -324
- package/VISUS-PROJECT-PLAN.md +0 -205
- package/cdk.json +0 -73
- package/infrastructure/app.ts +0 -39
- package/infrastructure/stack.ts +0 -298
- package/jest.config.js +0 -33
- package/jest.setup.js +0 -9
- package/lambda-deploy/index.js +0 -81512
- package/lambda-deploy/index.js.map +0 -7
- package/lambda-package/browser/__mocks__/playwright-renderer.d.ts +0 -25
- package/lambda-package/browser/__mocks__/playwright-renderer.d.ts.map +0 -1
- package/lambda-package/browser/__mocks__/playwright-renderer.js +0 -119
- package/lambda-package/browser/__mocks__/playwright-renderer.js.map +0 -1
- package/lambda-package/browser/playwright-renderer.d.ts +0 -40
- package/lambda-package/browser/playwright-renderer.d.ts.map +0 -1
- package/lambda-package/browser/playwright-renderer.js +0 -214
- package/lambda-package/browser/playwright-renderer.js.map +0 -1
- package/lambda-package/browser/reader.d.ts +0 -31
- package/lambda-package/browser/reader.d.ts.map +0 -1
- package/lambda-package/browser/reader.js +0 -98
- package/lambda-package/browser/reader.js.map +0 -1
- package/lambda-package/index.d.ts +0 -18
- package/lambda-package/index.d.ts.map +0 -1
- package/lambda-package/index.js +0 -238
- package/lambda-package/index.js.map +0 -1
- package/lambda-package/lambda-handler.d.ts +0 -28
- package/lambda-package/lambda-handler.d.ts.map +0 -1
- package/lambda-package/lambda-handler.js +0 -257
- package/lambda-package/lambda-handler.js.map +0 -1
- package/lambda-package/package-lock.json +0 -7435
- package/lambda-package/package.json +0 -74
- package/lambda-package/runtime.d.ts +0 -50
- package/lambda-package/runtime.d.ts.map +0 -1
- package/lambda-package/runtime.js +0 -86
- package/lambda-package/runtime.js.map +0 -1
- package/lambda-package/sanitizer/elicit-runner.d.ts +0 -48
- package/lambda-package/sanitizer/elicit-runner.d.ts.map +0 -1
- package/lambda-package/sanitizer/elicit-runner.js +0 -100
- package/lambda-package/sanitizer/elicit-runner.js.map +0 -1
- package/lambda-package/sanitizer/framework-mapper.d.ts +0 -24
- package/lambda-package/sanitizer/framework-mapper.d.ts.map +0 -1
- package/lambda-package/sanitizer/framework-mapper.js +0 -342
- package/lambda-package/sanitizer/framework-mapper.js.map +0 -1
- package/lambda-package/sanitizer/hitl-gate.d.ts +0 -69
- package/lambda-package/sanitizer/hitl-gate.d.ts.map +0 -1
- package/lambda-package/sanitizer/hitl-gate.js +0 -101
- package/lambda-package/sanitizer/hitl-gate.js.map +0 -1
- package/lambda-package/sanitizer/index.d.ts +0 -63
- package/lambda-package/sanitizer/index.d.ts.map +0 -1
- package/lambda-package/sanitizer/index.js +0 -105
- package/lambda-package/sanitizer/index.js.map +0 -1
- package/lambda-package/sanitizer/injection-detector.d.ts +0 -34
- package/lambda-package/sanitizer/injection-detector.d.ts.map +0 -1
- package/lambda-package/sanitizer/injection-detector.js +0 -89
- package/lambda-package/sanitizer/injection-detector.js.map +0 -1
- package/lambda-package/sanitizer/patterns.d.ts +0 -30
- package/lambda-package/sanitizer/patterns.d.ts.map +0 -1
- package/lambda-package/sanitizer/patterns.js +0 -372
- package/lambda-package/sanitizer/patterns.js.map +0 -1
- package/lambda-package/sanitizer/pii-allowlist.d.ts +0 -49
- package/lambda-package/sanitizer/pii-allowlist.d.ts.map +0 -1
- package/lambda-package/sanitizer/pii-allowlist.js +0 -231
- package/lambda-package/sanitizer/pii-allowlist.js.map +0 -1
- package/lambda-package/sanitizer/pii-redactor.d.ts +0 -41
- package/lambda-package/sanitizer/pii-redactor.d.ts.map +0 -1
- package/lambda-package/sanitizer/pii-redactor.js +0 -213
- package/lambda-package/sanitizer/pii-redactor.js.map +0 -1
- package/lambda-package/sanitizer/severity-classifier.d.ts +0 -33
- package/lambda-package/sanitizer/severity-classifier.d.ts.map +0 -1
- package/lambda-package/sanitizer/severity-classifier.js +0 -113
- package/lambda-package/sanitizer/severity-classifier.js.map +0 -1
- package/lambda-package/sanitizer/threat-reporter.d.ts +0 -66
- package/lambda-package/sanitizer/threat-reporter.d.ts.map +0 -1
- package/lambda-package/sanitizer/threat-reporter.js +0 -163
- package/lambda-package/sanitizer/threat-reporter.js.map +0 -1
- package/lambda-package/tools/fetch-structured.d.ts +0 -51
- package/lambda-package/tools/fetch-structured.d.ts.map +0 -1
- package/lambda-package/tools/fetch-structured.js +0 -237
- package/lambda-package/tools/fetch-structured.js.map +0 -1
- package/lambda-package/tools/fetch.d.ts +0 -49
- package/lambda-package/tools/fetch.d.ts.map +0 -1
- package/lambda-package/tools/fetch.js +0 -131
- package/lambda-package/tools/fetch.js.map +0 -1
- package/lambda-package/tools/read.d.ts +0 -51
- package/lambda-package/tools/read.d.ts.map +0 -1
- package/lambda-package/tools/read.js +0 -127
- package/lambda-package/tools/read.js.map +0 -1
- package/lambda-package/tools/search.d.ts +0 -45
- package/lambda-package/tools/search.d.ts.map +0 -1
- package/lambda-package/tools/search.js +0 -220
- package/lambda-package/tools/search.js.map +0 -1
- package/lambda-package/types.d.ts +0 -167
- package/lambda-package/types.d.ts.map +0 -1
- package/lambda-package/types.js +0 -16
- package/lambda-package/types.js.map +0 -1
- package/lambda-package/utils/format-converter.d.ts +0 -39
- package/lambda-package/utils/format-converter.d.ts.map +0 -1
- package/lambda-package/utils/format-converter.js +0 -191
- package/lambda-package/utils/format-converter.js.map +0 -1
- package/lambda-package/utils/truncate.d.ts +0 -26
- package/lambda-package/utils/truncate.d.ts.map +0 -1
- package/lambda-package/utils/truncate.js +0 -54
- package/lambda-package/utils/truncate.js.map +0 -1
- package/lambda.zip +0 -0
- package/test-output.txt +0 -4
- package/tests/auth-smoke.test.ts +0 -480
- package/tests/elicit-runner.test.ts +0 -232
- package/tests/fetch-tool.test.ts +0 -922
- package/tests/hitl-gate.test.ts +0 -267
- package/tests/injection-corpus.ts +0 -338
- package/tests/pii-allowlist.test.ts +0 -282
- package/tests/reader.test.ts +0 -353
- package/tests/sanitizer.test.ts +0 -358
- package/tests/search.test.ts +0 -456
- package/tests/threat-reporter.test.ts +0 -334
- package/tsconfig.cdk.json +0 -35
package/tests/auth-smoke.test.ts
DELETED
|
@@ -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
|
-
});
|