Skip to content

Commit 95dd9be

Browse files
authored
feat: Refactor to use resources directly and add malware scanning. (#6)
1 parent f6c4faf commit 95dd9be

21 files changed

Lines changed: 981 additions & 193 deletions

.github/workflows/tflint.yaml

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,13 @@ name: TFLint Checks
22

33
on:
44
push:
5+
branches: [main]
56
pull_request:
6-
branches:
7-
- main
87

98
permissions:
109
contents: read
1110
security-events: write
1211

1312
jobs:
14-
lint:
15-
runs-on: ubuntu-latest
16-
steps:
17-
- name: Checkout source code
18-
uses: actions/checkout@v6
19-
- name: Cache plugin directory
20-
uses: actions/cache@v5
21-
with:
22-
path: ~/.tflint.d/plugins
23-
key: tflint-${{ hashFiles('.tflint.hcl') }}
24-
- uses: terraform-linters/setup-tflint@v6
25-
name: Setup TFLint
26-
- name: Show version
27-
run: tflint --version
28-
- name: Init TFLint
29-
run: tflint --init
30-
- name: Run TFLint
31-
# Run TFLint, outputting the results to a SARIF file. We use `tee` so
32-
# that we can still see the output in the logs, and capture the exit
33-
# code properly with `pipefail`.
34-
run: |
35-
set -o pipefail
36-
tflint --format sarif --recursive \
37-
--config "$GITHUB_WORKSPACE/.tflint.hcl" \
38-
| tee tflint-results.sarif
39-
exit "${PIPESTATUS[0]}"
40-
- name: Parse SARIF file for annotations
41-
if: always()
42-
uses: jontyms/sarif-annotations@v0.0.3
43-
with:
44-
annotation-level: notice
45-
sarif-file: tflint-results.sarif
46-
# When run on main, upload the SARIF file to GitHub.
47-
- name: Upload SARIF result
48-
if: always() && github.ref == 'refs/heads/main'
49-
uses: github/codeql-action/upload-sarif@v4
50-
with:
51-
sarif_file: tflint-results.sarif
13+
tflint:
14+
uses: codeforamerica/github-actions/.github/workflows/tflint.yaml@main

.github/workflows/trivy.yaml

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,13 @@ name: Trivy Analysis
22

33
on:
44
push:
5+
branches: [main]
6+
pull_request:
57

68
permissions:
79
contents: read
810
security-events: write
911

1012
jobs:
1113
trivy:
12-
runs-on: ubuntu-latest
13-
steps:
14-
- name: Checkout source code
15-
uses: actions/checkout@v6
16-
- name: Run Trivy vulnerability scanner
17-
# v0.35.0
18-
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
19-
with:
20-
scan-type: config
21-
ignore-unfixed: true
22-
skip-dirs: "**/*/.terraform"
23-
exit-code: 1
24-
format: sarif
25-
output: trivy-results.sarif
26-
- name: Parse SARIF file for annotations
27-
if: always()
28-
uses: jontyms/sarif-annotations@v0.0.3
29-
with:
30-
annotation-level: notice
31-
sarif-file: trivy-results.sarif
32-
# When run on main, upload the SARIF file to GitHub.
33-
- name: Upload SARIF result
34-
if: always() && github.ref == 'refs/heads/main'
35-
uses: github/codeql-action/upload-sarif@v4
36-
with:
37-
sarif_file: trivy-results.sarif
14+
uses: codeforamerica/github-actions/.github/workflows/trivy.yaml@main

.tflint.hcl

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
1-
# Uncomment if your module uses the aws provider.
2-
# plugin "aws" {
3-
# enabled = true
4-
# version = "0.37.0"
5-
# source = "github.com/terraform-linters/tflint-ruleset-aws"
6-
# }
1+
plugin "aws" {
2+
enabled = true
3+
version = "0.48.0"
4+
signature = "pgp"
5+
source = "github.com/terraform-linters/tflint-ruleset-aws"
6+
}
77

88
plugin "terraform" {
99
preset = "all"
1010
enabled = true
1111
}
12-
13-
# TFLint doesn't understand the provider for_each syntax introduced with
14-
# OpenTofu 1.9, so we need to disable these rules so it doesn't error out.
15-
rule "terraform_required_providers" {
16-
enabled = false
17-
}
18-
rule "terraform_unused_required_providers" {
19-
enabled = false
20-
}

README.md

Lines changed: 137 additions & 36 deletions
Large diffs are not rendered by default.

data.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
data "aws_caller_identity" "identity" {}
22

33
data "aws_partition" "current" {}
4+
5+
data "aws_region" "current" {}

kms.tf

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
resource "aws_kms_key" "bucket" {
2+
for_each = var.kms.create ? toset(["this"]) : toset([])
3+
4+
description = "Encryption key for bucket ${local.bucket_name}"
5+
deletion_window_in_days = var.kms.recovery_period
6+
enable_key_rotation = true
7+
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/key-policy.yaml.tftpl", {
8+
account : data.aws_caller_identity.identity.account_id
9+
bucket : local.bucket_name
10+
partition : data.aws_partition.current.partition
11+
principals : var.kms.allowed_principals
12+
})))
13+
14+
tags = var.tags
15+
}
16+
17+
resource "aws_kms_alias" "bucket" {
18+
for_each = var.kms.create ? toset(["this"]) : toset([])
19+
20+
name = "alias/${local.bucket_name}"
21+
target_key_id = aws_kms_key.bucket["this"].arn
22+
}

