Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
431b9ad
feat: add trivy scan to plan to staging
lrasata Jan 6, 2026
bfdda35
feat: add trivy scan to plan to staging
lrasata Jan 6, 2026
02e6fa3
fix(networking): terraform aws vpc module should not allow using all …
lrasata Jan 6, 2026
d2c2cde
fix: bump locations_api module from v1.0.1 to v1.0.2
lrasata Jan 6, 2026
7ea60b6
fix(networking): terraform aws vpc module should not allow nacls rule…
lrasata Jan 6, 2026
eea4f5b
fix: added trivy ignore
lrasata Jan 6, 2026
0874521
fix: s3 bucket kms encrypted + adding logs
lrasata Jan 6, 2026
b39501e
fix: s3 bucket kms encrypted + adding logs
lrasata Jan 6, 2026
12a18b0
fix(database): trivy finding rds should always be encrypted for all envs
lrasata Jan 6, 2026
76cdfc3
fix(dataabse): restrict outbound traffic to private subnets
lrasata Jan 6, 2026
3dc6df1
fix(dataabse): restrict outbound traffic to private subnets
lrasata Jan 6, 2026
90a28e9
fix(dataabse): restrict outbound traffic to private subnets
lrasata Jan 6, 2026
f03a8f4
fix: encryption on s3_image_moderatora quarantine bucket
lrasata Jan 6, 2026
9671444
fix: added trivyignore rule
lrasata Jan 6, 2026
d9a05e8
fix: added trivyignore rules to allow public ingress
lrasata Jan 6, 2026
9e0267a
fix: tls policy version and drop invaid headers
lrasata Jan 6, 2026
9fa360c
fix: added trivyignore rule
lrasata Jan 6, 2026
87f4002
feat: trivy image scan docker image used by ecs
lrasata Jan 7, 2026
73998e3
feat: trivy docker image scan to not fail but report in findings in c…
lrasata Jan 7, 2026
04aaa12
chore: bump infra-s3-image-moderator to v1.1.1
lrasata Jan 7, 2026
903bad4
fix: provide cidr block in security group not subnet ids
lrasata Jan 7, 2026
d806d13
fix: aws kms alias for frontend layer
lrasata Jan 7, 2026
c962056
fix: frontend s3 cmk static web app bucket github action for spa
lrasata Jan 7, 2026
081fc31
fix: s3 cmk policy frontend to allow cluodfront to decrypt objects
lrasata Jan 7, 2026
5dfd1ad
fix: s3 bucket for access logs to be encrypted
lrasata Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 76 additions & 38 deletions .github/workflows/plan-pr-to-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ jobs:
-backend-config="encrypt=true"
terraform validate

- name: Run TFSec Security Check
uses: aquasecurity/tfsec-action@v1.0.0
- name: Trivy IaC Scan
uses: aquasecurity/trivy-action@0.20.0
with:
working_directory: terraform/layers/security
soft_fail: 'true'
format: sarif
additional_args: --minimum-severity MEDIUM
scan-type: config
scan-ref: terraform/layers/security
severity: HIGH,CRITICAL
exit-code: '1'

- name: Terraform Plan (staging)
working-directory: terraform/layers/security
Expand Down Expand Up @@ -108,13 +108,13 @@ jobs:
-backend-config="encrypt=true"
terraform validate

- name: Run TFSec Security Check
uses: aquasecurity/tfsec-action@v1.0.0
- name: Trivy IaC Scan
uses: aquasecurity/trivy-action@0.20.0
with:
working_directory: terraform/layers/networking
soft_fail: 'true'
format: sarif
additional_args: --minimum-severity MEDIUM
scan-type: config
scan-ref: terraform/layers/networking
severity: HIGH,CRITICAL
exit-code: '1'

- name: Terraform Plan (staging)
working-directory: terraform/layers/networking
Expand Down Expand Up @@ -169,13 +169,13 @@ jobs:
-backend-config="encrypt=true"
terraform validate

- name: Run TFSec Security Check
uses: aquasecurity/tfsec-action@v1.0.0
- name: Trivy IaC Scan
uses: aquasecurity/trivy-action@0.20.0
with:
working_directory: terraform/layers/database
soft_fail: 'true'
format: sarif
additional_args: --minimum-severity MEDIUM
scan-type: config
scan-ref: terraform/layers/database
severity: HIGH,CRITICAL
exit-code: '1'

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

