Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions .github/workflows/automatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,42 @@ jobs:
id: fmt
run: terraform fmt -check -diff

policy-checks:
runs-on: ubuntu-latest
if: github.event.action != 'closed'
needs: execute
permissions:
contents: read
id-token: write
pull-requests: write
steps:
- uses: actions/checkout@v4

- name: Terraform Init (for policies)
uses: ./.github/actions/terraform_init/
with:
terraform_deploy_role: ${{ vars.TERRAFORM_DEPLOY_ROLE }}

- name: Download terraform plan
uses: actions/download-artifact@v4
with:
name: tfplan

- name: Convert plan to JSON
run: |
terraform show -json tfplan > tfplan.json

- uses: overmindtech/policy-signals-action@v1
with:
policies-path: './policies'
terraform-plan-json: './tfplan.json'
overmind-api-key: ${{ secrets.OVM_API_KEY }}
ticket-link: ${{ needs.execute.outputs.run-url }}

execute:
runs-on: ubuntu-latest
outputs:
run-url: ${{ steps.submit-plan.outputs.run-url }}
permissions:
contents: read # required for checkout
id-token: write # mint AWS credentials through OIDC
Expand Down
126 changes: 126 additions & 0 deletions policies/cost-control.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package main

# Cost Control Policy
# Checks for expensive instance types and configurations

# Get all EC2 instances from terraform plan
ec2_instances[instance] {
instance := input.resource_changes[_]
instance.type == "aws_instance"
}

# Get all RDS instances from terraform plan
rds_instances[instance] {
instance := input.resource_changes[_]
instance.type == "aws_db_instance"
}

# Get all RDS clusters from terraform plan
rds_clusters[cluster] {
cluster := input.resource_changes[_]
cluster.type == "aws_rds_cluster"
}

# List of expensive EC2 instance types
expensive_ec2_types := {
"m5.24xlarge", "m5.16xlarge", "m5.12xlarge",
"r5.24xlarge", "r5.16xlarge", "r5.12xlarge",
"c5.24xlarge", "c5.18xlarge", "c5.12xlarge",
"x1.32xlarge", "x1.16xlarge",
"r4.16xlarge", "r4.8xlarge",
"m4.16xlarge", "m4.10xlarge",
"c4.8xlarge",
"p3.16xlarge", "p3.8xlarge", "p3.2xlarge",
"p2.16xlarge", "p2.8xlarge",
"g3.16xlarge", "g3.8xlarge"
}

# List of expensive RDS instance types
expensive_rds_types := {
"db.r5.24xlarge", "db.r5.16xlarge", "db.r5.12xlarge",
"db.r4.16xlarge", "db.r4.8xlarge",
"db.m5.24xlarge", "db.m5.16xlarge", "db.m5.12xlarge",
"db.m4.16xlarge", "db.m4.10xlarge",
"db.x1.32xlarge", "db.x1.16xlarge"
}

# High-cost regions (typically more expensive than us-east-1)
high_cost_regions := {
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2",
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3",
"sa-east-1"
}

# Deny expensive EC2 instance types
deny[msg] {
instance := ec2_instances[_]
expensive_ec2_types[instance.change.after.instance_type]
msg := sprintf("EC2 instance '%s' uses expensive instance type '%s' - consider using a smaller instance type", [instance.address, instance.change.after.instance_type])
}

# Deny expensive RDS instance types
deny[msg] {
instance := rds_instances[_]
expensive_rds_types[instance.change.after.instance_class]
msg := sprintf("RDS instance '%s' uses expensive instance type '%s' - consider using a smaller instance type", [instance.address, instance.change.after.instance_class])
}

# Deny RDS clusters without deletion protection in production
deny[msg] {
cluster := rds_clusters[_]
cluster.change.after.tags.Environment == "prod"
not cluster.change.after.deletion_protection
msg := sprintf("RDS cluster '%s' in production does not have deletion protection enabled", [cluster.address])
}

deny[msg] {
cluster := rds_clusters[_]
cluster.change.after.tags.Environment == "production"
not cluster.change.after.deletion_protection
msg := sprintf("RDS cluster '%s' in production does not have deletion protection enabled", [cluster.address])
}

# Warn about missing cost tracking tags
warn[msg] {
instance := ec2_instances[_]
not instance.change.after.tags.CostCenter
msg := sprintf("EC2 instance '%s' is missing 'CostCenter' tag for cost tracking", [instance.address])
}

