class ScanJobSerializer(NotEmptySerializer): """Serializer for the ScanJob model.""" scan = ScanField(required=True, many=False, queryset=Scan.objects.all()) sources = SourceField(many=True, read_only=True) scan_type = ValidStringChoiceField(read_only=True, choices=ScanTask.SCAN_TYPE_CHOICES) status = ValidStringChoiceField(read_only=True, choices=ScanTask.STATUS_CHOICES) status_message = CharField(read_only=True) tasks = TaskField(many=True, read_only=True) options = ScanOptionsSerializer(read_only=True, many=False) report_id = IntegerField(read_only=True) start_time = DateTimeField(required=False, read_only=True) end_time = DateTimeField(required=False, read_only=True) class Meta: """Metadata for serializer.""" model = ScanJob fields = [ 'id', 'scan', 'sources', 'scan_type', 'status', 'status_message', 'tasks', 'options', 'report_id', 'start_time', 'end_time' ] @staticmethod def validate_sources(sources): """Make sure the source is present.""" if not sources: raise ValidationError(_(messages.SJ_REQ_SOURCES)) return sources
class ScanJobSerializer(NotEmptySerializer): """Serializer for the ScanJob model.""" sources = SourceField(many=True, queryset=Source.objects.all()) scan_type = ValidStringChoiceField(required=False, choices=ScanTask.SCAN_TYPE_CHOICES) status = ValidStringChoiceField(required=False, read_only=True, choices=ScanTask.STATUS_CHOICES) status_message = CharField(required=False, read_only=True, max_length=256) tasks = TaskField(many=True, read_only=True) options = ScanOptionsSerializer(required=False, many=False) fact_collection_id = IntegerField(read_only=True) start_time = DateTimeField(required=False, read_only=True) end_time = DateTimeField(required=False, read_only=True) class Meta: """Metadata for serializer.""" model = ScanJob fields = ['id', 'sources', 'scan_type', 'status', 'status_message', 'tasks', 'options', 'fact_collection_id', 'start_time', 'end_time'] @transaction.atomic def create(self, validated_data): """Create a scan job.""" options = validated_data.pop('options', None) scanjob = super().create(validated_data) if options: options = ScanOptions.objects.create(**options) else: options = ScanOptions() options.save() scanjob.options = options scanjob.save() return scanjob @staticmethod def validate_sources(sources): """Make sure the source is present.""" if not sources: raise ValidationError(_(messages.SJ_REQ_SOURCES)) return sources
class SourceOptionsSerializer(NotEmptySerializer): """Serializer for the ScanOptions model.""" satellite_version = ValidStringChoiceField( required=False, choices=SourceOptions.SATELLITE_VERSION_CHOICES) ssl_protocol = ValidStringChoiceField( required=False, choices=SourceOptions.SSL_PROTOCOL_CHOICES) ssl_cert_verify = NullBooleanField(required=False) disable_ssl = NullBooleanField(required=False) class Meta: """Metadata for serializer.""" model = SourceOptions fields = [ 'satellite_version', 'ssl_protocol', 'ssl_cert_verify', 'disable_ssl' ]
class SourceOptionsSerializer(NotEmptySerializer): """Serializer for the SourceOptions model.""" ssl_protocol = ValidStringChoiceField( required=False, choices=SourceOptions.SSL_PROTOCOL_CHOICES) ssl_cert_verify = NullBooleanField(required=False) disable_ssl = NullBooleanField(required=False) use_paramiko = NullBooleanField(required=False) class Meta: """Metadata for serializer.""" model = SourceOptions fields = [ 'ssl_protocol', 'ssl_cert_verify', 'disable_ssl', 'use_paramiko' ]
class ScanSerializer(NotEmptySerializer): """Serializer for the Scan model.""" name = CharField(required=True, read_only=False, max_length=64) sources = SourceField(many=True, queryset=Source.objects.all()) scan_type = ValidStringChoiceField(required=False, choices=ScanTask.SCAN_TYPE_CHOICES) options = ScanOptionsSerializer(required=False, many=False) jobs = JobField(required=False, many=True, read_only=True) class Meta: """Metadata for serializer.""" model = Scan fields = [ 'id', 'name', 'sources', 'scan_type', 'options', 'jobs', 'most_recent_scanjob' ] read_only_fields = ['most_recent_scanjob'] @transaction.atomic def create(self, validated_data): """Create a scan.""" name = validated_data.get('name') check_for_existing_name(Scan.objects, name, _(messages.SCAN_NAME_ALREADY_EXISTS % name)) options = validated_data.pop('options', None) scan = super().create(validated_data) if options: optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop('enabled_extended_product_search', None) options = ScanOptions.objects.create(**options) if optional_products: optional_products = \ DisabledOptionalProductsOptions.objects.create( **optional_products) optional_products.save() options.disabled_optional_products = optional_products if extended_search: extended_search = \ ExtendedProductSearchOptions.objects.create( **extended_search) extended_search.save() options.enabled_extended_product_search = extended_search options.save() scan.options = options scan.save() return scan # pylint: disable=too-many-locals @transaction.atomic def update(self, instance, validated_data): """Update a scan.""" # If we ever add optional fields to Scan, we need to # add logic here to clear them on full update even if they are # not supplied. # pylint: disable=too-many-statements,too-many-branches name = validated_data.get('name') check_for_existing_name(Scan.objects, name, _(messages.HC_NAME_ALREADY_EXISTS % name), search_id=instance.id) name = validated_data.pop('name', None) scan_type = validated_data.pop('scan_type', None) sources = validated_data.pop('sources', None) old_options = instance.options options = validated_data.pop('options', None) if not self.partial: instance.name = name instance.scan_type = scan_type instance.sources = sources if options: optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop( 'enabled_extended_product_search', None) options = ScanOptions.objects.create(**options) if optional_products: optional_products = \ DisabledOptionalProductsOptions.objects.create( **optional_products) optional_products.save() options.disabled_optional_products = optional_products if extended_search: extended_search = \ ExtendedProductSearchOptions.objects.create( **extended_search) extended_search.save() options.enabled_extended_product_search = extended_search options.save() instance.options = options else: if name is not None: instance.name = name if scan_type is not None: instance.scan_type = scan_type if sources is not None: instance.sources = sources if options is not None: # grab the old options old_optional_products = old_options.disabled_optional_products old_extended_search = \ old_options.enabled_extended_product_search # set the defaults real_extended_search = \ {ScanOptions.JBOSS_EAP: ExtendedProductSearchOptions.EXT_JBOSS_EAP, ScanOptions.JBOSS_BRMS: ExtendedProductSearchOptions.EXT_JBOSS_BRMS, ScanOptions.JBOSS_FUSE: ExtendedProductSearchOptions.EXT_JBOSS_FUSE, ScanOptions.JBOSS_WS: ExtendedProductSearchOptions.EXT_JBOSS_WS} real_optional_products = \ {ScanOptions.JBOSS_EAP: DisabledOptionalProductsOptions.MODEL_OPT_JBOSS_EAP, ScanOptions.JBOSS_BRMS: DisabledOptionalProductsOptions.MODEL_OPT_JBOSS_BRMS, ScanOptions.JBOSS_FUSE: DisabledOptionalProductsOptions.MODEL_OPT_JBOSS_FUSE, ScanOptions.JBOSS_WS: DisabledOptionalProductsOptions.MODEL_OPT_JBOSS_WS} # update defaults with old options if they exist if old_extended_search: real_extended_search[ScanOptions.JBOSS_EAP] = \ old_extended_search.jboss_eap real_extended_search[ScanOptions.JBOSS_BRMS] = \ old_extended_search.jboss_brms real_extended_search[ScanOptions.JBOSS_FUSE] = \ old_extended_search.jboss_fuse real_extended_search[ScanOptions.JBOSS_WS] = \ old_extended_search.jboss_ws if old_extended_search.search_directories: real_extended_search['search_directories'] = \ old_extended_search.search_directories if old_optional_products: real_optional_products[ScanOptions.JBOSS_EAP] = \ old_optional_products.jboss_eap real_optional_products[ScanOptions.JBOSS_BRMS] = \ old_optional_products.jboss_brms real_optional_products[ScanOptions.JBOSS_FUSE] = \ old_optional_products.jboss_fuse real_optional_products[ScanOptions.JBOSS_WS] = \ old_optional_products.jboss_ws # grab the new options optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop( 'enabled_extended_product_search', None) if extended_search: # Grab the new extended search options jboss_eap_ext = \ extended_search.pop(ScanOptions.JBOSS_EAP, None) jboss_fuse_ext = \ extended_search.pop(ScanOptions.JBOSS_FUSE, None) jboss_brms_ext = \ extended_search.pop(ScanOptions.JBOSS_BRMS, None) jboss_ws_ext = \ extended_search.pop(ScanOptions.JBOSS_WS, None) search_directories = extended_search.pop( 'search_directories', None) # for each extended search option, set if provided # else retain the old option if jboss_eap_ext is not None: real_extended_search[ScanOptions.JBOSS_EAP] = \ jboss_eap_ext if jboss_brms_ext is not None: real_extended_search[ScanOptions.JBOSS_BRMS] = \ jboss_brms_ext if jboss_fuse_ext is not None: real_extended_search[ScanOptions.JBOSS_FUSE] = \ jboss_fuse_ext if jboss_ws_ext is not None: real_extended_search[ScanOptions.JBOSS_WS] = \ jboss_ws_ext if search_directories is not None: real_extended_search['search_directories'] = \ search_directories extended_search = \ ExtendedProductSearchOptions.objects.create( **real_extended_search) extended_search.save() else: extended_search = old_extended_search if optional_products: jboss_eap = \ optional_products.pop(ScanOptions.JBOSS_EAP, None) jboss_fuse = \ optional_products.pop(ScanOptions.JBOSS_FUSE, None) jboss_brms = \ optional_products.pop(ScanOptions.JBOSS_BRMS, None) jboss_ws = \ optional_products.pop(ScanOptions.JBOSS_WS, None) if jboss_eap is not None: real_optional_products[ScanOptions.JBOSS_EAP] = \ jboss_eap if jboss_brms is not None: real_optional_products[ScanOptions.JBOSS_BRMS] = \ jboss_brms if jboss_fuse is not None: real_optional_products[ScanOptions.JBOSS_FUSE] = \ jboss_fuse if jboss_ws is not None: real_optional_products[ScanOptions.JBOSS_WS] = \ jboss_ws optional_products = \ DisabledOptionalProductsOptions.objects.create( **real_optional_products) optional_products.save() else: optional_products = old_optional_products # create Scan Options for the instance instance.options = ScanOptions.objects.create(**options) # set the disabled products instance.options.disabled_optional_products = \ optional_products # set the enabled products instance.options.enabled_extended_product_search = \ extended_search instance.options.save() instance.save() return instance @staticmethod def validate_sources(sources): """Make sure the source is present.""" if not sources: raise ValidationError(_(messages.SJ_REQ_SOURCES)) return sources
class SourceSerializer(NotEmptySerializer): """Serializer for the Source model.""" name = CharField(required=True, max_length=64) source_type = ValidStringChoiceField(required=False, choices=Source.SOURCE_TYPE_CHOICES) port = IntegerField(required=False, min_value=0, allow_null=True) hosts = CustomJSONField(required=True) exclude_hosts = CustomJSONField(required=False) options = SourceOptionsSerializer(required=False, many=False) credentials = CredentialsField(many=True, queryset=Credential.objects.all()) class Meta: """Metadata for the serializer.""" model = Source fields = '__all__' @classmethod def validate_opts(cls, options, source_type): """Raise an error if options are invalid for the source type. :param options: dictionary of source options :param source_type: string denoting source type """ valid_net_options = ['use_paramiko'] valid_vc_options = ['ssl_cert_verify', 'ssl_protocol', 'disable_ssl'] valid_sat_options = ['ssl_cert_verify', 'ssl_protocol', 'disable_ssl'] if source_type == Source.SATELLITE_SOURCE_TYPE: invalid_options = [ opt for opt in options if opt not in valid_sat_options ] if invalid_options: error = { 'options': [ _(messages.SAT_INVALID_OPTIONS % ', '.join(invalid_options)) ] } raise ValidationError(error) if options.get('ssl_cert_verify') is None: options['ssl_cert_verify'] = True elif source_type == Source.VCENTER_SOURCE_TYPE: invalid_options = [ opt for opt in options if opt not in valid_vc_options ] if invalid_options: error = { 'options': [ _(messages.VC_INVALID_OPTIONS % ', '.join(invalid_options)) ] } raise ValidationError(error) if options.get('ssl_cert_verify') is None: options['ssl_cert_verify'] = True elif source_type == Source.NETWORK_SOURCE_TYPE: invalid_options = [ opt for opt in options if opt not in valid_net_options ] if invalid_options: error = { 'options': [ _(messages.NET_SSL_OPTIONS_NOT_ALLOWED % ', '.join(invalid_options)) ] } raise ValidationError(error) # pylint: disable=too-many-branches,too-many-statements @transaction.atomic def create(self, validated_data): """Create a source.""" name = validated_data.get('name') check_for_existing_name(Source.objects, name, _(messages.SOURCE_NAME_ALREADY_EXISTS % name)) if 'source_type' not in validated_data: error = {'source_type': [_(messages.SOURCE_TYPE_REQ)]} raise ValidationError(error) source_type = validated_data.get('source_type') credentials = validated_data.pop('credentials') hosts_list = validated_data.pop('hosts', None) exclude_hosts_list = validated_data.pop('exclude_hosts', None) port = None if 'port' in validated_data: port = validated_data['port'] options = validated_data.pop('options', None) if source_type == Source.NETWORK_SOURCE_TYPE: if credentials: for cred in credentials: SourceSerializer.check_credential_type(source_type, cred) if port is None: validated_data['port'] = 22 elif source_type == Source.VCENTER_SOURCE_TYPE: if port is None: validated_data['port'] = 443 if hosts_list and len(hosts_list) != 1: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if hosts_list and '[' in hosts_list[0]: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if exclude_hosts_list is not None: error = { 'exclude_hosts': [_(messages.VC_EXCLUDE_HOSTS_INCLUDED)] } raise ValidationError(error) if credentials and len(credentials) > 1: error = {'credentials': [_(messages.VC_ONE_CRED)]} raise ValidationError(error) if credentials and len(credentials) == 1: SourceSerializer.check_credential_type(source_type, credentials[0]) elif source_type == Source.SATELLITE_SOURCE_TYPE: if port is None: validated_data['port'] = 443 if hosts_list and len(hosts_list) != 1: error = {'hosts': [_(messages.SAT_ONE_HOST)]} raise ValidationError(error) if hosts_list and '[' in hosts_list[0]: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if exclude_hosts_list is not None: error = { 'exclude_hosts': [_(messages.SAT_EXCLUDE_HOSTS_INCLUDED)] } raise ValidationError(error) if credentials and len(credentials) > 1: error = {'credentials': [_(messages.SAT_ONE_CRED)]} raise ValidationError(error) if credentials and len(credentials) == 1: SourceSerializer.check_credential_type(source_type, credentials[0]) source = Source.objects.create(**validated_data) if options: SourceSerializer.validate_opts(options, source_type) options = SourceOptions.objects.create(**options) options.save() source.options = options elif not options and source_type == Source.SATELLITE_SOURCE_TYPE: options = SourceOptions() options.ssl_cert_verify = True options.save() source.options = options elif not options and source_type == Source.VCENTER_SOURCE_TYPE: options = SourceOptions() options.ssl_cert_verify = True options.save() source.options = options source.hosts = json.dumps(hosts_list) if exclude_hosts_list: source.exclude_hosts = json.dumps(exclude_hosts_list) for credential in credentials: source.credentials.add(credential) source.save() return source @transaction.atomic def update(self, instance, validated_data): """Update a source.""" # If we ever add optional fields to Source, we need to # add logic here to clear them on full update even if they are # not supplied. name = validated_data.get('name') check_for_existing_name(Source.objects, name, _(messages.SOURCE_NAME_ALREADY_EXISTS % name), search_id=instance.id) if 'source_type' in validated_data: error = {'source_type': [_(messages.SOURCE_TYPE_INV)]} raise ValidationError(error) source_type = instance.source_type credentials = validated_data.pop('credentials', None) hosts_list = validated_data.pop('hosts', None) exclude_hosts_list = validated_data.pop('exclude_hosts', None) options = validated_data.pop('options', None) if source_type == Source.NETWORK_SOURCE_TYPE: if credentials: for cred in credentials: SourceSerializer.check_credential_type(source_type, cred) elif source_type == Source.VCENTER_SOURCE_TYPE: if hosts_list and len(hosts_list) != 1: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if hosts_list and '[' in hosts_list[0]: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if exclude_hosts_list is not None: error = { 'exclude_hosts': [_(messages.VC_EXCLUDE_HOSTS_INCLUDED)] } raise ValidationError(error) if credentials and len(credentials) > 1: error = {'credentials': [_(messages.VC_ONE_CRED)]} raise ValidationError(error) if credentials and len(credentials) == 1: SourceSerializer.check_credential_type(source_type, credentials[0]) elif source_type == Source.SATELLITE_SOURCE_TYPE: if hosts_list and len(hosts_list) != 1: error = {'hosts': [_(messages.SAT_ONE_HOST)]} raise ValidationError(error) if hosts_list and '[' in hosts_list[0]: error = {'hosts': [_(messages.VC_ONE_HOST)]} raise ValidationError(error) if exclude_hosts_list is not None: error = { 'exclude_hosts': [_(messages.SAT_EXCLUDE_HOSTS_INCLUDED)] } raise ValidationError(error) if credentials and len(credentials) > 1: error = {'credentials': [_(messages.SAT_ONE_CRED)]} raise ValidationError(error) if credentials and len(credentials) == 1: SourceSerializer.check_credential_type(source_type, credentials[0]) for name, value in validated_data.items(): setattr(instance, name, value) instance.save() # If hosts_list was not supplied and this is a full update, # then we should already have raised a ValidationError before # this point, so it's safe to use hosts_list as an indicator # of whether to replace the hosts. if hosts_list: instance.hosts = json.dumps(hosts_list) if exclude_hosts_list: instance.exclude_hosts = json.dumps(exclude_hosts_list) # credentials is safe to use as a flag for the same reason as # hosts_data above. if credentials: instance.credentials.set(credentials) if options: SourceSerializer.validate_opts(options, source_type) if instance.options is None: options = SourceOptions.objects.create(**options) options.save() instance.options = options else: self.update_options(options, instance.options) instance.save() return instance @staticmethod def update_options(options, instance_options): """Update the incoming options overlapping the existing options. :param options: the passed in options :param instance_options: the existing options """ ssl_protocol = options.pop('ssl_protocol', None) ssl_cert_verify = options.pop('ssl_cert_verify', None) disable_ssl = options.pop('disable_ssl', None) use_paramiko = options.pop('use_paramiko', None) if ssl_protocol is not None: instance_options.ssl_protocol = ssl_protocol if ssl_cert_verify is not None: instance_options.ssl_cert_verify = ssl_cert_verify if disable_ssl is not None: instance_options.disable_ssl = disable_ssl if use_paramiko is not None: instance_options.use_paramiko = use_paramiko instance_options.save() @staticmethod def check_credential_type(source_type, credential): """Look for existing credential with same type as the source. :param source_type: The source type :param credential: The credential to obtain """ if credential.cred_type != source_type: error = {'source_type': [_(messages.SOURCE_CRED_WRONG_TYPE)]} raise ValidationError(error) @staticmethod def validate_name(name): """Validate the name of the Source.""" if not isinstance(name, str) or not name.isprintable(): raise ValidationError(_(messages.SOURCE_NAME_VALIDATION)) return name # pylint: disable=too-many-locals, too-many-branches, too-many-statements @staticmethod def validate_ipaddr_list(hosts): """Make sure the hosts list is present and has valid IP addresses.""" ipaddr_list = json.loads(hosts) if isinstance(ipaddr_list, list): ipaddr_list = [item for item in ipaddr_list if item] if not isinstance(ipaddr_list, list): raise ValidationError(_(messages.SOURCE_HOST_MUST_BE_JSON_ARRAY)) if not ipaddr_list: raise ValidationError(_(messages.SOURCE_HOSTS_CANNOT_BE_EMPTY)) for host_value in ipaddr_list: if not isinstance(host_value, str): raise ValidationError( _(messages.SOURCE_HOST_MUST_BE_JSON_ARRAY)) # Regex for octet, CIDR bit range, and check # to see if it is like an IP/CIDR octet_regex = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])' bit_range = r'(3[0-2]|[1-2][0-9]|[0-9])' relaxed_ip_pattern = r'[0-9]*\.[0-9]*\.[0-9\[\]:]*\.[0-9\[\]:]*' relaxed_cidr_pattern = r'[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\/[0-9]*' greedy_subset = r'[0-9]*' range_subset = r'[0-9]*-[0-9]*' relaxed_invalid_ip_range = [ r'{0}\.{0}\.{0}\.{0}-{0}\.{0}\.{0}\.{0}'.format(greedy_subset), r'{0}\.{0}\.{0}\.{1}'.format(greedy_subset, range_subset), r'{0}\.{0}\.{1}\.{0}'.format(greedy_subset, range_subset), r'{0}\.{1}\.{0}\.{0}'.format(greedy_subset, range_subset), r'{1}\.{0}\.{0}\.{0}'.format(greedy_subset, range_subset), r'{1}\.{1}\.{0}\.{0}'.format(greedy_subset, range_subset), r'{1}\.{0}\.{1}\.{0}'.format(greedy_subset, range_subset), r'{1}\.{0}\.{0}\.{1}'.format(greedy_subset, range_subset), r'{1}\.{1}\.{0}\.{1}'.format( greedy_subset, range_subset), r'{1}\.{0}\.{1}\.{1}'.format( greedy_subset, range_subset), r'{0}\.{0}\.{0}\.{0}'.format(range_subset), r'{0}\.{1}\.{0}\.{1}'.format(greedy_subset, range_subset), r'{0}\.{1}\.{1}\.{0}'.format(greedy_subset, range_subset) ] # type IP: 192.168.0.1 # type CIDR: 192.168.0.0/16 # type RANGE 1: 192.168.0.[1:15] # type RANGE 2: 192.168.[2:18].1 # type RANGE 3: 192.168.[2:18].[4:46] ip_regex_list = [ r'^{0}\.{0}\.{0}\.{0}$'.format(octet_regex), r'^{0}\.{0}\.{0}\.{0}\/{1}$'.format(octet_regex, bit_range), r'^{0}\.{0}\.{0}\.\[{0}:{0}\]$'.format(octet_regex), r'^{0}\.{0}\.\[{0}:{0}\]\.{0}$'.format(octet_regex), r'^{0}\.{0}\.\[{0}:{0}\]\.\[{0}:{0}\]$'.format(octet_regex) ] # type HOST: abcd # type HOST NUMERIC RANGE: abcd[2:4].foo.com # type HOST ALPHA RANGE: abcd[a:f].foo.com host_regex_list = [ r'[a-zA-Z0-9-_\.]+', r'[a-zA-Z0-9-_\.]*\[[0-9]+:[0-9]+\]*[a-zA-Z0-9-_\.]*', r'[a-zA-Z0-9-_\.]*\[[a-zA-Z]{1}:[a-zA-Z]{1}\][a-zA-Z0-9-_\.]*' ] normalized_hosts = [] host_errors = [] for host_range in ipaddr_list: result = None ip_match = re.match(relaxed_ip_pattern, host_range) cidr_match = re.match(relaxed_cidr_pattern, host_range) invalid_ip_range_match = [ re.match(invalid_ip_range, host_range) for invalid_ip_range in relaxed_invalid_ip_range ] is_likely_ip = ip_match and ip_match.end() == len(host_range) is_likely_cidr = cidr_match and cidr_match.end() == len(host_range) is_likely_invalid_ip_range = any(invalid_ip_range_match) if is_likely_invalid_ip_range: err_message = _(messages.NET_INVALID_RANGE_FORMAT % (host_range, )) result = ValidationError(err_message) elif is_likely_ip or is_likely_cidr: # This is formatted like an IP or CIDR # (e.g. #.#.#.# or #.#.#.#/#) for reg in ip_regex_list: match = re.match(reg, host_range) if match and match.end() == len(host_range): result = host_range break if result is None or is_likely_cidr: # Attempt to convert CIDR to ansible range if is_likely_cidr: try: normalized_cidr = SourceSerializer \ .cidr_to_ansible(host_range) result = normalized_cidr except ValidationError as validate_error: result = validate_error else: err_message = _(messages.NET_INVALID_RANGE_CIDR % (host_range, )) result = ValidationError(err_message) else: # Possibly a host_range addr for reg in host_regex_list: match = re.match(reg, host_range) if match and match.end() == len(host_range): result = host_range break if result is None: err_message = _(messages.NET_INVALID_HOST % (host_range, )) result = ValidationError(err_message) if isinstance(result, ValidationError): host_errors.append(result) elif result is not None: normalized_hosts.append(result) else: # This is an unexpected case. Allow/log for analysis normalized_hosts.append(host_range) logging.warning('%s did not match a pattern or produce error', host_range) if not host_errors: return normalized_hosts error_message = [error.detail.pop() for error in host_errors] raise ValidationError(error_message) @staticmethod def validate_hosts(hosts): """Validate hosts list.""" return SourceSerializer.validate_ipaddr_list(hosts) @staticmethod def validate_exclude_hosts(exclude_hosts): """Validate exclude_hosts list.""" return SourceSerializer.validate_ipaddr_list(exclude_hosts) # pylint: disable=too-many-locals @staticmethod def cidr_to_ansible(ip_range): """Convert an IP address range from CIDR to Ansible notation. :param ip_range: the IP range, as a string :returns: the IP range, as an Ansible-formatted string :raises NotCIDRException: if ip_range doesn't look similar to CIDR notation. If it does look like CIDR but isn't quite right, print out error messages and exit. """ # In the case of an input error, we want to distinguish between # strings that are "CIDR-like", so the user probably intended to # use CIDR and we should give them a CIDR error message, and not # at all CIDR-like, in which case we tell the caller to parse it a # different way. cidr_like = r'[0-9\.]*/[0-9]+' if not re.match(cidr_like, ip_range): err_msg = _(messages.NET_NO_CIDR_MATCH % (ip_range, str(cidr_like))) raise ValidationError(err_msg) try: base_address, prefix_bits = ip_range.split('/') except ValueError: err_msg = _(messages.NET_CIDR_INVALID % (ip_range, )) raise ValidationError(err_msg) prefix_bits = int(prefix_bits) if prefix_bits < 0 or prefix_bits > 32: err_msg = _(messages.NET_CIDR_BIT_MASK % { 'ip_range': ip_range, 'prefix_bits': prefix_bits }) raise ValidationError(err_msg) octet_strings = base_address.split('.') if len(octet_strings) != 4: err_msg = _(messages.NET_FOUR_OCTETS % (ip_range, )) raise ValidationError(err_msg) octets = [None] * 4 for i in range(4): if not octet_strings[i]: err_msg = _(messages.NET_EMPTY_OCTET % (ip_range, )) raise ValidationError(err_msg) val = int(octet_strings[i]) if val < 0 or val > 255: # pylint: disable=too-many-locals err_msg = _(messages.NET_CIDR_RANGE % { 'ip_range': ip_range, 'octet': val }) raise ValidationError(err_msg) octets[i] = val ansible_out = [None] * 4 for i in range(4): # "prefix_bits" is the number of high-order bits we want to # keep for the whole CIDR range. "mask" is the number of # low-order bits we want to mask off. Here prefix_bits is for # the whole IP address, but mask_bits is just for this octet. if prefix_bits <= i * 8: ansible_out[i] = '[0:255]' elif prefix_bits >= (i + 1) * 8: ansible_out[i] = str(octets[i]) else: # The number of bits of this octet that we want to # preserve this_octet_bits = prefix_bits - 8 * i assert 0 < this_octet_bits < 8 # mask is this_octet_bits 1's followed by (8 - # this_octet_bits) 0's. mask = -1 << (8 - this_octet_bits) lower_bound = octets[i] & mask upper_bound = lower_bound + ~mask ansible_out[i] = '[{0}:{1}]'.format(lower_bound, upper_bound) return '.'.join(ansible_out) @staticmethod def validate_port(port): """Validate the port.""" if not port: pass elif port < 0 or port > 65536: raise ValidationError(_(messages.NET_INVALID_PORT)) return port @staticmethod def validate_credentials(credentials): """Make sure the credentials list is present.""" if not credentials: raise ValidationError(_(messages.SOURCE_MIN_CREDS)) return credentials
class CredentialSerializer(NotEmptySerializer): """Serializer for the Credential model.""" # pylint: disable= no-self-use name = CharField(required=True, max_length=64) cred_type = ValidStringChoiceField(required=False, choices=Credential.CRED_TYPE_CHOICES) username = CharField(required=True, max_length=64) password = CharField(required=False, max_length=1024, allow_null=True, style={'input_type': 'password'}) ssh_keyfile = CharField(required=False, max_length=1024, allow_null=True) ssh_passphrase = CharField(required=False, max_length=1024, allow_null=True, style={'input_type': 'password'}) become_method = ValidStringChoiceField( required=False, choices=Credential.BECOME_METHOD_CHOICES) become_user = CharField(required=False, max_length=64) become_password = CharField(required=False, max_length=1024, allow_null=True, style={'input_type': 'password'}) class Meta: """Metadata for the serializer.""" model = Credential fields = '__all__' def create(self, validated_data): """Create host credential.""" name = validated_data.get('name') check_for_existing_name(Credential.objects, name, _(messages.HC_NAME_ALREADY_EXISTS % name)) if 'cred_type' not in validated_data: error = {'cred_type': [_(messages.CRED_TYPE_REQUIRED_CREATED)]} raise ValidationError(error) cred_type = validated_data.get('cred_type') become_method = validated_data.get('become_method') become_user = validated_data.get('become_user') if cred_type == Credential.NETWORK_CRED_TYPE and not become_method: # Set the default become_method to be sudo if not specified validated_data['become_method'] = Credential.BECOME_SUDO if cred_type == Credential.NETWORK_CRED_TYPE and not become_user: # Set the default become_user to root if not specified validated_data['become_user'] = Credential.BECOME_USER_DEFAULT if cred_type == Credential.VCENTER_CRED_TYPE: validated_data = self.validate_vcenter_cred(validated_data) elif cred_type == Credential.SATELLITE_CRED_TYPE: validated_data = self.validate_satellite_cred(validated_data) else: validated_data = self.validate_host_cred(validated_data) return super().create(validated_data) def update(self, instance, validated_data): """Update a host credential.""" name = validated_data.get('name') check_for_existing_name(Credential.objects, name, _(messages.HC_NAME_ALREADY_EXISTS % name), search_id=instance.id) if 'cred_type' in validated_data: error = {'cred_type': [_(messages.CRED_TYPE_NOT_ALLOWED_UPDATE)]} raise ValidationError(error) cred_type = instance.cred_type if cred_type == Credential.VCENTER_CRED_TYPE: validated_data = self.validate_vcenter_cred(validated_data) elif cred_type == Credential.SATELLITE_CRED_TYPE: validated_data = self.validate_satellite_cred(validated_data) else: validated_data = self.validate_host_cred(validated_data) return super().update(instance, validated_data) def validate_host_cred(self, attrs): """Validate the attributes for host creds.""" ssh_keyfile = 'ssh_keyfile' in attrs and attrs['ssh_keyfile'] password = '******' in attrs and attrs['password'] ssh_passphrase = 'ssh_passphrase' in attrs and attrs['ssh_passphrase'] if not (password or ssh_keyfile) and not self.partial: error = {'non_field_errors': [_(messages.HC_PWD_OR_KEYFILE)]} raise ValidationError(error) if password and ssh_keyfile: error = {'non_field_errors': [_(messages.HC_NOT_BOTH)]} raise ValidationError(error) if ssh_keyfile: keyfile = expand_filepath(ssh_keyfile) if not os.path.isfile(keyfile): error = { 'ssh_keyfile': [_(messages.HC_KEY_INVALID % (ssh_keyfile))] } raise ValidationError(error) attrs['ssh_keyfile'] = keyfile if ssh_passphrase and not ssh_keyfile and not self.partial: error = {'ssh_passphrase': [_(messages.HC_NO_KEY_W_PASS)]} raise ValidationError(error) return attrs def validate_vcenter_cred(self, attrs): """Validate the attributes for vcenter creds.""" # Required fields for vcenter if not self.partial: username = '******' in attrs and attrs['username'] password = '******' in attrs and attrs['password'] if not (password and username): error = {'non_field_errors': [_(messages.VC_PWD_AND_USERNAME)]} raise ValidationError(error) # Not allowed fields for vcenter ssh_keyfile = 'ssh_keyfile' in attrs and attrs['ssh_keyfile'] ssh_passphrase = 'ssh_passphrase' in attrs\ and attrs['ssh_passphrase'] become_password = '******' in attrs \ and attrs['become_password'] become_user = '******' in attrs and attrs['become_user'] become_method = 'become_method' in attrs \ and attrs['become_method'] if ssh_keyfile or ssh_passphrase or become_password or \ become_user or become_method: error = {'non_field_errors': [_(messages.VC_FIELDS_NOT_ALLOWED)]} raise ValidationError(error) return attrs def validate_satellite_cred(self, attrs): """Validate the attributes for satellite creds.""" # Required fields for satellite if not self.partial: username = '******' in attrs and attrs['username'] password = '******' in attrs and attrs['password'] if not (password and username): error = { 'non_field_errors': [_(messages.SAT_PWD_AND_USERNAME)] } raise ValidationError(error) # Not allowed fields for satellite ssh_keyfile = 'ssh_keyfile' in attrs and attrs['ssh_keyfile'] ssh_passphrase = 'ssh_passphrase' in attrs \ and attrs['ssh_passphrase'] become_password = '******' in attrs \ and attrs['become_password'] become_user = '******' in attrs and attrs['become_user'] become_method = 'become_method' in attrs \ and attrs['become_method'] if ssh_keyfile or ssh_passphrase or become_password or \ become_user or become_method: error = {'non_field_errors': [_(messages.SAT_FIELDS_NOT_ALLOWED)]} raise ValidationError(error) return attrs
class ScanSerializer(NotEmptySerializer): """Serializer for the Scan model.""" name = CharField(required=True, read_only=False, max_length=64) sources = SourceField(many=True, queryset=Source.objects.all()) scan_type = ValidStringChoiceField(required=False, choices=Scan.SCAN_TYPE_CHOICES) options = ScanOptionsSerializer(required=False, many=False) jobs = JobField(required=False, many=True, read_only=True) class Meta: """Metadata for serializer.""" model = Scan fields = [ 'id', 'name', 'sources', 'scan_type', 'options', 'jobs', 'most_recent_scanjob' ] read_only_fields = ['most_recent_scanjob'] @transaction.atomic def create(self, validated_data): """Create a scan.""" name = validated_data.get('name') check_for_existing_name(Scan.objects, name, _(messages.SCAN_NAME_ALREADY_EXISTS % name)) options = validated_data.pop('options', None) scan = super().create(validated_data) if options: optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop('enabled_extended_product_search', None) options = ScanOptions.objects.create(**options) if optional_products: optional_products = \ DisabledOptionalProductsOptions.objects.create( **optional_products) optional_products.save() options.disabled_optional_products = optional_products if extended_search: extended_search = \ ExtendedProductSearchOptions.objects.create( **extended_search) extended_search.save() options.enabled_extended_product_search = extended_search options.save() scan.options = options scan.save() return scan @transaction.atomic def update(self, instance, validated_data): """Update a scan.""" # If we ever add optional fields to Scan, we need to # add logic here to clear them on full update even if they are # not supplied. name = validated_data.get('name') check_for_existing_name(Scan.objects, name, _(messages.HC_NAME_ALREADY_EXISTS % name), search_id=instance.id) if not self.partial: return self._do_full_update(instance, validated_data) return self._do_partial_update(instance, validated_data) @staticmethod def _do_full_update(instance, validated_data): """Peform full update of scan.""" name = validated_data.pop('name', None) scan_type = validated_data.pop('scan_type', None) sources = validated_data.pop('sources', None) options = validated_data.pop('options', None) instance.name = name instance.scan_type = scan_type # clear all the sources and re-add them instance.sources.clear() for source in sources: instance.sources.add(source) instance.save() if options: optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop('enabled_extended_product_search', None) options = ScanOptions.objects.create(**options) if optional_products: optional_products = \ DisabledOptionalProductsOptions.objects.create( **optional_products) optional_products.save() options.disabled_optional_products = optional_products if extended_search: extended_search = \ ExtendedProductSearchOptions.objects.create( **extended_search) extended_search.save() options.enabled_extended_product_search = extended_search options.save() instance.options = options instance.save() return instance @staticmethod def _do_partial_update(instance, validated_data): """Peform partial update of a scan.""" # pylint: disable=too-many-branches,too-many-statements name = validated_data.pop('name', None) scan_type = validated_data.pop('scan_type', None) sources = validated_data.pop('sources', None) # Update values that are not options if name is not None: instance.name = name if scan_type is not None: instance.scan_type = scan_type if sources is not None: instance.sources.set(sources) options = validated_data.pop('options', None) if not options: instance.save() return instance # grab the new options optional_products = options.pop('disabled_optional_products', None) extended_search = options.pop('enabled_extended_product_search', None) # Update base options if options: options_instance = instance.options if not options_instance: options_instance = ScanOptions.objects.create(**options) options_instance.save() instance.options = options_instance instance.save() else: max_concurrency = options.pop('max_concurrency', None) if max_concurrency is not None: options_instance.max_concurrency = max_concurrency options_instance.save() if not optional_products and not extended_search: instance.save() return instance # Update disable optional products if optional_products: optional_products_instance = \ instance.options.disabled_optional_products if not optional_products_instance: # Need to create a new one optional_products_instance = \ DisabledOptionalProductsOptions.objects.create( **optional_products) optional_products_instance.save() instance.options.disabled_optional_products = \ optional_products_instance instance.options.save() else: # Existing values so update if optional_products.get(ScanOptions.JBOSS_EAP, None) is not None: optional_products_instance.jboss_eap = \ optional_products.get( ScanOptions.JBOSS_EAP, None) if optional_products.get(ScanOptions.JBOSS_FUSE, None) is not None: optional_products_instance.jboss_fuse = \ optional_products.get( ScanOptions.JBOSS_FUSE, None) if optional_products.get(ScanOptions.JBOSS_BRMS, None) is not None: optional_products_instance.jboss_brms = \ optional_products.get( ScanOptions.JBOSS_BRMS, None) if optional_products.get(ScanOptions.JBOSS_WS, None) is not None: optional_products_instance.jboss_ws = \ optional_products.get( ScanOptions.JBOSS_WS, None) optional_products_instance.save() # Update extended product search if extended_search: extended_search_instance = \ instance.options.enabled_extended_product_search if not extended_search_instance: # Create a new one extended_search_instance = \ ExtendedProductSearchOptions.objects.create( **extended_search) extended_search_instance.save() instance.options.enabled_extended_product_search = \ extended_search_instance instance.options.save() else: # Update existing instance # Grab the new extended search options if extended_search.get(ScanOptions.JBOSS_EAP, None) is not None: extended_search_instance.jboss_eap = extended_search.get( ScanOptions.JBOSS_EAP, None) if extended_search.get(ScanOptions.JBOSS_FUSE, None) is not None: extended_search_instance.jboss_fuse = extended_search.get( ScanOptions.JBOSS_FUSE, None) if extended_search.get(ScanOptions.JBOSS_BRMS, None) is not None: extended_search_instance.jboss_brms = extended_search.get( ScanOptions.JBOSS_BRMS, None) if extended_search.get(ScanOptions.JBOSS_WS, None) is not None: extended_search_instance.jboss_ws = extended_search.get( ScanOptions.JBOSS_WS, None) if extended_search.get('search_directories', None) is not None: extended_search_instance.search_directories = \ extended_search.get( 'search_directories', None) extended_search_instance.save() instance.save() return instance @staticmethod def validate_sources(sources): """Make sure the source is present.""" if not sources: raise ValidationError(_(messages.SJ_REQ_SOURCES)) return sources