- name: Trivy Image Scan (ECS backend image)
uses: aquasecurity/trivy-action@0.20.0
with:
scan-type: image
image-ref: ${{ env.TF_VAR_container_image }}
severity: HIGH,CRITICAL
exit-code: '0' # Do not fail the job on vulnerabilities
format: table
output: trivy-image-report.txt

- name: Upload Trivy image scan report
uses: actions/upload-artifact@v4
with:
name: trivy-image-backend
path: trivy-image-report.txt

- name: Terraform Validate
working-directory: terraform/layers/backend
run: |
Expand All @@ -242,13 +258,13 @@ jobs:
-backend-config="encrypt=true"
terraform validate

- name: Run TFSec Security Check
uses: aquasecurity/tfsec-action@v1.0.0
- name: Trivy IaC Scan
uses: aquasecurity/trivy-action@0.20.0
with:
working_directory: terraform/layers/backend
soft_fail: 'true'
format: sarif
additional_args: --minimum-severity MEDIUM
scan-type: config
scan-ref: terraform/layers/backend
severity: HIGH,CRITICAL
exit-code: '1'

- name: Terraform Plan (staging)
working-directory: terraform/layers/backend
Expand Down Expand Up @@ -312,13 +328,13 @@ jobs:
-backend-config="encrypt=true"
terraform validate

- name: Run TFSec Security Check
uses: aquasecurity/tfsec-action@v1.0.0
- name: Trivy IaC Scan
uses: aquasecurity/trivy-action@0.20.0
with:
working_directory: terraform/layers/frontend
soft_fail: 'true'
format: sarif
additional_args: --minimum-severity MEDIUM
scan-type: config
scan-ref: terraform/layers/frontend
severity: HIGH,CRITICAL
exit-code: '1'

- name: Terraform Plan (staging)
working-directory: terraform/layers/frontend
Expand Down Expand Up @@ -346,28 +362,50 @@ jobs:
with:
path: plans/ # Download to plans/ directory

- name: Combine plans and post comment
- name: Combine plans and Trivy findings and post comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = require('path');

let body = '### Terraform Plan (staging - All Layers)\n\n';
const plansDir = 'plans';
let combinedBody = '### Terraform Plan (staging - All Layers)\n\n';
const layers = ['security', 'networking', 'database', 'backend', 'frontend'];

for (const layer of layers) {
const planPath = path.join(plansDir, `terraform-plan-staging-${layer}`, 'tfplan.txt');
if (fs.existsSync(planPath)) {
const body = fs.readFileSync(planPath, 'utf8');
const MAX_LEN = 15000; // Shorter per layer to fit combined
const planPreview = body.length > MAX_LEN ? body.slice(0, MAX_LEN - 1000) + '\n\n...(truncated)' : body;
combinedBody += `#### ${layer}\n<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${planPreview}\n\`\`\`\n\n</details>\n\n`;
const content = fs.readFileSync(planPath, 'utf8');
const MAX_LEN = 15000;
const preview = content.length > MAX_LEN
? content.slice(0, MAX_LEN - 1000) + '\n\n...(truncated)'
: content;

body += `#### ${layer}\n<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${preview}\n\`\`\`\n\n</details>\n\n`;
}
}

// ---- Trivy Image Scan Section ----
const trivyPath = path.join(plansDir, 'trivy-image-backend', 'trivy-image-report.txt');
if (fs.existsSync(trivyPath)) {
const trivyReport = fs.readFileSync(trivyPath, 'utf8').trim();
if (trivyReport) {
const MAX_LEN = 15000;
const preview = trivyReport.length > MAX_LEN
? trivyReport.slice(0, MAX_LEN) + '\n\n...(truncated)'
: trivyReport;

body += '---\n\n';
body += '### 🔐 Trivy Image Scan (Backend ECS Image)\n\n';
body += `<details>\n<summary>Click to expand</summary>\n\n\`\`\`\n${preview}\n\`\`\`\n\n</details>\n\n`;
}
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: combinedBody
});
body
});
13 changes: 13 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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

AVD-AWS-0105

# ECS tasks require egress to 0.0.0.0/0 to reach the internet via NAT Gateway

AVD-AWS-0104

# ALB requires public ingress for HTTPS web access. This is an accepted risk for a public-facing web application.
AVD-AWS-0107

