terraform-compliance / cli

a lightweight, security focused, BDD test framework against terraform.
https://terraform-compliance.com
MIT License
1.36k stars 150 forks source link

Unable to account for "encrypted=true" for root_block_device and ebs_block_device on aws_instance #410

Open mdesmarest opened 4 years ago

mdesmarest commented 4 years ago

Note again on another issue I opened, really need to account for new vs updated resources as a feature like this can cause a resource that is flagged for encryption to be replaced if changes need to be made. Would love to have this for only new resources.

I am creating a feature to flag aws_instance (ec2) to ensure both root_block_device configures, and encrypted is configured and set to true. I need to also do the same for ebs_block_device. I am having issues filtering for one or the other as the encrypted flag for one causes the feature to pass for the other.

I can't account for the lack of encrypted for root_block_device if it is set for ebs_block_volume. I have tried to interpolate and slice and keep running into syntax errors. Please assist me with how to write a feature or features with a scenario pattern that matches this. I see this being a logistics hurdle for any resource where When it has encrypted Then it must have encrypted if a particular subproperty of a resource is used twice. See below for my code/plan and output of feature thus far. (sorry in advance for the long post)

Feature1:

Background: Check to make sure EC2 instances are in the configuration and not named bastion
        Given I have aws_instance defined
        When its name is not bastion

    Scenario: root_block_device must be configured on all non bastion hosts
        Given I have aws_instance defined
       When its name is not bastion
       When it has ebs_block_device
       Then it must have encrypted

This drills down to all encrypted, thus passing if one of the two properties contains encrypted. This prevents me from flagging on the absence of encryption on one or the other for ebs_block_device or root_block_device see stash below:

[
    {
        "address": "aws_instance.ebs_encrypted_FALSE",
        "values": false,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.ebs_encrypted_FALSE",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.ebs_encrypted_not_present",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.ebs_encrypted_not_present",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted_FALSE",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted_FALSE",
        "values": false,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted__field_missing",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted__field_missing",
        "values": true,
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_missing",
        "values": true,
        "type": "aws_instance"
    }
]

I have tried

Background: Check to make sure EC2 instances are in the configuration and not named bastion
        Given I have aws_instance defined
        When its name is not bastion

    Scenario: root_block_device must be configured on all non bastion hosts
        Given I have aws_instance defined
       When its name is not bastion
       When it has root_block_device

This gets me the entire stash of instances that have root block device, but I am lost to how I can drill into root_block_device for its encrypted property, if its a slicing issue, I can't get the syntax correct, easy to drill to in ipython though

The below scenario seems to get me to a good place but I can't drill further with additional Thens/Ands, probably a knowledge gap, see below:

Background: Check to make sure EC2 instances are in the configuration and not named bastion
        Given I have aws_instance defined
        When its name is not bastion

    Scenario: root_block_device must be configured on all non bastion hosts
        Given I have aws_instance defined
       When its name is not bastion
       When it has root_block_device

