def it_condition_have_proto_protocol_and_port_port_for_cidr( _step_obj, condition, proto, port, cidr): searching_for = dict(port=port, protocol=proto, cidr_blocks=cidr) for sg in _step_obj.context.stash: if sg['type'] != 'aws_security_group': raise TerraformComplianceInternalFailure( 'This method can only be used for aws_security_group resources ' 'for now. You tried to used it on {}'.format(sg['type'])) sg_obj = SecurityGroup(searching_for, sg['values'], address=sg['address']) if condition == 'must only': sg_obj.must_only_have() elif condition == 'must': sg_obj.must_have() elif condition == 'must not': sg_obj.must_not_have() else: raise TerraformComplianceInternalFailure( 'You can only use "must have", "must not have" and "must only have"' 'conditions on this step for now.' 'You tried to use "{}"'.format(condition)) result, message = sg_obj.validate() if result is False: Error(_step_obj, message) return True
def issubset(self, set1, set2): if not isinstance(set1, Iterable) or isinstance(set1, str): raise TerraformComplianceInternalFailure('{} should be a non-str iterable'.format(set1)) if not isinstance(set2, Iterable) or isinstance(set2, str): raise TerraformComplianceInternalFailure('{} should be a non-str iterable'.format(set2)) if not self.case_sensitive: set1 = set(str(e).lower() for e in set1) set2 = set(str(e).lower() for e in set2) return set1 <= set2
def it_condition_have_proto_protocol_and_port_port_for_cidr( _step_obj, condition, proto, port, cidr): proto = str(proto) cidr = str(cidr) # Set to True only if the condition is 'only' condition = condition == 'only' # In case we have a range if '-' in port: if condition: raise Failure( '"must only" scenario cases must be used either with individual port ' 'or multiple ports separated with comma.') from_port, to_port = port.split('-') ports = [from_port, to_port] # In case we have comma delimited ports elif ',' in port: ports = [port for port in port.split(',')] from_port = min(ports) to_port = max(ports) else: from_port = to_port = int(port) ports = list(set([str(from_port), str(to_port)])) from_port = int(from_port) if int(from_port) > 0 else 1 to_port = int(to_port) if int(to_port) > 0 else 1 ports[0] = ports[0] if int(ports[0]) > 0 else '1' looking_for = dict(proto=proto, from_port=int(from_port), to_port=int(to_port), ports=ports, cidr=cidr) for security_group in _step_obj.context.stash: if type(security_group['values']) is list: for sg in security_group['values']: check_sg_rules(plan_data=sg, security_group=looking_for, condition=condition) elif type(security_group['values']) is dict: check_sg_rules(plan_data=security_group['values'], security_group=looking_for, condition=condition) else: raise TerraformComplianceInternalFailure( 'Unexpected Security Group, ' 'must be either list or a dict: ' '{}'.format(security_group['values'])) return True
def __call__(self, parser, namespace, values, option_string=None): # Check if the given path is a file if not os.path.isfile(values): print('ERROR: {} is not a file.'.format(values)) sys.exit(1) # Check if the given file is a native terraform plan file given_file = filetype.guess(values) if given_file is not None: terraform_executable = getattr(namespace, 'terraform_file', None) values = convert_terraform_plan_to_json(os.path.abspath(values), terraform_executable) # Check if the given file is a json file. try: with open(values, 'r') as plan_file: data = json.load(plan_file) except json.decoder.JSONDecodeError: print('ERROR: {} is not a valid JSON file'.format(values)) sys.exit(1) except UnicodeDecodeError: print('ERROR: {} is not a valid JSON file.'.format(values)) print( ' Did you try to convert the binary plan file to json with ' '"terraform show -json {} > {}.json" ?'.format(values, values)) sys.exit(1) except: raise TerraformComplianceInternalFailure('Invalid file type.') # Check if this is a correct terraform plan file try: assert data['format_version'] assert data['terraform_version'] # Check if this is a state file if 'values' in data: assert data['values']['root_module']['resources'] # Then it must be a terraform plan file else: assert data['planned_values'] assert data['configuration'] except KeyError: print( 'ERROR: {} is not a valid terraform plan json output.'.format( values)) sys.exit(1) setattr(namespace, self.dest, os.path.abspath(values)) return True
def it_must_have_reference_address_referenced(_step_obj, reference_address): if _step_obj.context.stash: for resource in _step_obj.context.stash: if isinstance(resource, dict): if Defaults.address_pointer in resource and search_regex_in_list(reference_address, resource[Defaults.address_pointer]): return True else: raise TerraformComplianceInternalFailure('Unexpected resource structure: {}'.format(resource)) Error(_step_obj, '{} is not referenced within {}.'.format(reference_address, resource.get('address'))) else: Error(_step_obj, 'No entities found for this step to process. Check your filtering steps in this scenario.')
def python_version_check(): python_version = sys.version.split(' ')[0] if not python_version: raise TerraformComplianceInternalFailure('Could not determine python version. ' 'Please post this to issues: '.format(sys.version)) python_version = VersionInfo.parse(python_version) if compare(str(python_version), Defaults.supported_min_python_versions) < 0: console_write('ERROR: Python version {} is not supported. ' 'You must have minimum {} version.'.format(python_version, Defaults.supported_min_python_versions[0])) sys.exit(1) return True
def _its_value_condition_contain(_step_obj, condition, value, values): if isinstance(values, list): values = [str(v) for v in values] _fail_text = 'did not contain' if condition == 'must' else 'contains' fail_message = '{} property in {} {} {}, it is set to {}'.format( _step_obj.context.property_name, _step_obj.context.name, _fail_text, value, values, ) if condition == 'must': assert value in values, fail_message else: assert value not in values, fail_message else: raise TerraformComplianceInternalFailure('Can only check that if list contains value')
def _expand_ports(self, port): if '-' in port: self.from_port, self.to_port = [int(port_number) for port_number in port.split('-')] self.from_port = self.from_port if self.from_port > 0 else 1 self.to_port = self.to_port if self.to_port > 0 else 1 self.ports = self._get_port_range(self.from_port, self.to_port) self.port_is_single = False elif ',' in port: self.from_port = None self.to_port = None self.port_is_single = False self.ports = set([int(port_number) for port_number in port.split(',') if int(port_number) > 0]) elif port.isnumeric(): self.from_port = self.to_port = int(port) self.ports = self._get_port_range(self.from_port, self.to_port) self.port_is_single = True else: raise TerraformComplianceInternalFailure('Unexpected situation. ' 'Please report this port structure: {}'.format(vars(self)))
def it_condition_contain_something(_step_obj, something): prop_list = [] if _step_obj.context.type in ('resource', 'data'): for resource in _step_obj.context.stash: if type(resource) is not dict: resource = { 'values': resource, 'address': resource, 'type': _step_obj.context.name } values = resource.get('values', resource.get('expressions', {})) found_value = Null found_key = Null if type(values) is dict: found_key = values.get(something, seek_key_in_dict(values, something)) if type(found_key) is not list: found_key = [{something: found_key}] if len(found_key): found_key = found_key[0] if type(found_key) is dict: found_value = jsonify(found_key.get(something, {})) elif type(values) is list: for value in values: if type(value) is dict: # First search in the keys found_key = seek_key_in_dict(value, something) # Then search in the values with 'key' if not found_key: found_key = seek_regex_key_in_dict_values( value, 'key', something) if found_key: found_key = found_key[0] found_value = value.get('value') break if found_key is not Null and len(found_key): found_key = found_key[0] if type(found_key) is dict: found_value = jsonify(found_key.get(something, {})) else: raise TerraformComplianceInternalFailure( 'Unexpected value type {}. {}'.format( type(value), value)) if type(found_value) is dict and 'constant_value' in found_value: found_value = found_value['constant_value'] if found_key is not Null and found_key != []: prop_list.append({ 'address': resource['address'], 'values': found_value, 'type': _step_obj.context.name }) elif 'must' in _step_obj.context_sensitive_sentence: raise Failure('{} ({}) does not have {} property.'.format( resource['address'], resource.get('type', ''), something)) if prop_list: _step_obj.context.stash = prop_list _step_obj.context.property_name = something return True skip_step(_step_obj, resource=_step_obj.context.name, message='Can not find any {} property for {} resource in ' 'terraform plan.'.format(something, _step_obj.context.name)) elif _step_obj.context.type == 'provider': values = seek_key_in_dict(_step_obj.context.stash, something) if values: _step_obj.context.stash = values _step_obj.context.property_name = something return True skip_step( _step_obj, resource=_step_obj.context.name, message='Skipping the step since {} type does not have {} property.'. format(_step_obj.context.type, something))
def __call__(self, parser, namespace, values, option_string=None): # Check if the given path is a file if not os.path.isfile(values): print('ERROR: {} is not a file.'.format(values)) sys.exit(1) # Check if the given file is a native terraform plan file given_file = filetype.guess(values) if given_file is not None: terraform_executable = getattr(namespace, 'terraform_file', None) values = convert_terraform_plan_to_json(os.path.abspath(values), terraform_executable) # Check if the given file is a json file. try: plan_lines = [line for line in open(values, 'r', encoding='utf-8')] # Some Github Actions (hashicorp/setup-terraform) has internal wrappers which is # breaking the json file that is read by the terraform-compliance file_change_required = False if len(plan_lines) > 1: plan_lines = plan_lines[1] file_change_required = True else: plan_lines = plan_lines[0] data = json.loads(plan_lines) # Write the changed plan file to the same file, since it is used in other places. if file_change_required: with open(values, 'w', encoding='utf-8') as plan_file: plan_file.write(plan_lines) except json.JSONDecodeError: print('ERROR: {} is not a valid JSON file'.format(values)) sys.exit(1) except UnicodeDecodeError: print('ERROR: {} is not a valid JSON file.'.format(values)) print( ' Did you try to convert the binary plan file to json with ' '"terraform show -json {} > {}.json" ?'.format(values, values)) sys.exit(1) except: raise TerraformComplianceInternalFailure('Invalid file type.') # Check if this is a correct terraform plan file try: assert data['format_version'] assert data['terraform_version'] # Check if this is a state file if 'values' in data: assert data['values']['root_module'].get( 'resources', data['values']['root_module'].get('child_modules')) # Then it must be a terraform plan file else: assert data['planned_values'] assert data['configuration'] except KeyError: print( 'ERROR: {} is not a valid terraform plan json output.'.format( values)) sys.exit(1) setattr(namespace, self.dest, os.path.abspath(values)) return True