@stacksjs/cloud 0.58.48 → 0.58.50

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