@friggframework/devtools 2.0.0--canary.428.7665532.0 โ 2.0.0--canary.428.5364e8f.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.
|
@@ -510,6 +510,8 @@ class AWSDiscovery {
|
|
|
510
510
|
console.warn('This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes');
|
|
511
511
|
console.warn('Consider enabling selfHeal: true to fix this automatically');
|
|
512
512
|
// Return it anyway if it's Frigg-managed - we can fix the routes
|
|
513
|
+
// Mark that it's in a private subnet
|
|
514
|
+
natGateway._isInPrivateSubnet = true;
|
|
513
515
|
return natGateway;
|
|
514
516
|
} else {
|
|
515
517
|
console.warn('NAT Gateways MUST be placed in public subnets with Internet Gateway routes');
|
|
@@ -520,11 +522,13 @@ class AWSDiscovery {
|
|
|
520
522
|
|
|
521
523
|
if (isFriggNat) {
|
|
522
524
|
console.log(`Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId}`);
|
|
525
|
+
natGateway._isInPrivateSubnet = false;
|
|
523
526
|
return natGateway;
|
|
524
527
|
}
|
|
525
528
|
|
|
526
529
|
// Return first valid NAT Gateway that's in a public subnet
|
|
527
530
|
console.log(`Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId}`);
|
|
531
|
+
natGateway._isInPrivateSubnet = false;
|
|
528
532
|
return natGateway;
|
|
529
533
|
}
|
|
530
534
|
|
|
@@ -588,37 +592,99 @@ class AWSDiscovery {
|
|
|
588
592
|
*/
|
|
589
593
|
async findDefaultKmsKey() {
|
|
590
594
|
try {
|
|
595
|
+
// Log AWS account and region info for verification
|
|
596
|
+
console.log(`[KMS Discovery] Running in region: ${this.region}`);
|
|
597
|
+
try {
|
|
598
|
+
const accountId = await this.getAccountId();
|
|
599
|
+
console.log(`[KMS Discovery] AWS Account ID: ${accountId}`);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.warn('[KMS Discovery] Could not retrieve account ID:', error.message);
|
|
602
|
+
}
|
|
603
|
+
|
|
591
604
|
const command = new ListKeysCommand({});
|
|
592
605
|
const response = await this.kmsClient.send(command);
|
|
593
|
-
|
|
606
|
+
|
|
594
607
|
if (!response.Keys || response.Keys.length === 0) {
|
|
595
|
-
console.log('No KMS keys found in account');
|
|
608
|
+
console.log('[KMS Discovery] No KMS keys found in account');
|
|
596
609
|
return null;
|
|
597
610
|
}
|
|
598
611
|
|
|
612
|
+
console.log(`[KMS Discovery] Found ${response.Keys.length} total keys in account`);
|
|
613
|
+
let keysExamined = 0;
|
|
614
|
+
let customerManagedKeys = 0;
|
|
615
|
+
let enabledKeys = 0;
|
|
616
|
+
let pendingDeletionKeys = 0;
|
|
617
|
+
|
|
599
618
|
// Look for customer managed keys first
|
|
600
619
|
for (const key of response.Keys) {
|
|
601
620
|
try {
|
|
602
621
|
const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
|
|
603
622
|
const keyDetails = await this.kmsClient.send(describeCommand);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
keyDetails.KeyMetadata
|
|
608
|
-
|
|
609
|
-
|
|
623
|
+
keysExamined++;
|
|
624
|
+
|
|
625
|
+
if (keyDetails.KeyMetadata) {
|
|
626
|
+
const metadata = keyDetails.KeyMetadata;
|
|
627
|
+
|
|
628
|
+
// Log detailed key information
|
|
629
|
+
console.log(`[KMS Discovery] Key ${key.KeyId}:`, {
|
|
630
|
+
KeyManager: metadata.KeyManager,
|
|
631
|
+
KeyState: metadata.KeyState,
|
|
632
|
+
Enabled: metadata.Enabled,
|
|
633
|
+
DeletionDate: metadata.DeletionDate || 'Not scheduled for deletion',
|
|
634
|
+
Arn: metadata.Arn
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (metadata.KeyManager === 'CUSTOMER') {
|
|
638
|
+
customerManagedKeys++;
|
|
639
|
+
|
|
640
|
+
if (metadata.KeyState === 'Enabled') {
|
|
641
|
+
enabledKeys++;
|
|
642
|
+
} else if (metadata.KeyState === 'PendingDeletion') {
|
|
643
|
+
pendingDeletionKeys++;
|
|
644
|
+
console.warn(`[KMS Discovery] Skipping key ${key.KeyId} - State: PendingDeletion, DeletionDate: ${metadata.DeletionDate}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Explicitly check for enabled state AND absence of deletion
|
|
648
|
+
if (metadata.KeyManager === 'CUSTOMER' &&
|
|
649
|
+
metadata.KeyState === 'Enabled' &&
|
|
650
|
+
!metadata.DeletionDate) {
|
|
651
|
+
console.log(`[KMS Discovery] Found eligible customer managed KMS key: ${metadata.Arn}`);
|
|
652
|
+
return metadata.Arn;
|
|
653
|
+
} else if (metadata.KeyManager === 'CUSTOMER' &&
|
|
654
|
+
metadata.KeyState === 'Enabled' &&
|
|
655
|
+
metadata.DeletionDate) {
|
|
656
|
+
// This shouldn't happen according to AWS docs, but log it if it does
|
|
657
|
+
console.error(`[KMS Discovery] WARNING: Key ${key.KeyId} has KeyState='Enabled' but DeletionDate is set: ${metadata.DeletionDate}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
610
660
|
}
|
|
611
661
|
} catch (error) {
|
|
612
662
|
// Continue to next key if we can't describe this one
|
|
613
|
-
console.warn(`Could not describe key ${key.KeyId}:`, error.message);
|
|
663
|
+
console.warn(`[KMS Discovery] Could not describe key ${key.KeyId}:`, error.message);
|
|
614
664
|
continue;
|
|
615
665
|
}
|
|
616
666
|
}
|
|
617
667
|
|
|
618
|
-
|
|
668
|
+
// Summary logging
|
|
669
|
+
console.log('[KMS Discovery] Summary:', {
|
|
670
|
+
totalKeys: response.Keys.length,
|
|
671
|
+
keysExamined: keysExamined,
|
|
672
|
+
customerManagedKeys: customerManagedKeys,
|
|
673
|
+
enabledKeys: enabledKeys,
|
|
674
|
+
pendingDeletionKeys: pendingDeletionKeys
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (customerManagedKeys === 0) {
|
|
678
|
+
console.log('[KMS Discovery] No customer managed KMS keys found in account');
|
|
679
|
+
} else if (enabledKeys === 0) {
|
|
680
|
+
console.warn('[KMS Discovery] Found customer managed keys but none are in Enabled state');
|
|
681
|
+
} else {
|
|
682
|
+
console.warn('[KMS Discovery] Found enabled customer managed keys but none met all criteria');
|
|
683
|
+
}
|
|
684
|
+
|
|
619
685
|
return null;
|
|
620
686
|
} catch (error) {
|
|
621
|
-
console.error('Error finding default KMS key:', error);
|
|
687
|
+
console.error('[KMS Discovery] Error finding default KMS key:', error);
|
|
622
688
|
return null;
|
|
623
689
|
}
|
|
624
690
|
}
|
|
@@ -888,9 +954,13 @@ class AWSDiscovery {
|
|
|
888
954
|
const existingNatGateway = await this.findExistingNatGateway(vpc.VpcId);
|
|
889
955
|
let natGatewayId = null;
|
|
890
956
|
let elasticIpAllocationId = null;
|
|
891
|
-
|
|
957
|
+
let natGatewayInPrivateSubnet = false;
|
|
958
|
+
|
|
892
959
|
if (existingNatGateway) {
|
|
893
960
|
natGatewayId = existingNatGateway.NatGatewayId;
|
|
961
|
+
// Check if NAT Gateway is in a private subnet (from our detection)
|
|
962
|
+
natGatewayInPrivateSubnet = existingNatGateway._isInPrivateSubnet || false;
|
|
963
|
+
|
|
894
964
|
// Get the EIP allocation ID from the NAT Gateway
|
|
895
965
|
if (existingNatGateway.NatGatewayAddresses && existingNatGateway.NatGatewayAddresses.length > 0) {
|
|
896
966
|
elasticIpAllocationId = existingNatGateway.NatGatewayAddresses[0].AllocationId;
|
|
@@ -949,9 +1019,18 @@ class AWSDiscovery {
|
|
|
949
1019
|
defaultKmsKeyId: kmsKeyArn,
|
|
950
1020
|
existingNatGatewayId: natGatewayId,
|
|
951
1021
|
existingElasticIpAllocationId: elasticIpAllocationId,
|
|
1022
|
+
natGatewayInPrivateSubnet: natGatewayInPrivateSubnet,
|
|
952
1023
|
subnetConversionRequired: subnetStatus.requiresConversion,
|
|
953
|
-
privateSubnetsWithWrongRoutes:
|
|
954
|
-
|
|
1024
|
+
privateSubnetsWithWrongRoutes: (() => {
|
|
1025
|
+
const wrongRoutes = [];
|
|
1026
|
+
if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
|
|
1027
|
+
wrongRoutes.push(privateSubnets[0].SubnetId);
|
|
1028
|
+
}
|
|
1029
|
+
if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
|
|
1030
|
+
wrongRoutes.push(privateSubnets[1].SubnetId);
|
|
1031
|
+
}
|
|
1032
|
+
return wrongRoutes;
|
|
1033
|
+
})()
|
|
955
1034
|
};
|
|
956
1035
|
} catch (error) {
|
|
957
1036
|
console.error('Error discovering AWS resources:', error);
|
|
@@ -1,446 +1,545 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { mockClient } = require('aws-sdk-client-mock');
|
|
2
2
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
// Import AWS SDK commands
|
|
5
|
+
const {
|
|
6
|
+
EC2Client,
|
|
7
|
+
DescribeVpcsCommand,
|
|
8
|
+
DescribeSubnetsCommand,
|
|
9
|
+
DescribeSecurityGroupsCommand,
|
|
10
|
+
DescribeRouteTablesCommand,
|
|
11
|
+
DescribeNatGatewaysCommand,
|
|
12
|
+
DescribeAddressesCommand
|
|
13
|
+
} = require('@aws-sdk/client-ec2');
|
|
14
|
+
const {
|
|
15
|
+
KMSClient,
|
|
16
|
+
ListKeysCommand,
|
|
17
|
+
DescribeKeyCommand
|
|
18
|
+
} = require('@aws-sdk/client-kms');
|
|
19
|
+
const {
|
|
20
|
+
STSClient,
|
|
21
|
+
GetCallerIdentityCommand
|
|
22
|
+
} = require('@aws-sdk/client-sts');
|
|
23
|
+
|
|
24
|
+
// Create mock clients
|
|
25
|
+
const ec2Mock = mockClient(EC2Client);
|
|
26
|
+
const kmsMock = mockClient(KMSClient);
|
|
27
|
+
const stsMock = mockClient(STSClient);
|
|
12
28
|
|
|
13
29
|
describe('AWSDiscovery', () => {
|
|
14
30
|
let discovery;
|
|
15
|
-
let mockEC2Send;
|
|
16
|
-
let mockKMSSend;
|
|
17
|
-
let mockSTSSend;
|
|
18
31
|
|
|
19
32
|
beforeEach(() => {
|
|
33
|
+
// Reset all mocks before each test
|
|
34
|
+
ec2Mock.reset();
|
|
35
|
+
kmsMock.reset();
|
|
36
|
+
stsMock.reset();
|
|
37
|
+
|
|
20
38
|
discovery = new AWSDiscovery('us-east-1');
|
|
21
|
-
|
|
22
|
-
// Create mock send functions
|
|
23
|
-
mockEC2Send = jest.fn();
|
|
24
|
-
mockKMSSend = jest.fn();
|
|
25
|
-
mockSTSSend = jest.fn();
|
|
26
|
-
|
|
27
|
-
// Mock the client constructors and send methods
|
|
28
|
-
EC2Client.mockImplementation(() => ({
|
|
29
|
-
send: mockEC2Send
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
KMSClient.mockImplementation(() => ({
|
|
33
|
-
send: mockKMSSend
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
STSClient.mockImplementation(() => ({
|
|
37
|
-
send: mockSTSSend
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// Reset mocks
|
|
41
|
-
jest.clearAllMocks();
|
|
42
39
|
});
|
|
43
40
|
|
|
44
41
|
describe('getAccountId', () => {
|
|
45
42
|
it('should return AWS account ID', async () => {
|
|
46
43
|
const mockAccountId = '123456789012';
|
|
47
|
-
|
|
44
|
+
stsMock.on(GetCallerIdentityCommand).resolves({
|
|
48
45
|
Account: mockAccountId
|
|
49
46
|
});
|
|
50
47
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
expect(result).toBe(mockAccountId);
|
|
54
|
-
expect(mockSTSSend).toHaveBeenCalledWith(expect.any(GetCallerIdentityCommand));
|
|
48
|
+
const accountId = await discovery.getAccountId();
|
|
49
|
+
expect(accountId).toBe(mockAccountId);
|
|
55
50
|
});
|
|
56
51
|
|
|
57
52
|
it('should throw error when STS call fails', async () => {
|
|
58
|
-
|
|
59
|
-
mockSTSSend.mockRejectedValue(error);
|
|
53
|
+
stsMock.on(GetCallerIdentityCommand).rejects(new Error('STS error'));
|
|
60
54
|
|
|
61
|
-
await expect(discovery.getAccountId()).rejects.toThrow('STS
|
|
55
|
+
await expect(discovery.getAccountId()).rejects.toThrow('STS error');
|
|
62
56
|
});
|
|
63
57
|
});
|
|
64
58
|
|
|
65
59
|
describe('findDefaultVpc', () => {
|
|
66
|
-
it('should return default VPC when
|
|
67
|
-
const mockVpc = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
60
|
+
it('should return default VPC when available', async () => {
|
|
61
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: true };
|
|
62
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
63
|
+
Vpcs: [mockVpc]
|
|
64
|
+
});
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
const vpc = await discovery.findDefaultVpc();
|
|
67
|
+
expect(vpc).toEqual(mockVpc);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return first VPC when no default VPC exists', async () => {
|
|
71
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: false };
|
|
72
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
74
73
|
Vpcs: [mockVpc]
|
|
75
74
|
});
|
|
76
75
|
|
|
77
|
-
const
|
|
76
|
+
const vpc = await discovery.findDefaultVpc();
|
|
77
|
+
expect(vpc).toEqual(mockVpc);
|
|
78
|
+
});
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
80
|
+
it('should throw error when no VPCs found', async () => {
|
|
81
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
82
|
+
Vpcs: []
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await expect(discovery.findDefaultVpc()).rejects.toThrow('No VPC found in the account');
|
|
88
86
|
});
|
|
87
|
+
});
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
IsDefault: false,
|
|
94
|
-
State: 'available'
|
|
95
|
-
};
|
|
89
|
+
describe('isSubnetPrivate', () => {
|
|
90
|
+
const mockVpcId = 'vpc-12345678';
|
|
91
|
+
const mockSubnetId = 'subnet-12345678';
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
it('should return true for private subnet', async () => {
|
|
94
|
+
// Mock subnet lookup first
|
|
95
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
96
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
97
|
+
});
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
100
|
+
RouteTables: [{
|
|
101
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
102
|
+
Routes: [
|
|
103
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
104
|
+
]
|
|
105
|
+
}]
|
|
106
|
+
});
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
expect(
|
|
108
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
109
|
+
expect(isPrivate).toBe(true);
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
it('should
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
it('should return false for public subnet', async () => {
|
|
113
|
+
// Mock subnet lookup first
|
|
114
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
115
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
116
|
+
});
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
119
|
+
RouteTables: [{
|
|
120
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
121
|
+
Routes: [
|
|
122
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
123
|
+
]
|
|
124
|
+
}]
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
128
|
+
expect(isPrivate).toBe(false);
|
|
113
129
|
});
|
|
114
130
|
});
|
|
115
131
|
|
|
116
132
|
describe('findPrivateSubnets', () => {
|
|
117
133
|
const mockVpcId = 'vpc-12345678';
|
|
118
134
|
|
|
119
|
-
it('should return private subnets
|
|
135
|
+
it('should return private subnets', async () => {
|
|
120
136
|
const mockSubnets = [
|
|
121
|
-
{ SubnetId: 'subnet-private-1',
|
|
122
|
-
{ SubnetId: 'subnet-private-2',
|
|
137
|
+
{ SubnetId: 'subnet-private-1', AvailabilityZone: 'us-east-1a' },
|
|
138
|
+
{ SubnetId: 'subnet-private-2', AvailabilityZone: 'us-east-1b' }
|
|
123
139
|
];
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
129
|
-
})
|
|
130
|
-
.mockResolvedValueOnce({ // Route tables for private-1
|
|
131
|
-
RouteTables: [{
|
|
132
|
-
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
133
|
-
Routes: [{ GatewayId: 'local' }] // No IGW = private
|
|
134
|
-
}]
|
|
135
|
-
})
|
|
136
|
-
.mockResolvedValueOnce({ // Check subnet-private-2
|
|
137
|
-
Subnets: [{ SubnetId: 'subnet-private-2', VpcId: mockVpcId }]
|
|
138
|
-
})
|
|
139
|
-
.mockResolvedValueOnce({ // Route tables for private-2
|
|
140
|
-
RouteTables: [{
|
|
141
|
-
Associations: [{ SubnetId: 'subnet-private-2' }],
|
|
142
|
-
Routes: [{ GatewayId: 'local' }] // No IGW = private
|
|
143
|
-
}]
|
|
144
|
-
});
|
|
141
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
142
|
+
Subnets: mockSubnets
|
|
143
|
+
});
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
// Mock route tables - no IGW routes (private)
|
|
146
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
147
|
+
RouteTables: [{
|
|
148
|
+
Associations: [
|
|
149
|
+
{ SubnetId: 'subnet-private-1' },
|
|
150
|
+
{ SubnetId: 'subnet-private-2' }
|
|
151
|
+
],
|
|
152
|
+
Routes: [
|
|
153
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
154
|
+
]
|
|
155
|
+
}]
|
|
156
|
+
});
|
|
147
157
|
|
|
148
|
-
|
|
149
|
-
expect(
|
|
150
|
-
expect(result[1].SubnetId).toBe('subnet-private-2');
|
|
158
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId);
|
|
159
|
+
expect(subnets).toEqual(mockSubnets);
|
|
151
160
|
});
|
|
152
161
|
|
|
153
|
-
it('should
|
|
162
|
+
it('should throw error when no private subnets found and autoConvert is false', async () => {
|
|
154
163
|
const mockSubnets = [
|
|
155
|
-
{ SubnetId: 'subnet-public-1',
|
|
156
|
-
{ SubnetId: 'subnet-public-2',
|
|
157
|
-
{ SubnetId: 'subnet-public-3',
|
|
164
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
165
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' },
|
|
166
|
+
{ SubnetId: 'subnet-public-3', AvailabilityZone: 'us-east-1c' }
|
|
158
167
|
];
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.mockResolvedValueOnce({ Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }] })
|
|
164
|
-
.mockResolvedValueOnce({ // Route table for public-1
|
|
165
|
-
RouteTables: [{
|
|
166
|
-
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
167
|
-
Routes: [{ GatewayId: 'igw-12345' }] // IGW = public
|
|
168
|
-
}]
|
|
169
|
-
})
|
|
170
|
-
.mockResolvedValueOnce({ Subnets: [{ SubnetId: 'subnet-public-2', VpcId: mockVpcId }] })
|
|
171
|
-
.mockResolvedValueOnce({ // Route table for public-2
|
|
172
|
-
RouteTables: [{
|
|
173
|
-
Associations: [{ SubnetId: 'subnet-public-2' }],
|
|
174
|
-
Routes: [{ GatewayId: 'igw-12345' }] // IGW = public
|
|
175
|
-
}]
|
|
176
|
-
})
|
|
177
|
-
.mockResolvedValueOnce({ Subnets: [{ SubnetId: 'subnet-public-3', VpcId: mockVpcId }] })
|
|
178
|
-
.mockResolvedValueOnce({ // Route table for public-3
|
|
179
|
-
RouteTables: [{
|
|
180
|
-
Associations: [{ SubnetId: 'subnet-public-3' }],
|
|
181
|
-
Routes: [{ GatewayId: 'igw-12345' }] // IGW = public
|
|
182
|
-
}]
|
|
183
|
-
});
|
|
169
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
170
|
+
Subnets: mockSubnets
|
|
171
|
+
});
|
|
184
172
|
|
|
185
|
-
//
|
|
186
|
-
|
|
173
|
+
// Mock route tables - has IGW routes (public)
|
|
174
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
175
|
+
RouteTables: [{
|
|
176
|
+
Associations: [
|
|
177
|
+
{ SubnetId: 'subnet-public-1' },
|
|
178
|
+
{ SubnetId: 'subnet-public-2' },
|
|
179
|
+
{ SubnetId: 'subnet-public-3' }
|
|
180
|
+
],
|
|
181
|
+
Routes: [
|
|
182
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
183
|
+
]
|
|
184
|
+
}]
|
|
185
|
+
});
|
|
187
186
|
|
|
188
|
-
expect(
|
|
189
|
-
|
|
190
|
-
expect(result[0].SubnetId).toBe('subnet-public-2');
|
|
191
|
-
expect(result[1].SubnetId).toBe('subnet-public-3');
|
|
187
|
+
await expect(discovery.findPrivateSubnets(mockVpcId, false))
|
|
188
|
+
.rejects.toThrow('No private subnets found in VPC');
|
|
192
189
|
});
|
|
193
190
|
|
|
194
|
-
it('should
|
|
191
|
+
it('should return public subnets with warning when autoConvert is true', async () => {
|
|
195
192
|
const mockSubnets = [
|
|
196
|
-
{ SubnetId: 'subnet-public-1',
|
|
197
|
-
{ SubnetId: 'subnet-public-2',
|
|
193
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
194
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' }
|
|
198
195
|
];
|
|
199
196
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
197
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
198
|
+
Subnets: mockSubnets
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Mock route tables - has IGW routes (public)
|
|
202
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
203
|
+
RouteTables: [{
|
|
204
|
+
Associations: [
|
|
205
|
+
{ SubnetId: 'subnet-public-1' },
|
|
206
|
+
{ SubnetId: 'subnet-public-2' }
|
|
207
|
+
],
|
|
208
|
+
Routes: [
|
|
209
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
210
|
+
]
|
|
211
|
+
}]
|
|
212
|
+
});
|
|
216
213
|
|
|
217
|
-
await
|
|
218
|
-
|
|
219
|
-
);
|
|
214
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId, true);
|
|
215
|
+
expect(subnets).toHaveLength(2);
|
|
216
|
+
expect(subnets[0].SubnetId).toBe('subnet-public-1');
|
|
220
217
|
});
|
|
218
|
+
});
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{ SubnetId: 'subnet-private-1', VpcId: mockVpcId, AvailabilityZone: 'us-east-1a' },
|
|
225
|
-
{ SubnetId: 'subnet-public-1', VpcId: mockVpcId, AvailabilityZone: 'us-east-1b' },
|
|
226
|
-
{ SubnetId: 'subnet-public-2', VpcId: mockVpcId, AvailabilityZone: 'us-east-1c' }
|
|
227
|
-
];
|
|
220
|
+
describe('findPublicSubnets', () => {
|
|
221
|
+
const mockVpcId = 'vpc-12345678';
|
|
228
222
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
235
|
-
Routes: [{ GatewayId: 'local' }] // No IGW
|
|
236
|
-
}]
|
|
237
|
-
})
|
|
238
|
-
.mockResolvedValueOnce({ Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }] })
|
|
239
|
-
.mockResolvedValueOnce({ // Public route table
|
|
240
|
-
RouteTables: [{
|
|
241
|
-
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
242
|
-
Routes: [{ GatewayId: 'igw-12345' }]
|
|
243
|
-
}]
|
|
244
|
-
})
|
|
245
|
-
.mockResolvedValueOnce({ Subnets: [{ SubnetId: 'subnet-public-2', VpcId: mockVpcId }] })
|
|
246
|
-
.mockResolvedValueOnce({ // Public route table
|
|
247
|
-
RouteTables: [{
|
|
248
|
-
Associations: [{ SubnetId: 'subnet-public-2' }],
|
|
249
|
-
Routes: [{ GatewayId: 'igw-12345' }]
|
|
250
|
-
}]
|
|
251
|
-
});
|
|
223
|
+
it('should return public subnet', async () => {
|
|
224
|
+
const mockSubnet = { SubnetId: 'subnet-public-1' };
|
|
225
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
226
|
+
Subnets: [mockSubnet]
|
|
227
|
+
});
|
|
252
228
|
|
|
253
|
-
|
|
229
|
+
// Mock route table check to show it's public
|
|
230
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
231
|
+
RouteTables: [{
|
|
232
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
233
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Has IGW = public
|
|
234
|
+
}]
|
|
235
|
+
});
|
|
254
236
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
expect(result[0].SubnetId).toBe('subnet-private-1');
|
|
258
|
-
expect(result[1].SubnetId).toBe('subnet-public-1');
|
|
237
|
+
const subnet = await discovery.findPublicSubnets(mockVpcId);
|
|
238
|
+
expect(subnet).toEqual(mockSubnet);
|
|
259
239
|
});
|
|
260
240
|
|
|
261
241
|
it('should throw error when no subnets found', async () => {
|
|
262
|
-
|
|
242
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
243
|
+
Subnets: []
|
|
244
|
+
});
|
|
263
245
|
|
|
264
|
-
await expect(discovery.
|
|
246
|
+
await expect(discovery.findPublicSubnets(mockVpcId))
|
|
247
|
+
.rejects.toThrow('No subnets found in VPC');
|
|
265
248
|
});
|
|
266
249
|
});
|
|
267
250
|
|
|
268
|
-
describe('
|
|
269
|
-
const mockSubnetId = 'subnet-12345678';
|
|
251
|
+
describe('findDefaultSecurityGroup', () => {
|
|
270
252
|
const mockVpcId = 'vpc-12345678';
|
|
271
253
|
|
|
272
|
-
it('should return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.mockResolvedValueOnce({ // DescribeRouteTables
|
|
278
|
-
RouteTables: [{
|
|
279
|
-
Associations: [{ SubnetId: mockSubnetId }],
|
|
280
|
-
Routes: [{
|
|
281
|
-
GatewayId: 'igw-12345678',
|
|
282
|
-
DestinationCidrBlock: '0.0.0.0/0'
|
|
283
|
-
}]
|
|
284
|
-
}]
|
|
285
|
-
});
|
|
254
|
+
it('should return default security group', async () => {
|
|
255
|
+
const mockSecurityGroup = {
|
|
256
|
+
GroupId: 'sg-12345678',
|
|
257
|
+
GroupName: 'default'
|
|
258
|
+
};
|
|
286
259
|
|
|
287
|
-
|
|
260
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
261
|
+
SecurityGroups: [mockSecurityGroup]
|
|
262
|
+
});
|
|
288
263
|
|
|
289
|
-
|
|
264
|
+
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
265
|
+
expect(sg).toEqual(mockSecurityGroup);
|
|
290
266
|
});
|
|
291
267
|
|
|
292
|
-
it('should
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
268
|
+
it('should throw error when no default security group found', async () => {
|
|
269
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
270
|
+
SecurityGroups: []
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await expect(discovery.findDefaultSecurityGroup(mockVpcId))
|
|
274
|
+
.rejects.toThrow('No security group found for VPC');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('findPrivateRouteTable', () => {
|
|
279
|
+
const mockVpcId = 'vpc-12345678';
|
|
280
|
+
|
|
281
|
+
it('should return private route table', async () => {
|
|
282
|
+
const mockRouteTable = {
|
|
283
|
+
RouteTableId: 'rtb-12345678',
|
|
284
|
+
Routes: [
|
|
285
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
286
|
+
]
|
|
287
|
+
};
|
|
306
288
|
|
|
307
|
-
|
|
289
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
290
|
+
RouteTables: [mockRouteTable]
|
|
291
|
+
});
|
|
308
292
|
|
|
309
|
-
|
|
293
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
294
|
+
expect(rt).toEqual(mockRouteTable);
|
|
310
295
|
});
|
|
311
296
|
|
|
312
|
-
it('should
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
Associations: [{ Main: true }], // Main route table
|
|
320
|
-
Routes: [{
|
|
321
|
-
GatewayId: 'igw-12345678',
|
|
322
|
-
DestinationCidrBlock: '0.0.0.0/0'
|
|
323
|
-
}]
|
|
324
|
-
}]
|
|
325
|
-
});
|
|
297
|
+
it('should return first route table when no private route table found', async () => {
|
|
298
|
+
const mockRouteTable = {
|
|
299
|
+
RouteTableId: 'rtb-12345678',
|
|
300
|
+
Routes: [
|
|
301
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
302
|
+
]
|
|
303
|
+
};
|
|
326
304
|
|
|
327
|
-
|
|
305
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
306
|
+
RouteTables: [mockRouteTable]
|
|
307
|
+
});
|
|
328
308
|
|
|
329
|
-
|
|
309
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
310
|
+
expect(rt).toEqual(mockRouteTable);
|
|
330
311
|
});
|
|
312
|
+
});
|
|
331
313
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
314
|
+
describe('findDefaultKmsKey', () => {
|
|
315
|
+
it('should return default KMS key ARN', async () => {
|
|
316
|
+
const mockKeyId = '12345678-1234-1234-1234-123456789012';
|
|
317
|
+
const mockKeyArn = `arn:aws:kms:us-east-1:123456789012:key/${mockKeyId}`;
|
|
318
|
+
|
|
319
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
320
|
+
Keys: [{ KeyId: mockKeyId }]
|
|
321
|
+
});
|
|
338
322
|
|
|
339
|
-
|
|
323
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
324
|
+
KeyMetadata: {
|
|
325
|
+
Arn: mockKeyArn,
|
|
326
|
+
KeyManager: 'CUSTOMER',
|
|
327
|
+
KeyState: 'Enabled'
|
|
328
|
+
}
|
|
329
|
+
});
|
|
340
330
|
|
|
341
|
-
|
|
331
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
332
|
+
expect(keyArn).toBe(mockKeyArn);
|
|
342
333
|
});
|
|
343
334
|
|
|
344
|
-
it('should
|
|
345
|
-
|
|
335
|
+
it('should return null when no AWS-managed keys found', async () => {
|
|
336
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
337
|
+
Keys: []
|
|
338
|
+
});
|
|
346
339
|
|
|
347
|
-
await
|
|
348
|
-
|
|
349
|
-
);
|
|
340
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
341
|
+
expect(keyArn).toBeNull();
|
|
350
342
|
});
|
|
351
343
|
});
|
|
352
344
|
|
|
353
|
-
describe('
|
|
345
|
+
describe('findAvailableElasticIP', () => {
|
|
346
|
+
it('should return available Elastic IP', async () => {
|
|
347
|
+
const mockElasticIP = {
|
|
348
|
+
AllocationId: 'eipalloc-12345',
|
|
349
|
+
PublicIp: '52.1.2.3'
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
353
|
+
Addresses: [mockElasticIP]
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
357
|
+
expect(eip).toEqual(mockElasticIP);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should return null when no available Elastic IPs', async () => {
|
|
361
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
362
|
+
Addresses: []
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
366
|
+
expect(eip).toBeNull();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('findExistingNatGateway', () => {
|
|
354
371
|
const mockVpcId = 'vpc-12345678';
|
|
355
372
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
373
|
+
beforeEach(() => {
|
|
374
|
+
// Create a fresh discovery instance for each test
|
|
375
|
+
discovery = new AWSDiscovery('us-east-1');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should return NAT Gateway in public subnet', async () => {
|
|
379
|
+
const mockNatGateway = {
|
|
380
|
+
NatGatewayId: 'nat-12345678',
|
|
381
|
+
SubnetId: 'subnet-public-1',
|
|
382
|
+
State: 'available',
|
|
383
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
384
|
+
Tags: []
|
|
361
385
|
};
|
|
362
386
|
|
|
363
|
-
|
|
364
|
-
|
|
387
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
388
|
+
NatGateways: [mockNatGateway]
|
|
365
389
|
});
|
|
366
390
|
|
|
367
|
-
|
|
391
|
+
// Mock subnet lookup
|
|
392
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
393
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
394
|
+
});
|
|
368
395
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
// Mock route table - has IGW (public)
|
|
397
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
398
|
+
RouteTables: [{
|
|
399
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
400
|
+
Routes: [{ GatewayId: 'igw-12345' }]
|
|
401
|
+
}]
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
405
|
+
|
|
406
|
+
expect(result).toBeDefined();
|
|
407
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
408
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
378
409
|
});
|
|
379
410
|
|
|
380
|
-
it('should
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
411
|
+
it('should detect NAT Gateway in private subnet', async () => {
|
|
412
|
+
const mockNatGateway = {
|
|
413
|
+
NatGatewayId: 'nat-12345678',
|
|
414
|
+
SubnetId: 'subnet-private-1',
|
|
415
|
+
State: 'available',
|
|
416
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
417
|
+
Tags: [
|
|
418
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
419
|
+
]
|
|
385
420
|
};
|
|
386
421
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
422
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
423
|
+
NatGateways: [mockNatGateway]
|
|
424
|
+
});
|
|
390
425
|
|
|
391
|
-
|
|
426
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
427
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
428
|
+
});
|
|
392
429
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
430
|
+
// Mock route table - no IGW (private)
|
|
431
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
432
|
+
RouteTables: [{
|
|
433
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
434
|
+
Routes: [{ GatewayId: 'local' }]
|
|
435
|
+
}]
|
|
436
|
+
});
|
|
396
437
|
|
|
397
|
-
|
|
398
|
-
mockEC2Send.mockResolvedValue({ SecurityGroups: [] });
|
|
438
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
399
439
|
|
|
400
|
-
|
|
440
|
+
expect(result).toBeDefined();
|
|
441
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
442
|
+
expect(result._isInPrivateSubnet).toBe(true);
|
|
401
443
|
});
|
|
402
|
-
});
|
|
403
444
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
445
|
+
it('should skip non-Frigg NAT Gateway in private subnet', async () => {
|
|
446
|
+
const mockNatGateways = [
|
|
447
|
+
{
|
|
448
|
+
NatGatewayId: 'nat-other-12345',
|
|
449
|
+
SubnetId: 'subnet-private-1',
|
|
450
|
+
State: 'available',
|
|
451
|
+
Tags: [] // No Frigg tags
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
NatGatewayId: 'nat-good-12345',
|
|
455
|
+
SubnetId: 'subnet-public-1',
|
|
456
|
+
State: 'available',
|
|
457
|
+
Tags: []
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
462
|
+
NatGateways: mockNatGateways
|
|
463
|
+
});
|
|
408
464
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
465
|
+
// First call for subnet-private-1
|
|
466
|
+
ec2Mock.on(DescribeSubnetsCommand)
|
|
467
|
+
.resolvesOnce({
|
|
468
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
412
469
|
})
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
Arn: mockKeyArn,
|
|
417
|
-
KeyManager: 'CUSTOMER',
|
|
418
|
-
KeyState: 'Enabled'
|
|
419
|
-
}
|
|
470
|
+
// Second call for subnet-public-1
|
|
471
|
+
.resolvesOnce({
|
|
472
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
420
473
|
});
|
|
421
474
|
|
|
422
|
-
|
|
475
|
+
// First call for private subnet route table
|
|
476
|
+
ec2Mock.on(DescribeRouteTablesCommand)
|
|
477
|
+
.resolvesOnce({
|
|
478
|
+
RouteTables: [{
|
|
479
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
480
|
+
Routes: [{ GatewayId: 'local' }] // Private
|
|
481
|
+
}]
|
|
482
|
+
})
|
|
483
|
+
// Second call for public subnet route table
|
|
484
|
+
.resolvesOnce({
|
|
485
|
+
RouteTables: [{
|
|
486
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
487
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
488
|
+
}]
|
|
489
|
+
});
|
|
423
490
|
|
|
424
|
-
const result = await discovery.
|
|
491
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
425
492
|
|
|
426
|
-
expect(result).
|
|
493
|
+
expect(result).toBeDefined();
|
|
494
|
+
expect(result.NatGatewayId).toBe('nat-good-12345');
|
|
495
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
427
496
|
});
|
|
428
497
|
|
|
429
|
-
it('should
|
|
430
|
-
|
|
431
|
-
|
|
498
|
+
it('should prioritize Frigg-managed NAT Gateways', async () => {
|
|
499
|
+
const mockNatGateways = [
|
|
500
|
+
{
|
|
501
|
+
NatGatewayId: 'nat-other-12345',
|
|
502
|
+
SubnetId: 'subnet-public-1',
|
|
503
|
+
State: 'available',
|
|
504
|
+
Tags: []
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
NatGatewayId: 'nat-frigg-12345',
|
|
508
|
+
SubnetId: 'subnet-public-2',
|
|
509
|
+
State: 'available',
|
|
510
|
+
Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }]
|
|
511
|
+
}
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
515
|
+
NatGateways: mockNatGateways
|
|
516
|
+
});
|
|
432
517
|
|
|
433
|
-
|
|
518
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
519
|
+
Subnets: [{ SubnetId: 'subnet-public-2', VpcId: mockVpcId }]
|
|
520
|
+
});
|
|
434
521
|
|
|
435
|
-
|
|
436
|
-
|
|
522
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
523
|
+
RouteTables: [{
|
|
524
|
+
Associations: [{ SubnetId: 'subnet-public-2' }],
|
|
525
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
526
|
+
}]
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
437
530
|
|
|
438
|
-
|
|
439
|
-
|
|
531
|
+
expect(result).toBeDefined();
|
|
532
|
+
expect(result.NatGatewayId).toBe('nat-frigg-12345');
|
|
533
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
534
|
+
});
|
|
440
535
|
|
|
441
|
-
|
|
536
|
+
it('should return null when no NAT Gateways found', async () => {
|
|
537
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
538
|
+
NatGateways: []
|
|
539
|
+
});
|
|
442
540
|
|
|
443
|
-
|
|
541
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
542
|
+
expect(result).toBeNull();
|
|
444
543
|
});
|
|
445
544
|
});
|
|
446
545
|
|
|
@@ -457,7 +556,9 @@ describe('AWSDiscovery', () => {
|
|
|
457
556
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
458
557
|
const mockNatGateway = {
|
|
459
558
|
NatGatewayId: 'nat-12345678',
|
|
460
|
-
|
|
559
|
+
SubnetId: 'subnet-public-1',
|
|
560
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
561
|
+
_isInPrivateSubnet: false
|
|
461
562
|
};
|
|
462
563
|
|
|
463
564
|
// Mock all the discovery methods
|
|
@@ -484,6 +585,7 @@ describe('AWSDiscovery', () => {
|
|
|
484
585
|
defaultKmsKeyId: mockKmsArn,
|
|
485
586
|
existingNatGatewayId: 'nat-12345678',
|
|
486
587
|
existingElasticIpAllocationId: 'eipalloc-12345',
|
|
588
|
+
natGatewayInPrivateSubnet: false,
|
|
487
589
|
subnetConversionRequired: false,
|
|
488
590
|
privateSubnetsWithWrongRoutes: []
|
|
489
591
|
});
|
|
@@ -517,8 +619,10 @@ describe('AWSDiscovery', () => {
|
|
|
517
619
|
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
518
620
|
jest.spyOn(discovery, 'findAvailableElasticIP').mockResolvedValue(null);
|
|
519
621
|
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
520
|
-
.
|
|
521
|
-
|
|
622
|
+
.mockImplementation((subnetId) => {
|
|
623
|
+
// subnet-1 is public, subnet-2 is private
|
|
624
|
+
return Promise.resolve(subnetId === 'subnet-2');
|
|
625
|
+
});
|
|
522
626
|
|
|
523
627
|
const result = await discovery.discoverResources({ selfHeal: true });
|
|
524
628
|
|
|
@@ -560,15 +664,54 @@ describe('AWSDiscovery', () => {
|
|
|
560
664
|
});
|
|
561
665
|
});
|
|
562
666
|
|
|
667
|
+
it('should detect NAT Gateway in private subnet in discoverResources', async () => {
|
|
668
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
669
|
+
const mockSubnets = [
|
|
670
|
+
{ SubnetId: 'subnet-1' },
|
|
671
|
+
{ SubnetId: 'subnet-2' }
|
|
672
|
+
];
|
|
673
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
674
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
675
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
676
|
+
const mockNatGateway = {
|
|
677
|
+
NatGatewayId: 'nat-12345678',
|
|
678
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
679
|
+
_isInPrivateSubnet: true // NAT is in private subnet
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
683
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
684
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
685
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
686
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
687
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
688
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(mockNatGateway);
|
|
689
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
690
|
+
.mockResolvedValueOnce(true)
|
|
691
|
+
.mockResolvedValueOnce(true);
|
|
692
|
+
|
|
693
|
+
const result = await discovery.discoverResources();
|
|
694
|
+
|
|
695
|
+
expect(result).toMatchObject({
|
|
696
|
+
defaultVpcId: 'vpc-12345678',
|
|
697
|
+
existingNatGatewayId: 'nat-12345678',
|
|
698
|
+
natGatewayInPrivateSubnet: true, // Should be true
|
|
699
|
+
subnetConversionRequired: false,
|
|
700
|
+
privateSubnetsWithWrongRoutes: []
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
563
704
|
it('should handle single subnet scenario', async () => {
|
|
564
705
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
565
706
|
const mockSubnets = [{ SubnetId: 'subnet-1' }]; // Only one subnet
|
|
707
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
566
708
|
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
567
709
|
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
568
710
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
569
711
|
|
|
570
712
|
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
571
713
|
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
714
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
572
715
|
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
573
716
|
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
574
717
|
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script to verify subnet detection and classification logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
8
|
+
|
|
9
|
+
async function testSubnetLogic() {
|
|
10
|
+
console.log('๐งช Testing Subnet Detection and Classification Logic');
|
|
11
|
+
console.log('โ'.repeat(60));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const discovery = new AWSDiscovery();
|
|
15
|
+
|
|
16
|
+
// Test with selfHeal enabled
|
|
17
|
+
console.log('\n๐ Testing with selfHeal: true');
|
|
18
|
+
console.log('-'.repeat(40));
|
|
19
|
+
|
|
20
|
+
const resources = await discovery.discoverResources({ selfHeal: true });
|
|
21
|
+
|
|
22
|
+
console.log('\n๐ Results:');
|
|
23
|
+
console.log(` VPC ID: ${resources.defaultVpcId}`);
|
|
24
|
+
console.log(` Private Subnet 1: ${resources.privateSubnetId1}`);
|
|
25
|
+
console.log(` Private Subnet 2: ${resources.privateSubnetId2}`);
|
|
26
|
+
console.log(` Public Subnet: ${resources.publicSubnetId}`);
|
|
27
|
+
console.log(` Conversion Required: ${resources.subnetConversionRequired}`);
|
|
28
|
+
|
|
29
|
+
if (resources.privateSubnetsWithWrongRoutes && resources.privateSubnetsWithWrongRoutes.length > 0) {
|
|
30
|
+
console.log(`\nโ ๏ธ Subnets needing conversion:`);
|
|
31
|
+
resources.privateSubnetsWithWrongRoutes.forEach(subnet => {
|
|
32
|
+
console.log(` - ${subnet}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('\nโ
Test completed successfully!');
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('\nโ Test failed:', error.message);
|
|
40
|
+
console.error('\nStack trace:', error.stack);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Run the test
|
|
46
|
+
testSubnetLogic().catch(error => {
|
|
47
|
+
console.error('Unhandled error:', error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.428.
|
|
4
|
+
"version": "2.0.0--canary.428.5364e8f.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"@babel/eslint-parser": "^7.18.9",
|
|
10
10
|
"@babel/parser": "^7.25.3",
|
|
11
11
|
"@babel/traverse": "^7.25.3",
|
|
12
|
-
"@friggframework/schemas": "2.0.0--canary.428.
|
|
13
|
-
"@friggframework/test": "2.0.0--canary.428.
|
|
12
|
+
"@friggframework/schemas": "2.0.0--canary.428.5364e8f.0",
|
|
13
|
+
"@friggframework/test": "2.0.0--canary.428.5364e8f.0",
|
|
14
14
|
"@hapi/boom": "^10.0.1",
|
|
15
15
|
"@inquirer/prompts": "^5.3.8",
|
|
16
16
|
"axios": "^1.7.2",
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
"serverless-http": "^2.7.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@friggframework/eslint-config": "2.0.0--canary.428.
|
|
36
|
-
"@friggframework/prettier-config": "2.0.0--canary.428.
|
|
35
|
+
"@friggframework/eslint-config": "2.0.0--canary.428.5364e8f.0",
|
|
36
|
+
"@friggframework/prettier-config": "2.0.0--canary.428.5364e8f.0",
|
|
37
|
+
"aws-sdk-client-mock": "^4.1.0",
|
|
38
|
+
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
37
39
|
"jest": "^30.1.3",
|
|
38
40
|
"prettier": "^2.7.1",
|
|
39
41
|
"serverless": "3.39.0",
|
|
@@ -66,5 +68,5 @@
|
|
|
66
68
|
"publishConfig": {
|
|
67
69
|
"access": "public"
|
|
68
70
|
},
|
|
69
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "5364e8f51732aa43ffbb4431fdcea2bfa69fb632"
|
|
70
72
|
}
|