locals.tf

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
locals {
2-
bucket_name = join("-", [var.project, var.environment, var.name])
3-
kms_key_arn = var.encryption_key_arn != null ? var.encryption_key_arn : aws_kms_key.bucket["this"].arn
2+
# Number of random characters in the suffix appended when add_suffix is
3+
# enabled. The suffix also includes a leading hyphen.
4+
suffix_length = 8
5+
6+
base_name = join("-", [var.project, var.environment, var.name])
7+
8+
# When a suffix is added, truncate the base name so the base plus the suffix
9+
# stays within the 63-character bucket name limit, stripping any trailing
10+
# hyphen left at the truncation boundary.
11+
max_base_length = 63 - local.suffix_length - 1
12+
truncated_base = replace(substr(local.base_name, 0, local.max_base_length), "/-+$/", "")
13+
14+
bucket_name = var.add_suffix ? "${local.truncated_base}-${one(random_string.suffix[*].result)}" : local.base_name
15+
16+
# Base bucket policy rendered from the template, before merging in any
17+
# additional statements provided by the caller.
18+
base_bucket_policy = yamldecode(templatefile("${path.module}/templates/bucket-policy.yaml.tftpl", {
19+
account = data.aws_caller_identity.identity.account_id
20+
bucket = local.bucket_name
21+
partition = data.aws_partition.current.partition
22+
restrict_malware = var.malware_scanning.restrict_access
23+
scan_role_arn = var.malware_scanning.restrict_access ? aws_iam_role.malware_scanning["this"].arn : ""
24+
}))
25+
26+
kms_key_arn = var.kms.create ? aws_kms_key.bucket["this"].arn : var.kms.arn
427
logs_path = "/AWSLogs/${data.aws_caller_identity.identity.account_id}"
5-
tags = merge({ use : "file-uploads" }, var.tags)
28+
tags = merge({ sensitivity = var.sensitivity }, var.tags)
629
}

main.tf

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,143 @@
1-
resource "aws_kms_key" "bucket" {
2-
for_each = var.encryption_key_arn != null ? toset([]) : toset(["this"])
3-
4-
description = "Encryption key for bucket ${local.bucket_name}"
5-
deletion_window_in_days = var.key_recovery_period
6-
enable_key_rotation = true
7-
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/key-policy.yaml.tftpl", {
8-
account : data.aws_caller_identity.identity.account_id
9-
bucket : local.bucket_name
10-
partition : data.aws_partition.current.partition
11-
principals : var.allowed_principals
12-
})))
1+
resource "random_string" "suffix" {
2+
count = var.add_suffix ? 1 : 0
3+
4+
length = local.suffix_length
5+
lower = true
6+
numeric = true
7+
special = false
8+
upper = false
9+
}
10+
11+
resource "aws_s3_bucket" "this" {
12+
bucket = local.bucket_name
13+
force_destroy = var.force_delete
1314

1415
tags = local.tags
16+
17+
lifecycle {
18+
precondition {
19+
condition = length(local.bucket_name) >= 3 && length(local.bucket_name) <= 63
20+
error_message = <<-EOT
21+
The bucket name must be between 3 and 63 characters. Shorten project,
22+
environment, or name, or set add_suffix to true to truncate the name
23+
automatically.
24+
EOT
25+
}
26+
}
1527
}
1628

