# GAC policy evaluations ======================================================= # Unique GAC Controls: # GAC-003, GAC-008, GAC-012, GAC-013, GAC-014, GAC-015, GAC-016, GAC-022, # GAC-023, GAC-025, GAC-026 # # Overlap GAC controls from existing policies: # GAC-001 → STR-001 S3 Block Public Access # GAC-002 → STR-004 S3 Server-Side Encryption # GAC-004 → STR-005 S3 TLS-Only Access # GAC-005 → CMP-009 EBS Volume Encryption # GAC-006 → CMP-010 EBS Default Encryption # GAC-007 → CMP-001 IMDSv2 on EC2 # GAC-009 → DAT-002 RDS Storage Encryption # GAC-010 → DAT-001 RDS Not Publicly Accessible # GAC-011 → DAT-004 RDS Deletion Protection # GAC-017 → CMP-005 Unrestricted SSH Ingress # GAC-018 → CMP-006 Unrestricted RDP Ingress # GAC-019 → CMP-008 Default Security Group Rules # GAC-020 → NCD-007 ALB HTTP to HTTPS Redirect # GAC-021 → NCD-008 CloudFront HTTPS to Viewers # GAC-024 → DAT-006 DynamoDB Point-in-Time Recovery package controls.aws.gac import data.common.definitions as defs import data.common.exemptions as exemptions import data.common.outputs as outputs import data.common.policydoc as policydoc # GAC-003 — S3 buckets must have versioning enabled --------------------------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-003" some _, rc in input.resource_changes rc.type == "aws_s3_bucket" rc.change.actions[_] in ["create", "update"] not _versioning_exists(rc.name) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("S3 bucket '%s' does not have versioning enabled", [rc.name]), ) } deny contains msg if { policy_id := "ICP-TF-AWS-GAC-003" some _, rc in input.resource_changes rc.type == "aws_s3_bucket_versioning" rc.change.actions[_] in ["create", "update"] some vc in rc.change.after.versioning_configuration vc.status != "Enabled" meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("S3 bucket versioning '%s' is not set to Enabled", [rc.name]), ) } _versioning_exists(bucket_name) if { some r in input.configuration.root_module.resources r.type == "aws_s3_bucket_versioning" some ref in r.expressions.bucket.references ref == concat(".", ["aws_s3_bucket", bucket_name]) } # GAC-008 — EBS snapshots must not be shared publicly ------------------------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-008" some _, rc in input.resource_changes rc.type == "aws_snapshot_create_volume_permission" rc.change.actions[_] in ["create", "update"] rc.change.after.account_id == "all" meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("EBS snapshot permission '%s' shares snapshot publicly (account_id = all)", [rc.name]), ) } # GAC-012 — RDS DB instances must have adequate backup retention -------------- # RDS Automated backups is capped at 35 days. HCA's requirement of 365 days # shall be set up with AWS Backups deny contains msg if { policy_id := "ICP-TF-AWS-GAC-012" some _, rc in input.resource_changes rc.type == "aws_db_instance" rc.change.actions[_] in ["create", "update"] not _adequate_backup_retention(rc.change.after.backup_retention_period) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("RDS instance '%s' does not have max retention configured (expected 35 days)", [rc.name]), ) } _adequate_backup_retention(days) if { days != null days >= 35 } # GAC-013 — RDS snapshots must not be shared publicly ------------------------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-013" some _, rc in input.resource_changes rc.type in {"aws_db_snapshot", "aws_db_cluster_snapshot"} rc.change.actions[_] in ["create", "update"] "all" in rc.change.after.shared_accounts meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("RDS snapshot '%s' is shared publicly (shared_accounts contains 'all')", [rc.name]), ) } # GAC-014 — IAM policies must not grant full administrative privileges --------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-014" some _, rc in input.resource_changes rc.type in {"aws_iam_policy", "aws_iam_role_policy", "aws_iam_user_policy", "aws_iam_group_policy"} rc.change.actions[_] in ["create", "update"] doc := json.unmarshal(rc.change.after.policy) some stmt in doc.Statement stmt.Effect == "Allow" some action in policydoc.as_list(stmt.Action) action == "*" some resource in policydoc.as_list(stmt.Resource) resource == "*" meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("'%s.%s' grants full administrative privileges (Action * on Resource *)", [rc.type, rc.name]), ) } # GAC-015 — IAM roles must not use wildcard trust principals or actions -------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-015" some _, rc in input.resource_changes rc.type == "aws_iam_role" rc.change.actions[_] in ["create", "update"] doc := json.unmarshal(rc.change.after.assume_role_policy) some stmt in doc.Statement stmt.Effect == "Allow" policydoc.public_principal(stmt) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("IAM role '%s' trust policy uses a wildcard principal", [rc.name]), ) } deny contains msg if { policy_id := "ICP-TF-AWS-GAC-015" some _, rc in input.resource_changes rc.type == "aws_iam_role" rc.change.actions[_] in ["create", "update"] doc := json.unmarshal(rc.change.after.assume_role_policy) some stmt in doc.Statement stmt.Effect == "Allow" some action in policydoc.as_list(stmt.Action) action == "*" meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("IAM role '%s' trust policy uses a wildcard action", [rc.name]), ) } # GAC-016 — IAM account password policy must enforce strong requirements ------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-016" some _, rc in input.resource_changes rc.type == "aws_iam_account_password_policy" rc.change.actions[_] in ["create", "update"] not _strong_password_policy(rc.change.after) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("IAM account password policy '%s' does not meet strong requirements", [rc.name]), ) } _strong_password_policy(after) if { after.minimum_password_length >= 14 after.require_symbols == true after.require_numbers == true after.require_uppercase_characters == true after.require_lowercase_characters == true after.password_reuse_prevention >= 24 after.max_password_age <= 90 } # GAC-022 — CloudFront must use modern TLS minimum protocol version ------------ deny contains msg if { policy_id := "ICP-TF-AWS-GAC-022" some _, rc in input.resource_changes rc.type == "aws_cloudfront_distribution" rc.change.actions[_] in ["create", "update"] not _modern_tls(rc.change.after) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("CloudFront distribution '%s' does not use a modern TLS minimum protocol version", [rc.name]), ) } _modern_tls(after) if { some cert in after.viewer_certificate _tls_version_ok(cert.minimum_protocol_version) } _tls_version_ok(v) if startswith(v, "TLSv1.2") _tls_version_ok(v) if startswith(v, "TLSv1.3") # GAC-023 — EKS cluster API endpoint public access must be disabled or restricted deny contains msg if { policy_id := "ICP-TF-AWS-GAC-023" some _, rc in input.resource_changes rc.type == "aws_eks_cluster" rc.change.actions[_] in ["create", "update"] not _eks_endpoint_safe(rc.change.after) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("EKS cluster '%s' has public API endpoint access enabled without CIDR restrictions", [rc.name]), ) } _eks_endpoint_safe(after) if { some vpc in after.vpc_config vpc.endpoint_public_access == false } _eks_endpoint_safe(after) if { some vpc in after.vpc_config vpc.endpoint_public_access == true not "0.0.0.0/0" in vpc.public_access_cidrs } # GAC-025 — Provider default_tags must include mandatory tags ------------------ deny contains msg if { policy_id := "ICP-TF-AWS-GAC-025" some name, provider in input.configuration.provider_config startswith(name, "aws") not provider.expressions.default_tags[0].tags.constant_value meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("AWS provider '%s' has no default_tags configured", [name]), ) } deny contains msg if { policy_id := "ICP-TF-AWS-GAC-025" some name, provider in input.configuration.provider_config startswith(name, "aws") provider.expressions.default_tags[0].tags.constant_value some tag in defs.mandatory_provider_tags not tag in object.keys(provider.expressions.default_tags[0].tags.constant_value) meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("AWS provider '%s' is missing mandatory default tag '%s'", [name, tag]), ) } # GAC-026 — Taggable resources must carry all baseline tags -------------------- deny contains msg if { policy_id := "ICP-TF-AWS-GAC-026" some _, rc in input.resource_changes rc.change.actions != ["delete"] _is_taggable(rc) real_tags := _as_object(object.get(rc.change.after, "tags", null)) problems := array.concat( [sprintf("missing '%s'", [tag]) | some tag in object.keys(defs.mandatory_baseline_tags) not tag in object.keys(real_tags) ], [sprintf("'%s' has invalid value '%s'", [tag, real_tags[tag]]) | some tag, allowed in defs.mandatory_baseline_tags tag in object.keys(real_tags) not "*" in allowed not real_tags[tag] in allowed ], ) count(problems) > 0 meta := data.policies[policy_id] meta.status == "active" not exemptions.policy_exemption(policy_id) not exemptions.severity_exemption(meta.severity) msg := outputs.violation( policy_id, meta, sprintf("Resource '%s.%s' tag issues: %s", [rc.type, rc.name, concat("; ", sort(problems))]), ) } _is_taggable(rc) if { "tags" in object.keys(rc.change.after) } _is_taggable(rc) if { "tags" in object.keys(rc.change.after_unknown) } _as_object(tags) := tags if tags != null _as_object(tags) := {} if tags == null