@sureshgururajan/aws-console-private-access-validator 1.0.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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # AWS Console Private Access Validator MCP Server
2
+
3
+ An MCP (Model Context Protocol) server that validates CloudFormation templates for AWS Console Private Access requirements.
4
+
5
+ ## Features
6
+
7
+ Validates CloudFormation templates for:
8
+ - Required VPC endpoints (Console, Signin, SSM, EC2Messages, SSMMessages, S3)
9
+ - Endpoint policies restricting access to specific accounts
10
+ - Route53 private hosted zones for console and signin
11
+ - Security group configuration for HTTPS access
12
+ - EC2 instance setup with IAM roles
13
+ - NAT Gateway for private subnet egress
14
+ - Network configuration and routing
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ## Build
23
+
24
+ ```bash
25
+ npm run build
26
+ ```
27
+
28
+ ## Running the MCP Server
29
+
30
+ ```bash
31
+ npm start
32
+ ```
33
+
34
+ The server will listen on stdin/stdout for MCP protocol messages.
35
+
36
+ ## Testing
37
+
38
+ Test the validator against a CloudFormation template:
39
+
40
+ ```bash
41
+ npm test -- /path/to/template.json
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Tool: `validate-cloudformation`
47
+
48
+ Validates a CloudFormation template for AWS Console Private Access requirements.
49
+
50
+ **Input Parameters:**
51
+ - `template` (string, required): CloudFormation template as JSON string
52
+ - `region` (string, optional): AWS region (default: "us-east-1")
53
+
54
+ **Output:**
55
+ ```json
56
+ {
57
+ "valid": true/false,
58
+ "checks": [
59
+ {
60
+ "name": "Check name",
61
+ "status": "pass" | "fail" | "warning",
62
+ "message": "Description of what was checked",
63
+ "details": "Additional details if needed"
64
+ }
65
+ ],
66
+ "summary": "Overall validation summary"
67
+ }
68
+ ```
69
+
70
+ ## Validation Checks
71
+
72
+ 1. **VPC Endpoints** - Verifies all required interface and gateway endpoints exist
73
+ 2. **Endpoint Policies** - Checks that policies are attached and properly configured
74
+ 3. **Route53 Hosted Zones** - Validates private hosted zones for console and signin
75
+ 4. **Security Groups** - Checks for HTTPS (port 443) access rules
76
+ 5. **EC2 Instance** - Verifies instance exists with IAM role (optional)
77
+ 6. **NAT Gateway** - Checks for NAT Gateway for private subnet egress
78
+ 7. **Network Configuration** - Validates private subnets and route tables
79
+
80
+ ## Example
81
+
82
+ ```bash
83
+ # Generate CloudFormation template
84
+ npx cdk synth > template.json
85
+
86
+ # Validate the template
87
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"validate-cloudformation","arguments":{"template":"<template-json>"}}}' | npm start
88
+ ```
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@sureshgururajan/aws-console-private-access-validator",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "MCP server for validating AWS Console Private Access CloudFormation templates",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "test": "node dist/test.js",
11
+ "dev": "ts-node src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^0.5.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0",
18
+ "typescript": "^5.0.0",
19
+ "ts-node": "^10.9.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }
25
+
package/src/index.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ Tool,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+ import { ConsolePrivateAccessValidator } from './validator.js';
9
+ import { CloudFormationTemplate } from './types.js';
10
+
11
+ const server = new Server(
12
+ {
13
+ name: 'aws-console-private-access-validator',
14
+ version: '1.0.0',
15
+ },
16
+ {
17
+ capabilities: {
18
+ tools: {},
19
+ },
20
+ }
21
+ );
22
+
23
+ const tools: Tool[] = [
24
+ {
25
+ name: 'validate-cloudformation',
26
+ description:
27
+ 'Validates a CloudFormation template for AWS Console Private Access requirements',
28
+ inputSchema: {
29
+ type: 'object' as const,
30
+ properties: {
31
+ template: {
32
+ type: 'string',
33
+ description: 'CloudFormation template as JSON string',
34
+ },
35
+ region: {
36
+ type: 'string',
37
+ description: 'AWS region (default: us-east-1)',
38
+ default: 'us-east-1',
39
+ },
40
+ },
41
+ required: ['template'],
42
+ },
43
+ },
44
+ ];
45
+
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
47
+ tools,
48
+ }));
49
+
50
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
51
+ if (request.params.name === 'validate-cloudformation') {
52
+ const { template, region = 'us-east-1' } = request.params.arguments as {
53
+ template: string;
54
+ region?: string;
55
+ };
56
+
57
+ try {
58
+ const parsedTemplate: CloudFormationTemplate = JSON.parse(template);
59
+ const validator = new ConsolePrivateAccessValidator(parsedTemplate, region);
60
+ const result = validator.validate();
61
+
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text: JSON.stringify(result, null, 2),
67
+ },
68
+ ],
69
+ };
70
+ } catch (error) {
71
+ const errorMessage = error instanceof Error ? error.message : String(error);
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: JSON.stringify(
77
+ {
78
+ valid: false,
79
+ checks: [],
80
+ summary: `Error parsing template: ${errorMessage}`,
81
+ },
82
+ null,
83
+ 2
84
+ ),
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ }
91
+
92
+ return {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: `Unknown tool: ${request.params.name}`,
97
+ },
98
+ ],
99
+ isError: true,
100
+ };
101
+ });
102
+
103
+ async function main() {
104
+ const transport = new StdioServerTransport();
105
+ await server.connect(transport);
106
+ }
107
+
108
+ main().catch((error) => {
109
+ process.exit(1);
110
+ });
package/src/test.ts ADDED
@@ -0,0 +1,31 @@
1
+ import fs from 'fs';
2
+ import { ConsolePrivateAccessValidator } from './validator.js';
3
+
4
+ // Read the CloudFormation template
5
+ const templatePath = process.argv[2] || '/tmp/template.json';
6
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
7
+ const template = JSON.parse(templateContent);
8
+
9
+ // Run validation
10
+ const validator = new ConsolePrivateAccessValidator(template, 'us-east-1');
11
+ const result = validator.validate();
12
+
13
+ // Print results
14
+ console.log('\n=== AWS Console Private Access Validation Results ===\n');
15
+ console.log(`Valid: ${result.valid ? '✓ YES' : '✗ NO'}\n`);
16
+
17
+ console.log('Checks:');
18
+ result.checks.forEach((check) => {
19
+ const icon = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : '⚠';
20
+ console.log(` ${icon} ${check.name}`);
21
+ console.log(` Status: ${check.status}`);
22
+ console.log(` Message: ${check.message}`);
23
+ if (check.details) {
24
+ console.log(` Details: ${check.details}`);
25
+ }
26
+ });
27
+
28
+ console.log(`\nSummary: ${result.summary}\n`);
29
+
30
+ // Exit with appropriate code
31
+ process.exit(result.valid ? 0 : 1);
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ export interface ValidationCheck {
2
+ name: string;
3
+ status: 'pass' | 'fail' | 'warning';
4
+ message: string;
5
+ details?: string;
6
+ }
7
+
8
+ export interface ValidationResult {
9
+ valid: boolean;
10
+ checks: ValidationCheck[];
11
+ summary: string;
12
+ }
13
+
14
+ export interface CloudFormationTemplate {
15
+ Resources?: Record<string, any>;
16
+ Outputs?: Record<string, any>;
17
+ Parameters?: Record<string, any>;
18
+ }
@@ -0,0 +1,310 @@
1
+ import { ValidationCheck, ValidationResult, CloudFormationTemplate } from './types';
2
+
3
+ export class ConsolePrivateAccessValidator {
4
+ private template: CloudFormationTemplate;
5
+ private region: string;
6
+ private checks: ValidationCheck[] = [];
7
+
8
+ constructor(template: CloudFormationTemplate, region: string = 'us-east-1') {
9
+ this.template = template;
10
+ this.region = region;
11
+ }
12
+
13
+ validate(): ValidationResult {
14
+ this.checks = [];
15
+
16
+ this.checkVpcEndpoints();
17
+ this.checkEndpointPolicies();
18
+ this.checkRoute53HostedZones();
19
+ this.checkSecurityGroups();
20
+ this.checkEc2Instance();
21
+ this.checkNatGateway();
22
+ this.checkNetworkConfiguration();
23
+
24
+ const failCount = this.checks.filter(c => c.status === 'fail').length;
25
+ const valid = failCount === 0;
26
+
27
+ return {
28
+ valid,
29
+ checks: this.checks,
30
+ summary: this.generateSummary(valid, failCount),
31
+ };
32
+ }
33
+
34
+ private getServiceName(serviceName: any): string | null {
35
+ if (typeof serviceName === 'string') {
36
+ return serviceName;
37
+ }
38
+ if (serviceName?.['Fn::Join']) {
39
+ const parts = serviceName['Fn::Join'][1];
40
+ if (Array.isArray(parts)) {
41
+ // Handle Ref to AWS::Region by replacing with actual region
42
+ return parts
43
+ .map((part: any) => {
44
+ if (typeof part === 'string') {
45
+ return part;
46
+ }
47
+ if (part?.Ref === 'AWS::Region') {
48
+ return this.region;
49
+ }
50
+ return '';
51
+ })
52
+ .join('');
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ private checkVpcEndpoints(): void {
59
+ const requiredEndpoints = [
60
+ { name: 'console', service: `com.amazonaws.${this.region}.console` },
61
+ { name: 'signin', service: `com.amazonaws.${this.region}.signin` },
62
+ { name: 'ssm', service: `com.amazonaws.${this.region}.ssm` },
63
+ { name: 'ec2messages', service: `com.amazonaws.${this.region}.ec2messages` },
64
+ { name: 'ssmmessages', service: `com.amazonaws.${this.region}.ssmmessages` },
65
+ ];
66
+
67
+ const resources = this.template.Resources || {};
68
+ const interfaceEndpoints = Object.values(resources).filter(
69
+ (r: any) => r.Type === 'AWS::EC2::VPCEndpoint' && r.Properties?.VpcEndpointType === 'Interface'
70
+ );
71
+
72
+ for (const endpoint of requiredEndpoints) {
73
+ const found = interfaceEndpoints.some((e: any) => {
74
+ const serviceName = this.getServiceName(e.Properties?.ServiceName);
75
+ return serviceName && serviceName.includes(endpoint.service);
76
+ });
77
+
78
+ this.checks.push({
79
+ name: `VPC Endpoint: ${endpoint.name}`,
80
+ status: found ? 'pass' : 'fail',
81
+ message: found
82
+ ? `Interface VPC endpoint for ${endpoint.name} found`
83
+ : `Missing interface VPC endpoint for ${endpoint.name}`,
84
+ });
85
+ }
86
+
87
+ // Check for S3 Gateway endpoint
88
+ const s3Gateway = Object.values(resources).some(
89
+ (r: any) => {
90
+ const serviceName = this.getServiceName(r.Properties?.ServiceName);
91
+ return (
92
+ r.Type === 'AWS::EC2::VPCEndpoint' &&
93
+ r.Properties?.VpcEndpointType === 'Gateway' &&
94
+ serviceName &&
95
+ serviceName.includes('s3')
96
+ );
97
+ }
98
+ );
99
+
100
+ this.checks.push({
101
+ name: 'VPC Endpoint: S3 Gateway',
102
+ status: s3Gateway ? 'pass' : 'fail',
103
+ message: s3Gateway ? 'S3 Gateway VPC endpoint found' : 'Missing S3 Gateway VPC endpoint',
104
+ });
105
+ }
106
+
107
+ private checkEndpointPolicies(): void {
108
+ const resources = this.template.Resources || {};
109
+ const consoleEndpoint = Object.entries(resources).find(
110
+ ([_, r]: [string, any]) => {
111
+ const serviceName = this.getServiceName(r.Properties?.ServiceName);
112
+ return (
113
+ r.Type === 'AWS::EC2::VPCEndpoint' &&
114
+ serviceName &&
115
+ serviceName.includes('console')
116
+ );
117
+ }
118
+ );
119
+
120
+ const signinEndpoint = Object.entries(resources).find(
121
+ ([_, r]: [string, any]) => {
122
+ const serviceName = this.getServiceName(r.Properties?.ServiceName);
123
+ return (
124
+ r.Type === 'AWS::EC2::VPCEndpoint' &&
125
+ serviceName &&
126
+ serviceName.includes('signin')
127
+ );
128
+ }
129
+ );
130
+
131
+ for (const [name, endpoint] of [
132
+ ['Console', consoleEndpoint],
133
+ ['Signin', signinEndpoint],
134
+ ]) {
135
+ if (!endpoint) continue;
136
+
137
+ const [_, resource] = endpoint as [string, any];
138
+ const hasPolicy = resource.Properties?.PolicyDocument;
139
+
140
+ this.checks.push({
141
+ name: `Endpoint Policy: ${name}`,
142
+ status: hasPolicy ? 'pass' : 'fail',
143
+ message: hasPolicy
144
+ ? `${name} endpoint has a policy attached`
145
+ : `${name} endpoint is missing a policy`,
146
+ details: hasPolicy
147
+ ? this.validatePolicyContent(resource.Properties.PolicyDocument)
148
+ : undefined,
149
+ });
150
+ }
151
+ }
152
+
153
+ private validatePolicyContent(policy: any): string {
154
+ if (!policy.Statement || policy.Statement.length === 0) {
155
+ return 'Policy has no statements';
156
+ }
157
+
158
+ const statement = policy.Statement[0];
159
+ const hasAccountCondition = statement.Condition?.StringEquals?.['aws:PrincipalAccount'];
160
+
161
+ if (hasAccountCondition) {
162
+ return 'Policy restricts access to specific account(s)';
163
+ }
164
+
165
+ return 'Policy does not restrict access by account';
166
+ }
167
+
168
+ private checkRoute53HostedZones(): void {
169
+ const resources = this.template.Resources || {};
170
+ const hostedZones = Object.values(resources).filter(
171
+ (r: any) => r.Type === 'AWS::Route53::HostedZone'
172
+ );
173
+
174
+ const requiredZones = ['console.aws.amazon.com', 'signin.aws.amazon.com'];
175
+
176
+ for (const zone of requiredZones) {
177
+ const found = hostedZones.some((hz: any) => {
178
+ const zoneName = hz.Properties?.Name;
179
+ // Route53 zone names may have a trailing dot
180
+ return zoneName === zone || zoneName === `${zone}.`;
181
+ });
182
+
183
+ this.checks.push({
184
+ name: `Route53 Hosted Zone: ${zone}`,
185
+ status: found ? 'pass' : 'fail',
186
+ message: found
187
+ ? `Private hosted zone for ${zone} found`
188
+ : `Missing private hosted zone for ${zone}`,
189
+ });
190
+ }
191
+
192
+ // Check for Route53 records
193
+ const recordSets = Object.values(resources).filter(
194
+ (r: any) => r.Type === 'AWS::Route53::RecordSet'
195
+ );
196
+
197
+ this.checks.push({
198
+ name: 'Route53 Records',
199
+ status: recordSets.length > 0 ? 'pass' : 'warning',
200
+ message:
201
+ recordSets.length > 0
202
+ ? `Found ${recordSets.length} Route53 records`
203
+ : 'No Route53 records found',
204
+ });
205
+ }
206
+
207
+ private checkSecurityGroups(): void {
208
+ const resources = this.template.Resources || {};
209
+ const securityGroups = Object.values(resources).filter(
210
+ (r: any) => r.Type === 'AWS::EC2::SecurityGroup'
211
+ );
212
+
213
+ const hasHttpsIngress = securityGroups.some((sg: any) => {
214
+ const ingress = sg.Properties?.SecurityGroupIngress || [];
215
+ return ingress.some(
216
+ (rule: any) =>
217
+ (rule.FromPort === 443 || rule.IpProtocol === 'tcp') &&
218
+ (rule.ToPort === 443 || rule.IpProtocol === 'tcp')
219
+ );
220
+ });
221
+
222
+ this.checks.push({
223
+ name: 'Security Group: HTTPS Access',
224
+ status: hasHttpsIngress ? 'pass' : 'warning',
225
+ message: hasHttpsIngress
226
+ ? 'Security group allows HTTPS (port 443) traffic'
227
+ : 'No security group rule found for HTTPS (port 443)',
228
+ });
229
+ }
230
+
231
+ private checkEc2Instance(): void {
232
+ const resources = this.template.Resources || {};
233
+ const instance = Object.values(resources).find((r: any) => r.Type === 'AWS::EC2::Instance');
234
+
235
+ this.checks.push({
236
+ name: 'EC2 Instance',
237
+ status: instance ? 'pass' : 'warning',
238
+ message: instance ? 'EC2 instance found' : 'No EC2 instance found (optional)',
239
+ });
240
+
241
+ if (instance) {
242
+ const hasIamRole = (instance as any).Properties?.IamInstanceProfile;
243
+ this.checks.push({
244
+ name: 'EC2 IAM Role',
245
+ status: hasIamRole ? 'pass' : 'warning',
246
+ message: hasIamRole
247
+ ? 'EC2 instance has IAM instance profile'
248
+ : 'EC2 instance missing IAM instance profile',
249
+ });
250
+ }
251
+ }
252
+
253
+ private checkNatGateway(): void {
254
+ const resources = this.template.Resources || {};
255
+ const natGateway = Object.values(resources).find((r: any) => r.Type === 'AWS::EC2::NatGateway');
256
+
257
+ this.checks.push({
258
+ name: 'NAT Gateway',
259
+ status: natGateway ? 'pass' : 'warning',
260
+ message: natGateway
261
+ ? 'NAT Gateway found for private subnet egress'
262
+ : 'No NAT Gateway found (required for private subnet internet access)',
263
+ });
264
+ }
265
+
266
+ private checkNetworkConfiguration(): void {
267
+ const resources = this.template.Resources || {};
268
+
269
+ // Check for private subnets
270
+ const privateSubnets = Object.values(resources).filter(
271
+ (r: any) =>
272
+ r.Type === 'AWS::EC2::Subnet' &&
273
+ !r.Properties?.MapPublicIpOnLaunch
274
+ );
275
+
276
+ this.checks.push({
277
+ name: 'Private Subnets',
278
+ status: privateSubnets.length > 0 ? 'pass' : 'fail',
279
+ message:
280
+ privateSubnets.length > 0
281
+ ? `Found ${privateSubnets.length} private subnet(s)`
282
+ : 'No private subnets found',
283
+ });
284
+
285
+ // Check for route tables
286
+ const routeTables = Object.values(resources).filter(
287
+ (r: any) => r.Type === 'AWS::EC2::RouteTable'
288
+ );
289
+
290
+ this.checks.push({
291
+ name: 'Route Tables',
292
+ status: routeTables.length > 0 ? 'pass' : 'warning',
293
+ message:
294
+ routeTables.length > 0
295
+ ? `Found ${routeTables.length} route table(s)`
296
+ : 'No route tables found',
297
+ });
298
+ }
299
+
300
+ private generateSummary(valid: boolean, failCount: number): string {
301
+ const passCount = this.checks.filter(c => c.status === 'pass').length;
302
+ const warningCount = this.checks.filter(c => c.status === 'warning').length;
303
+
304
+ if (valid) {
305
+ return `✓ Validation passed. All required checks passed (${passCount} passed, ${warningCount} warnings).`;
306
+ }
307
+
308
+ return `✗ Validation failed. ${failCount} check(s) failed, ${passCount} passed, ${warningCount} warnings.`;
309
+ }
310
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "esnext",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules"]
20
+ }