diff --git a/.gitignore b/.gitignore
index 6bd276bd..595156e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
venv
*.pyc
*.sublime-*
-tf_files/*
+.idea/*
+build
+dist
+example/tf_files/*
+terraform_compliance.egg-info
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..293846cb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2017-2018 Emre Erkunt, emre.erkunt@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index fb484c8f..28de5b1e 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,159 @@
-# terraform-compliance
-----------------------
-
-A compliancy tool that runs against terraform files kept in local with BDD style configuration.
-
-This tool uses ;
-
-* [radish-bdd](https://github.com/radish-bdd/radish)
-* [terraform-validate](https://github.com/elmundio87/terraform_validate)
-
+
terraform-compliance
+
+
+ BDD Testing for Terraform Files
+
+
+ A lightweight BDD-testing Compliance Framework
+
+
+
+
+
+
+## Table of Contents
+- [Features](#features)
+- [Example](#example)
+- [FAQ](#faq)
+- [Installation](#installation)
+- [Usage](#usage)
+
+## Features
+- __compliance:__ Test your infrastructure as code before you deploy. Enforce your people to follow the policies.
+- __behaviour driven development:__ We have BDD for nearly everything, why not for IaC ?
+- __fixed steps:__ fixed steps coming with the package, just focus on your BDD feature/scenario files.
+- __portable:__ just install it from `pip`
+- __why ?:__ why not ?
+
+## Example

-
-### Installation
-Clone this repository
-
- # git clone https://github.com/eeerkunt/terraform-compliance
- # cd terraform-compliance
-
-
-(OPTIONAL) Create a virtualenv and activate it
-
- # virtualenv venv
- # source venv/bin/activate
-
-
-Install requirements
-
- # pip install -r requirements.txt
-
-and start playing
+```sh
+[~] $ terraform-compliance -f example/example_01 -t example/tf_files
+terraform-compliance v0.0.1 initiated
+Features : /Users/sharky/Repository/terraform-compliance/example/example_01
+Steps : /Users/sharky/Repository/venv2/lib/python2.7/site-packages/terraform_compliance/steps
+TF Files : /Users/sharky/Repository/terraform-compliance/example/tf_files
+Validating terraform files.
+All HCL files look good.
+Running tests.
+Feature: Security Groups should be used to protect services/instances # /path/to/example/example_01/aws/security_groups.feature
+ In order to improve security
+ As engineers
+ We'll use AWS Security Groups as a Perimeter Defence
+
+ Scenario Outline: Policy Structure
+ Given I define AWS Security Group
+ Then it must contain
+
+ Examples:
+ | policy_name |
+ | ingress |
+ | egress |
+
+ Scenario Outline: Well-known insecure protocol exposure on Public Network for ingress traffic
+ Given I define AWS Security Group
+ Then it must contain ingress
+ with protocol and not port for 0.0.0.0/0
+
+ Examples:
+ | ProtocolName | proto | portNumber |
+ | HTTP | tcp | 80 |
+ | Telnet | tcp | 23 |
+ | SSH | tcp | 22 |
+ | MySQL | tcp | 3306 |
+ | MSSQL | tcp | 1443 |
+ | NetBIOS | tcp | 139 |
+ | RDP | tcp | 3389 |
+
+Feature: Subnets should be defined properly for network security # /path/to/example/example_01/aws/subnets.feature
+ In order to improve security
+ And decrease impact radius
+ As engineers
+ We'll use a layered architecture in our AWS Environment
+
+ Scenario: Subnet Count
+ Given I define AWS Subnet
+ When I count them
+ Then I expect the result is more than 2
+ AssertionError: 0 is not more than 2
+
+2 features (1 passed, 1 failed)
+10 scenarios (9 passed, 1 failed)
+28 steps (27 passed, 1 failed)
+
+[~] $ echo $?
+1
+[~] $
+```
+
+## FAQ
+
+- __Q.__ Where are the steps defined ?
+- __A.__ They all comes with `terraform-compliance`, you can just focus on BDD feature/scenario files.
+
+- __Q.__ What if I would like to add more steps ?
+- __A.__ You are welcome to contribute on any test, or just add an issue it will be added.
+
+- __Q.__ Where should `terraform-compliance` run ?
+- __A.__ Ideally in a CI/CD tool, where company policies are defined as feature files and all IaC is tested against. Trust, but verify.
+
+## Installation
+```sh
+[~] $ pip install terraform-compliance
+```
### Usage
-Store your terraform files into any directory and pass that directory as an argument to radish.
-
-For e.g. Assuming that your terraform files are stored in `/path/to/tf/files` and tests are stored in `./providers/aws`to run tests against it ;
-
- ~# radish providers/aws /path/to/tf/files
-
-
-All radish features are available
-
-### Going further
-
-The capabilities are stored in [radish](radish) directory where you can add any steps as a capability.
-
-Tests, or _features/scenarios_ in radish-style, are stored in [provider](provider) directory.
+```sh
+[~] $ terraform-compliance --help
+usage: terraform-compliance [-h] [--features feature_directory]
+ [--tfdir terraform_directory]
+
+BDD Test Framework for Hashicorp terraform
+
+optional arguments:
+ -h, --help show this help message and exit
+ --features feature_directory, -f feature_directory
+ Directory consists of BDD features
+ --tfdir terraform_directory, -t terraform_directory
+ Directory consists of Terraform Files
+```
+
+You can also push additional arguments that is specific for `radish`. Just to explain how it works ;
+
+For e.g.
+```sh
+[~] $ terraform-compliance -f /path/to/features -t /path/to/terraform_files -v
+terraform-compliance v0.0.1 initiated
+Features : /Users/sharky/Repository/terraform-compliance/example/example_01
+Steps : /Users/sharky/Repository/venv2/lib/python2.7/site-packages/terraform_compliance/steps
+TF Files : /Users/sharky/Repository/terraform-compliance/example/tf_files
+Validating terraform files.
+All HCL files look good.
+Running tests.
+0.8.6
+```
+Please note that `0.8.6` is the `radish` version comes from `-v` parameter.
+
+
+## License
+[MIT](https://tldrlegal.com/license/mit-license)
diff --git a/provider/aws/security_groups.feature b/example/example_01/aws/security_groups.feature
similarity index 100%
rename from provider/aws/security_groups.feature
rename to example/example_01/aws/security_groups.feature
diff --git a/provider/aws/subnets.feature b/example/example_01/aws/subnets.feature
similarity index 100%
rename from provider/aws/subnets.feature
rename to example/example_01/aws/subnets.feature
diff --git a/radish/terrain.py b/radish/terrain.py
deleted file mode 100644
index 0bf0e7a2..00000000
--- a/radish/terrain.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from radish import before, after, step
-import terraform_validate
-import os
-import sys
-
-@before.each_scenario
-def init_terraform_files(step):
- if len(sys.argv) <= 2:
- raise Exception("Usage: radish ")
-
- tf_dir = os.path.join(os.path.abspath(sys.argv[2]))
- step.context.validator = terraform_validate.Validator(tf_dir)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 9b18737e..1fd11b49 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
terraform_validate
-radish-bdd
+radish
+radish-bdd
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..5e409001
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..eb0da791
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,53 @@
+"""
+BDD test framework for terraform
+"""
+from setuptools import find_packages, setup
+from terraform_compliance.main import __app_name__, __version__
+
+dependencies = [
+ 'radish-bdd',
+ 'radish',
+ 'terraform-validate'
+]
+
+setup(
+ name=__app_name__,
+ version=__version__,
+ url='https://github.com/eerkunt/terraform-compliance',
+ license='MIT',
+ author='Emre Erkunt',
+ author_email='emre.erkunt@gmail.com',
+ description='BDD test framework for terraform',
+ long_description=__doc__,
+ packages=find_packages(exclude=['tests']),
+ include_package_data=True,
+ zip_safe=False,
+ platforms='any',
+ install_requires=dependencies,
+ entry_points={
+ 'console_scripts': [
+ 'terraform-compliance=terraform_compliance.main:cli',
+ ],
+ },
+ classifiers=[
+ # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ # 'Development Status :: 1 - Planning',
+ # 'Development Status :: 2 - Pre-Alpha',
+ # 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
+ # 'Development Status :: 5 - Production/Stable',
+ # 'Development Status :: 6 - Mature',
+ # 'Development Status :: 7 - Inactive',
+ 'Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: POSIX',
+ 'Operating System :: MacOS',
+ 'Operating System :: Unix',
+ 'Operating System :: Microsoft :: Windows',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ]
+)
diff --git a/terraform_compliance/__init__.py b/terraform_compliance/__init__.py
new file mode 100644
index 00000000..ab83d11c
--- /dev/null
+++ b/terraform_compliance/__init__.py
@@ -0,0 +1 @@
+from terraform_validate import Validator
diff --git a/terraform_compliance/main.py b/terraform_compliance/main.py
new file mode 100644
index 00000000..f08a2082
--- /dev/null
+++ b/terraform_compliance/main.py
@@ -0,0 +1,74 @@
+import sys
+import os
+from argparse import ArgumentParser, ArgumentTypeError, Action
+from terraform_compliance import Validator
+from radish.main import main as call_radish
+
+__app_name__ = "terraform-compliance"
+__version__ = "0.0.1"
+
+
+class ArgHandling(object):
+ def __init__(self):
+ self.features = None
+ self.steps = None
+ self.tf_dir = None
+
+class readable_dir(Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ prospective_dir = values
+ if not os.path.isdir(prospective_dir):
+ print('Invalid use or arguments: {}'.format(prospective_dir))
+ sys.exit(1)
+
+ if os.access(prospective_dir, os.R_OK):
+ setattr(namespace, self.dest, prospective_dir)
+ else:
+ print('Invalid use or arguments: {}'.format(prospective_dir))
+ sys.exit(1)
+
+
+def cli():
+ argument = ArgHandling()
+ parser = ArgumentParser(prog=__app_name__,
+ description="BDD Test Framework for Hashicorp terraform")
+ try:
+ parser.add_argument("--features", "-f", dest="features", metavar='feature_directory', action=readable_dir,
+ help="Directory consists of BDD features", required=True)
+ parser.add_argument("--tfdir", "-t", dest="tf_dir", metavar='terraform_directory', action=readable_dir,
+ help="Directory consists of Terraform Files", required=True)
+ except argparse.ArgumentTypeError:
+ print('Invalid use or arguments: {}'.format(sys.exc_info()[1]))
+ sys.exit(1)
+
+ # parser.parse_args(namespace=argument)
+ _, radish_arguments = parser.parse_known_args(namespace=argument)
+
+ print('{} v{} initiated'.format(__app_name__, __version__))
+
+ features_directory = os.path.join(os.path.abspath(argument.features))
+ steps_directory = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'steps')
+ tf_directory = os.path.join(os.path.abspath(argument.tf_dir))
+
+ print('Features : {}'.format(features_directory))
+ print('Steps : {}'.format(steps_directory))
+ print('TF Files : {}'.format(tf_directory))
+
+ commands = ['radish',
+ features_directory,
+ '--basedir', steps_directory,
+ '--user-data=tf_dir={}'.format(tf_directory)]
+ commands.extend(radish_arguments)
+
+ try:
+ print('Validating terraform files.')
+ Validator(tf_directory)
+ print('All HCL files look good.')
+
+ except ValueError:
+ print('Unable to validate Terraform Files.')
+ print('ERROR: {}'.format(sys.exc_info()[1]))
+ sys.exit(1)
+
+ print('Running tests.')
+ return call_radish(args=commands[1:])
diff --git a/terraform_compliance/steps/__init__.py b/terraform_compliance/steps/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/radish/steps.py b/terraform_compliance/steps/steps.py
similarity index 58%
rename from radish/steps.py
rename to terraform_compliance/steps/steps.py
index 1910fa83..373fd895 100644
--- a/radish/steps.py
+++ b/terraform_compliance/steps/steps.py
@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
-from radish import step, arg_expr
-import terraform_validate
-import os
-import sys
+from radish import step, world
untaggable_resources = [
"aws_route_table",
@@ -47,91 +44,84 @@
"Name": "^\${var.platform}_\${var.environment}_.*"
}
-# New Arguments
-@arg_expr("ANY", r"[\.\/_\-A-Za-z0-9\s]+")
-def arg_exp_for_secure_text(text):
- return text
-@step(r'Given I define {resource:ANY}')
+@step(r'Given I define {resource}')
def define_a_resource(step, resource):
-
- if(resource in resource_name.keys()):
+ if (resource in resource_name.keys()):
resource = resource_name[resource]
step.context.resource_type = resource
- step.context.stash = step.context.resources = step.context.validator.resources(resource)
+ step.context.stash = step.context.resources = world.config.terraform.resources(resource)
+
-@step(r'When I {action:ANY} them')
-def i_action_them(step, action):
- if action == "count":
+@step(r'When I {action_type} them')
+def i_action_them(step, action_type):
+ if action_type == "count":
step.context.stash = len(step.context.stash.resource_list)
- elif action == "sum":
+ elif action_type == "sum":
step.context.stash = sum(step.context.stash.resource_list)
else:
- AssertionError("Invalid action in the scenario: "+str(action))
+ AssertionError("Invalid action_type in the scenario: {}".format(action))
+
-@step(r'Then I expect the result is {operator:ANY} than {number:d}')
-def i_expect_the_result_is_operator_than_number(step, operator, number):
+@step(r'Then I expect the result is {operator} than {number:d}')
+def func(step, operator, number):
value = int(step.context.stash)
if operator == "more":
- assert value > number, str(value)+" is not more than "+str(number)
+ assert value > number, str(value) + " is not more than " + str(number)
elif operator == "more and equal":
- assert value >= number, str(value)+" is not more and equal than "+str(number)
+ assert value >= number, str(value) + " is not more and equal than " + str(number)
elif operator == "less":
- assert value < number, str(value)+" is not less than "+str(number)
+ assert value < number, str(value) + " is not less than " + str(number)
elif operator == "less and equal":
- assert value <= number, str(value)+" is not less and equal than "+str(number)
+ assert value <= number, str(value) + " is not less and equal than " + str(number)
else:
- AssertionError("Invalid operator: "+str(operator))
+ AssertionError("Invalid operator: " + str(operator))
-@step('it contains {property:ANY}')
-def it_contains_a(step, property):
- if(property in resource_name.keys()):
- property = resource_name[property]
+@step('it must contain {something}')
+def func(step, something):
+ world.config.terraform.error_if_property_missing()
- step.context.resource_type = property
- step.context.resources = step.context.resources.property(property)
+ if something in resource_name.keys():
+ something = resource_name[something]
-@step('it must contain {property:ANY}')
-def it_must_contain_a(step, property):
- step.context.validator.error_if_property_missing()
- if(property in resource_name.keys()):
- property = resource_name[property]
+ step.context.resource_type = something
+ step.context.resources = step.context.resources.property(something)
- step.context.resource_type = property
- step.context.resources = step.context.resources.property(property)
@step('encryption must be enabled')
-def encryption_must_be_enabled(step):
- step.context.validator.error_if_property_missing()
+def func(step):
+ world.config.terraform.error_if_property_missing()
prop = encryption_property[step.context.resource_type]
step.context.resources.property(prop).should_equal(True)
@step(u'it must have the "([^"]*)" tag')
-def it_must_have_the_tag(step, tag):
- step.context.validator.error_if_property_missing()
+def func(step, tag):
+ world.config.terraform.error_if_property_missing()
step.context.tag = tag
step.context.properties = step.context.resources.property('tags')
step.context.properties.should_have_properties(tag)
@step(u'And its value must match the "([^"]*)" regex')
-def it_must_have_the_tag(step, regex_type):
- step.context.validator.error_if_property_missing()
+def func(step, regex_type):
+ world.config.terraform.error_if_property_missing()
step.context.regex = regex[regex_type]
step.context.properties.property(regex_type).should_match_regex(step.context.regex)
+
@step(u'And its value must be set by a variable')
-def and_its_value_must_be_set_by_a_variable(step):
+def func(step):
step.context.resources.property('tags').property(step.context.tag).should_match_regex('\${var.(.*)}')
-@step(r'with {proto:ANY} protocol and not port {value:d} for {cidr:ANY}')
-def and_it_must_have_key_attribute_with_value_value(step, proto, value, cidr):
+
+@step(r'with {proto} protocol and not port {port} for {cidr}')
+def func(step, proto, port, cidr):
proto = str(proto)
- value = int(value)
+ port = int(port)
cidr = str(cidr)
giveError = False
@@ -161,19 +151,15 @@ def and_it_must_have_key_attribute_with_value_value(step, proto, value, cidr):
else:
cidr_blocks = str(i.property_value[y])
-
-
if int(to_port) > int(from_port):
- if int(from_port) <= value <= int(to_port) and protocol == proto and cidr_blocks == cidr:
+ if int(from_port) <= port <= int(to_port) and protocol == proto and cidr_blocks == cidr:
giveError = True
elif int(from_port) > int(to_port):
- if int(to_port) <= value <= int(from_port) or protocol == proto and cidr_blocks == cidr:
+ if int(to_port) <= port <= int(from_port) or protocol == proto and cidr_blocks == cidr:
giveError = True
elif int(from_port) == int(to_port):
- if int(from_port) == value and protocol == proto and cidr_blocks == cidr:
+ if int(from_port) == port and protocol == proto and cidr_blocks == cidr:
giveError = True
if giveError:
- raise AssertionError("Found "+proto+"/"+str(value)+" for "+str(cidr_blocks))
-
-
+ raise AssertionError("Found " + proto + "/" + str(port) + " for " + str(cidr_blocks))
diff --git a/terraform_compliance/steps/terrain.py b/terraform_compliance/steps/terrain.py
new file mode 100644
index 00000000..3b956425
--- /dev/null
+++ b/terraform_compliance/steps/terrain.py
@@ -0,0 +1,7 @@
+from radish import before, world
+from terraform_compliance import Validator
+
+
+@before.each_feature
+def load_terraform_data(feature):
+ world.config.terraform = Validator(world.config.user_data['tf_dir'])
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+