@friggframework/devtools 2.0.0-next.37 → 2.0.0-next.38
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/frigg-cli/generate-command/__tests__/generate-command.test.js +1 -1
- package/frigg-cli/generate-command/index.js +1 -1
- package/frigg-cli/init-command/backend-first-handler.js +1 -1
- package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +2 -2
- package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +1 -1
- package/infrastructure/GENERATE-IAM-DOCS.md +2 -2
- package/infrastructure/README.md +3 -2
- package/infrastructure/__tests__/fixtures/mock-aws-resources.js +4 -4
- package/infrastructure/__tests__/helpers/test-utils.js +1 -1
- package/infrastructure/aws-discovery.js +14 -12
- package/infrastructure/build-time-discovery.js +3 -3
- package/infrastructure/build-time-discovery.test.js +1 -1
- package/infrastructure/iam-generator.js +15 -3
- package/infrastructure/iam-generator.test.js +4 -4
- package/infrastructure/integration.test.js +7 -7
- package/infrastructure/run-discovery.js +4 -4
- package/infrastructure/serverless-template.js +95 -103
- package/infrastructure/serverless-template.test.js +241 -38
- package/package.json +7 -6
|
@@ -54,7 +54,7 @@ describe('Generate Command', () => {
|
|
|
54
54
|
// Mock app definition
|
|
55
55
|
jest.doMock(mockAppDefinitionPath, () => ({
|
|
56
56
|
vpc: { enable: true },
|
|
57
|
-
encryption: {
|
|
57
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
58
58
|
ssm: { enable: true },
|
|
59
59
|
websockets: { enable: false }
|
|
60
60
|
}), { virtual: true });
|
|
@@ -228,7 +228,7 @@ async function generateCommand(options = {}) {
|
|
|
228
228
|
function analyzeAppFeatures(appDefinition) {
|
|
229
229
|
const features = {
|
|
230
230
|
vpc: appDefinition.vpc?.enable === true,
|
|
231
|
-
kms: appDefinition.encryption?.
|
|
231
|
+
kms: appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms',
|
|
232
232
|
ssm: appDefinition.ssm?.enable === true,
|
|
233
233
|
websockets: appDefinition.websockets?.enable === true,
|
|
234
234
|
// Add more feature detection as needed
|
|
@@ -670,7 +670,7 @@ To integrate Frigg into your production application:
|
|
|
670
670
|
const appDefinition = {
|
|
671
671
|
integrations: [], // Will be populated based on selected integrations
|
|
672
672
|
user: { password: true },
|
|
673
|
-
encryption: {
|
|
673
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
674
674
|
vpc: { enable: true },
|
|
675
675
|
security: {
|
|
676
676
|
cors: {
|
|
@@ -9,7 +9,7 @@ AWS Discovery automatically finds your default AWS resources (VPC, subnets, secu
|
|
|
9
9
|
AWS Discovery runs automatically during `frigg build` and `frigg deploy` when your AppDefinition includes:
|
|
10
10
|
|
|
11
11
|
- `vpc.enable: true` - VPC support
|
|
12
|
-
- `encryption.
|
|
12
|
+
- `encryption.fieldLevelEncryptionMethod: 'kms'` - KMS encryption
|
|
13
13
|
- `ssm.enable: true` - SSM Parameter Store
|
|
14
14
|
|
|
15
15
|
## Fail-Fast Behavior
|
|
@@ -222,7 +222,7 @@ If you're stuck, try this recovery process:
|
|
|
222
222
|
// backend/index.js - temporarily disable problematic features
|
|
223
223
|
const appDefinition = {
|
|
224
224
|
vpc: { enable: false },
|
|
225
|
-
encryption: {
|
|
225
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
226
226
|
ssm: { enable: false }
|
|
227
227
|
};
|
|
228
228
|
```
|
|
@@ -354,7 +354,7 @@ Additional permissions needed when your app definition includes `vpc: { enable:
|
|
|
354
354
|
|
|
355
355
|
### KMS Support
|
|
356
356
|
|
|
357
|
-
Additional permissions needed when your app definition includes `encryption: {
|
|
357
|
+
Additional permissions needed when your app definition includes `encryption: { fieldLevelEncryptionMethod: 'kms' }`:
|
|
358
358
|
|
|
359
359
|
```json
|
|
360
360
|
{
|
|
@@ -58,7 +58,7 @@ The command analyzes your `backend/index.js` AppDefinition and generates IAM pol
|
|
|
58
58
|
- Route table and security group management
|
|
59
59
|
- Elastic IP allocation
|
|
60
60
|
|
|
61
|
-
#### KMS Encryption (`encryption.
|
|
61
|
+
#### KMS Encryption (`encryption.fieldLevelEncryptionMethod: 'kms'`)
|
|
62
62
|
|
|
63
63
|
- KMS key usage for Lambda and S3
|
|
64
64
|
- Data encryption and decryption permissions
|
|
@@ -85,7 +85,7 @@ const appDefinition = {
|
|
|
85
85
|
enable: true,
|
|
86
86
|
},
|
|
87
87
|
encryption: {
|
|
88
|
-
|
|
88
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
89
89
|
},
|
|
90
90
|
ssm: {
|
|
91
91
|
enable: false,
|
package/infrastructure/README.md
CHANGED
|
@@ -155,7 +155,8 @@ const appDefinition = {
|
|
|
155
155
|
|
|
156
156
|
// KMS encryption
|
|
157
157
|
encryption: {
|
|
158
|
-
|
|
158
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
159
|
+
createResourceIfNoneFound: true
|
|
159
160
|
},
|
|
160
161
|
|
|
161
162
|
// SSM Parameter Store
|
|
@@ -217,7 +218,7 @@ const serverlessConfig = await composeServerlessDefinition(appDefinition);
|
|
|
217
218
|
const appDefinition = {
|
|
218
219
|
name: 'secure-app',
|
|
219
220
|
vpc: { enable: true },
|
|
220
|
-
encryption: {
|
|
221
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
221
222
|
ssm: { enable: true },
|
|
222
223
|
integrations: [{ Definition: { name: 'salesforce' } }],
|
|
223
224
|
};
|
|
@@ -151,7 +151,7 @@ const mockAppDefinitions = {
|
|
|
151
151
|
|
|
152
152
|
kmsOnly: {
|
|
153
153
|
name: 'kms-test-app',
|
|
154
|
-
encryption: {
|
|
154
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
155
155
|
integrations: []
|
|
156
156
|
},
|
|
157
157
|
|
|
@@ -164,7 +164,7 @@ const mockAppDefinitions = {
|
|
|
164
164
|
allFeatures: {
|
|
165
165
|
name: 'full-feature-app',
|
|
166
166
|
vpc: { enable: true },
|
|
167
|
-
encryption: {
|
|
167
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
168
168
|
ssm: { enable: true },
|
|
169
169
|
integrations: [{
|
|
170
170
|
Definition: {
|
|
@@ -281,7 +281,7 @@ const mockEnvironmentVariables = {
|
|
|
281
281
|
AWS_DISCOVERY_SUBNET_ID_1: mockSubnets[0].SubnetId,
|
|
282
282
|
AWS_DISCOVERY_SUBNET_ID_2: mockSubnets[1].SubnetId,
|
|
283
283
|
AWS_DISCOVERY_ROUTE_TABLE_ID: mockRouteTables[0].RouteTableId,
|
|
284
|
-
AWS_DISCOVERY_KMS_KEY_ID:
|
|
284
|
+
AWS_DISCOVERY_KMS_KEY_ID:mockKmsKeyMetadata.Arn
|
|
285
285
|
};
|
|
286
286
|
|
|
287
287
|
// Fallback environment variables for error scenarios
|
|
@@ -291,7 +291,7 @@ const mockFallbackEnvironmentVariables = {
|
|
|
291
291
|
AWS_DISCOVERY_SUBNET_ID_1: 'subnet-fallback-1',
|
|
292
292
|
AWS_DISCOVERY_SUBNET_ID_2: 'subnet-fallback-2',
|
|
293
293
|
AWS_DISCOVERY_ROUTE_TABLE_ID: 'rtb-fallback',
|
|
294
|
-
AWS_DISCOVERY_KMS_KEY_ID:
|
|
294
|
+
AWS_DISCOVERY_KMS_KEY_ID:'arn:aws:kms:*:*:key/*'
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
// Mock AWS SDK responses
|
|
@@ -115,7 +115,7 @@ function createMockAppDefinition(features = {}, integrations = []) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (features.kms) {
|
|
118
|
-
appDefinition.encryption = {
|
|
118
|
+
appDefinition.encryption = { fieldLevelEncryptionMethod: 'kms' };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (features.ssm) {
|
|
@@ -453,18 +453,16 @@ class AWSDiscovery {
|
|
|
453
453
|
|
|
454
454
|
/**
|
|
455
455
|
* Find the default KMS key for the account
|
|
456
|
-
* @returns {Promise<string>} KMS key ARN or
|
|
456
|
+
* @returns {Promise<string|null>} KMS key ARN or null if no key found
|
|
457
457
|
*/
|
|
458
458
|
async findDefaultKmsKey() {
|
|
459
459
|
try {
|
|
460
|
-
// First try to find a key with alias/aws/lambda
|
|
461
460
|
const command = new ListKeysCommand({});
|
|
462
461
|
const response = await this.kmsClient.send(command);
|
|
463
462
|
|
|
464
463
|
if (!response.Keys || response.Keys.length === 0) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return `arn:aws:kms:${this.region}:${accountId}:key/*`;
|
|
464
|
+
console.log('No KMS keys found in account');
|
|
465
|
+
return null;
|
|
468
466
|
}
|
|
469
467
|
|
|
470
468
|
// Look for customer managed keys first
|
|
@@ -476,21 +474,21 @@ class AWSDiscovery {
|
|
|
476
474
|
if (keyDetails.KeyMetadata &&
|
|
477
475
|
keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
|
|
478
476
|
keyDetails.KeyMetadata.KeyState === 'Enabled') {
|
|
477
|
+
console.log(`Found customer managed KMS key: ${keyDetails.KeyMetadata.Arn}`);
|
|
479
478
|
return keyDetails.KeyMetadata.Arn;
|
|
480
479
|
}
|
|
481
480
|
} catch (error) {
|
|
482
481
|
// Continue to next key if we can't describe this one
|
|
482
|
+
console.warn(`Could not describe key ${key.KeyId}:`, error.message);
|
|
483
483
|
continue;
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return `arn:aws:kms:${this.region}:${accountId}:key/*`;
|
|
487
|
+
console.log('No customer managed KMS keys found');
|
|
488
|
+
return null;
|
|
490
489
|
} catch (error) {
|
|
491
490
|
console.error('Error finding default KMS key:', error);
|
|
492
|
-
|
|
493
|
-
return '*';
|
|
491
|
+
return null;
|
|
494
492
|
}
|
|
495
493
|
}
|
|
496
494
|
|
|
@@ -503,7 +501,7 @@ class AWSDiscovery {
|
|
|
503
501
|
* @returns {string} return.privateSubnetId2 - Second private subnet ID
|
|
504
502
|
* @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
|
|
505
503
|
* @returns {string} return.privateRouteTableId - Private route table ID
|
|
506
|
-
* @returns {string} return.defaultKmsKeyId - Default KMS key ARN
|
|
504
|
+
* @returns {string|null} return.defaultKmsKeyId - Default KMS key ARN or null if not found
|
|
507
505
|
* @throws {Error} If resource discovery fails
|
|
508
506
|
*/
|
|
509
507
|
async discoverResources() {
|
|
@@ -526,7 +524,11 @@ class AWSDiscovery {
|
|
|
526
524
|
console.log(`Found route table: ${routeTable.RouteTableId}`);
|
|
527
525
|
|
|
528
526
|
const kmsKeyArn = await this.findDefaultKmsKey();
|
|
529
|
-
|
|
527
|
+
if (kmsKeyArn) {
|
|
528
|
+
console.log(`Found KMS key: ${kmsKeyArn}`);
|
|
529
|
+
} else {
|
|
530
|
+
console.log('No KMS key found');
|
|
531
|
+
}
|
|
530
532
|
|
|
531
533
|
// Try to find existing NAT Gateway
|
|
532
534
|
const existingNatGateway = await this.findExistingNatGateway(vpc.VpcId);
|
|
@@ -137,8 +137,8 @@ class BuildTimeDiscovery {
|
|
|
137
137
|
console.log('Running pre-build AWS discovery hook...');
|
|
138
138
|
|
|
139
139
|
// Only run discovery if VPC, KMS, or SSM features are enabled
|
|
140
|
-
const needsDiscovery = appDefinition.vpc?.enable ||
|
|
141
|
-
appDefinition.encryption?.
|
|
140
|
+
const needsDiscovery = appDefinition.vpc?.enable ||
|
|
141
|
+
appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
142
142
|
appDefinition.ssm?.enable;
|
|
143
143
|
|
|
144
144
|
if (!needsDiscovery) {
|
|
@@ -159,7 +159,7 @@ class BuildTimeDiscovery {
|
|
|
159
159
|
AWS_DISCOVERY_SUBNET_ID_2: resources.privateSubnetId2,
|
|
160
160
|
AWS_DISCOVERY_PUBLIC_SUBNET_ID: resources.publicSubnetId,
|
|
161
161
|
AWS_DISCOVERY_ROUTE_TABLE_ID: resources.privateRouteTableId,
|
|
162
|
-
AWS_DISCOVERY_KMS_KEY_ID: resources.defaultKmsKeyId
|
|
162
|
+
AWS_DISCOVERY_KMS_KEY_ID: resources.defaultKmsKeyId // Keep consistent naming convention (even though it's an ARN)
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
// Set environment variables for serverless to use
|
|
@@ -250,7 +250,7 @@ describe('BuildTimeDiscovery', () => {
|
|
|
250
250
|
|
|
251
251
|
it('should run discovery when KMS is enabled', async () => {
|
|
252
252
|
const appDefinition = {
|
|
253
|
-
encryption: {
|
|
253
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
254
254
|
integrations: []
|
|
255
255
|
};
|
|
256
256
|
|
|
@@ -35,7 +35,7 @@ function generateIAMCloudFormation(appDefinition, options = {}) {
|
|
|
35
35
|
vpc: appDefinition.vpc?.enable === true,
|
|
36
36
|
kms:
|
|
37
37
|
appDefinition.encryption
|
|
38
|
-
?.
|
|
38
|
+
?.fieldLevelEncryptionMethod === 'kms',
|
|
39
39
|
ssm: appDefinition.ssm?.enable === true,
|
|
40
40
|
websockets: appDefinition.websockets?.enable === true,
|
|
41
41
|
};
|
|
@@ -605,6 +605,19 @@ function generateIAMCloudFormation(appDefinition, options = {}) {
|
|
|
605
605
|
},
|
|
606
606
|
},
|
|
607
607
|
},
|
|
608
|
+
{
|
|
609
|
+
Sid: 'FriggKMSManagement',
|
|
610
|
+
Effect: 'Allow',
|
|
611
|
+
Action: [
|
|
612
|
+
'kms:CreateKey',
|
|
613
|
+
'kms:PutKeyPolicy',
|
|
614
|
+
'kms:EnableKeyRotation',
|
|
615
|
+
'kms:TagResource',
|
|
616
|
+
'kms:UntagResource',
|
|
617
|
+
'kms:ListResourceTags',
|
|
618
|
+
],
|
|
619
|
+
Resource: '*',
|
|
620
|
+
},
|
|
608
621
|
],
|
|
609
622
|
},
|
|
610
623
|
},
|
|
@@ -724,8 +737,7 @@ function getFeatureSummary(appDefinition) {
|
|
|
724
737
|
core: true, // Always enabled
|
|
725
738
|
vpc: appDefinition.vpc?.enable === true,
|
|
726
739
|
kms:
|
|
727
|
-
appDefinition.encryption?.
|
|
728
|
-
true,
|
|
740
|
+
appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms',
|
|
729
741
|
ssm: appDefinition.ssm?.enable === true,
|
|
730
742
|
websockets: appDefinition.websockets?.enable === true,
|
|
731
743
|
};
|
|
@@ -7,7 +7,7 @@ describe('IAM Generator', () => {
|
|
|
7
7
|
name: 'test-app',
|
|
8
8
|
integrations: ['Integration1', 'Integration2'],
|
|
9
9
|
vpc: { enable: true },
|
|
10
|
-
encryption: {
|
|
10
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
11
11
|
ssm: { enable: true },
|
|
12
12
|
websockets: { enable: true }
|
|
13
13
|
};
|
|
@@ -46,7 +46,7 @@ describe('IAM Generator', () => {
|
|
|
46
46
|
name: 'test-app',
|
|
47
47
|
integrations: [],
|
|
48
48
|
vpc: { enable: false },
|
|
49
|
-
encryption: {
|
|
49
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
50
50
|
ssm: { enable: false },
|
|
51
51
|
websockets: { enable: false }
|
|
52
52
|
};
|
|
@@ -77,7 +77,7 @@ describe('IAM Generator', () => {
|
|
|
77
77
|
const appDefinition = {
|
|
78
78
|
name: 'test-app',
|
|
79
79
|
integrations: [],
|
|
80
|
-
encryption: {
|
|
80
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
const yaml = generateIAMCloudFormation(appDefinition);
|
|
@@ -106,7 +106,7 @@ describe('IAM Generator', () => {
|
|
|
106
106
|
name: 'test-app',
|
|
107
107
|
integrations: [],
|
|
108
108
|
vpc: { enable: true },
|
|
109
|
-
encryption: {
|
|
109
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
110
110
|
ssm: { enable: true }
|
|
111
111
|
};
|
|
112
112
|
|
|
@@ -49,7 +49,7 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
49
49
|
const appDefinition = {
|
|
50
50
|
name: 'test-frigg-app',
|
|
51
51
|
vpc: { enable: true },
|
|
52
|
-
encryption: {
|
|
52
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
53
53
|
ssm: { enable: true },
|
|
54
54
|
integrations: [{
|
|
55
55
|
Definition: {
|
|
@@ -67,7 +67,7 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
67
67
|
process.env.AWS_DISCOVERY_SUBNET_ID_1 = discoveredResources.privateSubnetId1;
|
|
68
68
|
process.env.AWS_DISCOVERY_SUBNET_ID_2 = discoveredResources.privateSubnetId2;
|
|
69
69
|
process.env.AWS_DISCOVERY_ROUTE_TABLE_ID = discoveredResources.privateRouteTableId;
|
|
70
|
-
process.env.AWS_DISCOVERY_KMS_KEY_ID =
|
|
70
|
+
process.env.AWS_DISCOVERY_KMS_KEY_ID =discoveredResources.defaultKmsKeyId;
|
|
71
71
|
|
|
72
72
|
// Generate serverless configuration
|
|
73
73
|
const serverlessConfig = composeServerlessDefinition(appDefinition);
|
|
@@ -173,11 +173,11 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
173
173
|
it('should generate config with only KMS enabled', async () => {
|
|
174
174
|
const appDefinition = {
|
|
175
175
|
name: 'kms-only-app',
|
|
176
|
-
encryption: {
|
|
176
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
177
177
|
integrations: []
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
-
process.env.AWS_DISCOVERY_KMS_KEY_ID =
|
|
180
|
+
process.env.AWS_DISCOVERY_KMS_KEY_ID =mockAWSResources.defaultKmsKeyId;
|
|
181
181
|
|
|
182
182
|
const serverlessConfig = composeServerlessDefinition(appDefinition);
|
|
183
183
|
|
|
@@ -231,7 +231,7 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
231
231
|
expect(mockBuildTimeDiscovery.preBuildHook).toHaveBeenCalledWith(
|
|
232
232
|
expect.objectContaining({
|
|
233
233
|
vpc: { enable: true },
|
|
234
|
-
encryption: {
|
|
234
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
235
235
|
}),
|
|
236
236
|
'us-east-1'
|
|
237
237
|
);
|
|
@@ -302,7 +302,7 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
302
302
|
process.env.AWS_DISCOVERY_SECURITY_GROUP_ID = mockAWSResources.defaultSecurityGroupId;
|
|
303
303
|
process.env.AWS_DISCOVERY_SUBNET_ID_1 = mockAWSResources.privateSubnetId1;
|
|
304
304
|
process.env.AWS_DISCOVERY_SUBNET_ID_2 = mockAWSResources.privateSubnetId2;
|
|
305
|
-
process.env.AWS_DISCOVERY_KMS_KEY_ID =
|
|
305
|
+
process.env.AWS_DISCOVERY_KMS_KEY_ID =mockAWSResources.defaultKmsKeyId;
|
|
306
306
|
|
|
307
307
|
// In a real deployment, serverless framework would resolve these environment variables
|
|
308
308
|
// For testing, we can verify the placeholders are correctly formatted
|
|
@@ -353,7 +353,7 @@ describe('VPC/KMS/SSM Integration Tests', () => {
|
|
|
353
353
|
|
|
354
354
|
const appDefinition = {
|
|
355
355
|
vpc: { enable: true },
|
|
356
|
-
encryption: {
|
|
356
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
357
357
|
integrations: []
|
|
358
358
|
};
|
|
359
359
|
|
|
@@ -41,7 +41,7 @@ async function runDiscovery() {
|
|
|
41
41
|
|
|
42
42
|
// Check if discovery is needed
|
|
43
43
|
const needsDiscovery = appDefinition.vpc?.enable ||
|
|
44
|
-
appDefinition.encryption?.
|
|
44
|
+
appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
45
45
|
appDefinition.ssm?.enable;
|
|
46
46
|
|
|
47
47
|
if (!needsDiscovery) {
|
|
@@ -51,7 +51,7 @@ async function runDiscovery() {
|
|
|
51
51
|
|
|
52
52
|
console.log('📋 App requires AWS discovery for:');
|
|
53
53
|
if (appDefinition.vpc?.enable) console.log(' ✅ VPC support');
|
|
54
|
-
if (appDefinition.encryption?.
|
|
54
|
+
if (appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') console.log(' ✅ KMS encryption');
|
|
55
55
|
if (appDefinition.ssm?.enable) console.log(' ✅ SSM parameters');
|
|
56
56
|
|
|
57
57
|
// Run discovery
|
|
@@ -82,7 +82,7 @@ async function runDiscovery() {
|
|
|
82
82
|
} else {
|
|
83
83
|
console.error('🚨 Discovery is required because your AppDefinition has these features enabled:');
|
|
84
84
|
if (appDefinition.vpc?.enable) console.error(' ❌ VPC support (vpc.enable: true)');
|
|
85
|
-
if (appDefinition.encryption?.
|
|
85
|
+
if (appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') console.error(' ❌ KMS encryption (encryption.fieldLevelEncryptionMethod: \'kms\')');
|
|
86
86
|
if (appDefinition.ssm?.enable) console.error(' ❌ SSM parameters (ssm.enable: true)');
|
|
87
87
|
console.error('');
|
|
88
88
|
console.error('💡 To fix this issue:');
|
|
@@ -95,7 +95,7 @@ async function runDiscovery() {
|
|
|
95
95
|
|
|
96
96
|
console.error('🔧 Or disable features in backend/index.js:');
|
|
97
97
|
console.error(' vpc: { enable: false }');
|
|
98
|
-
console.error(' encryption: {
|
|
98
|
+
console.error(' encryption: { fieldLevelEncryptionMethod: \'aes\' }');
|
|
99
99
|
console.error(' ssm: { enable: false }');
|
|
100
100
|
|
|
101
101
|
process.exit(1);
|
|
@@ -10,8 +10,7 @@ const { AWSDiscovery } = require('./aws-discovery');
|
|
|
10
10
|
const shouldRunDiscovery = (AppDefinition) => {
|
|
11
11
|
return (
|
|
12
12
|
AppDefinition.vpc?.enable === true ||
|
|
13
|
-
AppDefinition.encryption?.
|
|
14
|
-
true ||
|
|
13
|
+
AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
15
14
|
AppDefinition.ssm?.enable === true
|
|
16
15
|
);
|
|
17
16
|
};
|
|
@@ -493,10 +492,7 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
493
492
|
};
|
|
494
493
|
|
|
495
494
|
// KMS Interface Endpoint (paid, but useful if using KMS)
|
|
496
|
-
if (
|
|
497
|
-
AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption ===
|
|
498
|
-
true
|
|
499
|
-
) {
|
|
495
|
+
if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
500
496
|
vpcResources.FriggKMSVPCEndpoint = {
|
|
501
497
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
502
498
|
Properties: {
|
|
@@ -891,9 +887,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
891
887
|
};
|
|
892
888
|
|
|
893
889
|
// KMS Configuration based on App Definition
|
|
894
|
-
if (
|
|
895
|
-
AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true
|
|
896
|
-
) {
|
|
890
|
+
if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
897
891
|
// Check if a KMS key was discovered
|
|
898
892
|
if (discoveredResources.defaultKmsKeyId) {
|
|
899
893
|
// Use the existing discovered KMS key
|
|
@@ -907,55 +901,112 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
907
901
|
Resource: [discoveredResources.defaultKmsKeyId],
|
|
908
902
|
});
|
|
909
903
|
|
|
910
|
-
|
|
911
|
-
discoveredResources.defaultKmsKeyId;
|
|
904
|
+
// KMS_KEY_ARN will be set later from custom.kmsGrants for consistency
|
|
912
905
|
} else {
|
|
913
|
-
// No existing key found
|
|
914
|
-
|
|
906
|
+
// No existing key found - check if we should create one or error
|
|
907
|
+
if (AppDefinition.encryption?.createResourceIfNoneFound === true) {
|
|
908
|
+
// Create a new KMS key
|
|
909
|
+
console.log('No existing KMS key found, creating a new one...');
|
|
915
910
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
'
|
|
911
|
+
definition.resources.Resources.FriggKMSKey = {
|
|
912
|
+
Type: 'AWS::KMS::Key',
|
|
913
|
+
Properties: {
|
|
914
|
+
EnableKeyRotation: true,
|
|
915
|
+
Description: 'Frigg KMS key for field-level encryption',
|
|
916
|
+
KeyPolicy: {
|
|
917
|
+
Version: '2012-10-17',
|
|
918
|
+
Statement: [
|
|
919
|
+
{
|
|
920
|
+
Sid: 'AllowRootAccountAdmin',
|
|
921
|
+
Effect: 'Allow',
|
|
922
|
+
Principal: {
|
|
923
|
+
AWS: {
|
|
924
|
+
'Fn::Sub':
|
|
925
|
+
'arn:aws:iam::${AWS::AccountId}:root',
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
Action: 'kms:*',
|
|
929
|
+
Resource: '*',
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
Sid: 'AllowLambdaService',
|
|
933
|
+
Effect: 'Allow',
|
|
934
|
+
Principal: {
|
|
935
|
+
Service: 'lambda.amazonaws.com',
|
|
936
|
+
},
|
|
937
|
+
Action: [
|
|
938
|
+
'kms:GenerateDataKey',
|
|
939
|
+
'kms:Decrypt',
|
|
940
|
+
'kms:DescribeKey',
|
|
941
|
+
],
|
|
942
|
+
Resource: '*',
|
|
943
|
+
Condition: {
|
|
944
|
+
StringEquals: {
|
|
945
|
+
'kms:ViaService': `lambda.${
|
|
946
|
+
process.env.AWS_REGION ||
|
|
947
|
+
'us-east-1'
|
|
948
|
+
}.amazonaws.com`,
|
|
949
|
+
},
|
|
930
950
|
},
|
|
931
951
|
},
|
|
932
|
-
|
|
933
|
-
|
|
952
|
+
],
|
|
953
|
+
},
|
|
954
|
+
Tags: [
|
|
955
|
+
{
|
|
956
|
+
Key: 'Name',
|
|
957
|
+
Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
Key: 'Purpose',
|
|
961
|
+
Value: 'Field-level encryption for Frigg application',
|
|
934
962
|
},
|
|
935
963
|
],
|
|
936
964
|
},
|
|
937
|
-
}
|
|
938
|
-
};
|
|
965
|
+
};
|
|
939
966
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
967
|
+
definition.provider.iamRoleStatements.push({
|
|
968
|
+
Effect: 'Allow',
|
|
969
|
+
Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
|
|
970
|
+
Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
|
|
971
|
+
});
|
|
945
972
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
973
|
+
definition.provider.environment.KMS_KEY_ARN = {
|
|
974
|
+
'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// Configure KMS grants to reference the created key
|
|
978
|
+
definition.custom.kmsGrants = {
|
|
979
|
+
kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
|
|
980
|
+
};
|
|
981
|
+
} else {
|
|
982
|
+
// No key found and createIfNoneFound is not enabled - error
|
|
983
|
+
throw new Error(
|
|
984
|
+
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
985
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
986
|
+
);
|
|
987
|
+
}
|
|
949
988
|
}
|
|
950
989
|
|
|
951
990
|
definition.plugins.push('serverless-kms-grants');
|
|
952
991
|
|
|
953
|
-
// Configure KMS grants
|
|
954
|
-
definition.custom.kmsGrants
|
|
955
|
-
|
|
992
|
+
// Configure KMS grants if not already set (when using existing key)
|
|
993
|
+
if (!definition.custom.kmsGrants) {
|
|
994
|
+
definition.custom.kmsGrants = {
|
|
995
|
+
kmsKeyId:
|
|
996
|
+
discoveredResources.defaultKmsKeyId ||
|
|
997
|
+
'${env:AWS_DISCOVERY_KMS_KEY_ID}',
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Always set KMS_KEY_ARN from custom.kmsGrants for consistency
|
|
1002
|
+
// This translates AWS_DISCOVERY_KMS_KEY_ID to the runtime variable KMS_KEY_ARN
|
|
1003
|
+
if (!definition.provider.environment.KMS_KEY_ARN) {
|
|
1004
|
+
// Use the discovered value directly when available (from in-process discovery)
|
|
1005
|
+
// Otherwise fall back to environment variable (from separate discovery process)
|
|
1006
|
+
definition.provider.environment.KMS_KEY_ARN =
|
|
956
1007
|
discoveredResources.defaultKmsKeyId ||
|
|
957
|
-
'${env:AWS_DISCOVERY_KMS_KEY_ID}'
|
|
958
|
-
}
|
|
1008
|
+
'${env:AWS_DISCOVERY_KMS_KEY_ID}';
|
|
1009
|
+
}
|
|
959
1010
|
}
|
|
960
1011
|
|
|
961
1012
|
// VPC Configuration based on App Definition
|
|
@@ -1252,62 +1303,6 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1252
1303
|
definition.custom[queueReference] = queueName;
|
|
1253
1304
|
}
|
|
1254
1305
|
}
|
|
1255
|
-
|
|
1256
|
-
// Discovery has already run successfully at this point if needed
|
|
1257
|
-
// The discoveredResources object contains all the necessary AWS resources
|
|
1258
|
-
|
|
1259
|
-
// Add websocket function if enabled
|
|
1260
|
-
if (AppDefinition.websockets?.enable === true) {
|
|
1261
|
-
definition.functions.defaultWebsocket = {
|
|
1262
|
-
handler:
|
|
1263
|
-
'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
1264
|
-
events: [
|
|
1265
|
-
{
|
|
1266
|
-
websocket: {
|
|
1267
|
-
route: '$connect',
|
|
1268
|
-
},
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
1271
|
-
websocket: {
|
|
1272
|
-
route: '$default',
|
|
1273
|
-
},
|
|
1274
|
-
},
|
|
1275
|
-
{
|
|
1276
|
-
websocket: {
|
|
1277
|
-
route: '$disconnect',
|
|
1278
|
-
},
|
|
1279
|
-
},
|
|
1280
|
-
],
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// Discovery has already run successfully at this point if needed
|
|
1285
|
-
// The discoveredResources object contains all the necessary AWS resources
|
|
1286
|
-
|
|
1287
|
-
// Add websocket function if enabled
|
|
1288
|
-
if (AppDefinition.websockets?.enable === true) {
|
|
1289
|
-
definition.functions.defaultWebsocket = {
|
|
1290
|
-
handler:
|
|
1291
|
-
'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
1292
|
-
events: [
|
|
1293
|
-
{
|
|
1294
|
-
websocket: {
|
|
1295
|
-
route: '$connect',
|
|
1296
|
-
},
|
|
1297
|
-
},
|
|
1298
|
-
{
|
|
1299
|
-
websocket: {
|
|
1300
|
-
route: '$default',
|
|
1301
|
-
},
|
|
1302
|
-
},
|
|
1303
|
-
{
|
|
1304
|
-
websocket: {
|
|
1305
|
-
route: '$disconnect',
|
|
1306
|
-
},
|
|
1307
|
-
},
|
|
1308
|
-
],
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
1306
|
}
|
|
1312
1307
|
|
|
1313
1308
|
// Discovery has already run successfully at this point if needed
|
|
@@ -1338,9 +1333,6 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1338
1333
|
};
|
|
1339
1334
|
}
|
|
1340
1335
|
|
|
1341
|
-
// Discovery has already run successfully at this point if needed
|
|
1342
|
-
// The discoveredResources object contains all the necessary AWS resources
|
|
1343
|
-
|
|
1344
1336
|
// Modify handler paths to point to the correct node_modules location
|
|
1345
1337
|
definition.functions = modifyHandlerPaths(definition.functions);
|
|
1346
1338
|
|
|
@@ -118,16 +118,9 @@ describe('composeServerlessDefinition', () => {
|
|
|
118
118
|
|
|
119
119
|
const result = await composeServerlessDefinition(appDefinition);
|
|
120
120
|
|
|
121
|
-
expect(result.provider.vpc).
|
|
122
|
-
expect(result.
|
|
123
|
-
|
|
124
|
-
securityGroupIds: ['${env:AWS_DISCOVERY_SECURITY_GROUP_ID}'],
|
|
125
|
-
subnetIds: [
|
|
126
|
-
'${env:AWS_DISCOVERY_SUBNET_ID_1}',
|
|
127
|
-
'${env:AWS_DISCOVERY_SUBNET_ID_2}'
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
});
|
|
121
|
+
expect(result.provider.vpc).toBeDefined();
|
|
122
|
+
expect(result.provider.vpc.securityGroupIds).toEqual(['sg-123456']);
|
|
123
|
+
expect(result.provider.vpc.subnetIds).toEqual(['subnet-123456', 'subnet-789012']);
|
|
131
124
|
});
|
|
132
125
|
|
|
133
126
|
it('should add VPC endpoint for S3 when VPC is enabled', async () => {
|
|
@@ -138,15 +131,9 @@ describe('composeServerlessDefinition', () => {
|
|
|
138
131
|
|
|
139
132
|
const result = await composeServerlessDefinition(appDefinition);
|
|
140
133
|
|
|
141
|
-
expect(result.resources.Resources.VPCEndpointS3).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
VpcId: '${env:AWS_DISCOVERY_VPC_ID}',
|
|
145
|
-
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
146
|
-
VpcEndpointType: 'Gateway',
|
|
147
|
-
RouteTableIds: ['${env:AWS_DISCOVERY_ROUTE_TABLE_ID}']
|
|
148
|
-
}
|
|
149
|
-
});
|
|
134
|
+
expect(result.resources.Resources.VPCEndpointS3).toBeDefined();
|
|
135
|
+
expect(result.resources.Resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
136
|
+
expect(result.resources.Resources.VPCEndpointS3.Properties.VpcId).toBe('vpc-123456');
|
|
150
137
|
});
|
|
151
138
|
|
|
152
139
|
it('should not add VPC configuration when vpc.enable is false', async () => {
|
|
@@ -158,7 +145,6 @@ describe('composeServerlessDefinition', () => {
|
|
|
158
145
|
const result = await composeServerlessDefinition(appDefinition);
|
|
159
146
|
|
|
160
147
|
expect(result.provider.vpc).toBeUndefined();
|
|
161
|
-
expect(result.custom.vpc).toBeUndefined();
|
|
162
148
|
expect(result.resources.Resources.VPCEndpointS3).toBeUndefined();
|
|
163
149
|
});
|
|
164
150
|
|
|
@@ -170,14 +156,13 @@ describe('composeServerlessDefinition', () => {
|
|
|
170
156
|
const result = await composeServerlessDefinition(appDefinition);
|
|
171
157
|
|
|
172
158
|
expect(result.provider.vpc).toBeUndefined();
|
|
173
|
-
expect(result.custom.vpc).toBeUndefined();
|
|
174
159
|
});
|
|
175
160
|
});
|
|
176
161
|
|
|
177
162
|
describe('KMS Configuration', () => {
|
|
178
|
-
it('should add KMS configuration when encryption is enabled', async () => {
|
|
163
|
+
it('should add KMS configuration when encryption is enabled and key is found', async () => {
|
|
179
164
|
const appDefinition = {
|
|
180
|
-
encryption: {
|
|
165
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
181
166
|
integrations: []
|
|
182
167
|
};
|
|
183
168
|
|
|
@@ -193,24 +178,188 @@ describe('composeServerlessDefinition', () => {
|
|
|
193
178
|
'kms:GenerateDataKey',
|
|
194
179
|
'kms:Decrypt'
|
|
195
180
|
],
|
|
196
|
-
Resource: ['
|
|
181
|
+
Resource: ['arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012']
|
|
197
182
|
});
|
|
198
183
|
|
|
199
184
|
// Check environment variable
|
|
200
|
-
expect(result.provider.environment.KMS_KEY_ARN).toBe('
|
|
185
|
+
expect(result.provider.environment.KMS_KEY_ARN).toBe('arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012');
|
|
201
186
|
|
|
202
187
|
// Check plugin
|
|
203
188
|
expect(result.plugins).toContain('serverless-kms-grants');
|
|
204
189
|
|
|
205
190
|
// Check custom configuration
|
|
206
191
|
expect(result.custom.kmsGrants).toEqual({
|
|
207
|
-
kmsKeyId: '
|
|
192
|
+
kmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should create new KMS key when encryption is enabled, no key found, and createResourceIfNoneFound is true', async () => {
|
|
197
|
+
// Mock AWS discovery to return no KMS key
|
|
198
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
199
|
+
const mockDiscoverResources = jest.fn().mockResolvedValue({
|
|
200
|
+
defaultVpcId: 'vpc-123456',
|
|
201
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
202
|
+
privateSubnetId1: 'subnet-123456',
|
|
203
|
+
privateSubnetId2: 'subnet-789012',
|
|
204
|
+
publicSubnetId: 'subnet-public',
|
|
205
|
+
defaultRouteTableId: 'rtb-123456',
|
|
206
|
+
defaultKmsKeyId: null // No KMS key found
|
|
207
|
+
});
|
|
208
|
+
AWSDiscovery.mockImplementation(() => ({
|
|
209
|
+
discoverResources: mockDiscoverResources
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const appDefinition = {
|
|
213
|
+
encryption: {
|
|
214
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
215
|
+
createResourceIfNoneFound: true
|
|
216
|
+
},
|
|
217
|
+
integrations: []
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
221
|
+
|
|
222
|
+
// Check that KMS key resource was created
|
|
223
|
+
expect(result.resources.Resources.FriggKMSKey).toEqual({
|
|
224
|
+
Type: 'AWS::KMS::Key',
|
|
225
|
+
Properties: {
|
|
226
|
+
EnableKeyRotation: true,
|
|
227
|
+
Description: 'Frigg KMS key for field-level encryption',
|
|
228
|
+
KeyPolicy: {
|
|
229
|
+
Version: '2012-10-17',
|
|
230
|
+
Statement: [
|
|
231
|
+
{
|
|
232
|
+
Sid: 'AllowRootAccountAdmin',
|
|
233
|
+
Effect: 'Allow',
|
|
234
|
+
Principal: {
|
|
235
|
+
AWS: {
|
|
236
|
+
'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root'
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
Action: 'kms:*',
|
|
240
|
+
Resource: '*'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
Sid: 'AllowLambdaService',
|
|
244
|
+
Effect: 'Allow',
|
|
245
|
+
Principal: {
|
|
246
|
+
Service: 'lambda.amazonaws.com'
|
|
247
|
+
},
|
|
248
|
+
Action: [
|
|
249
|
+
'kms:GenerateDataKey',
|
|
250
|
+
'kms:Decrypt',
|
|
251
|
+
'kms:DescribeKey'
|
|
252
|
+
],
|
|
253
|
+
Resource: '*',
|
|
254
|
+
Condition: {
|
|
255
|
+
StringEquals: {
|
|
256
|
+
'kms:ViaService': 'lambda.us-east-1.amazonaws.com'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
Tags: [
|
|
263
|
+
{
|
|
264
|
+
Key: 'Name',
|
|
265
|
+
Value: '${self:service}-${self:provider.stage}-frigg-kms-key'
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
Key: 'Purpose',
|
|
269
|
+
Value: 'Field-level encryption for Frigg application'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Check IAM permissions for the new key
|
|
276
|
+
const kmsPermission = result.provider.iamRoleStatements.find(
|
|
277
|
+
statement => statement.Action.includes('kms:GenerateDataKey')
|
|
278
|
+
);
|
|
279
|
+
expect(kmsPermission).toEqual({
|
|
280
|
+
Effect: 'Allow',
|
|
281
|
+
Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
|
|
282
|
+
Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }]
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Check environment variable
|
|
286
|
+
expect(result.provider.environment.KMS_KEY_ARN).toEqual({
|
|
287
|
+
'Fn::GetAtt': ['FriggKMSKey', 'Arn']
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Check plugin
|
|
291
|
+
expect(result.plugins).toContain('serverless-kms-grants');
|
|
292
|
+
|
|
293
|
+
// Check custom configuration
|
|
294
|
+
// When creating a new key, it should reference the CloudFormation resource
|
|
295
|
+
expect(result.custom.kmsGrants).toEqual({
|
|
296
|
+
kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should throw error when encryption is enabled, no key found, and createResourceIfNoneFound is false', async () => {
|
|
301
|
+
// Mock AWS discovery to return no KMS key
|
|
302
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
303
|
+
const mockDiscoverResources = jest.fn().mockResolvedValue({
|
|
304
|
+
defaultVpcId: 'vpc-123456',
|
|
305
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
306
|
+
privateSubnetId1: 'subnet-123456',
|
|
307
|
+
privateSubnetId2: 'subnet-789012',
|
|
308
|
+
publicSubnetId: 'subnet-public',
|
|
309
|
+
defaultRouteTableId: 'rtb-123456',
|
|
310
|
+
defaultKmsKeyId: null // No KMS key found
|
|
311
|
+
});
|
|
312
|
+
AWSDiscovery.mockImplementation(() => ({
|
|
313
|
+
discoverResources: mockDiscoverResources
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
const appDefinition = {
|
|
317
|
+
encryption: {
|
|
318
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
319
|
+
createResourceIfNoneFound: false
|
|
320
|
+
},
|
|
321
|
+
integrations: []
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
|
|
325
|
+
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
326
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should throw error when encryption is enabled, no key found, and createResourceIfNoneFound is not specified', async () => {
|
|
331
|
+
// Mock AWS discovery to return no KMS key
|
|
332
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
333
|
+
const mockDiscoverResources = jest.fn().mockResolvedValue({
|
|
334
|
+
defaultVpcId: 'vpc-123456',
|
|
335
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
336
|
+
privateSubnetId1: 'subnet-123456',
|
|
337
|
+
privateSubnetId2: 'subnet-789012',
|
|
338
|
+
publicSubnetId: 'subnet-public',
|
|
339
|
+
defaultRouteTableId: 'rtb-123456',
|
|
340
|
+
defaultKmsKeyId: null // No KMS key found
|
|
208
341
|
});
|
|
342
|
+
AWSDiscovery.mockImplementation(() => ({
|
|
343
|
+
discoverResources: mockDiscoverResources
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const appDefinition = {
|
|
347
|
+
encryption: {
|
|
348
|
+
fieldLevelEncryptionMethod: 'kms'
|
|
349
|
+
// createResourceIfNoneFound not specified, defaults to false
|
|
350
|
+
},
|
|
351
|
+
integrations: []
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
|
|
355
|
+
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
356
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
357
|
+
);
|
|
209
358
|
});
|
|
210
359
|
|
|
211
360
|
it('should not add KMS configuration when encryption is disabled', async () => {
|
|
212
361
|
const appDefinition = {
|
|
213
|
-
encryption: {
|
|
362
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
214
363
|
integrations: []
|
|
215
364
|
};
|
|
216
365
|
|
|
@@ -315,10 +464,9 @@ describe('composeServerlessDefinition', () => {
|
|
|
315
464
|
expect(result.functions.testIntegration).toEqual({
|
|
316
465
|
handler: 'node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.testIntegration.handler',
|
|
317
466
|
events: [{
|
|
318
|
-
|
|
467
|
+
httpApi: {
|
|
319
468
|
path: '/api/testIntegration-integration/{proxy+}',
|
|
320
|
-
method: 'ANY'
|
|
321
|
-
cors: true
|
|
469
|
+
method: 'ANY'
|
|
322
470
|
}
|
|
323
471
|
}]
|
|
324
472
|
});
|
|
@@ -389,7 +537,7 @@ describe('composeServerlessDefinition', () => {
|
|
|
389
537
|
it('should combine VPC, KMS, and SSM configurations', async () => {
|
|
390
538
|
const appDefinition = {
|
|
391
539
|
vpc: { enable: true },
|
|
392
|
-
encryption: {
|
|
540
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
393
541
|
ssm: { enable: true },
|
|
394
542
|
integrations: [mockIntegration]
|
|
395
543
|
};
|
|
@@ -398,7 +546,7 @@ describe('composeServerlessDefinition', () => {
|
|
|
398
546
|
|
|
399
547
|
// VPC
|
|
400
548
|
expect(result.provider.vpc).toBeDefined();
|
|
401
|
-
|
|
549
|
+
// custom.vpc doesn't exist in the serverless template
|
|
402
550
|
expect(result.resources.Resources.VPCEndpointS3).toBeDefined();
|
|
403
551
|
|
|
404
552
|
// KMS
|
|
@@ -428,7 +576,7 @@ describe('composeServerlessDefinition', () => {
|
|
|
428
576
|
it('should handle partial configuration combinations', async () => {
|
|
429
577
|
const appDefinition = {
|
|
430
578
|
vpc: { enable: true },
|
|
431
|
-
encryption: {
|
|
579
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
432
580
|
integrations: []
|
|
433
581
|
};
|
|
434
582
|
|
|
@@ -459,9 +607,9 @@ describe('composeServerlessDefinition', () => {
|
|
|
459
607
|
expect(result.resources.Resources.ApiGatewayAlarm5xx).toBeDefined();
|
|
460
608
|
|
|
461
609
|
// Check default functions
|
|
462
|
-
expect(result.functions.defaultWebsocket).toBeDefined();
|
|
463
610
|
expect(result.functions.auth).toBeDefined();
|
|
464
611
|
expect(result.functions.user).toBeDefined();
|
|
612
|
+
expect(result.functions.health).toBeDefined();
|
|
465
613
|
|
|
466
614
|
// Check default plugins
|
|
467
615
|
expect(result.plugins).toContain('serverless-jetpack');
|
|
@@ -496,11 +644,64 @@ describe('composeServerlessDefinition', () => {
|
|
|
496
644
|
|
|
497
645
|
const result = await composeServerlessDefinition(appDefinition);
|
|
498
646
|
|
|
499
|
-
expect(result.provider.environment.STAGE).toBe('${opt:stage}');
|
|
647
|
+
expect(result.provider.environment.STAGE).toBe('${opt:stage, "dev"}');
|
|
500
648
|
expect(result.provider.environment.AWS_NODEJS_CONNECTION_REUSE_ENABLED).toBe(1);
|
|
501
649
|
});
|
|
502
650
|
});
|
|
503
651
|
|
|
652
|
+
describe('WebSocket Configuration', () => {
|
|
653
|
+
it('should add websocket function when websockets.enable is true', async () => {
|
|
654
|
+
const appDefinition = {
|
|
655
|
+
websockets: { enable: true },
|
|
656
|
+
integrations: []
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
660
|
+
|
|
661
|
+
expect(result.functions.defaultWebsocket).toEqual({
|
|
662
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
663
|
+
events: [
|
|
664
|
+
{
|
|
665
|
+
websocket: {
|
|
666
|
+
route: '$connect',
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
websocket: {
|
|
671
|
+
route: '$default',
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
websocket: {
|
|
676
|
+
route: '$disconnect',
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('should not add websocket function when websockets.enable is false', async () => {
|
|
684
|
+
const appDefinition = {
|
|
685
|
+
websockets: { enable: false },
|
|
686
|
+
integrations: []
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
690
|
+
|
|
691
|
+
expect(result.functions.defaultWebsocket).toBeUndefined();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should not add websocket function when websockets is not defined', async () => {
|
|
695
|
+
const appDefinition = {
|
|
696
|
+
integrations: []
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
700
|
+
|
|
701
|
+
expect(result.functions.defaultWebsocket).toBeUndefined();
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
504
705
|
describe('Edge Cases', () => {
|
|
505
706
|
it('should handle empty app definition', async () => {
|
|
506
707
|
const appDefinition = {};
|
|
@@ -515,7 +716,9 @@ describe('composeServerlessDefinition', () => {
|
|
|
515
716
|
integrations: null
|
|
516
717
|
};
|
|
517
718
|
|
|
518
|
-
|
|
719
|
+
// Should not throw, just ignore invalid integrations
|
|
720
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
721
|
+
expect(result).toBeDefined();
|
|
519
722
|
});
|
|
520
723
|
|
|
521
724
|
it('should handle integration with missing Definition', async () => {
|
|
@@ -524,7 +727,7 @@ describe('composeServerlessDefinition', () => {
|
|
|
524
727
|
integrations: [invalidIntegration]
|
|
525
728
|
};
|
|
526
729
|
|
|
527
|
-
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
|
|
730
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow('Invalid integration: missing Definition or name');
|
|
528
731
|
});
|
|
529
732
|
|
|
530
733
|
it('should handle integration with missing name', async () => {
|
|
@@ -535,7 +738,7 @@ describe('composeServerlessDefinition', () => {
|
|
|
535
738
|
integrations: [invalidIntegration]
|
|
536
739
|
};
|
|
537
740
|
|
|
538
|
-
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
|
|
741
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow('Invalid integration: missing Definition or name');
|
|
539
742
|
});
|
|
540
743
|
});
|
|
541
744
|
});
|
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-next.
|
|
4
|
+
"version": "2.0.0-next.38",
|
|
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-next.
|
|
13
|
-
"@friggframework/test": "2.0.0-next.
|
|
12
|
+
"@friggframework/schemas": "2.0.0-next.38",
|
|
13
|
+
"@friggframework/test": "2.0.0-next.38",
|
|
14
14
|
"@hapi/boom": "^10.0.1",
|
|
15
15
|
"@inquirer/prompts": "^5.3.8",
|
|
16
16
|
"axios": "^1.7.2",
|
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
"serverless-http": "^2.7.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
36
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
35
|
+
"@friggframework/eslint-config": "2.0.0-next.38",
|
|
36
|
+
"@friggframework/prettier-config": "2.0.0-next.38",
|
|
37
|
+
"jest": "^30.1.3",
|
|
37
38
|
"prettier": "^2.7.1",
|
|
38
39
|
"serverless": "3.39.0",
|
|
39
40
|
"serverless-dotenv-plugin": "^6.0.0",
|
|
@@ -65,5 +66,5 @@
|
|
|
65
66
|
"publishConfig": {
|
|
66
67
|
"access": "public"
|
|
67
68
|
},
|
|
68
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "a9c9c28fd9abdc8c96a38b8ab0fbb3cbf7d89960"
|
|
69
70
|
}
|