Skip to content

Commit

Permalink
Merge branch 'master' into changelog-from-1.2.5
Browse files Browse the repository at this point in the history
  • Loading branch information
eerkunt authored Jun 12, 2020
2 parents de6ee16 + 26a2337 commit cd784d8
Show file tree
Hide file tree
Showing 23 changed files with 294 additions and 2 deletions.
65 changes: 65 additions & 0 deletions docs/pages/bdd-references/then.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ to
This step requires fundamental knowledge about regular expressions due the pattern matching algorithm. It is highly
recommended to check for your patterns in [regex101](https://regex101.com/) before you implement your tests.

All values are compared with the regex. If the value referred by "it" on the plan is a dictionary or list, this step will fail if any element in the value fails the [condition](#){: .p-1 .text-green-200 .fw-700} match.

> __Possible sentences :__
>
>
Expand All @@ -258,6 +260,69 @@ match the
"[search_regex](#){: .p-1 .text-blue-100 .fw-700}"
regex
>
[Then](#){: .p-1 .text-red-200}
all of its values
[condition](#){: .p-1 .text-green-200 .fw-700}
match the
"[search_regex](#){: .p-1 .text-blue-100 .fw-700}"
regex
>
| key | Description | Examples |
|:---:|:----------|:-|
| [condition](#){: .p-1 .text-green-200 .fw-700} | defines positive or negative comparison | Only supports for : <br>▪ Leave it empty for positive comparison<br>▪ Use `must not` for negative comparison |
| [search_regex](#){: .p-1 .text-blue-100 .fw-700} | any valid regular expression | `^some_name$` `^(you|can|use|or|like|this)$` `\d+` |

__Please note that__, in case you are using a [Scenario Outline instead of a Scenario](/pages/bdd-references/#scenario)
and if you need to use `|` (or) regular expression operator within your [search_regex](#){: .p-1 .text-blue-100 .fw-700}
regex, then you must use escape characters (`\`) for not to interfere with Scenario Outline structure. In these situations
use `\|` instead of `|`.

__Warning:__ Terraform plan files may not always match the corresponding .tf files 1:1. In those cases, this step will match with the plan file and not the .tf file.

------------------------
### [Then](#){: .p-1 .text-red-200} any of its values [condition](#){: .p-1 .text-green-200 .fw-700} match the "[search regex](#){: .p-1 .text-blue-100 .fw-700}" regex
This step requires fundamental knowledge about regular expressions due the pattern matching algorithm. It is highly
recommended to check for your patterns in [regex101](https://regex101.com/) before you implement your tests.

All values are compared with the regex. If the value referred by "it" on the plan is a dictionary or list, this step will pass if any element in the value passes the [condition](#){: .p-1 .text-green-200 .fw-700} match.

> __Possible sentences :__
>
>
[Then](#){: .p-1 .text-red-200}
any of its values
[condition](#){: .p-1 .text-green-200 .fw-700}
match the
"[search_regex](#){: .p-1 .text-blue-100 .fw-700}"
regex
>
| key | Description | Examples |
|:---:|:----------|:-|
| [condition](#){: .p-1 .text-green-200 .fw-700} | defines positive or negative comparison | Only supports for : <br>▪ Leave it empty for positive comparison<br>▪ Use `must not` for negative comparison |
| [search_regex](#){: .p-1 .text-blue-100 .fw-700} | any valid regular expression | `^some_name$` `^(you|can|use|or|like|this)$` `\d+` |

__Please note that__, in case you are using a [Scenario Outline instead of a Scenario](/pages/bdd-references/#scenario)
and if you need to use `|` (or) regular expression operator within your [search_regex](#){: .p-1 .text-blue-100 .fw-700}
regex, then you must use escape characters (`\`) for not to interfere with Scenario Outline structure. In these situations
use `\|` instead of `|`.

------------------------
### [Then](#){: .p-1 .text-red-200} its singular value [condition](#){: .p-1 .text-green-200 .fw-700} match the "[search regex](#){: .p-1 .text-blue-100 .fw-700}" regex
This step requires fundamental knowledge about regular expressions due the pattern matching algorithm. It is highly
recommended to check for your patterns in [regex101](https://regex101.com/) before you implement your tests.

Very similar to [Then its value condition match the "search regex" regex](/pages/bdd-references/then.html#then-its-value-condition-match-the-search-regex-regex), but fail if the corresponding value is not one of (bool, int, float, str).

> __Possible sentences :__
>
>
[Then](#){: .p-1 .text-red-200}
its singular value
[condition](#){: .p-1 .text-green-200 .fw-700}
match the
"[search_regex](#){: .p-1 .text-blue-100 .fw-700}"
regex
>
| key | Description | Examples |
|:---:|:----------|:-|
| [condition](#){: .p-1 .text-green-200 .fw-700} | defines positive or negative comparison | Only supports for : <br>▪ Leave it empty for positive comparison<br>▪ Use `must not` for negative comparison |
Expand Down
2 changes: 1 addition & 1 deletion terraform_compliance/common/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def search_regex_in_list(regex, target_list):

def seek_value_in_dict(needle, haystack, address=None):
findings = []
if isinstance(haystack, (str, int, bool, float)) and needle in haystack:
if isinstance(haystack, (str, int, bool, float)) and str(needle) in str(haystack):
findings.append(dict(values=needle, address=None))

elif isinstance(haystack, dict):
Expand Down
35 changes: 35 additions & 0 deletions terraform_compliance/steps/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
from terraform_compliance.steps.then.property_is_enabled import property_is_enabled
from terraform_compliance.steps.then.security_group_related import it_condition_have_proto_protocol_and_port_port_for_cidr
from terraform_compliance.steps.then.its_value_condition_match_the_search_regex import its_value_condition_match_the_search_regex_regex
from terraform_compliance.steps.then.its_value_condition_match_the_search_regex import its_singular_value_condition_match_the_search_regex_regex
from terraform_compliance.steps.then.its_value_condition_match_the_search_regex import any_of_its_values_condition_match_the_search_regex_regex
from terraform_compliance.steps.then.its_value_condition_contain import its_value_condition_contain
from terraform_compliance.steps.then.it_must_have_reference_address_referenced import it_must_have_reference_address_referenced
from terraform_compliance.steps.then.its_key_condition_be_value import its_key_condition_be_value
Expand Down Expand Up @@ -147,6 +149,7 @@ def wrapper(_step_obj, operator, number, _stash=EmptyStash):


@then(u'its value {condition:ANY} match the "{search_regex}" regex')
@then(u'all of its values {condition:ANY} match the "{search_regex}" regex')
def wrapper(_step_obj, condition, search_regex, _stash=EmptyStash):
if not hasattr(_step_obj.context, 'case_insensitivity') or _step_obj.context.case_insensitivity:
return its_value_condition_match_the_search_regex_regex(_step_obj,
Expand All @@ -162,6 +165,38 @@ def wrapper(_step_obj, condition, search_regex, _stash=EmptyStash):
case_insensitive=False)


@then(u'its singular value {condition:ANY} match the "{search_regex}" regex')
def wrapper(_step_obj, condition, search_regex, _stash=EmptyStash):
if not hasattr(_step_obj.context, 'case_insensitivity') or _step_obj.context.case_insensitivity:
return its_singular_value_condition_match_the_search_regex_regex(_step_obj,
condition,
search_regex,
_stash=EmptyStash,
case_insensitive=True)
else:
return its_singular_value_condition_match_the_search_regex_regex(_step_obj,
condition,
search_regex,
_stash=EmptyStash,
case_insensitive=False)


@then(u'any of its values {condition:ANY} match the "{search_regex}" regex')
def wrapper(_step_obj, condition, search_regex, _stash=EmptyStash):
if not hasattr(_step_obj.context, 'case_insensitivity') or _step_obj.context.case_insensitivity:
return any_of_its_values_condition_match_the_search_regex_regex(_step_obj,
condition,
search_regex,
_stash=EmptyStash,
case_insensitive=True)
else:
return any_of_its_values_condition_match_the_search_regex_regex(_step_obj,
condition,
search_regex,
_stash=EmptyStash,
case_insensitive=False)


@then(u'its value {condition:ANY} be null')
def wrapper(_step_obj, condition):
return its_value_condition_match_the_search_regex_regex(_step_obj, condition, u'(\x00|^$|^null|^None)$')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def fail(condition, name=None):
regex_flags = re.IGNORECASE if case_insensitive else 0
regex_flag_error_text = 'case insensitive' if case_insensitive else 'case sensitive'

if isinstance(values, (str, int, bool)) or values is None:
if isinstance(values, (str, int, bool, float)) or values is None:
matches = re.match(regex, str(values), flags=regex_flags)

if (condition == 'must' and matches is None) or (condition == "must not" and matches is not None):
Expand Down Expand Up @@ -66,3 +66,67 @@ def fail(condition, name=None):
search_regex,
value,
case_insensitive=case_insensitive)


def any_of_its_values_condition_match_the_search_regex_regex(_step_obj, condition, search_regex, _stash=EmptyStash, case_insensitive=True):
def fail(condition, name=None):
text = 'matches' if condition == 'must not' else 'does not match'
name = name if (
name is not None or name is not False) else _step_obj.context.name
pattern = 'Null/None' if regex == '\x00' else regex
Error(_step_obj, '{} property in {} {} {} with {} {} regex. '
'It is set to {}.'.format(_step_obj.context.property_name,
name,
_step_obj.context.type,
text,
pattern,
regex_flag_error_text,
values))

found = False
def search(values):
nonlocal found
if found:
return True

if isinstance(values, (str, int, bool, float)) or values is None:
matches = re.match(regex, str(values), flags=regex_flags)

if (condition == 'must' and matches is not None) or (condition == "must not" and matches is None):
found = True
return found

elif isinstance(values, list):
return any(map(search, values))

elif isinstance(values, dict):
if not hasattr(_step_obj.context, 'address'):
_step_obj.context.address = None

_step_obj.context.address = values.get('address', _step_obj.context.address)

if 'values' in values:
return search(values['values'])
else:
return any(map(search, values.values()))

return False

regex = r'{}'.format(search_regex)
values = _step_obj.context.stash if _stash is EmptyStash else _stash
regex_flags = re.IGNORECASE if case_insensitive else 0
regex_flag_error_text = 'case insensitive' if case_insensitive else 'case sensitive'

if not search(values):
_stash = get_resource_name_from_stash(_step_obj.context.stash, _stash, _step_obj.context.address)
fail(condition, name=_stash.get('address'))


def its_singular_value_condition_match_the_search_regex_regex(_step_obj, condition, search_regex, _stash=EmptyStash, case_insensitive=True):
values = _step_obj.context.stash if _stash is EmptyStash else _stash

if isinstance(values, (dict, list)):
Error(_step_obj, '{} is multivalued! Please use any/all versions of this step instead.'.format(_step_obj.context.property_name,))
return

its_value_condition_match_the_search_regex_regex(_step_obj, condition, search_regex, _stash, case_insensitive)
24 changes: 24 additions & 0 deletions tests/functional/test_any_match_regex/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
resource "azurerm_storage_account" "example" {
name = "storageaccountname"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "GRS"
network_rules {
bypass = ["AzureServices", "Logging", "Metrics"]
default_action = "Deny"
ip_rules = ["100.1.1.123/32"]
}
tags = {
environment = "staging"
}
}

resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}

provider "azurerm" {
features {}
}
1 change: 1 addition & 0 deletions tests/functional/test_any_match_regex/plan.out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format_version":"0.1","terraform_version":"0.12.25","planned_values":{"root_module":{"resources":[{"address":"azurerm_resource_group.example","mode":"managed","type":"azurerm_resource_group","name":"example","provider_name":"azurerm","schema_version":0,"values":{"location":"westeurope","name":"example-resources","tags":null,"timeouts":null}},{"address":"azurerm_storage_account.example","mode":"managed","type":"azurerm_storage_account","name":"example","provider_name":"azurerm","schema_version":2,"values":{"account_kind":"StorageV2","account_replication_type":"GRS","account_tier":"Standard","custom_domain":[],"enable_https_traffic_only":true,"is_hns_enabled":false,"location":"westeurope","name":"storageaccountname","network_rules":[{"bypass":["AzureServices","Logging","Metrics"],"default_action":"Deny","ip_rules":["100.1.1.123/32"]}],"resource_group_name":"example-resources","static_website":[],"tags":{"environment":"staging"},"timeouts":null}}]}},"resource_changes":[{"address":"azurerm_resource_group.example","mode":"managed","type":"azurerm_resource_group","name":"example","provider_name":"azurerm","change":{"actions":["create"],"before":null,"after":{"location":"westeurope","name":"example-resources","tags":null,"timeouts":null},"after_unknown":{"id":true}}},{"address":"azurerm_storage_account.example","mode":"managed","type":"azurerm_storage_account","name":"example","provider_name":"azurerm","change":{"actions":["create"],"before":null,"after":{"account_kind":"StorageV2","account_replication_type":"GRS","account_tier":"Standard","custom_domain":[],"enable_https_traffic_only":true,"is_hns_enabled":false,"location":"westeurope","name":"storageaccountname","network_rules":[{"bypass":["AzureServices","Logging","Metrics"],"default_action":"Deny","ip_rules":["100.1.1.123/32"]}],"resource_group_name":"example-resources","static_website":[],"tags":{"environment":"staging"},"timeouts":null},"after_unknown":{"access_tier":true,"blob_properties":true,"custom_domain":[],"id":true,"identity":true,"network_rules":[{"bypass":[false,false,false],"ip_rules":[false],"virtual_network_subnet_ids":true}],"primary_access_key":true,"primary_blob_connection_string":true,"primary_blob_endpoint":true,"primary_blob_host":true,"primary_connection_string":true,"primary_dfs_endpoint":true,"primary_dfs_host":true,"primary_file_endpoint":true,"primary_file_host":true,"primary_location":true,"primary_queue_endpoint":true,"primary_queue_host":true,"primary_table_endpoint":true,"primary_table_host":true,"primary_web_endpoint":true,"primary_web_host":true,"queue_properties":true,"secondary_access_key":true,"secondary_blob_connection_string":true,"secondary_blob_endpoint":true,"secondary_blob_host":true,"secondary_connection_string":true,"secondary_dfs_endpoint":true,"secondary_dfs_host":true,"secondary_file_endpoint":true,"secondary_file_host":true,"secondary_location":true,"secondary_queue_endpoint":true,"secondary_queue_host":true,"secondary_table_endpoint":true,"secondary_table_host":true,"secondary_web_endpoint":true,"secondary_web_host":true,"static_website":[],"tags":{}}}}],"configuration":{"provider_config":{"azurerm":{"name":"azurerm","expressions":{"features":[{}]}}},"root_module":{"resources":[{"address":"azurerm_resource_group.example","mode":"managed","type":"azurerm_resource_group","name":"example","provider_config_key":"azurerm","expressions":{"location":{"constant_value":"West Europe"},"name":{"constant_value":"example-resources"}},"schema_version":0},{"address":"azurerm_storage_account.example","mode":"managed","type":"azurerm_storage_account","name":"example","provider_config_key":"azurerm","expressions":{"account_replication_type":{"constant_value":"GRS"},"account_tier":{"constant_value":"Standard"},"location":{"references":["azurerm_resource_group.example"]},"name":{"constant_value":"storageaccountname"},"network_rules":[{"bypass":{"constant_value":["AzureServices","Logging","Metrics"]},"default_action":{"constant_value":"Deny"},"ip_rules":{"constant_value":["100.1.1.123/32"]}}],"resource_group_name":{"references":["azurerm_resource_group.example"]},"tags":{"constant_value":{"environment":"staging"}}},"schema_version":2}]}}}
6 changes: 6 additions & 0 deletions tests/functional/test_any_match_regex/test.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Feature: Feature for issue #285
Scenario: Ensure 'Trusted Microsoft Services' is enabled for Storage Account access
Given I have azurerm_storage_account defined
Then it must contain network_rules
And it must contain bypass
And any of its values must match the "(^AzureServices$)" regex
1 change: 1 addition & 0 deletions tests/functional/test_any_match_regex_fail/.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Failure: bypass property in azurerm_storage_account.example resource does not match with \(\^AzureServicesBad\$\) case insensitive regex. It is set to \[\{\'address\': \'azurerm_storage_account.example\', \'values\': \[\[\'AzureServices\', \'Logging\', \'Metrics\'\], \[False, False, False\]\], \'type\': \'azurerm_storage_account\'\}\].
Empty file.
24 changes: 24 additions & 0 deletions tests/functional/test_any_match_regex_fail/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
resource "azurerm_storage_account" "example" {
name = "storageaccountname"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "GRS"
network_rules {
bypass = ["AzureServices", "Logging", "Metrics"]
default_action = "Deny"
ip_rules = ["100.1.1.123/32"]
}
tags = {
environment = "staging"
}
}

resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}

provider "azurerm" {
features {}
}
Loading

0 comments on commit cd784d8

Please sign in to comment.