thinkwork-cli 0.9.0 → 0.9.2
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/LICENSE +202 -0
- package/README.md +2 -2
- package/dist/cli.js +1315 -330
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
- package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +153 -2
- package/dist/terraform/schema.graphql +17 -0
- package/package.json +15 -14
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Compliance Audit Bucket — Variables
|
|
3
|
+
################################################################################
|
|
4
|
+
|
|
5
|
+
variable "stage" {
|
|
6
|
+
description = "Deployment stage"
|
|
7
|
+
type = string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
variable "account_id" {
|
|
11
|
+
description = "AWS account ID. Used in the anchor Lambda role's trust policy as an aws:SourceAccount condition for confused-deputy defense — even if a malicious principal somehow obtained the role's ARN, they would need to assume it from inside this account."
|
|
12
|
+
type = string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
variable "region" {
|
|
16
|
+
description = "AWS region. Used in the anchor Lambda role's trust-policy aws:SourceArn pin to constrain AssumeRole to the predictable function ARN `arn:aws:lambda:{region}:{account_id}:function:thinkwork-{stage}-api-compliance-anchor`. Required. Whitespace is rejected (a trailing space silently produces a malformed ARN that never matches at AssumeRole time)."
|
|
17
|
+
type = string
|
|
18
|
+
|
|
19
|
+
validation {
|
|
20
|
+
condition = length(var.region) > 0 && var.region == trimspace(var.region)
|
|
21
|
+
error_message = "region must be non-empty and free of leading/trailing whitespace. A whitespace-padded value silently produces a malformed aws:SourceArn that never matches."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "compliance_reader_secret_arn" {
|
|
26
|
+
description = "ARN of the Secrets Manager secret holding the `compliance_reader` Aurora role credentials (Phase 3 U2). The U8a anchor Lambda's anchor_secrets policy enumerates this ARN explicitly."
|
|
27
|
+
type = string
|
|
28
|
+
|
|
29
|
+
validation {
|
|
30
|
+
condition = length(var.compliance_reader_secret_arn) > 0
|
|
31
|
+
error_message = "compliance_reader_secret_arn must be non-empty."
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "compliance_drainer_secret_arn" {
|
|
36
|
+
description = "ARN of the Secrets Manager secret holding the `compliance_drainer` Aurora role credentials (Phase 3 U2 / PR #887). The U8a anchor Lambda uses this to UPDATE compliance.tenant_anchor_state."
|
|
37
|
+
type = string
|
|
38
|
+
|
|
39
|
+
validation {
|
|
40
|
+
condition = length(var.compliance_drainer_secret_arn) > 0
|
|
41
|
+
error_message = "compliance_drainer_secret_arn must be non-empty."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
variable "bucket_name" {
|
|
46
|
+
description = "Name of the compliance audit-anchor S3 bucket (master-plan canonical: thinkwork-{stage}-compliance-anchors)"
|
|
47
|
+
type = string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
variable "kms_key_arn" {
|
|
51
|
+
description = "ARN of the customer-managed KMS key used for SSE-KMS encryption of anchor objects. Must be non-empty - wired from module.kms.key_arn at the composite root."
|
|
52
|
+
type = string
|
|
53
|
+
|
|
54
|
+
validation {
|
|
55
|
+
condition = length(var.kms_key_arn) > 0
|
|
56
|
+
error_message = "kms_key_arn must be non-empty. Check that module.kms is enabled in the composite root (var.create_kms_key = true)."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
variable "mode" {
|
|
61
|
+
description = "S3 Object Lock retention mode. GOVERNANCE allows a privileged role with s3:BypassGovernanceRetention to delete or shorten retention; COMPLIANCE is irreversible (even AWS root cannot delete or shorten until retention expires). Default GOVERNANCE per master plan Decision #2; flip to COMPLIANCE in prod via tfvars at audit-engagement time."
|
|
62
|
+
type = string
|
|
63
|
+
default = "GOVERNANCE"
|
|
64
|
+
|
|
65
|
+
validation {
|
|
66
|
+
condition = contains(["GOVERNANCE", "COMPLIANCE"], var.mode)
|
|
67
|
+
error_message = "mode must be either GOVERNANCE or COMPLIANCE."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
variable "retention_days" {
|
|
72
|
+
description = "Default Object Lock retention in days, applied to every PutObject under the anchors/ prefix unless an explicit per-object retention overrides it. SOC2 Type 1 baseline is 12 months (365)."
|
|
73
|
+
type = number
|
|
74
|
+
default = 365
|
|
75
|
+
|
|
76
|
+
validation {
|
|
77
|
+
condition = var.retention_days > 0
|
|
78
|
+
error_message = "retention_days must be greater than 0."
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Phase 3 U8b — explicit override for COMPLIANCE-mode in non-prod stages.
|
|
83
|
+
# COMPLIANCE mode is irreversible; once any object is written under it, that
|
|
84
|
+
# object is undeleteable for the full retention window — even AWS root cannot
|
|
85
|
+
# remove it. Defaulting to false keeps a "stage typo" (stage = "prod-canary"
|
|
86
|
+
# instead of "prod") from silently producing an unrecoverable dev bucket. The
|
|
87
|
+
# operator must set this true in tfvars at audit-engagement time on the
|
|
88
|
+
# specific non-prod stage that should run COMPLIANCE.
|
|
89
|
+
variable "allow_compliance_in_non_prod" {
|
|
90
|
+
description = "Permit var.mode = COMPLIANCE on non-prod stages. Default false. Set true ONLY at audit-engagement time on the specific non-prod stage where COMPLIANCE is intended; the bucket's WORM bytes are unrecoverable once retention starts."
|
|
91
|
+
type = bool
|
|
92
|
+
default = false
|
|
93
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Compliance Exports Bucket — Data Module (U11.U2)
|
|
3
|
+
#
|
|
4
|
+
# Ephemeral S3 bucket for SOC2 walkthrough export artifacts produced by the
|
|
5
|
+
# U11 export runner Lambda. Distinct from the WORM-protected
|
|
6
|
+
# compliance-audit-bucket: this is a 7-day-lifecycle short-lived artifact
|
|
7
|
+
# bucket; nothing here is the system of record. The system of record is
|
|
8
|
+
# `compliance.audit_events` in Aurora; the runner just rematerializes
|
|
9
|
+
# filtered slices into CSV/NDJSON for auditor download.
|
|
10
|
+
#
|
|
11
|
+
# Why not Object Lock:
|
|
12
|
+
# - Exports are derivable from the Aurora rows + the runner code; losing
|
|
13
|
+
# an export does not lose evidence.
|
|
14
|
+
# - Object Lock + 7-day retention is incoherent (the lock makes the
|
|
15
|
+
# object undeleteable for 7 days then it does become deletable, which
|
|
16
|
+
# is just a reverse-engineered version of expiration with extra cost).
|
|
17
|
+
# - Auditors want a presigned URL that works for ~15 minutes; the bucket
|
|
18
|
+
# itself is plumbing, not a trust anchor.
|
|
19
|
+
#
|
|
20
|
+
# Why versioning suspended:
|
|
21
|
+
# - Export artifacts are write-once-by-name (key includes jobId UUID); a
|
|
22
|
+
# second write would imply a runner bug, not a legitimate update. We
|
|
23
|
+
# prefer the second write to fail noisily on a precondition rather than
|
|
24
|
+
# silently produce a v2 object.
|
|
25
|
+
#
|
|
26
|
+
# Bucket policy (defense-in-depth):
|
|
27
|
+
# - EnforceHTTPS: deny any s3:* over plain HTTP.
|
|
28
|
+
#
|
|
29
|
+
# IAM role (runner Lambda's eventual identity, defined below):
|
|
30
|
+
# - Allow s3:PutObject + s3:GetObject + s3:GetObjectAttributes +
|
|
31
|
+
# s3:AbortMultipartUpload on ${bucket}/* (any prefix; runner picks).
|
|
32
|
+
# - Allow s3:ListBucket on ${bucket} (multipart upload listing).
|
|
33
|
+
# - Explicit Deny on every other S3 ARN (NotResource defense).
|
|
34
|
+
#
|
|
35
|
+
# Inert seam (U11.U2):
|
|
36
|
+
# - Module ships the role; the standalone Lambda function in
|
|
37
|
+
# terraform/modules/app/lambda-api/handlers.tf assumes this role.
|
|
38
|
+
# - The Lambda body is a stub that throws "not implemented" —
|
|
39
|
+
# U11.U3 swaps in the live runner.
|
|
40
|
+
################################################################################
|
|
41
|
+
|
|
42
|
+
resource "aws_s3_bucket" "exports" {
|
|
43
|
+
bucket = var.bucket_name
|
|
44
|
+
|
|
45
|
+
# Hardcoded false — even though objects expire after 7 days, an
|
|
46
|
+
# accidental terraform destroy on a bucket holding in-flight export
|
|
47
|
+
# artifacts would interrupt an auditor mid-download.
|
|
48
|
+
force_destroy = false
|
|
49
|
+
|
|
50
|
+
tags = {
|
|
51
|
+
Name = var.bucket_name
|
|
52
|
+
Stage = var.stage
|
|
53
|
+
Purpose = "compliance-exports"
|
|
54
|
+
Expiration = "${var.expiration_days}d"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
resource "aws_s3_bucket_versioning" "exports" {
|
|
59
|
+
bucket = aws_s3_bucket.exports.id
|
|
60
|
+
|
|
61
|
+
# Suspended — write-once-by-jobId; we don't want v1/v2 of the same key.
|
|
62
|
+
versioning_configuration {
|
|
63
|
+
status = "Suspended"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "exports" {
|
|
68
|
+
bucket = aws_s3_bucket.exports.id
|
|
69
|
+
|
|
70
|
+
rule {
|
|
71
|
+
apply_server_side_encryption_by_default {
|
|
72
|
+
sse_algorithm = "AES256"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resource "aws_s3_bucket_public_access_block" "exports" {
|
|
78
|
+
bucket = aws_s3_bucket.exports.id
|
|
79
|
+
|
|
80
|
+
block_public_acls = true
|
|
81
|
+
block_public_policy = true
|
|
82
|
+
ignore_public_acls = true
|
|
83
|
+
restrict_public_buckets = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resource "aws_s3_bucket_lifecycle_configuration" "exports" {
|
|
87
|
+
bucket = aws_s3_bucket.exports.id
|
|
88
|
+
|
|
89
|
+
rule {
|
|
90
|
+
id = "exports-expiration"
|
|
91
|
+
status = "Enabled"
|
|
92
|
+
|
|
93
|
+
# Apply to every object — exports always live in keyed prefixes the
|
|
94
|
+
# runner picks, but the lifecycle is uniform across the bucket.
|
|
95
|
+
filter {}
|
|
96
|
+
|
|
97
|
+
expiration {
|
|
98
|
+
days = var.expiration_days
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Belt-and-suspenders for any failed multipart uploads — abort after
|
|
102
|
+
# 1 day so we don't accumulate orphaned upload IDs.
|
|
103
|
+
abort_incomplete_multipart_upload {
|
|
104
|
+
days_after_initiation = 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resource "aws_s3_bucket_policy" "exports" {
|
|
110
|
+
bucket = aws_s3_bucket.exports.id
|
|
111
|
+
|
|
112
|
+
depends_on = [aws_s3_bucket_public_access_block.exports]
|
|
113
|
+
|
|
114
|
+
policy = jsonencode({
|
|
115
|
+
Version = "2012-10-17"
|
|
116
|
+
Statement = [
|
|
117
|
+
{
|
|
118
|
+
Sid = "EnforceHTTPS"
|
|
119
|
+
Effect = "Deny"
|
|
120
|
+
Principal = "*"
|
|
121
|
+
Action = "s3:*"
|
|
122
|
+
Resource = [
|
|
123
|
+
aws_s3_bucket.exports.arn,
|
|
124
|
+
"${aws_s3_bucket.exports.arn}/*",
|
|
125
|
+
]
|
|
126
|
+
Condition = {
|
|
127
|
+
Bool = {
|
|
128
|
+
"aws:SecureTransport" = "false"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
################################################################################
|
|
137
|
+
# IAM role for the runner Lambda (U11.U3 will assume this role; in U11.U2
|
|
138
|
+
# the standalone Lambda function references this role's ARN, but its body
|
|
139
|
+
# is the inert stub).
|
|
140
|
+
################################################################################
|
|
141
|
+
|
|
142
|
+
resource "aws_iam_role" "runner_lambda" {
|
|
143
|
+
name = "thinkwork-${var.stage}-compliance-export-runner-role"
|
|
144
|
+
|
|
145
|
+
# Trust policy: Lambda service principal + account-pin only.
|
|
146
|
+
#
|
|
147
|
+
# We do NOT pin `aws:SourceArn` to the function ARN. The runner is
|
|
148
|
+
# invoked via `aws_lambda_event_source_mapping` (SQS → Lambda); when
|
|
149
|
+
# AWS Lambda calls `sts:AssumeRole` to validate the mapping at
|
|
150
|
+
# CreateEventSourceMapping time, the `aws:SourceArn` context key is
|
|
151
|
+
# the SQS queue ARN, not the Lambda function ARN. Pinning to the
|
|
152
|
+
# function ARN — even with `StringEqualsIfExists` — caused "Please
|
|
153
|
+
# add Lambda as a Trusted Entity for ..." failures
|
|
154
|
+
# (deploy runs 25557118131 and 25560679065).
|
|
155
|
+
#
|
|
156
|
+
# `aws:SourceAccount` strict-equals is the substantive confused-deputy
|
|
157
|
+
# guard — even if the role ARN leaks, only this account can use it.
|
|
158
|
+
# The Lambda service principal restriction in `Principal` keeps
|
|
159
|
+
# non-Lambda services from assuming.
|
|
160
|
+
#
|
|
161
|
+
# The anchor Lambda role (compliance-audit-bucket) keeps a strict
|
|
162
|
+
# SourceArn pin to the function ARN because it's scheduler-triggered;
|
|
163
|
+
# EventBridge Scheduler always passes the function ARN as SourceArn.
|
|
164
|
+
# SQS event source mapping does not — that's what makes this role
|
|
165
|
+
# different.
|
|
166
|
+
assume_role_policy = jsonencode({
|
|
167
|
+
Version = "2012-10-17"
|
|
168
|
+
Statement = [{
|
|
169
|
+
Effect = "Allow"
|
|
170
|
+
Principal = { Service = "lambda.amazonaws.com" }
|
|
171
|
+
Action = "sts:AssumeRole"
|
|
172
|
+
Condition = {
|
|
173
|
+
StringEquals = {
|
|
174
|
+
"aws:SourceAccount" = var.account_id
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}]
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
resource "aws_iam_role_policy_attachment" "runner_basic" {
|
|
182
|
+
role = aws_iam_role.runner_lambda.name
|
|
183
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
resource "aws_iam_role_policy" "runner_s3_allow" {
|
|
187
|
+
name = "exports-s3-allow"
|
|
188
|
+
role = aws_iam_role.runner_lambda.id
|
|
189
|
+
|
|
190
|
+
policy = jsonencode({
|
|
191
|
+
Version = "2012-10-17"
|
|
192
|
+
Statement = [
|
|
193
|
+
{
|
|
194
|
+
Sid = "ExportsObjectsAllow"
|
|
195
|
+
Effect = "Allow"
|
|
196
|
+
Action = [
|
|
197
|
+
"s3:PutObject",
|
|
198
|
+
"s3:GetObject",
|
|
199
|
+
"s3:GetObjectAttributes",
|
|
200
|
+
"s3:AbortMultipartUpload",
|
|
201
|
+
]
|
|
202
|
+
Resource = "${aws_s3_bucket.exports.arn}/*"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
Sid = "ExportsBucketAllow"
|
|
206
|
+
Effect = "Allow"
|
|
207
|
+
Action = [
|
|
208
|
+
"s3:ListBucket",
|
|
209
|
+
"s3:ListBucketMultipartUploads",
|
|
210
|
+
"s3:GetBucketLocation",
|
|
211
|
+
]
|
|
212
|
+
Resource = aws_s3_bucket.exports.arn
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resource "aws_iam_role_policy" "runner_secrets" {
|
|
219
|
+
count = length(var.database_secret_arn) > 0 ? 1 : 0
|
|
220
|
+
name = "exports-runner-secrets"
|
|
221
|
+
role = aws_iam_role.runner_lambda.id
|
|
222
|
+
|
|
223
|
+
# The runner reads the writer-pool DB credentials at module-load via
|
|
224
|
+
# GetSecretValue. Without this grant the runner throws AccessDenied
|
|
225
|
+
# at the first SQS-triggered invocation (deploy run 25561658625
|
|
226
|
+
# failed the smoke gate with this exact error). Scoped to the single
|
|
227
|
+
# secret ARN — no wildcard.
|
|
228
|
+
policy = jsonencode({
|
|
229
|
+
Version = "2012-10-17"
|
|
230
|
+
Statement = [
|
|
231
|
+
{
|
|
232
|
+
Sid = "RunnerDatabaseSecretRead"
|
|
233
|
+
Effect = "Allow"
|
|
234
|
+
Action = ["secretsmanager:GetSecretValue"]
|
|
235
|
+
Resource = var.database_secret_arn
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resource "aws_iam_role_policy" "runner_s3_deny_other_buckets" {
|
|
242
|
+
name = "exports-s3-deny-other-buckets"
|
|
243
|
+
role = aws_iam_role.runner_lambda.id
|
|
244
|
+
|
|
245
|
+
# Defense-in-depth: even if a future inline-policy attachment or AWS-
|
|
246
|
+
# managed-policy widens the role's S3 grants, this explicit deny
|
|
247
|
+
# restricts the runner to the exports bucket only. NotResource ensures
|
|
248
|
+
# the deny applies to every S3 ARN that ISN'T the exports bucket.
|
|
249
|
+
policy = jsonencode({
|
|
250
|
+
Version = "2012-10-17"
|
|
251
|
+
Statement = [
|
|
252
|
+
{
|
|
253
|
+
Sid = "DenyAllOtherS3Buckets"
|
|
254
|
+
Effect = "Deny"
|
|
255
|
+
Action = [
|
|
256
|
+
"s3:PutObject",
|
|
257
|
+
"s3:GetObject",
|
|
258
|
+
"s3:DeleteObject",
|
|
259
|
+
"s3:ListBucket",
|
|
260
|
+
"s3:AbortMultipartUpload",
|
|
261
|
+
]
|
|
262
|
+
NotResource = [
|
|
263
|
+
aws_s3_bucket.exports.arn,
|
|
264
|
+
"${aws_s3_bucket.exports.arn}/*",
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
})
|
|
269
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Compliance Exports Bucket — Outputs
|
|
3
|
+
################################################################################
|
|
4
|
+
|
|
5
|
+
output "bucket_name" {
|
|
6
|
+
description = "Name (id) of the compliance exports S3 bucket."
|
|
7
|
+
value = aws_s3_bucket.exports.id
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
output "bucket_arn" {
|
|
11
|
+
description = "ARN of the compliance exports S3 bucket."
|
|
12
|
+
value = aws_s3_bucket.exports.arn
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
output "runner_role_arn" {
|
|
16
|
+
description = "ARN of the IAM role the U11 export runner Lambda assumes. Inert in U11.U2 — the function exists with a stub body until U11.U3."
|
|
17
|
+
value = aws_iam_role.runner_lambda.arn
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
output "runner_role_name" {
|
|
21
|
+
description = "Name of the IAM role the runner Lambda assumes. Used by sibling app-tier modules that need to attach inline policies (e.g., DLQ SendMessage, SQS receive) to this role without re-deriving the name from the ARN."
|
|
22
|
+
value = aws_iam_role.runner_lambda.name
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Compliance Exports Bucket — Variables
|
|
3
|
+
################################################################################
|
|
4
|
+
|
|
5
|
+
variable "stage" {
|
|
6
|
+
description = "Deployment stage."
|
|
7
|
+
type = string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
variable "account_id" {
|
|
11
|
+
description = "AWS account ID. Pinned in the runner Lambda's trust policy as aws:SourceAccount for confused-deputy defense."
|
|
12
|
+
type = string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
variable "region" {
|
|
16
|
+
description = "AWS region. Used in the runner Lambda's trust-policy aws:SourceArn pin to constrain AssumeRole to the predictable function ARN `arn:aws:lambda:{region}:{account_id}:function:thinkwork-{stage}-api-compliance-export-runner`."
|
|
17
|
+
type = string
|
|
18
|
+
|
|
19
|
+
validation {
|
|
20
|
+
condition = length(var.region) > 0 && var.region == trimspace(var.region)
|
|
21
|
+
error_message = "region must be non-empty and free of leading/trailing whitespace."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "bucket_name" {
|
|
26
|
+
description = "Name of the compliance exports S3 bucket. Canonical pattern: `thinkwork-{stage}-compliance-exports`."
|
|
27
|
+
type = string
|
|
28
|
+
|
|
29
|
+
validation {
|
|
30
|
+
condition = length(var.bucket_name) > 0
|
|
31
|
+
error_message = "bucket_name must be non-empty."
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "database_secret_arn" {
|
|
36
|
+
description = "ARN of the Secrets Manager secret holding the writer-pool DB credentials. The runner Lambda reads this at module-load to construct the Aurora connection string for INSERT/UPDATE on compliance.export_jobs and SELECT on compliance.audit_events. Default empty so existing module consumers don't break before they wire it; the runner throws a deterministic error if unset at runtime."
|
|
37
|
+
type = string
|
|
38
|
+
default = ""
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
variable "expiration_days" {
|
|
42
|
+
description = "Number of days an export object lives before lifecycle expiration. SOC2 walkthrough auditors typically download artifacts within hours; 7 days is the audit-window default."
|
|
43
|
+
type = number
|
|
44
|
+
default = 7
|
|
45
|
+
|
|
46
|
+
validation {
|
|
47
|
+
condition = var.expiration_days >= 1 && var.expiration_days <= 90
|
|
48
|
+
error_message = "expiration_days must be between 1 and 90 inclusive."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# S3 Backups Bucket — Data Module
|
|
3
|
+
#
|
|
4
|
+
# Separate bucket for pre-destructive-migration CSV exports and other
|
|
5
|
+
# operational snapshots. Distinct from the primary storage bucket because:
|
|
6
|
+
#
|
|
7
|
+
# - No CORS (client browsers never read this bucket)
|
|
8
|
+
# - Block public access fully (public-access tickets cannot accidentally
|
|
9
|
+
# grant access via aws_s3_bucket_public_access_block)
|
|
10
|
+
# - Server-side encryption on by default
|
|
11
|
+
# - Lifecycle rule auto-expires `pre-drop/` objects after 90 days so cost
|
|
12
|
+
# stays bounded without a separate sweeper
|
|
13
|
+
# - HTTPS-only bucket policy
|
|
14
|
+
#
|
|
15
|
+
# Used by:
|
|
16
|
+
# - packages/database-pg/drizzle/0027_thread_cleanup_drops.sql (U5 of the
|
|
17
|
+
# thread-detail cleanup plan) via `aws_s3.query_export_to_s3` calls.
|
|
18
|
+
# - Any future destructive migration that wants a pre-apply row-data
|
|
19
|
+
# snapshot.
|
|
20
|
+
#
|
|
21
|
+
# Pairs with an IAM role on the Aurora cluster (see
|
|
22
|
+
# `terraform/modules/data/aurora-postgres/main.tf`) so the cluster can
|
|
23
|
+
# PutObject directly without credentials in the SQL file.
|
|
24
|
+
################################################################################
|
|
25
|
+
|
|
26
|
+
variable "stage" {
|
|
27
|
+
description = "Deployment stage"
|
|
28
|
+
type = string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
variable "bucket_name" {
|
|
32
|
+
description = "Name of the S3 backups bucket"
|
|
33
|
+
type = string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resource "aws_s3_bucket" "backups" {
|
|
37
|
+
bucket = var.bucket_name
|
|
38
|
+
|
|
39
|
+
tags = {
|
|
40
|
+
Name = var.bucket_name
|
|
41
|
+
Stage = var.stage
|
|
42
|
+
# Identifies the bucket's purpose so operators can spot accidental reads
|
|
43
|
+
# in CloudTrail and so cost-allocation tags split backups out of the
|
|
44
|
+
# primary storage bucket.
|
|
45
|
+
Purpose = "operational-backups"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resource "aws_s3_bucket_public_access_block" "backups" {
|
|
50
|
+
bucket = aws_s3_bucket.backups.id
|
|
51
|
+
|
|
52
|
+
block_public_acls = true
|
|
53
|
+
block_public_policy = true
|
|
54
|
+
ignore_public_acls = true
|
|
55
|
+
restrict_public_buckets = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "backups" {
|
|
59
|
+
bucket = aws_s3_bucket.backups.id
|
|
60
|
+
|
|
61
|
+
rule {
|
|
62
|
+
apply_server_side_encryption_by_default {
|
|
63
|
+
sse_algorithm = "AES256"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resource "aws_s3_bucket_lifecycle_configuration" "backups" {
|
|
69
|
+
bucket = aws_s3_bucket.backups.id
|
|
70
|
+
|
|
71
|
+
rule {
|
|
72
|
+
id = "expire-pre-drop-snapshots"
|
|
73
|
+
status = "Enabled"
|
|
74
|
+
|
|
75
|
+
filter {
|
|
76
|
+
prefix = "pre-drop/"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expiration {
|
|
80
|
+
days = 90
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resource "aws_s3_bucket_policy" "backups_https_only" {
|
|
86
|
+
bucket = aws_s3_bucket.backups.id
|
|
87
|
+
|
|
88
|
+
# aws_s3_bucket_public_access_block must apply first; otherwise account-
|
|
89
|
+
# level BPA defaults can intermittently reject the bucket-policy PUT with
|
|
90
|
+
# AccessDenied during initial apply.
|
|
91
|
+
depends_on = [aws_s3_bucket_public_access_block.backups]
|
|
92
|
+
|
|
93
|
+
policy = jsonencode({
|
|
94
|
+
Version = "2012-10-17"
|
|
95
|
+
Statement = [
|
|
96
|
+
{
|
|
97
|
+
Sid = "EnforceHTTPS"
|
|
98
|
+
Effect = "Deny"
|
|
99
|
+
Principal = "*"
|
|
100
|
+
Action = "s3:*"
|
|
101
|
+
Resource = [
|
|
102
|
+
aws_s3_bucket.backups.arn,
|
|
103
|
+
"${aws_s3_bucket.backups.arn}/*",
|
|
104
|
+
]
|
|
105
|
+
Condition = {
|
|
106
|
+
Bool = {
|
|
107
|
+
"aws:SecureTransport" = "false"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
output "bucket_name" {
|
|
116
|
+
description = "Name of the S3 backups bucket"
|
|
117
|
+
value = aws_s3_bucket.backups.id
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
output "bucket_arn" {
|
|
121
|
+
description = "ARN of the S3 backups bucket"
|
|
122
|
+
value = aws_s3_bucket.backups.arn
|
|
123
|
+
}
|
|
@@ -35,6 +35,14 @@ resource "aws_s3_bucket" "main" {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
resource "aws_s3_bucket_versioning" "main" {
|
|
39
|
+
bucket = aws_s3_bucket.main.id
|
|
40
|
+
|
|
41
|
+
versioning_configuration {
|
|
42
|
+
status = "Enabled"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
resource "aws_s3_bucket_cors_configuration" "main" {
|
|
39
47
|
bucket = aws_s3_bucket.main.id
|
|
40
48
|
|
|
@@ -47,6 +55,11 @@ resource "aws_s3_bucket_cors_configuration" "main" {
|
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
resource "aws_s3_bucket_notification" "eventbridge" {
|
|
59
|
+
bucket = aws_s3_bucket.main.id
|
|
60
|
+
eventbridge = true
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
resource "aws_s3_bucket_policy" "https_only" {
|
|
51
64
|
bucket = aws_s3_bucket.main.id
|
|
52
65
|
|
|
@@ -107,7 +107,7 @@ variable "admin_logout_urls" {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
variable "mobile_callback_urls" {
|
|
110
|
-
description = "OAuth callback URLs for the mobile client.
|
|
110
|
+
description = "OAuth callback URLs for the mobile client. Host apps that embed the SDK register their own deep-link here. Proper per-host app client isolation is 0.3.0 work — this is the stopgap capture of the drift from the CLI-applied URIs."
|
|
111
111
|
type = list(string)
|
|
112
112
|
default = [
|
|
113
113
|
"exp://localhost:8081",
|
|
@@ -119,7 +119,7 @@ variable "mobile_callback_urls" {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
variable "mobile_logout_urls" {
|
|
122
|
-
description = "OAuth logout URLs for the mobile client.
|
|
122
|
+
description = "OAuth logout URLs for the mobile client. Host apps that embed the SDK register their own deep-link here (see `mobile_callback_urls` for rationale)."
|
|
123
123
|
type = list(string)
|
|
124
124
|
default = [
|
|
125
125
|
"exp://localhost:8081",
|