Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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'
        ]
Exemple #5
0
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
Exemple #8
0
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