warn[msg] {
instance := rds_instances[_]
not instance.change.after.tags.CostCenter
msg := sprintf("RDS instance '%s' is missing 'CostCenter' tag for cost tracking", [instance.address])
}

# Warn about instances in high-cost regions for production workloads
warn[msg] {
instance := ec2_instances[_]
instance.change.after.tags.Environment == "prod"
provider_region := input.configuration.provider_config.aws.expressions.region.constant_value
high_cost_regions[provider_region]
msg := sprintf("Production EC2 instance '%s' - ensure you're using the most cost-effective region", [instance.address])
}

warn[msg] {
instance := ec2_instances[_]
instance.change.after.tags.Environment == "production"
provider_region := input.configuration.provider_config.aws.expressions.region.constant_value
high_cost_regions[provider_region]
msg := sprintf("Production EC2 instance '%s' - ensure you're using the most cost-effective region", [instance.address])
}

# Warn about dev instances without auto-shutdown
warn[msg] {
instance := ec2_instances[_]
instance.change.after.tags.Environment == "dev"
not instance.change.after.tags.AutoShutdown
msg := sprintf("Development EC2 instance '%s' should have 'AutoShutdown' tag to reduce costs", [instance.address])
}

warn[msg] {
instance := ec2_instances[_]
instance.change.after.tags.Environment == "development"
not instance.change.after.tags.AutoShutdown
msg := sprintf("Development EC2 instance '%s' should have 'AutoShutdown' tag to reduce costs", [instance.address])
}
87 changes: 87 additions & 0 deletions policies/s3-security.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

# S3 Security Policy
# Checks for S3 buckets without proper security configurations

# Get all S3 bucket resources from terraform plan
s3_buckets[bucket] {
bucket := input.resource_changes[_]
bucket.type == "aws_s3_bucket"
}

s3_bucket_public_access_blocks[block] {
block := input.resource_changes[_]
block.type == "aws_s3_bucket_public_access_block"
}

s3_bucket_encryption[encrypt] {
encrypt := input.resource_changes[_]
encrypt.type == "aws_s3_bucket_server_side_encryption_configuration"
}

# Deny S3 buckets without Environment tag
deny[msg] {
bucket := s3_buckets[_]
not bucket.change.after.tags.Environment
msg := sprintf("S3 bucket '%s' is missing the required 'Environment' tag", [bucket.address])
}

# Deny S3 buckets without Name tag
deny[msg] {
bucket := s3_buckets[_]
not bucket.change.after.tags.Name
msg := sprintf("S3 bucket '%s' is missing the required 'Name' tag", [bucket.address])
}

# Deny S3 buckets without Owner tag
deny[msg] {
bucket := s3_buckets[_]
not bucket.change.after.tags.Owner
msg := sprintf("S3 bucket '%s' is missing the required 'Owner' tag", [bucket.address])
}

# Deny S3 buckets without Project tag
deny[msg] {
bucket := s3_buckets[_]
not bucket.change.after.tags.Project
msg := sprintf("S3 bucket '%s' is missing the required 'Project' tag", [bucket.address])
}

# Deny S3 buckets without encryption
deny[msg] {
bucket := s3_buckets[_]
not has_bucket_encryption(bucket.change.after.bucket)
msg := sprintf("S3 bucket '%s' does not have server-side encryption configured", [bucket.address])
}

# Warn about S3 buckets without public access block
warn[msg] {
bucket := s3_buckets[_]
not has_public_access_block(bucket.change.after.bucket)
msg := sprintf("S3 bucket '%s' does not have public access block configured - consider adding one for security", [bucket.address])
}

# Warn about S3 buckets that might be publicly accessible
warn[msg] {
bucket := s3_buckets[_]
bucket.change.after.acl == "public-read"
msg := sprintf("S3 bucket '%s' has public-read ACL - ensure this is intentional", [bucket.address])
}

warn[msg] {
bucket := s3_buckets[_]
bucket.change.after.acl == "public-read-write"
msg := sprintf("S3 bucket '%s' has public-read-write ACL - this is a security risk", [bucket.address])
}

# Helper function to check if bucket has encryption configuration
has_bucket_encryption(bucket_name) {
encryption := s3_bucket_encryption[_]
encryption.change.after.bucket == bucket_name
}

# Helper function to check if bucket has public access block
has_public_access_block(bucket_name) {
block := s3_bucket_public_access_blocks[_]
block.change.after.bucket == bucket_name
}
Loading