17-
resource "aws_kms_alias" "bucket" {
18-
for_each = var.encryption_key_arn != null ? toset([]) : toset(["this"])
29+
resource "aws_s3_bucket_public_access_block" "this" {
30+
bucket = aws_s3_bucket.this.id
1931

20-
name = "alias/${local.bucket_name}"
21-
target_key_id = aws_kms_key.bucket["this"].arn
32+
block_public_acls = true
33+
block_public_policy = true
34+
ignore_public_acls = true
35+
restrict_public_buckets = true
2236
}
2337

24-
module "this" {
25-
source = "boldlink/s3/aws"
26-
version = "2.6.0"
38+
resource "aws_s3_bucket_ownership_controls" "this" {
39+
bucket = aws_s3_bucket.this.id
2740

28-
bucket = local.bucket_name
29-
force_destroy = var.force_delete
41+
rule {
42+
object_ownership = "BucketOwnerEnforced"
43+
}
44+
}
3045

31-
bucket_policy = jsonencode(yamldecode(templatefile("${path.module}/templates/bucket-policy.yaml.tftpl", {
32-
partition : data.aws_partition.current.partition
33-
bucket : local.bucket_name
34-
})))
46+
resource "aws_s3_bucket_versioning" "this" {
47+
bucket = aws_s3_bucket.this.id
3548

36-
lifecycle_configuration = [{
49+
versioning_configuration {
50+
status = "Enabled"
51+
}
52+
}
53+
54+
resource "aws_s3_bucket_object_lock_configuration" "this" {
55+
for_each = var.object_lock.enabled ? toset(["this"]) : toset([])
56+
57+
# Object lock requires versioning to be enabled on the bucket first.
58+
depends_on = [aws_s3_bucket_versioning.this]
59+
60+
bucket = aws_s3_bucket.this.id
61+
62+
dynamic "rule" {
63+
for_each = var.object_lock.days != null ? toset(["this"]) : toset([])
64+
65+
content {
66+
default_retention {
67+
mode = var.object_lock.mode
68+
days = var.object_lock.days
69+
}
70+
}
71+
}
72+
}
73+
74+
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
75+
bucket = aws_s3_bucket.this.id
76+
77+
rule {
78+
apply_server_side_encryption_by_default {
79+
kms_master_key_id = local.kms_key_arn
80+
sse_algorithm = "aws:kms"
81+
}
82+
83+
bucket_key_enabled = true
84+
}
85+
}
86+
87+
resource "aws_s3_bucket_logging" "this" {
88+
bucket = aws_s3_bucket.this.id
89+
90+
target_bucket = var.logging_bucket
91+
target_prefix = "${local.logs_path}/s3accesslogs/${local.bucket_name}"
92+
}
93+
94+
resource "aws_s3_bucket_lifecycle_configuration" "this" {
95+
# Referencing noncurrent-version behavior requires versioning to be enabled
96+
# first.
97+
depends_on = [aws_s3_bucket_versioning.this]
98+
99+
bucket = aws_s3_bucket.this.id
100+
101+
rule {
37102
id = "state"
38103
status = "Enabled"
39104

40-
filter = {
105+
filter {
41106
prefix = ""
42107
}
43108

44-
abort_incomplete_multipart_upload_days = var.abort_incomplete_multipart_upload_days
109+
abort_incomplete_multipart_upload {
110+
days_after_initiation = var.abort_incomplete_multipart_upload_days
111+
}
45112

46-
noncurrent_version_expiration = [{
113+
noncurrent_version_expiration {
47114
noncurrent_days = var.noncurrent_version_expiration_days
48-
}]
49-
50-
transition = var.storage_class_transitions
51-
}]
52-
53-
sse_bucket_key_enabled = true
54-
sse_kms_master_key_arn = local.kms_key_arn
55-
sse_sse_algorithm = "aws:kms"
115+
}
56116

57-
versioning_status = "Enabled"
117+
dynamic "expiration" {
118+
for_each = var.expiration != null ? toset(["this"]) : toset([])
119+
content {
120+
days = var.expiration
121+
}
122+
}
58123

59-
s3_logging = {
60-
target_bucket = var.logging_bucket
61-
target_prefix = "${local.logs_path}/s3accesslogs/${local.bucket_name}"
124+
dynamic "transition" {
125+
for_each = var.storage_class_transitions
126+
content {
127+
days = transition.value.days
128+
storage_class = transition.value.storage_class
129+
}
130+
}
62131
}
132+
}
63133

64-
tags = local.tags
134+
resource "aws_s3_bucket_policy" "this" {
135+
# The public access block must be in place before a bucket policy can be
136+
# applied.
137+
depends_on = [aws_s3_bucket_public_access_block.this]
138+
139+
bucket = aws_s3_bucket.this.id
140+
policy = jsonencode(merge(local.base_bucket_policy, {
141+
Statement = concat(local.base_bucket_policy.Statement, var.additional_policy_statements)
142+
}))
65143
}

0 commit comments

Comments
 (0)