Skip to content

Commit 0484fba

Browse files
authored
feat: add trivy scan to plan to staging (#7)
* feat: add trivy scan to plan to staging * fix(networking): terraform aws vpc module should not allow using all ports * fix: bump locations_api module from v1.0.1 to v1.0.2 * fix(networking): terraform aws vpc module should not allow nacls rules to allow using all ports * fix: added trivy ignore * fix: s3 bucket kms encrypted + adding logs * fix: s3 bucket kms encrypted + adding logs * fix(database): trivy finding rds should always be encrypted for all envs * fix(dataabse): restrict outbound traffic to private subnets * fix: encryption on s3_image_moderatora quarantine bucket * fix: added trivyignore rule * fix: added trivyignore rules to allow public ingress * fix: tls policy version and drop invaid headers * fix: added trivyignore rule * feat: trivy image scan docker image used by ecs * feat: trivy docker image scan to not fail but report in findings in comment * chore: bump infra-s3-image-moderator to v1.1.1 * fix: provide cidr block in security group not subnet ids * fix: aws kms alias for frontend layer * fix: frontend s3 cmk static web app bucket github action for spa * fix: s3 cmk policy frontend to allow cluodfront to decrypt objects * fix: s3 bucket for access logs to be encrypted
1 parent 57679e0 commit 0484fba

File tree

11 files changed

+352
-44
lines changed

11 files changed

+352
-44
lines changed

.github/workflows/plan-pr-to-staging.yml

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ jobs:
4444
-backend-config="encrypt=true"
4545
terraform validate
4646
47-
- name: Run TFSec Security Check
48-
uses: aquasecurity/tfsec-action@v1.0.0
47+
- name: Trivy IaC Scan
48+
uses: aquasecurity/trivy-action@0.20.0
4949
with:
50-
working_directory: terraform/layers/security
51-
soft_fail: 'true'
52-
format: sarif
53-
additional_args: --minimum-severity MEDIUM
50+
scan-type: config
51+
scan-ref: terraform/layers/security
52+
severity: HIGH,CRITICAL
53+
exit-code: '1'
5454

5555
- name: Terraform Plan (staging)
5656
working-directory: terraform/layers/security
@@ -108,13 +108,13 @@ jobs:
108108
-backend-config="encrypt=true"
109109
terraform validate
110110
111-
- name: Run TFSec Security Check
112-
uses: aquasecurity/tfsec-action@v1.0.0
111+
- name: Trivy IaC Scan
112+
uses: aquasecurity/trivy-action@0.20.0
113113
with:
114-
working_directory: terraform/layers/networking
115-
soft_fail: 'true'
116-
format: sarif
117-
additional_args: --minimum-severity MEDIUM
114+
scan-type: config
115+
scan-ref: terraform/layers/networking
116+
severity: HIGH,CRITICAL
117+
exit-code: '1'
118118

119119
- name: Terraform Plan (staging)
120120
working-directory: terraform/layers/networking
@@ -169,13 +169,13 @@ jobs:
169169
-backend-config="encrypt=true"
170170
terraform validate
171171
172-
- name: Run TFSec Security Check
173-
uses: aquasecurity/tfsec-action@v1.0.0
172+
- name: Trivy IaC Scan
173+
uses: aquasecurity/trivy-action@0.20.0
174174
with:
175-
working_directory: terraform/layers/database
176-
soft_fail: 'true'
177-
format: sarif
178-
additional_args: --minimum-severity MEDIUM
175+
scan-type: config
176+
scan-ref: terraform/layers/database
177+
severity: HIGH,CRITICAL
178+
exit-code: '1'
179179

180180
- name: Terraform Plan (staging)
181181
working-directory: terraform/layers/database
@@ -230,6 +230,22 @@ jobs:
230230
role-to-assume: arn:aws:iam::387836084035:role/githubTripPlannerInfraManager
231231
aws-region: ${{ secrets.AWS_REGION }}
232232

233+
- name: Trivy Image Scan (ECS backend image)
234+
uses: aquasecurity/trivy-action@0.20.0
235+
with:
236+
scan-type: image
237+
image-ref: ${{ env.TF_VAR_container_image }}
238+
severity: HIGH,CRITICAL
239+
exit-code: '0' # Do not fail the job on vulnerabilities
240+
format: table
241+
output: trivy-image-report.txt
242+
243+
- name: Upload Trivy image scan report
244+
uses: actions/upload-artifact@v4
245+
with:
246+
name: trivy-image-backend
247+
path: trivy-image-report.txt
248+
233249
- name: Terraform Validate
234250
working-directory: terraform/layers/backend
235251
run: |
@@ -242,13 +258,13 @@ jobs:
242258
-backend-config="encrypt=true"
243259
terraform validate
244260
245-
- name: Run TFSec Security Check
246-
uses: aquasecurity/tfsec-action@v1.0.0
261+
- name: Trivy IaC Scan
262+
uses: aquasecurity/trivy-action@0.20.0
247263
with:
248-
working_directory: terraform/layers/backend
249-
soft_fail: 'true'
250-
format: sarif
251-
additional_args: --minimum-severity MEDIUM
264+
scan-type: config
265+
scan-ref: terraform/layers/backend
266+
severity: HIGH,CRITICAL
267+
exit-code: '1'
252268

253269
- name: Terraform Plan (staging)
254270
working-directory: terraform/layers/backend
@@ -312,13 +328,13 @@ jobs:
312328
-backend-config="encrypt=true"
313329
terraform validate
314330
315-
- name: Run TFSec Security Check
316-
uses: aquasecurity/tfsec-action@v1.0.0
331+
- name: Trivy IaC Scan
332+
uses: aquasecurity/trivy-action@0.20.0
317333
with:
318-
working_directory: terraform/layers/frontend
319-
soft_fail: 'true'
320-
format: sarif
321-
additional_args: --minimum-severity MEDIUM
334+
scan-type: config
335+
scan-ref: terraform/layers/frontend
336+
severity: HIGH,CRITICAL
337+
exit-code: '1'
322338

323339
- name: Terraform Plan (staging)
324340
working-directory: terraform/layers/frontend
@@ -346,28 +362,50 @@ jobs:
346362
with:
347363
path: plans/ # Download to plans/ directory
348364

349-
- name: Combine plans and post comment
365+
- name: Combine plans and Trivy findings and post comment
350366
uses: actions/github-script@v7
351367
with:
352368
github-token: ${{ secrets.GITHUB_TOKEN }}
353369
script: |
354370
const fs = require('fs');
355371
const path = require('path');
372+
373+
let body = '### Terraform Plan (staging - All Layers)\n\n';
356374
const plansDir = 'plans';
357-
let combinedBody = '### Terraform Plan (staging - All Layers)\n\n';
358375
const layers = ['security', 'networking', 'database', 'backend', 'frontend'];
376+
359377
for (const layer of layers) {
360378
const planPath = path.join(plansDir, `terraform-plan-staging-${layer}`, 'tfplan.txt');
361379
if (fs.existsSync(planPath)) {
362-
const body = fs.readFileSync(planPath, 'utf8');
363-
const MAX_LEN = 15000; // Shorter per layer to fit combined
364-
const planPreview = body.length > MAX_LEN ? body.slice(0, MAX_LEN - 1000) + '\n\n...(truncated)' : body;
365-
combinedBody += `#### ${layer}\n<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${planPreview}\n\`\`\`\n\n</details>\n\n`;
380+
const content = fs.readFileSync(planPath, 'utf8');
381+
const MAX_LEN = 15000;
382+
const preview = content.length > MAX_LEN
383+
? content.slice(0, MAX_LEN - 1000) + '\n\n...(truncated)'
384+
: content;
385+
386+
body += `#### ${layer}\n<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${preview}\n\`\`\`\n\n</details>\n\n`;
366387
}
367388
}
389+
390+
// ---- Trivy Image Scan Section ----
391+
const trivyPath = path.join(plansDir, 'trivy-image-backend', 'trivy-image-report.txt');
392+
if (fs.existsSync(trivyPath)) {
393+
const trivyReport = fs.readFileSync(trivyPath, 'utf8').trim();
394+
if (trivyReport) {
395+
const MAX_LEN = 15000;
396+
const preview = trivyReport.length > MAX_LEN
397+
? trivyReport.slice(0, MAX_LEN) + '\n\n...(truncated)'
398+
: trivyReport;
399+
400+
body += '---\n\n';
401+
body += '### 🔐 Trivy Image Scan (Backend ECS Image)\n\n';
402+
body += `<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${preview}\n\`\`\`\n\n</details>\n\n`;
403+
}
404+
}
405+
368406
await github.rest.issues.createComment({
369407
owner: context.repo.owner,
370408
repo: context.repo.repo,
371409
issue_number: context.issue.number,
372-
body: combinedBody
373-
});
410+
body
411+
});

.trivyignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# AVD-AWS-0105: Network ACL rule allows ingress from public internet. This is required for public-facing web applications to allow HTTP/HTTPS traffic from any IP. We acknowledge this as an accepted risk for our use case
2+
3+
AVD-AWS-0105
4+
5+
# ECS tasks require egress to 0.0.0.0/0 to reach the internet via NAT Gateway
6+
7+
AVD-AWS-0104
8+
9+
# ALB requires public ingress for HTTPS web access. This is an accepted risk for a public-facing web application.
10+
AVD-AWS-0107
11+
12+
# ALB is intentionally public to serve internet traffic for the web application.
13+
AVD-AWS-0053

terraform/layers/backend/main.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ module "file_uploader" {
8383
}
8484

8585
module "image_moderator" {
86-
source = "git::https://github.com/lrasata/infra-s3-image-moderator//modules/s3_image_moderator?ref=v1.1.0"
86+
source = "git::https://github.com/lrasata/infra-s3-image-moderator//modules/s3_image_moderator?ref=v1.1.1"
8787

8888
region = var.region
8989
environment = var.environment
90+
app_id = var.app_id
9091
s3_src_bucket_name = module.file_uploader.uploads_bucket_id
9192
s3_src_bucket_arn = module.file_uploader.uploads_bucket_arn
9293
s3_quarantine_bucket_name = "${var.quarantine_bucket_name}-${data.aws_caller_identity.current.account_id}"

terraform/layers/backend/modules/alb/main.tf

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module "terraform_aws_alb" {
3535
{
3636
port = 443
3737
protocol = "HTTPS"
38-
ssl_policy = "ELBSecurityPolicy-2016-08"
38+
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
3939
certificate_arn = var.backend_certificate_arn
4040

4141
default_action = {
@@ -45,6 +45,8 @@ module "terraform_aws_alb" {
4545
}
4646
]
4747

48+
drop_invalid_header_fields = true
49+
4850
}
4951

5052
resource "aws_lb_listener" "http_redirect" {
@@ -71,7 +73,9 @@ resource "aws_security_group" "sg_alb" {
7173
App = var.app_id
7274
}
7375

76+
7477
ingress {
78+
description = "Allow public HTTPS access for web app"
7579
from_port = 443
7680
to_port = 443
7781
protocol = "tcp"
@@ -82,7 +86,7 @@ resource "aws_security_group" "sg_alb" {
8286
from_port = 0
8387
to_port = 0
8488
protocol = "-1"
85-
cidr_blocks = ["0.0.0.0/0"]
89+
cidr_blocks = ["0.0.0.0/0"] # ECS tasks require egress to 0.0.0.0/0 to reach the internet via NAT Gateway
8690
}
8791
}
8892

terraform/layers/database/main.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module "db" {
1515
engine_version = "15"
1616
instance_class = var.environment == "prod" ? "db.t3.medium" : "db.t3.micro"
1717
allocated_storage = var.environment == "prod" ? 50 : 20 # GB
18-
storage_encrypted = var.environment == "prod" ? true : false
18+
storage_encrypted = true
1919
multi_az = var.environment == "ephemeral" ? false : true
2020
backup_retention_period = var.environment == "prod" ? 7 : 0 # number of days
2121

@@ -62,6 +62,6 @@ resource "aws_security_group" "sg_rds" {
6262
from_port = 0
6363
to_port = 0
6464
protocol = "-1"
65-
cidr_blocks = ["0.0.0.0/0"]
65+
cidr_blocks = try(data.terraform_remote_state.networking.outputs.private_subnet_cidrs, ["10.0.0.0/16"])
6666
}
6767
}

terraform/layers/frontend/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ module "route53" {
6262
}
6363

6464
module "locations_api" {
65-
source = "git::https://github.com/lrasata/locations-api.git//modules/locations_api?ref=v1.0.1"
65+
source = "git::https://github.com/lrasata/locations-api.git//modules/locations_api?ref=v1.0.2"
6666

6767
region = var.region
6868
environment = var.environment

terraform/layers/frontend/modules/s3/main.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ resource "aws_s3_bucket" "s3_bucket" {
44
Environment = var.environment
55
App = var.app_id
66
}
7+
8+
}
9+
10+
# Enable server-side encryption with KMS
11+
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads_encryption" {
12+
bucket = aws_s3_bucket.s3_bucket.id
13+
14+
rule {
15+
apply_server_side_encryption_by_default {
16+
sse_algorithm = "aws:kms"
17+
kms_master_key_id = aws_kms_key.s3_cmk.arn
18+
}
19+
}
720
}
821

922
# Block public access to the S3 bucket
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ============================================================================
2+
# Logging target bucket - S3 access logs
3+
# ============================================================================
4+
resource "aws_s3_bucket" "log_target" {
5+
bucket = "${var.environment}-${var.app_id}-s3-access-logs"
6+
}
7+
8+
resource "aws_s3_bucket_ownership_controls" "log_target_ownership" {
9+
bucket = aws_s3_bucket.log_target.id
10+
11+
rule {
12+
object_ownership = "BucketOwnerEnforced"
13+
}
14+
}
15+
16+
resource "aws_s3_bucket_versioning" "log_target_versioning" {
17+
bucket = aws_s3_bucket.log_target.id
18+
19+
versioning_configuration {
20+
status = "Enabled"
21+
}
22+
}
23+
24+
resource "aws_s3_bucket_public_access_block" "log_target_block" {
25+
bucket = aws_s3_bucket.log_target.id
26+
27+
block_public_acls = true
28+
block_public_policy = true
29+
ignore_public_acls = true
30+
restrict_public_buckets = true
31+
}
32+
33+
resource "aws_s3_bucket_policy" "log_target_policy" {
34+
bucket = aws_s3_bucket.log_target.id
35+
36+
policy = jsonencode({
37+
Version = "2012-10-17"
38+
Statement = [
39+
{
40+
Effect = "Allow"
41+
Principal = { Service = "logging.s3.amazonaws.com" }
42+
Action = "s3:PutObject"
43+
Resource = "${aws_s3_bucket.log_target.arn}/*"
44+
}
45+
]
46+
})
47+
}
48+
49+
resource "aws_s3_bucket_logging" "s3_bucket_logging" {
50+
bucket = aws_s3_bucket.s3_bucket.id
51+
target_bucket = aws_s3_bucket.log_target.id
52+
target_prefix = "${var.environment}-${var.app_id}-s3-bucket-access-logs/"
53+
54+
depends_on = [
55+
aws_s3_bucket_policy.log_target_policy,
56+
aws_s3_bucket_ownership_controls.log_target_ownership
57+
]
58+
}
59+
60+
# Enable server-side encryption with customer-managed KMS key for access logs bucket
61+
resource "aws_s3_bucket_server_side_encryption_configuration" "log_target_encryption" {
62+
bucket = aws_s3_bucket.log_target.id
63+
rule {
64+
apply_server_side_encryption_by_default {
65+
sse_algorithm = "aws:kms"
66+
kms_master_key_id = aws_kms_key.s3_cmk.arn
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)