Terraform Cloud - Governance

Imports

In the last guide, we referenced an import called tfplan. This import provides access to a Terraform plan and is created as a result of terraform plan and is the input of terraform apply.

Imports enable a Sentinel policy to access reusable libraries and external data and functions. Imports are what enable Sentinel policies to do more than look at only local context for making policy decisions. There are four Terraform Sentinel imports to be aware of: tfplan, tfstate, tfconfig, and tfrun. For more specific examples of each, visit the Terraform Cloud Sentinel docs.

Sentinel also comes with a set of standard imports. Standard imports are available to every Sentinel policy to help policy writers with common tasks such as working with the time, network addresses, and more.

To use an import, you use the import keyword at the top of your policy. This specifies the name of the import you want to use. The application you're writing the policy for must already be configured to provide that import.

Details on imports can be found in the import section in the language reference.

Standard Imports

As an introduction to imports, let's look at a standard import called strings

import "tfplan"
import "strings"

##### Functions #####

# Find all resources of a specific type from all modules using the tfplan import
find_resources_from_plan = func(type) {

  resources = {}

  # Iterate over all modules in the tfplan import
  for tfplan.module_paths as path {
    # Iterate over the named resources of desired type in the module
    for tfplan.module(path).resources[type] else {} as name, instances {
      # Iterate over resource instances
      for instances as index, r {

        # Get the address of the instance
        if length(path) == 0 {
          # root module
          address = type + "." + name + "[" + string(index) + "]"
        } else {
          # non-root module
          address = "module." + strings.join(path, ".module.") + "." +
                    type + "." + name + "[" + string(index) + "]"
        }

        # Add the instance to resources map, setting the key to the address
        resources[address] = r
      }
    }
  }

  return resources
}

# Validate that all instances of a specified resource type being modified have
# a specified top-level attribute in a given list
validate_attribute_in_list = func(type, attribute, allowed_values) {

  validated = true

  # Get all resource instances of the specified type
  resource_instances = find_resources_from_plan(type)

  # Loop through the resource instances
  for resource_instances as address, r {

    # Skip resource instances that are being destroyed
    # to avoid unnecessary policy violations
    if r.destroy and not r.requires_new {
      print("Skipping resource", address, "that is being destroyed.")
      continue
    }

    # Determine if the attribute is computed
    if r.diff[attribute].computed else false is true {
      print("Resource", address, "has attribute", attribute,
            "that is computed.")
      # If you want computed values to cause the policy to fail,
      # uncomment the next line.
      # validated = false
    } else {
      # Validate that each instance has allowed value
      if (r.applied[attribute] else "") not in allowed_values {
        print("Resource", address, "has attribute", attribute, "with value",
              r.applied[attribute] else "",
              "that is not in the allowed list:", allowed_values)
        validated = false
      }
    }

  }
  return validated
}

##### Lists #####

# Allowed EC2 Zones
allowed_zones = [
  "us-east-1a",
  "us-east-1b",
  "us-east-1c",
  "us-east-1d",
  "us-east-1e",
  "us-east-1f",
]

##### Rules #####

# Main rule that calls the validation function and evaluates results
main = rule {
  validate_attribute_in_list("aws_instance", "availability_zone", allowed_zones)
}

The strings import has operations to join and split strings, convert their case, test if they have a specific prefix or suffix, and trim them.

Mocking Imports

The first option to developing policies locally is to mock the import values. When mocking an import, you don't need the import plugin available. This can be useful since some imports may not be available as a plugin and may only be available to the application the policy runs in.

Mocks are specified via the configuration file. Mocks can also be used for testing.

You can supply mock configuration one of two ways, depending on your use case:

Using static data: Use this method when you can accurately represent your mock data in JSON and do not need to mock complex Sentinel features such as functions. Using Sentinel code: Use this method when using a static JSON object is insufficient, such as when you need to mock functions or other complex Sentinel features. Our example does not require complex data to be mocked, so a static JSON object is sufficient:

{
  "mock": {
    "time": {
      "hour": 12,
      "day": "tuesday"
    }
  }
}

This can be used via the CLI:

$ sentinel apply -config=config.json policy.sentinel
Pass

Launching Imports

If you have access to the plugin binary, you can launch the import. The benefit of this is that it is really using the import to test your policy. If the import changes, your policies may start failing. If you only use mock data and the import changes, your policies will still appear to work.

Imports are configured in the configuration file:

{
    "imports": {
        "time": {
            "path": "/path/to/sentinel-time-import"
        }
    }
}