@sureshgururajan/aws-console-private-access-validator 1.0.3 → 1.0.5
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 +88 -88
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test.d.ts +0 -0
- package/dist/test.d.ts.map +0 -0
- package/dist/test.js +0 -0
- package/dist/test.js.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/validator.d.ts +0 -0
- package/dist/validator.d.ts.map +0 -0
- package/dist/validator.js +3 -1
- package/dist/validator.js.map +1 -1
- package/package.json +30 -29
- package/src/index.d.ts +1 -0
- package/src/index.js +85 -0
- package/src/index.ts +111 -110
- package/src/test.d.ts +1 -0
- package/src/test.js +26 -0
- package/src/test.ts +31 -31
- package/src/types.d.ts +16 -0
- package/src/types.js +2 -0
- package/src/types.ts +18 -18
- package/src/validator.d.ts +18 -0
- package/src/validator.js +236 -0
- package/src/validator.ts +310 -310
- package/tsconfig.json +20 -20
package/src/validator.ts
CHANGED
|
@@ -1,310 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"module": "
|
|
5
|
-
"lib": ["
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
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
|
+
}
|