# ALB is intentionally public to serve internet traffic for the web application.
AVD-AWS-0053
3 changes: 2 additions & 1 deletion terraform/layers/backend/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ module "file_uploader" {
}

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

region = var.region
environment = var.environment
app_id = var.app_id
s3_src_bucket_name = module.file_uploader.uploads_bucket_id
s3_src_bucket_arn = module.file_uploader.uploads_bucket_arn
s3_quarantine_bucket_name = "${var.quarantine_bucket_name}-${data.aws_caller_identity.current.account_id}"
Expand Down
8 changes: 6 additions & 2 deletions terraform/layers/backend/modules/alb/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module "terraform_aws_alb" {
{
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = var.backend_certificate_arn

default_action = {
Expand All @@ -45,6 +45,8 @@ module "terraform_aws_alb" {
}
]

drop_invalid_header_fields = true

}

resource "aws_lb_listener" "http_redirect" {
Expand All @@ -71,7 +73,9 @@ resource "aws_security_group" "sg_alb" {
App = var.app_id
}


ingress {
description = "Allow public HTTPS access for web app"
from_port = 443
to_port = 443
protocol = "tcp"
Expand All @@ -82,7 +86,7 @@ resource "aws_security_group" "sg_alb" {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
cidr_blocks = ["0.0.0.0/0"] # ECS tasks require egress to 0.0.0.0/0 to reach the internet via NAT Gateway
}
}

Expand Down
4 changes: 2 additions & 2 deletions terraform/layers/database/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module "db" {
engine_version = "15"
instance_class = var.environment == "prod" ? "db.t3.medium" : "db.t3.micro"
allocated_storage = var.environment == "prod" ? 50 : 20 # GB
storage_encrypted = var.environment == "prod" ? true : false
storage_encrypted = true
multi_az = var.environment == "ephemeral" ? false : true
backup_retention_period = var.environment == "prod" ? 7 : 0 # number of days

Expand Down Expand Up @@ -62,6 +62,6 @@ resource "aws_security_group" "sg_rds" {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
cidr_blocks = try(data.terraform_remote_state.networking.outputs.private_subnet_cidrs, ["10.0.0.0/16"])
}
}
2 changes: 1 addition & 1 deletion terraform/layers/frontend/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ module "route53" {
}

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

region = var.region
environment = var.environment
Expand Down
13 changes: 13 additions & 0 deletions terraform/layers/frontend/modules/s3/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ resource "aws_s3_bucket" "s3_bucket" {
Environment = var.environment
App = var.app_id
}

}

# Enable server-side encryption with KMS
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads_encryption" {
bucket = aws_s3_bucket.s3_bucket.id

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3_cmk.arn
}
}
}

# Block public access to the S3 bucket
Expand Down
69 changes: 69 additions & 0 deletions terraform/layers/frontend/modules/s3/s3_access_logs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ============================================================================
# Logging target bucket - S3 access logs
# ============================================================================
resource "aws_s3_bucket" "log_target" {
bucket = "${var.environment}-${var.app_id}-s3-access-logs"
}

resource "aws_s3_bucket_ownership_controls" "log_target_ownership" {
bucket = aws_s3_bucket.log_target.id

rule {
object_ownership = "BucketOwnerEnforced"
}
}

resource "aws_s3_bucket_versioning" "log_target_versioning" {
bucket = aws_s3_bucket.log_target.id

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_public_access_block" "log_target_block" {
bucket = aws_s3_bucket.log_target.id

block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "log_target_policy" {
bucket = aws_s3_bucket.log_target.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "logging.s3.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.log_target.arn}/*"
}
]
})
}

resource "aws_s3_bucket_logging" "s3_bucket_logging" {
bucket = aws_s3_bucket.s3_bucket.id
target_bucket = aws_s3_bucket.log_target.id
target_prefix = "${var.environment}-${var.app_id}-s3-bucket-access-logs/"

depends_on = [
aws_s3_bucket_policy.log_target_policy,
aws_s3_bucket_ownership_controls.log_target_ownership
]
}

# Enable server-side encryption with customer-managed KMS key for access logs bucket
resource "aws_s3_bucket_server_side_encryption_configuration" "log_target_encryption" {
bucket = aws_s3_bucket.log_target.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3_cmk.arn
}
}
}
Loading