@stacksjs/cloud 0.65.0 → 0.67.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.
@@ -1,272 +0,0 @@
1
- import type { aws_route53 as route53 } from 'aws-cdk-lib'
2
- import type { Construct } from 'constructs'
3
- import type { NestedCloudProps } from '../types'
4
- // waf and encryption
5
- import { config } from '@stacksjs/config'
6
- import {
7
- aws_certificatemanager as acm,
8
- aws_cloudfront as cloudfront,
9
- Duration,
10
- aws_kms as kms,
11
- RemovalPolicy,
12
- Tags,
13
- aws_wafv2 as wafv2,
14
- } from 'aws-cdk-lib'
15
-
16
- export interface StorageStackProps extends NestedCloudProps {
17
- zone: route53.IHostedZone
18
- }
19
-
20
- export class SecurityStack {
21
- firewall: wafv2.CfnWebACL
22
- kmsKey: kms.Key
23
- certificate: acm.Certificate
24
- originAccessIdentity: cloudfront.OriginAccessIdentity
25
-
26
- constructor(scope: Construct, props: StorageStackProps) {
27
- const firewallOptions = config.cloud.firewall
28
-
29
- if (!firewallOptions)
30
- throw new Error('No firewall options found in config')
31
-
32
- const options = {
33
- defaultAction: { allow: {} },
34
- scope: 'CLOUDFRONT',
35
- visibilityConfig: {
36
- sampledRequestsEnabled: true,
37
- cloudWatchMetricsEnabled: true,
38
- metricName: 'firewallMetric',
39
- },
40
- rules: this.getFirewallRules(scope),
41
- }
42
-
43
- this.firewall = new wafv2.CfnWebACL(scope, 'StacksWebFirewall', options)
44
- Tags.of(this.firewall).add('Name', 'waf-cloudfront', { priority: 300 })
45
- Tags.of(this.firewall).add('Purpose', 'CloudFront', { priority: 300 })
46
- Tags.of(this.firewall).add('CreatedBy', 'CloudFormation', {
47
- priority: 300,
48
- })
49
-
50
- this.kmsKey = new kms.Key(scope, 'EncryptionKey', {
51
- alias: 'stacks-encryption-key',
52
- description: 'KMS key for Stacks Cloud',
53
- enableKeyRotation: true,
54
- removalPolicy: RemovalPolicy.DESTROY,
55
- pendingWindow: Duration.days(30),
56
- })
57
-
58
- this.certificate = new acm.Certificate(scope, 'Certificate', {
59
- domainName: props.domain,
60
- validation: acm.CertificateValidation.fromDns(props.zone),
61
- subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`, `docs.${props.domain}`],
62
- })
63
-
64
- this.originAccessIdentity = new cloudfront.OriginAccessIdentity(scope, 'OAI')
65
- }
66
-
67
- getFirewallRules(scope: Construct): wafv2.CfnWebACL.RuleProperty[] {
68
- const rules: wafv2.CfnWebACL.RuleProperty[] = []
69
- const priorities = []
70
-
71
- if (config.security.firewall?.countryCodes?.length) {
72
- priorities.push(1)
73
- rules.push({
74
- name: 'CountryRule',
75
- priority: priorities.length,
76
- statement: {
77
- geoMatchStatement: {
78
- countryCodes: config.security.firewall.countryCodes as string[],
79
- },
80
- },
81
- action: {
82
- block: {},
83
- },
84
- visibilityConfig: {
85
- sampledRequestsEnabled: true,
86
- cloudWatchMetricsEnabled: true,
87
- metricName: 'CountryRule',
88
- },
89
- })
90
- }
91
-
92
- if (config.security.firewall?.ipAddresses?.length) {
93
- const ipSet = new wafv2.CfnIPSet(scope, 'IpSet', {
94
- name: 'IpSet',
95
- description: 'IP Set',
96
- scope: 'CLOUDFRONT',
97
- addresses: config.security.firewall.ipAddresses as string[],
98
- ipAddressVersion: 'IPV4',
99
- })
100
-
101
- priorities.push(1)
102
- rules.push({
103
- name: 'IpAddressRule',
104
- priority: priorities.length,
105
- statement: {
106
- ipSetReferenceStatement: {
107
- arn: ipSet.attrArn,
108
- },
109
- },
110
- action: {
111
- block: {},
112
- },
113
- visibilityConfig: {
114
- sampledRequestsEnabled: true,
115
- cloudWatchMetricsEnabled: true,
116
- metricName: 'IpAddressRule',
117
- },
118
- })
119
- }
120
-
121
- if (config.security.firewall?.httpHeaders?.length) {
122
- config.security.firewall.httpHeaders.forEach((header: string | undefined, index: number) => {
123
- priorities.push(1)
124
- rules.push({
125
- name: `HttpHeaderRule${index}`,
126
- priority: priorities.length,
127
- statement: {
128
- byteMatchStatement: {
129
- fieldToMatch: {
130
- singleHeader: {
131
- name: header,
132
- },
133
- },
134
- positionalConstraint: 'EXACTLY',
135
- searchString: 'true',
136
- textTransformations: [
137
- {
138
- priority: 0,
139
- type: 'NONE',
140
- },
141
- ],
142
- },
143
- },
144
- action: {
145
- block: {},
146
- },
147
- visibilityConfig: {
148
- sampledRequestsEnabled: true,
149
- cloudWatchMetricsEnabled: true,
150
- metricName: `HttpHeaderRule${index}`,
151
- },
152
- })
153
- })
154
- }
155
-
156
- // if (config.security.firewall?.queryString?.length) {
157
- // priorities.push(1)
158
- // rules.push({
159
- // name: 'QueryStringRule',
160
- // priority: priorities.length,
161
- // statement: {
162
- // byteMatchStatement: {
163
- // fieldToMatch: {
164
- // queryString: {},
165
- // },
166
- // positionalConstraint: 'EXACTLY',
167
- // searchString: config.security.firewall.queryString.join(', '),
168
- // textTransformations: [
169
- // {
170
- // priority: 0,
171
- // type: 'NONE',
172
- // },
173
- // ],
174
- // },
175
- // },
176
- // action: {
177
- // block: {},
178
- // },
179
- // visibilityConfig: {
180
- // sampledRequestsEnabled: true,
181
- // cloudWatchMetricsEnabled: true,
182
- // metricName: 'QueryStringRule',
183
- // },
184
- // })
185
- // }
186
-
187
- // if (config.security.firewall?.rateLimitPerMinute) {
188
- // priorities.push(1)
189
- // rules.push({
190
- // name: 'RateLimitRule',
191
- // priority: priorities.length,
192
- // statement: {
193
- // rateBasedStatement: {
194
- // limit: config.security.firewall.rateLimitPerMinute,
195
- // aggregateKeyType: 'IP',
196
- // },
197
- // },
198
- // action: {
199
- // block: {},
200
- // },
201
- // visibilityConfig: {
202
- // sampledRequestsEnabled: true,
203
- // cloudWatchMetricsEnabled: true,
204
- // metricName: 'RateLimitRule',
205
- // },
206
- // })
207
- // }
208
-
209
- // if (config.security.firewall?.useIpReputationLists) {
210
- // priorities.push(1)
211
- // rules.push({
212
- // name: 'IpReputationRule',
213
- // priority: priorities.length,
214
- // statement: {
215
- // managedRuleGroupStatement: {
216
- // vendorName: 'AWS',
217
- // name: 'AWSManagedRulesAmazonIpReputationList',
218
- // },
219
- // },
220
- // action: {
221
- // block: {},
222
- // },
223
- // visibilityConfig: {
224
- // sampledRequestsEnabled: true,
225
- // cloudWatchMetricsEnabled: true,
226
- // metricName: 'IpReputationRule',
227
- // },
228
- // })
229
- // }
230
-
231
- // if (config.security.firewall?.useKnownBadInputsRuleSet) {
232
- // priorities.push(1)
233
- // rules.push({
234
- // name: 'KnownBadInputsRule',
235
- // priority: priorities.length,
236
- // statement: {
237
- // managedRuleGroupStatement: {
238
- // vendorName: 'AWS',
239
- // name: 'AWSManagedRulesKnownBadInputsRuleSet',
240
- // },
241
- // },
242
- // action: {
243
- // block: {},
244
- // },
245
- // visibilityConfig: {
246
- // sampledRequestsEnabled: true,
247
- // cloudWatchMetricsEnabled: true,
248
- // metricName: 'KnownBadInputsRule',
249
- // },
250
- // })
251
- // }
252
- // also add
253
- // }, {
254
- // "name": "AWSManagedRulesAnonymousIpList",
255
- // "priority": 40,
256
- // "overrideAction": "none",
257
- // "excludedRules": []
258
- // }, {
259
- // "name": "AWSManagedRulesLinuxRuleSet",
260
- // "priority": 50,
261
- // "overrideAction": "none",
262
- // "excludedRules": []
263
- // }, {
264
- // "name": "AWSManagedRulesUnixRuleSet",
265
- // "priority": 60,
266
- // "overrideAction": "none",
267
- // "excludedRules": [],
268
- // }];
269
-
270
- return rules
271
- }
272
- }
@@ -1,197 +0,0 @@
1
- import type { aws_cloudfront as cloudfront, aws_kms as kms } from 'aws-cdk-lib'
2
- import type { Construct } from 'constructs'
3
- import type { NestedCloudProps } from '../types'
4
- import { config } from '@stacksjs/config'
5
- import { path as p } from '@stacksjs/path'
6
- import { hasFiles } from '@stacksjs/storage'
7
- import { aws_backup as backup, aws_iam as iam, RemovalPolicy, aws_s3 as s3, Tags } from 'aws-cdk-lib'
8
-
9
- export interface StorageStackProps extends NestedCloudProps {
10
- kmsKey: kms.Key
11
- originAccessIdentity: cloudfront.OriginAccessIdentity
12
- }
13
-
14
- export class StorageStack {
15
- publicBucket: s3.Bucket
16
- privateBucket: s3.Bucket
17
- docsBucket?: s3.Bucket
18
- logBucket: s3.Bucket
19
- bucketPrefix: string
20
- vault: backup.BackupVault
21
- backupPlan: backup.BackupPlan
22
- backupRole: iam.Role
23
-
24
- constructor(scope: Construct, props: StorageStackProps) {
25
- this.bucketPrefix = `${props.slug}-${props.appEnv}`
26
-
27
- this.publicBucket = new s3.Bucket(scope, 'PublicBucket', {
28
- bucketName: `${this.bucketPrefix}-public-${props.timestamp}`,
29
- versioned: true,
30
- autoDeleteObjects: true,
31
- removalPolicy: RemovalPolicy.DESTROY,
32
- encryption: s3.BucketEncryption.S3_MANAGED,
33
- websiteIndexDocument: 'index.html',
34
- websiteErrorDocument: 'index.html',
35
- publicReadAccess: true,
36
- blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
37
- })
38
-
39
- Tags.of(this.publicBucket).add('daily-backup', 'true')
40
-
41
- if (this.shouldDeployDocs()) {
42
- this.docsBucket = new s3.Bucket(scope, 'DocsBucket', {
43
- bucketName: `${this.bucketPrefix}-docs-${props.timestamp}`,
44
- versioned: true,
45
- autoDeleteObjects: true,
46
- removalPolicy: RemovalPolicy.DESTROY,
47
- encryption: s3.BucketEncryption.S3_MANAGED,
48
- websiteIndexDocument: 'index.html',
49
- websiteErrorDocument: 'index.html',
50
- publicReadAccess: true,
51
- blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
52
- })
53
-
54
- Tags.of(this.docsBucket).add('weekly-backup', 'true')
55
- }
56
-
57
- this.privateBucket = new s3.Bucket(scope, 'PrivateBucket', {
58
- bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
59
- versioned: true,
60
- removalPolicy: RemovalPolicy.DESTROY,
61
- autoDeleteObjects: true,
62
- encryption: s3.BucketEncryption.S3_MANAGED,
63
- enforceSSL: true,
64
- publicReadAccess: false,
65
- blockPublicAccess: {
66
- blockPublicAcls: true,
67
- blockPublicPolicy: true,
68
- ignorePublicAcls: true,
69
- restrictPublicBuckets: true,
70
- },
71
- })
72
-
73
- Tags.of(this.privateBucket).add('daily-backup', 'true')
74
-
75
- this.logBucket = new s3.Bucket(scope, 'LogsBucket', {
76
- bucketName: `${this.bucketPrefix}-logs-${props.timestamp}`,
77
- removalPolicy: RemovalPolicy.RETAIN, // somehow, if we try to auto-delete logs, it fails bc the bucket is not empty (even though objects should be deleted) -> that's why we let buddy cloud:cleanup handle this which is auto triggered when buddy undeploy is ran
78
- // autoDeleteObjects: true,
79
- blockPublicAccess: new s3.BlockPublicAccess({
80
- blockPublicAcls: false,
81
- ignorePublicAcls: true,
82
- blockPublicPolicy: true,
83
- restrictPublicBuckets: true,
84
- }),
85
- objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
86
- })
87
-
88
- Tags.of(this.logBucket).add('daily-backup', 'true')
89
-
90
- this.backupRole = this.createBackupRole(scope)
91
-
92
- // Daily 35 day retention
93
- this.vault = new backup.BackupVault(scope, 'BackupVault', {
94
- backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
95
- encryptionKey: props.kmsKey,
96
- removalPolicy: RemovalPolicy.DESTROY,
97
- })
98
-
99
- this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, 'BackupPlan', this.vault)
100
-
101
- this.backupPlan.addSelection('Selection', {
102
- role: this.backupRole,
103
- resources: [backup.BackupResource.fromTag('daily-backup', 'true')],
104
- })
105
- }
106
-
107
- createBackupRole(scope: Construct): iam.Role {
108
- const backupRole = new iam.Role(scope, 'BackupRole', {
109
- assumedBy: new iam.ServicePrincipal('backup.amazonaws.com'),
110
- })
111
-
112
- backupRole.addToPolicy(
113
- new iam.PolicyStatement({
114
- actions: [
115
- 's3:GetInventoryConfiguration',
116
- 's3:PutInventoryConfiguration',
117
- 's3:ListBucketVersions',
118
- 's3:ListBucket',
119
- 's3:GetBucketVersioning',
120
- 's3:GetBucketNotification',
121
- 's3:PutBucketNotification',
122
- 's3:GetBucketLocation',
123
- 's3:GetBucketTagging',
124
- ],
125
- resources: ['arn:aws:s3:::*'],
126
- sid: 'S3BucketBackupPermissions',
127
- }),
128
- )
129
-
130
- backupRole.addToPolicy(
131
- new iam.PolicyStatement({
132
- actions: [
133
- 's3:GetObjectAcl',
134
- 's3:GetObject',
135
- 's3:GetObjectVersionTagging',
136
- 's3:GetObjectVersionAcl',
137
- 's3:GetObjectTagging',
138
- 's3:GetObjectVersion',
139
- ],
140
- resources: ['arn:aws:s3:::*/*'],
141
- sid: 'S3ObjectBackupPermissions',
142
- }),
143
- )
144
-
145
- backupRole.addToPolicy(
146
- new iam.PolicyStatement({
147
- actions: ['s3:ListAllMyBuckets'],
148
- resources: ['*'],
149
- sid: 'S3GlobalPermissions',
150
- }),
151
- )
152
-
153
- backupRole.addToPolicy(
154
- new iam.PolicyStatement({
155
- actions: ['kms:Decrypt', 'kms:DescribeKey'],
156
- resources: ['*'],
157
- sid: 'KMSBackupPermissions',
158
- conditions: {
159
- StringLike: {
160
- 'kms:ViaService': 's3.*.amazonaws.com',
161
- },
162
- },
163
- }),
164
- )
165
-
166
- backupRole.addToPolicy(
167
- new iam.PolicyStatement({
168
- actions: [
169
- 'events:DescribeRule',
170
- 'events:EnableRule',
171
- 'events:PutRule',
172
- 'events:DeleteRule',
173
- 'events:PutTargets',
174
- 'events:RemoveTargets',
175
- 'events:ListTargetsByRule',
176
- 'events:DisableRule',
177
- ],
178
- resources: ['arn:aws:events:*:*:rule/AwsBackupManagedRule*'],
179
- sid: 'EventsPermissions',
180
- }),
181
- )
182
-
183
- backupRole.addToPolicy(
184
- new iam.PolicyStatement({
185
- actions: ['cloudwatch:GetMetricData', 'events:ListRules'],
186
- resources: ['*'],
187
- sid: 'EventsMetricsGlobalPermissions',
188
- }),
189
- )
190
-
191
- return backupRole
192
- }
193
-
194
- shouldDeployDocs(): boolean {
195
- return (hasFiles(p.projectPath('docs')) || config.app.docMode) ?? false
196
- }
197
- }
@@ -1,55 +0,0 @@
1
- const config = {
2
- suffix: '.html',
3
- removeTrailingSlash: false,
4
- }
5
-
6
- const regexSuffixless = /\/[^/.]+$/ // e.g. "/some/page" but not "/", "/some/" or "/some.jpg"
7
- const regexTrailingSlash = /.+\/$/ // e.g. "/some/" or "/some/page/" but not root "/"
8
-
9
- // TODO: narrow types here
10
- export function handler(event: any, context: any, callback: any): void {
11
- const { request } = event.Records[0].cf
12
- const { uri } = request
13
- const { suffix } = config
14
-
15
- if (uri === '/') {
16
- request.uri = '/index.html'
17
- callback(null, request)
18
- return
19
- }
20
-
21
- // Append ".html" to origin request
22
- if (uri.match(regexSuffixless)) {
23
- request.uri = uri + suffix
24
- callback(null, request)
25
- return
26
- }
27
-
28
- // Remove trailing slash and append ".html" to origin request
29
- if (uri.match(regexTrailingSlash)) {
30
- request.uri = `${uri.slice(0, -1)}.html`
31
- callback(null, request)
32
- return
33
- }
34
-
35
- // Redirect (301) non-root requests ending in "/" to URI without trailing slash
36
- // if (removeTrailingSlash && uri.match(/.+\/$/)) {
37
- // const response = {
38
- // // body: '',
39
- // // bodyEncoding: 'text',
40
- // headers: {
41
- // location: [{
42
- // key: 'Location',
43
- // value: uri.slice(0, -1),
44
- // }],
45
- // },
46
- // status: '301',
47
- // statusDescription: 'Moved Permanently',
48
- // }
49
- // callback(null, response)
50
- // return
51
- // }
52
-
53
- // If nothing matches, return request unchanged
54
- callback(null, request)
55
- }