I get the stash that only produces root_block_devices, this is good but how do I drill further? `and it must have encrypted skips, and any slicing I attempt does not need to work. Stash:

[
    {
        "address": "aws_instance.ebs_encrypted_FALSE",
        "values": [
            {
                "delete_on_termination": true,
                "encrypted": true,
                "volume_size": 200,
                "volume_type": "gp2"
            },
            {
                "device_name": true,
                "iops": true,
                "kms_key_id": true,
                "volume_id": true
            }
        ],
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.ebs_encrypted_not_present",
        "values": [
            {
                "delete_on_termination": true,
                "encrypted": true,
                "volume_size": 200,
                "volume_type": "gp2"
            },
            {
                "device_name": true,
                "iops": true,
                "kms_key_id": true,
                "volume_id": true
            }
        ],
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted_FALSE",
        "values": [
            {
                "delete_on_termination": true,
                "encrypted": false,
                "volume_size": 200,
                "volume_type": "gp2"
            },
            {
                "device_name": true,
                "iops": true,
                "kms_key_id": true,
                "volume_id": true
            }
        ],
        "type": "aws_instance"
    },
    {
        "address": "aws_instance.root_block_encrypted__field_missing",
        "values": [
            {
                "delete_on_termination": true,
                "volume_size": 200,
                "volume_type": "gp2"
            },
            {
                "device_name": true,
                "encrypted": true,
                "iops": true,
                "kms_key_id": true,
                "volume_id": true
            }
        ],
        "type": "aws_instance"
    }
]

Resource Blocks:

#root_block device is declared, and encrypted is declared and set to false, EBS has encrypted and set to true
resource "aws_instance" "root_block_encrypted_FALSE" {
  ami                     = "ami-003634241a8fcdec0"
  instance_type           = "t2.medium"
  key_name                = "tfcompliance_inf"
  disable_api_termination = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = false
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
    encrypted = false
  }

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = true
  }

  tags = {
    budget-area = "security"
    group   = "cybersecurity"
  }
}
#root_block_device is missing the encrypted field
resource "aws_instance" "root_block_encrypted__field_missing" {
  ami                     = "ami-003634241a8fcdec0"
  instance_type           = "t2.medium"
  key_name                = "tfcompliance_inf"
  disable_api_termination = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = false
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
  }

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = true
  }

  tags = {
    budget-area = "security"
    group   = "cybersecurity"
  }
}
#root_block_volume entirely missing
resource "aws_instance" "root_block_missing" {
  ami                     = "ami-003634241a8fcdec0"
  instance_type           = "t2.medium"
  key_name                = "tfcompliance_inf"
  disable_api_termination = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = false
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = true
  }

  tags = {
    budget-area = "security"
    group   = "cybersecurity"
  }
}
#root_block_volume declared and set to true, ebs encrypted not declared
resource "aws_instance" "ebs_encrypted_not_present" {
  ami               = "ami-003634241a8fcdec0"
  instance_type     = "t2.medium"
  key_name          = "tfcompliance_inf"
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
    encrypted = true
  }
  ebs_block_device {
    device_name           = "/dev/sdg"
    volume_size           = 50
    volume_type           = "gp2"
    delete_on_termination = true

  }

  tags = {
    budget-area = "security"
    group   = "Cybersecurity"
  }
}

resource "aws_instance" "ebs_encrypted_FALSE" {
  ami               = "ami-003634241a8fcdec0"
  instance_type     = "t2.medium"
  key_name          = "tfcompliance_inf"
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
    encrypted = true
  }
  ebs_block_device {
    device_name           = "/dev/sdg"
    volume_size           = 50
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = false

  }

  tags = {
    budget-area = "security"
    group   = "Cybersecurity"
  }
}
#bastion flagging
resource "aws_instance" "bastion" {
  ami                  = "ami-003634241a8fcdec0"
  instance_type        = "t2.medium"
  key_name             = "tfcompliance_inf"
  ebs_optimized               = true
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 16
    volume_type = "gp2"
    encrypted = false
  }

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = true
  }

}
#bastion flagging
resource "aws_instance" "Bastion" {
  ami                  = "ami-0bbe6b35405ecebdb"
  instance_type        = "m5.large"
  key_name             = "tfcompliance_inf"

  disable_api_termination     = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = true

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = false
  }

  tags = {
    Name        = "bastion"
    Component   = "bastion"
    Environment = "prod"
    Product     = "bastion"
  }
}

#bastion flagging
resource "aws_instance" "bastioN" {
  ami                  = "ami-0bbe6b35405ecebdb"
  instance_type        = "m5.large"
  key_name             = "tfcompliance_inf"

  disable_api_termination     = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = true

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = false
  }

  tags = {
    Name        = "bastion"
    Component   = "bastion"
    Environment = "prod"
    Product     = "bastion"
  }
}
byarbrough commented 4 years ago

This gets it close, but has the glaring error of permitting an unencrypted ebs_block_device if the root device is encrypted and the ebs_block_device does not specify encrypted.

Feature: Check to make sure EC2 instances are encrypted if they are not named bastion

    Scenario: root_block_device must be configured on all non bastion hosts
        Given I have aws_instance defined
        When its name is not bastion
        Then it must have root_block_device
        And it must have encrypted
        And its value must be true

    Scenario: ebs_block_device, if present, must be configured on all non bastion hosts
        Given I have aws_instance defined
        When its name is not bastion
        When it has ebs_block_device
        Then it must have encrypted
        And its value must be true
Seems like compliance is regexing the first encrypted value it finds (although it does overwrite it if it finds another one). Root Device EBS Volume Scenario 1 Scenario 2
Encrypted Encrypted Pass Pass
Encrypted Not Encrypted Pass Fail
Encrypted Not defined Pass Skip
Encrypted Defined but "encrypted not present" Pass (bad) Pass This is the problem
Not Encrypted Encrypted Fail Pass
Not Encrypted Not Encrypted Fail Fail
Not Encrypted Not Defined Fail Skip
Not Encrypted Defined but "encrypted not present" Fail Fail
mdesmarest commented 4 years ago

Thanks for making a chart, based on the blocks I posted above, This is similar on logic to what I had come to Except

for scenario 1: in addition to your findings above There apppears to be a Parsing error with terraform compliance, I have to report a bug on

Scenario: root_block_device must be configured on all non bastion hosts
        Given I have aws_instance defined
        When its name is not bastion
        Then it must have root_block_device
        And it must have encrypted <skips here
        And its value must be true < Skips here

This should NOT SKIP,

When I look at the stash for And it must have encrypted

        When its name is not bastion
>> Press enter to continue
        Failure: aws_instance.root_block_missing (aws_instance) does not have root_block_device property.
        Then it must have root_block_device
          Failure: 
>** REMOVED OTHER RESOURCES***
        "address": "aws_instance.root_block_encrypted__field_missing",    <show as part of the stash
        "values": [
            {
                "delete_on_termination": true,
                "volume_size": 200,
                "volume_type": "gp2"
            },
            {
                "device_name": true,
                "encrypted": true, < show encrypted:true
                "iops": true,
                "kms_key_id": true,
                "volume_id": true
            }
        ],
        "type": "aws_instance"
    }
]

The initial stash also shows this, so this is happening in the initial stash pass:


 When its name is not bastion
>> Press enter to continues
** REMOVED OTHER RESOURCES for brevity**
    {
        ***REMOVED BLOCKS FOR BREVITY****
            "ebs_block_device": [
                {
                    "delete_on_termination": true,
                    "device_name": "/dev/sdb",
                    "encrypted": true,
                    "volume_size": 1000,
                    "volume_type": "gp2"
                },
                {
                    "iops": true,
                    "kms_key_id": true,
                    "snapshot_id": true,
                    "volume_id": true
                }
            ],
            "ebs_optimized": true,
            "get_password_data": false,
            "hibernation": null,
            "iam_instance_profile": null,
            "instance_initiated_shutdown_behavior": null,
            "instance_type": "t2.medium",
            "key_name": "tfcompliance_inf",
            "monitoring": true,
            "root_block_device": [
                {
                    "delete_on_termination": true,
                    "volume_size": 200,
                    "volume_type": "gp2"
                },
                {
                    "device_name": true,
                    "encrypted": true, <<< ******SHOWING ENCRYPTED as true******
                    "iops": true,
                    "kms_key_id": true,
                    "volume_id": true
                }
            ],

When you look at the actual plan JSON this is not correct, Terraform compliance is parsing incorrectly: Resource block:

resource "aws_instance" "root_block_encrypted__field_missing" {
  ami                     = "ami-003634241a8fcdec0"
  instance_type           = "t2.medium"
  key_name                = "tfcompliance_inf"
  disable_api_termination = true
  monitoring                  = true
  ebs_optimized               = true
  associate_public_ip_address = false
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
  }

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 1000
    volume_type           = "gp2"
    delete_on_termination = true
    encrypted             = true
  }

  tags = {
    budget-area = "security"
    group   = "cybersecurity"
  }
}

ACTUAL JSON OUTPUT FOR RESOURCE:

{
"address":"aws_instance.root_block_encrypted__field_missing",
*REDACTED FOR BREVITY
"disable_api_termination":true,
"ebs_block_device":[
{
"delete_on_termination":true,
"device_name":"/dev/sdb",
"encrypted":true,
"volume_size":1000,
"volume_type":"gp2"
}
],
"ebs_optimized":true,
"get_password_data":false,
"hibernation":null,
"iam_instance_profile":null,
"instance_initiated_shutdown_behavior":null,
"instance_type":"t2.medium",
"key_name":"tfcompliance_inf",
"monitoring":true,
"root_block_device":[
{
"delete_on_termination":true,
"volume_size":200,
"volume_type":"gp2". <<<< NO ENCRYPTION IN THE PLAN JSON,  terraform compliance sees it as True
}
],
*REDACTED FOR BREVITY****

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#encrypted. Default for encrypted field is false

A parsing issue also occurs for This piece of terraform code, ebs_block_device encryption is not declared in block:

resource "aws_instance" "ebs_encrypted_not_present" {
  ami               = "ami-003634241a8fcdec0"
  instance_type     = "t2.medium"
  key_name          = "tfcompliance_inf"
  security_groups = [aws_security_group.ingress_host.id,aws_security_group.egress_host.id]

  root_block_device {
    volume_size = 200
    volume_type = "gp2"
    encrypted = true
  }
  ebs_block_device {
    device_name           = "/dev/sdg"
    volume_size           = 50
    volume_type           = "gp2"
    delete_on_termination = true

  }

  tags = {
    budget-area = "security"
    group   = "Cybersecurity"
  }
}

STASH SHOWS INCORRECT, shows ebs_block_device encrypted = true https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#encrypted default is also FALSE on terraform registry

{
        "address": "aws_instance.ebs_encrypted_not_present",
        ** REDACTED FOR BREVITY***
            "ebs_block_device": [
                {
                    "delete_on_termination": true,
                    "device_name": "/dev/sdg",
                    "volume_size": 50,
                    "volume_type": "gp2"
                },
                {
                    "encrypted": true,  <<<< SHOWS AS TRUE IN STASH
                    "iops": true,
                    "kms_key_id": true,
                    "snapshot_id": true,
                    "volume_id": true
                }
            ],
            "ebs_optimized": null,
            "get_password_data": false,
            "hibernation": null,
            "iam_instance_profile": null,
            "instance_initiated_shutdown_behavior": null,
            "instance_type": "t2.medium",
            "key_name": "tfcompliance_inf",
            "monitoring": null,
            "root_block_device": [
                {
                    "delete_on_termination": true,
                    "encrypted": true,
                    "volume_size": 200,
                    "volume_type": "gp2"
                },
                {
                    "device_name": true,
                    "iops": true,
                    "kms_key_id": true,
                    "volume_id": true
                }
            ],

        },

Plan JSON OUTPUT ALSO CONCURS. that terraform compliance is not passing the correct values to the stash:

{
"address":"aws_instance.ebs_encrypted_not_present",
"mode":"managed",
"type":"aws_instance",
"name":"ebs_encrypted_not_present",
"provider_name":"aws",
"schema_version":1,
"values":{
"ami":"ami-003634241a8fcdec0",
"credit_specification":[
],
"disable_api_termination":null,
"ebs_block_device":[
{
"delete_on_termination":true,
"device_name":"/dev/sdg",
"volume_size":50,
"volume_type":"gp2"
}
],
"ebs_optimized":null,
"get_password_data":false,
"hibernation":null,
"iam_instance_profile":null,
"instance_initiated_shutdown_behavior":null,
"instance_type":"t2.medium",
"key_name":"tfcompliance_inf",
"monitoring":null,
"root_block_device":[
{
"delete_on_termination":true,
"encrypted":true,
"volume_size":200,
"volume_type":"gp2"
}
],
eerkunt commented 3 years ago

This could be the best issue descriptions (and conversation) we might ever had. Thanks a lot for this guys! We spent few hours to really understand the root cause of this problem and we found it! PR is created but it has some other problems.

Will work on it, fix it and then release a new version as soon as possible :)

Thanks again for this SUPER detailed issue! ❤️ ❤️ ❤️

eerkunt commented 3 years ago

Just released 1.3.8. Based on our tests it fixed the issue but please have a try and let us know 🙏

mdesmarest commented 3 years ago

@eerkunt @Kudbettin See my reply on 416 Looks good, fantastic turn around on this!