diff --git a/lib/ansible/modules/network/f5/bigip_firewall_dos_vector.py b/lib/ansible/modules/network/f5/bigip_firewall_dos_vector.py new file mode 100644 index 00000000000..26920d794fa --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_firewall_dos_vector.py @@ -0,0 +1,1303 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified'} + +DOCUMENTATION = r''' +--- +module: bigip_firewall_dos_vector +short_description: Manage attack vector configuration in an AFM DoS profile +description: + - Manage attack vector configuration in an AFM DoS profile. In addition to the normal + AFM DoS profile vectors, this module can manage the device-configuration vectors. + See the module documentation for details about this method. +version_added: 2.8 +options: + name: + description: + - Specifies the name of the vector to modify. + - Vectors that ship with the device are "hard-coded" so-to-speak in that the list + of vectors is known to the system and users cannot add new vectors. Users only + manipulate the existing vectors; all of which are disabled by default. + - When C(ext-hdr-too-large), configures the "IPv6 extension header too large" + Network Security vector. + - When C(hop-cnt-low), configures the "IPv6 hop count <= " Network + Security vector. + - When C(host-unreachable), configures the "Host Unreachable" Network Security + vector. + - When C(icmp-frag), configures the "ICMP Fragment" Network Security vector. + - When C(icmpv4-flood), configures the "ICMPv4 flood" Network Security vector. + - When C(icmpv6-flood), configures the "ICMPv6 flood" Network Security vector. + - When C(ip-frag-flood), configures the "IP Fragment Flood" Network Security vector. + - When C(ip-low-ttl), configures the "TTL <= " Network Security vector. + - When C(ip-opt-frames), configures the "IP Option Frames" Network Security vector. + - When C(ipv6-ext-hdr-frames), configures the "IPv6 Extended Header Frames" + Network Security vector. + - When C(ipv6-frag-flood), configures the "IPv6 Fragment Flood" Network Security + vector. + - When C(opt-present-with-illegal-len), configures the "Option Present With Illegal + Length" Network Security vector. + - When C(sweep), configures the "Sweep" Network Security vector. + - When C(tcp-bad-urg), configures the "TCP Flags-Bad URG" Network Security vector. + - When C(tcp-half-open), configures the "TCP Half Open" Network Security vector. + - When C(tcp-opt-overruns-tcp-hdr), configures the "TCP Option Overruns TCP Header" + Network Security vector. + - When C(tcp-psh-flood), configures the "TCP PUSH Flood" Network Security vector. + - When C(tcp-rst-flood), configures the "TCP RST Flood" Network Security vector. + - When C(tcp-syn-flood), configures the "TCP SYN Flood" Network Security vector. + - When C(tcp-syn-oversize), configures the "TCP SYN Oversize" Network Security + vector. + - When C(tcp-synack-flood), configures the "TCP SYN ACK Flood" Network Security + vector. + - When C(tcp-window-size), configures the "TCP Window Size" Network Security + vector. + - When C(tidcmp), configures the "TIDCMP" Network Security vector. + - When C(too-many-ext-hdrs), configures the "Too Many Extension Headers" Network + Security vector. + - When C(udp-flood), configures the "UDP Flood" Network Security vector. + - When C(unk-tcp-opt-type), configures the "Unknown TCP Option Type" Network + Security vector. + - When C(a), configures the "DNS A Query" DNS Protocol Security vector. + - When C(aaaa), configures the "DNS AAAA Query" DNS Protocol Security vector. + - When C(any), configures the "DNS ANY Query" DNS Protocol Security vector. + - When C(axfr), configures the "DNS AXFR Query" DNS Protocol Security vector. + - When C(cname), configures the "DNS CNAME Query" DNS Protocol Security vector. + - When C(dns-malformed), configures the "dns-malformed" DNS Protocol Security vector. + - When C(ixfr), configures the "DNS IXFR Query" DNS Protocol Security vector. + - When C(mx), configures the "DNS MX Query" DNS Protocol Security vector. + - When C(ns), configures the "DNS NS Query" DNS Protocol Security vector. + - When C(other), configures the "DNS OTHER Query" DNS Protocol Security vector. + - When C(ptr), configures the "DNS PTR Query" DNS Protocol Security vector. + - When C(qdcount), configures the "DNS QDCOUNT Query" DNS Protocol Security vector. + - When C(soa), configures the "DNS SOA Query" DNS Protocol Security vector. + - When C(srv), configures the "DNS SRV Query" DNS Protocol Security vector. + - When C(txt), configures the "DNS TXT Query" DNS Protocol Security vector. + - When C(ack), configures the "SIP ACK Method" SIP Protocol Security vector. + - When C(bye), configures the "SIP BYE Method" SIP Protocol Security vector. + - When C(cancel), configures the "SIP CANCEL Method" SIP Protocol Security vector. + - When C(invite), configures the "SIP INVITE Method" SIP Protocol Security vector. + - When C(message), configures the "SIP MESSAGE Method" SIP Protocol Security vector. + - When C(notify), configures the "SIP NOTIFY Method" SIP Protocol Security vector. + - When C(options), configures the "SIP OPTIONS Method" SIP Protocol Security vector. + - When C(other), configures the "SIP OTHER Method" SIP Protocol Security vector. + - When C(prack), configures the "SIP PRACK Method" SIP Protocol Security vector. + - When C(publish), configures the "SIP PUBLISH Method" SIP Protocol Security vector. + - When C(register), configures the "SIP REGISTER Method" SIP Protocol Security vector. + - When C(sip-malformed), configures the "sip-malformed" SIP Protocol Security vector. + - When C(subscribe), configures the "SIP SUBSCRIBE Method" SIP Protocol Security vector. + - When C(uri-limit), configures the "uri-limit" SIP Protocol Security vector. + choices: + - ext-hdr-too-large + - hop-cnt-low + - host-unreachable + - icmp-frag + - icmpv4-flood + - icmpv6-flood + - ip-frag-flood + - ip-low-ttl + - ip-opt-frames + - ipv6-frag-flood + - opt-present-with-illegal-len + - sweep + - tcp-bad-urg + - tcp-half-open + - tcp-opt-overruns-tcp-hdr + - tcp-psh-flood + - tcp-rst-flood + - tcp-syn-flood + - tcp-syn-oversize + - tcp-synack-flood + - tcp-window-size + - tidcmp + - too-many-ext-hdrs + - udp-flood + - unk-tcp-opt-type + - a + - aaaa + - any + - axfr + - cname + - dns-malformed + - ixfr + - mx + - ns + - other + - ptr + - qdcount + - soa + - srv + - txt + - ack + - bye + - cancel + - invite + - message + - notify + - options + - other + - prack + - publish + - register + - sip-malformed + - subscribe + - uri-limit + profile: + description: + - Specifies the name of the profile to manage vectors in. + - The name C(device-config) is reserved for use by this module. + - Vectors can be managed in either DoS Profiles, or Device Configuration. By + specifying a profile of 'device-config', this module will specifically tailor + configuration of the provided vectors to the Device Configuration. + required: True + auto_blacklist: + description: + - Automatically blacklists detected bad actors. + - To enable this parameter, the C(bad_actor_detection) must also be enabled. + - This parameter is not supported by the C(dns-malformed) vector. + - This parameter is not supported by the C(qdcount) vector. + type: bool + bad_actor_detection: + description: + - Whether Bad Actor detection is enabled or disabled for a vector, if available. + - This parameter must be enabled to enable the C(auto_blacklist) parameter. + - This parameter is not supported by the C(dns-malformed) vector. + - This parameter is not supported by the C(qdcount) vector. + type: bool + attack_ceiling: + description: + - Specifies the absolute maximum allowable for packets of this type. + - This setting rate limits packets to the packets per second setting, when + specified. + - To set no hard limit and allow automatic thresholds to manage all rate limiting, + set this to C(infinite). + attack_floor: + description: + - Specifies packets per second to identify an attack. + - These settings provide an absolute minimum of packets to allow before the attack + is identified. + - As the automatic detection thresholds adjust to traffic and CPU usage on the + system over time, this attack floor becomes less relevant. + - This value may not exceed the value in C(attack_floor). + allow_advertisement: + description: + - Specifies that addresses that are identified for blacklisting are advertised to + BGP routers + type: bool + simulate_auto_threshold: + description: + - Specifies that results of the current automatic thresholds are logged, though + manual thresholds are enforced, and no action is taken on automatic thresholds. + - The C(sweep) vector does not support this parameter. + type: bool + blacklist_detection_seconds: + description: + - Detection, in seconds, before blacklisting occurs. + blacklist_duration: + description: + - Duration, in seconds, that the blacklist will last. + per_source_ip_detection_threshold: + description: + - Specifies the number of packets per second to identify an IP address as a bad + actor. + per_source_ip_mitigation_threshold: + description: + - Specifies the rate limit applied to a source IP that is identified as a bad + actor. + detection_threshold_percent: + description: + - Lists the threshold percent increase over time that the system must detect in + traffic in order to detect this attack. + - The C(tcp-half-open) vector does not support this parameter. + aliases: + - rate_increase + detection_threshold_eps: + description: + - Lists how many packets per second the system must discover in traffic in order + to detect this attack. + aliases: + - rate_threshold + mitigation_threshold_eps: + description: + - Specify the maximum number of this type of packet per second the system allows + for a vector. + - The system drops packets once the traffic level exceeds the rate limit. + aliases: + - rate_limit + threshold_mode: + description: + - The C(dns-malformed) vector does not support C(fully-automatic), or C(stress-based-mitigation) + for this parameter. + - The C(qdcount) vector does not support C(fully-automatic), or C(stress-based-mitigation) + for this parameter. + - The C(sip-malformed) vector does not support C(fully-automatic), or C(stress-based-mitigation) + for this parameter. + choices: + - manual + - stress-based-mitigation + - fully-automatic + state: + description: + - When C(state) is C(mitigate), ensures that the vector enforces limits and + thresholds. + - When C(state) is C(detect-only), ensures that the vector does not enforce limits + and thresholds (rate limiting, dopping, etc), but is still tracked in logs and statistics. + - When C(state) is C(disabled), ensures that the vector does not enforce limits + and thresholds, but is still tracked in logs and statistics. + - When C(state) is C(learn-only), ensures that the vector does not "detect" any attacks. + Only learning and stat collecting is performed. + choices: + - mitigate + - detect-only + - learn-only + - disabled + required: True + partition: + description: + - Device partition to manage resources on. + default: Common +extends_documentation_fragment: f5 +requirements: + - BIG-IP >= v13.0.0 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Enable DNS AAAA vector mitigation + bigip_firewall_dos_vector: + name: aaaa + state: mitigate + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +allow_advertisement: + description: The new Allow External Advertisement setting. + returned: changed + type: bool + sample: yes +auto_blacklist: + description: The new Auto Blacklist setting. + returned: changed + type: bool + sample: no +bad_actor_detection: + description: The new Bad Actor Detection setting. + returned: changed + type: bool + sample: no +blacklist_detection_seconds: + description: The new Sustained Attack Detection Time setting. + returned: changed + type: int + sample: 60 +blacklist_duration: + description: The new Category Duration Time setting. + returned: changed + type: int + sample: 14400 +attack_ceiling: + description: The new Attack Ceiling EPS setting. + returned: changed + type: string + sample: infinite +attack_floor: + description: The new Attack Floor EPS setting. + returned: changed + type: string + sample: infinite +blacklist_category: + description: The new Category Name setting. + returned: changed + type: string + sample: /Common/cloud_provider_networks +per_source_ip_detection_threshold: + description: The new Per Source IP Detection Threshold EPS setting. + returned: changed + type: string + sample: 23 +per_source_ip_mitigation_threshold: + description: The new Per Source IP Mitigation Threshold EPS setting. + returned: changed + type: string + sample: infinite +detection_threshold_percent: + description: The new Detection Threshold Percent setting. + returned: changed + type: string + sample: infinite +detection_threshold_eps: + description: The new Detection Threshold EPS setting. + returned: changed + type: string + sample: infinite +mitigation_threshold_eps: + description: The new Mitigation Threshold EPS setting. + returned: changed + type: string + sample: infinite +threshold_mode: + description: The new Mitigation Threshold EPS setting. + returned: changed + type: string + sample: infinite +simulate_auto_threshold: + description: The new Simulate Auto Threshold setting. + returned: changed + type: bool + sample: no +state: + description: The new state of the vector. + returned: changed + type: string + sample: mitigate +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.six import iteritems + +try: + from library.module_utils.network.f5.bigip import F5RestClient + from library.module_utils.network.f5.common import F5ModuleError + from library.module_utils.network.f5.common import AnsibleF5Parameters + from library.module_utils.network.f5.common import cleanup_tokens + from library.module_utils.network.f5.common import fq_name + from library.module_utils.network.f5.common import f5_argument_spec + from library.module_utils.network.f5.common import exit_json + from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.common import transform_name + from library.module_utils.network.f5.common import flatten_boolean +except ImportError: + from ansible.module_utils.network.f5.bigip import F5RestClient + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import AnsibleF5Parameters + from ansible.module_utils.network.f5.common import cleanup_tokens + from ansible.module_utils.network.f5.common import fq_name + from ansible.module_utils.network.f5.common import f5_argument_spec + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.common import transform_name + from ansible.module_utils.network.f5.common import flatten_boolean + + +NETWORK_SECURITY_VECTORS = [ + 'ext-hdr-too-large', # IPv6 extension header too large + 'hop-cnt-low', # IPv6 hop count <= + 'host-unreachable', # Host Unreachable + 'icmp-frag', # ICMP Fragment + 'icmpv4-flood', # ICMPv4 flood + 'icmpv6-flood', # ICMPv6 flood + 'ip-frag-flood', # IP Fragment Flood + 'ip-low-ttl', # TTL <= + 'ip-opt-frames', # IP Option Frames + 'ipv6-ext-hdr-frames', # IPv6 Extended Header Frames + 'ipv6-frag-flood', # IPv6 Fragment Flood + 'opt-present-with-illegal-len', # Option Present With Illegal Length + 'sweep', # Sweep + 'tcp-bad-urg', # TCP Flags-Bad URG + 'tcp-half-open', # TCP Half Open + 'tcp-opt-overruns-tcp-hdr', # TCP Option Overruns TCP Header + 'tcp-psh-flood', # TCP PUSH Flood + 'tcp-rst-flood', # TCP RST Flood + 'tcp-syn-flood', # TCP SYN Flood + 'tcp-syn-oversize', # TCP SYN Oversize + 'tcp-synack-flood', # TCP SYN ACK Flood + 'tcp-window-size', # TCP Window Size + 'tidcmp', # TIDCMP + 'too-many-ext-hdrs', # Too Many Extension Headers + 'udp-flood', # UDP Flood + 'unk-tcp-opt-type', # Unknown TCP Option Type +] + +PROTOCOL_SIP_VECTORS = [ + 'ack', # SIP ACK Method + 'bye', # SIP BYE Method + 'cancel', # SIP CANCEL Method + 'invite', # SIP INVITE Method + 'message', # SIP MESSAGE Method + 'notify', # SIP NOTIFY Method + 'options', # SIP OPTIONS Method + 'other', # SIP OTHER Method + 'prack', # SIP PRACK Method + 'publish', # SIP PUBLISH Method + 'register', # SIP REGISTER Method + 'sip-malformed', # sip-malformed + 'subscribe', # SIP SUBSCRIBE Method + 'uri-limit', # uri-limit +] + +PROTOCOL_DNS_VECTORS = [ + 'a', # DNS A Query + 'aaaa', # DNS AAAA Query + 'any', # DNS ANY Query + 'axfr', # DNS AXFR Query + 'cname', # DNS CNAME Query + 'dns-malformed', # dns-malformed + 'ixfr', # DNS IXFR Query + 'mx', # DNS MX Query + 'ns', # DNS NS Query + 'other', # DNS OTHER Query + 'ptr', # DNS PTR Query + 'qdcount', # DNS QDCOUNT LIMIT + 'soa', # DNS SOA Query + 'srv', # DNS SRV Query + 'txt', # DNS TXT Query +] + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'allowAdvertisement': 'allow_advertisement', + 'autoBlacklisting': 'auto_blacklist', + + # "autoThreshold": "disabled", + # This is a deprecated parameter in 13.1.0. Use threshold_mode instead + + 'badActor': 'bad_actor_detection', + 'blacklistCategory': 'blacklist_category', + 'blacklistDetectionSeconds': 'blacklist_detection_seconds', + 'blacklistDuration': 'blacklist_duration', + 'ceiling': 'attack_ceiling', + # "enforce": "enabled", + 'floor': 'attack_floor', + 'perSourceIpDetectionPps': 'per_source_ip_detection_threshold', + 'perSourceIpLimitPps': 'per_source_ip_mitigation_threshold', + 'rateIncrease': 'detection_threshold_percent', + 'rateLimit': 'mitigation_threshold_eps', + 'rateThreshold': 'detection_threshold_eps', + 'simulateAutoThreshold': 'simulate_auto_threshold', + 'thresholdMode': 'threshold_mode', + + # device-config specific settings + 'scrubbingDetectionSeconds': 'sustained_attack_detection_time', + 'scrubbingDuration': 'category_detection_time', + 'perDstIpDetectionPps': 'per_dest_ip_detection_threshold', + 'perDstIpLimitPps': 'per_dest_ip_mitigation_threshold', + + # The following are not enabled for device-config because I + # do not know what parameters in TMUI they map to. Additionally, + # they do not appear to have any "help" documentation available + # in ``tmsh help security dos device-config``. + # + # "allowUpstreamScrubbing": "disabled", + # "attackedDst": "disabled", + # "autoScrubbing": "disabled", + 'defaultInternalRateLimit': 'mitigation_threshold_eps', + 'detectionThresholdPercent': 'detection_threshold_percent', + 'detectionThresholdPps': 'detection_threshold_eps', + } + + api_attributes = [ + 'allowAdvertisement', + 'autoBlacklisting', + 'autoThreshold', + 'badActor', + 'blacklistCategory', + 'blacklistDetectionSeconds', + 'blacklistDuration', + 'ceiling', + 'enforce', + 'floor', + 'perSourceIpDetectionPps', + 'perSourceIpLimitPps', + 'rateIncrease', + 'rateLimit', + 'rateThreshold', + 'simulateAutoThreshold', + 'state', + 'thresholdMode', + + # device-config specific + 'scrubbingDetectionSeconds', + 'scrubbingDuration', + 'perDstIpDetectionPps', + 'perDstIpLimitPps', + 'defaultInternalRateLimit', + 'detectionThresholdPercent', + 'detectionThresholdPps', + + # Attributes on the DoS profiles that hold the different vectors + # + # Each of these attributes is a list of dictionaries. Each dictionary + # contains the settings that affect the way the vector works. + # + # The vectors appear to all have the same attributes even if those + # attributes are not used. There may be cases where this is not true, + # however, and for those vectors we should either include specific + # error detection, or pass the unfiltered values through to mcpd and + # handle any unintuitive error messages that mcpd returns. + 'dosDeviceVector', + 'dnsQueryVector', + 'networkAttackVector', + 'sipAttackVector', + ] + + returnables = [ + 'allow_advertisement', + 'auto_blacklist', + 'bad_actor_detection', + 'blacklist_detection_seconds', + 'blacklist_duration', + 'attack_ceiling', + 'attack_floor', + 'blacklist_category', + 'per_source_ip_detection_threshold', + 'per_source_ip_mitigation_threshold', + 'detection_threshold_percent', + 'detection_threshold_eps', + 'mitigation_threshold_eps', + 'threshold_mode', + 'simulate_auto_threshold', + 'state', + ] + + updatables = [ + 'allow_advertisement', + 'auto_blacklist', + 'bad_actor_detection', + 'blacklist_detection_seconds', + 'blacklist_duration', + 'attack_ceiling', + 'attack_floor', + 'blacklist_category', + 'per_source_ip_detection_threshold', + 'per_source_ip_mitigation_threshold', + 'detection_threshold_percent', + 'detection_threshold_eps', + 'mitigation_threshold_eps', + 'threshold_mode', + 'simulate_auto_threshold', + 'state', + ] + + @property + def allow_advertisement(self): + return flatten_boolean(self._values['allow_advertisement']) + + @property + def auto_blacklist(self): + return flatten_boolean(self._values['auto_blacklist']) + + @property + def simulate_auto_threshold(self): + return flatten_boolean(self._values['simulate_auto_threshold']) + + @property + def bad_actor_detection(self): + return flatten_boolean(self._values['bad_actor_detection']) + + @property + def detection_threshold_percent(self): + if self._values['detection_threshold_percent'] in [None, "infinite"]: + return self._values['detection_threshold_percent'] + return int(self._values['detection_threshold_percent']) + + @property + def per_source_ip_mitigation_threshold(self): + if self._values['per_source_ip_mitigation_threshold'] in [None, "infinite"]: + return self._values['per_source_ip_mitigation_threshold'] + return int(self._values['per_source_ip_mitigation_threshold']) + + @property + def per_dest_ip_mitigation_threshold(self): + if self._values['per_dest_ip_mitigation_threshold'] in [None, "infinite"]: + return self._values['per_dest_ip_mitigation_threshold'] + return int(self._values['per_dest_ip_mitigation_threshold']) + + @property + def mitigation_threshold_eps(self): + if self._values['mitigation_threshold_eps'] in [None, "infinite"]: + return self._values['mitigation_threshold_eps'] + return int(self._values['mitigation_threshold_eps']) + + @property + def detection_threshold_eps(self): + if self._values['detection_threshold_eps'] in [None, "infinite"]: + return self._values['detection_threshold_eps'] + return int(self._values['detection_threshold_eps']) + + @property + def attack_ceiling(self): + if self._values['attack_ceiling'] in [None, "infinite"]: + return self._values['attack_ceiling'] + return int(self._values['attack_ceiling']) + + @property + def blacklist_category(self): + if self._values['blacklist_category'] is None: + return None + return fq_name(self.partition, self._values['blacklist_category']) + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + @property + def allow_advertisement(self): + if self._values['allow_advertisement'] is None: + return None + if self._values['allow_advertisement'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def auto_blacklist(self): + if self._values['auto_blacklist'] is None: + return None + if self._values['auto_blacklist'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def simulate_auto_threshold(self): + if self._values['simulate_auto_threshold'] is None: + return None + if self._values['simulate_auto_threshold'] == 'yes': + return 'enabled' + return 'disabled' + + @property + def bad_actor_detection(self): + if self._values['bad_actor_detection'] is None: + return None + if self._values['bad_actor_detection'] == 'yes': + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def allow_advertisement(self): + return flatten_boolean(self._values['allow_advertisement']) + + @property + def auto_blacklist(self): + return flatten_boolean(self._values['auto_blacklist']) + + @property + def simulate_auto_threshold(self): + return flatten_boolean(self._values['simulate_auto_threshold']) + + @property + def bad_actor_detection(self): + return flatten_boolean(self._values['bad_actor_detection']) + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + want = getattr(self.want, param) + try: + have = getattr(self.have, param) + if want != have: + return want + except AttributeError: + return want + + +class BaseManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + + # A list of all the vectors queried from the API when reading current info + # from the device. This is used when updating the API as the value that needs + # to be updated is a list of vectors and PATCHing a list would override any + # default settings. + self.vectors = dict() + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + result = dict() + + changed = self.present() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + return self.update() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def format_vectors(self, vectors): + result = None + for x in vectors: + vector = ApiParameters(params=x) + self.vectors[vector.name] = x + if vector.name == self.want.name: + result = vector + if not result: + return ApiParameters() + return result + + def _update(self, vtype): + self.have = self.format_vectors(self.read_current_from_device()) + if not self.should_update(): + return False + if self.module.check_mode: + return True + + # A disabled vector does not appear in the list of existing vectors + if self.want.state == 'disabled': + if self.want.profile == 'device-config' and self.have.state == 'disabled': + return False + # For non-device-config + if self.want.name not in self.vectors: + return False + + # At this point we know the existing vector is not disabled, so we need + # to change it in some way. + # + # First, if we see that the vector is in the current list of vectors, + # we are going to update it + changes = dict(self.changes.api_params()) + if self.want.name in self.vectors: + self.vectors[self.want.name].update(changes) + else: + # else, we are going to add it to the list of vectors + self.vectors[self.want.name] = changes + + # Since the name attribute is not a parameter tracked in the Parameter + # classes, we will add the name to the list of attributes so that when + # we update the API, it creates the correct vector + self.vectors[self.want.name].update({'name': self.want.name}) + + # Finally, the disabled state forces us to remove the vector from the + # list. However, items are only removed from the list if the profile + # being configured is not a device-config + if self.want.state == 'disabled': + if self.want.profile != 'device-config': + del self.vectors[self.want.name] + + # All of the vectors must be re-assembled into a list of dictionaries + # so that when we PATCH the API endpoint, the vectors list is filled + # correctly. + # + # There are **not** individual API endpoints for the individual vectors. + # Instead, the endpoint includes a list of vectors that is part of the + # DoS profile + result = [v for k, v in iteritems(self.vectors)] + + self.changes = Changes(params={vtype: result}) + self.update_on_device() + return True + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.kwargs = kwargs + + def exec_module(self): + if self.module.params['profile'] == 'device-config': + manager = self.get_manager('v1') + elif self.module.params['name'] in NETWORK_SECURITY_VECTORS: + manager = self.get_manager('v2') + elif self.module.params['name'] in PROTOCOL_DNS_VECTORS: + manager = self.get_manager('v3') + elif self.module.params['name'] in PROTOCOL_SIP_VECTORS: + manager = self.get_manager('v4') + else: + raise F5ModuleError( + "Unknown vector type specified." + ) + return manager.exec_module() + + def get_manager(self, type): + if type == 'v1': + return DeviceConfigManager(**self.kwargs) + elif type == 'v2': + return NetworkSecurityManager(**self.kwargs) + elif type == 'v3': + return ProtocolDnsManager(**self.kwargs) + elif type == 'v4': + return ProtocolSipManager(**self.kwargs) + + +class DeviceConfigManager(BaseManager): + """Manages AFM DoS Device Configuration settings. + + DeviceConfiguration is a special type of profile that is specific to the + BIG-IP device's management interface; not the data plane interfaces. + + There are many similar vectors that can be managed here. This configuration + is a super-set of the base DoS profile vector configuration and includes + several attributes per-vector that are not found in the DoS profile configuration. + These include, + + * allowUpstreamScrubbing + * attackedDst + * autoScrubbing + * defaultInternalRateLimit + * detectionThresholdPercent + * detectionThresholdPps + * perDstIpDetectionPps + * perDstIpLimitPps + * scrubbingDetectionSeconds + * scrubbingDuration + """ + def __init__(self, *args, **kwargs): + super(DeviceConfigManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + name = self.normalize_names_in_device_config(self.want.name) + + self.want.update({'name': name}) + + return self._update('dosDeviceVector') + + def normalize_names_in_device_config(self, name): + # Overwrite specific names because they do not align with DoS Profile names + # + # The following names (on the right) differ from the functionally equivalent + # names (on the left) found in DoS Profiles. This seems like a bug to me, + # but I do not expect it to be fixed, so this works around it in the meantime. + name_map = { + 'hop-cnt-low': 'hop-cnt-leq-one', + 'ip-low-ttl': 'ttl-leq-one', + } + + # Attempt to normalize, else just return the name. This handles the default + # case where the name is actually correct and would not be found in the + # ``name_map`` above. + result = name_map.get(name, name) + return result + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/device-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'dos-device-config') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/device-config/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', 'dos-device-config') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = response.get('dosDeviceVector', []) + return result + + +class NetworkSecurityManager(BaseManager): + """Manages AFM DoS Profile Network Security settings. + + Network Security settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(NetworkSecurityManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('networkAttackVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/dos-network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/dos-network/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response.get('networkAttackVector', []) + + +class ProtocolDnsManager(BaseManager): + """Manages AFM DoS Profile Protocol DNS settings. + + Protocol DNS settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(ProtocolDnsManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('dnsQueryVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-dns/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-dns/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response.get('dnsQueryVector', []) + + +class ProtocolSipManager(BaseManager): + """Manages AFM DoS Profile Protocol SIP settings. + + Protocol SIP settings are a sub-collection attached to each profile. + + There are many similar vectors that can be managed here. This configuration + is a sub-set of the device-config DoS vector configuration and excludes + several attributes per-vector that are found in the device-config configuration. + These include, + + * rateIncrease + * rateLimit + * rateThreshold + """ + def __init__(self, *args, **kwargs): + super(ProtocolSipManager, self).__init__(**kwargs) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def update(self): + return self._update('sipAttackVector') + + def update_on_device(self): + params = self.changes.api_params() + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-sip/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def read_current_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/protocol-sip/{3}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.profile), + self.want.profile + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return response.get('sipAttackVector', []) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + choices=[ + 'ext-hdr-too-large', + 'hop-cnt-low', + 'host-unreachable', + 'icmp-frag', + 'icmpv4-flood', + 'icmpv6-flood', + 'ip-frag-flood', + 'ip-low-ttl', + 'ip-opt-frames', + 'ipv6-frag-flood', + 'opt-present-with-illegal-len', + 'sweep', + 'tcp-bad-urg', + 'tcp-half-open', + 'tcp-opt-overruns-tcp-hdr', + 'tcp-psh-flood', + 'tcp-rst-flood', + 'tcp-syn-flood', + 'tcp-syn-oversize', + 'tcp-synack-flood', + 'tcp-window-size', + 'tidcmp', + 'too-many-ext-hdrs', + 'udp-flood', + 'unk-tcp-opt-type', + 'a', + 'aaaa', + 'any', + 'axfr', + 'cname', + 'dns-malformed', + 'ixfr', + 'mx', + 'ns', + 'other', + 'ptr', + 'qdcount', + 'soa', + 'srv', + 'txt', + 'ack', + 'bye', + 'cancel', + 'invite', + 'message', + 'notify', + 'options', + 'other', + 'prack', + 'publish', + 'register', + 'sip-malformed', + 'subscribe', + 'uri-limit', + ] + ), + profile=dict(required=True), + allow_advertisement=dict(type='bool'), + auto_blacklist=dict(type='bool'), + simulate_auto_threshold=dict(type='bool'), + bad_actor_detection=dict(type='bool'), + blacklist_detection_seconds=dict(type='int'), + blacklist_duration=dict(type='int'), + attack_ceiling=dict(), + attack_floor=dict(), + per_source_ip_detection_threshold=dict(), + per_source_ip_mitigation_threshold=dict(), + # sustained_attack_detection_time=dict(), + # category_detection_time=dict(), + # per_dest_ip_detection_threshold=dict(), + # per_dest_ip_mitigation_threshold=dict(), + + detection_threshold_percent=dict( + aliases=['rate_increase'] + ), + detection_threshold_eps=dict( + aliases=['rate_threshold'] + ), + mitigation_threshold_eps=dict( + aliases=['rate_limit'] + ), + threshold_mode=dict( + choices=['manual', 'stress-based-mitigation', 'fully-automatic'] + ), + state=dict( + choices=['mitigate', 'detect-only', 'learn-only', 'disabled'], + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + client = F5RestClient(**module.params) + + try: + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + exit_json(module, results, client) + except F5ModuleError as ex: + cleanup_tokens(client) + fail_json(module, ex, client) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/f5/test_bigip_firewall_dos_vector.py b/test/units/modules/network/f5/test_bigip_firewall_dos_vector.py new file mode 100644 index 00000000000..3a758f8100d --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_firewall_dos_vector.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import sys + +from nose.plugins.skip import SkipTest +if sys.version_info < (2, 7): + raise SkipTest("F5 Ansible modules require Python >= 2.7") + +from ansible.module_utils.basic import AnsibleModule + +try: + from library.modules.bigip_firewall_dos_vector import ApiParameters + from library.modules.bigip_firewall_dos_vector import ModuleParameters + from library.modules.bigip_firewall_dos_vector import ModuleManager + from library.modules.bigip_firewall_dos_vector import ArgumentSpec + from library.modules.bigip_firewall_dos_vector import ProtocolDnsManager + + # In Ansible 2.8, Ansible changed import paths. + from test.units.compat import unittest + from test.units.compat.mock import Mock + from test.units.compat.mock import patch + + from test.units.modules.utils import set_module_args +except ImportError: + try: + from ansible.modules.network.f5.bigip_firewall_dos_vector import ApiParameters + from ansible.modules.network.f5.bigip_firewall_dos_vector import ModuleParameters + from ansible.modules.network.f5.bigip_firewall_dos_vector import ModuleManager + from ansible.modules.network.f5.bigip_firewall_dos_vector import ArgumentSpec + from ansible.modules.network.f5.bigip_firewall_dos_vector import ProtocolDnsManager + + # Ansible 2.8 imports + from units.compat import unittest + from units.compat.mock import Mock + from units.compat.mock import patch + + from units.modules.utils import set_module_args + except ImportError: + raise SkipTest("F5 Ansible modules require the f5-sdk Python library") + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestParameters(unittest.TestCase): + def test_module_parameters(self): + args = dict( + name='foo', + state='mitigate' + ) + + p = ModuleParameters(params=args) + assert p.name == 'foo' + assert p.state == 'mitigate' + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_dns(self, *args): + set_module_args(dict( + name='aaaa', + state='mitigate', + profile='foo', + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + + m1 = ProtocolDnsManager(module=module) + m1.read_current_from_device = Mock(return_value=[]) + m1.update_on_device = Mock(return_value=True) + + mm = ModuleManager(module=module) + mm.get_manager = Mock(return_value=m1) + mm.exists = Mock(side_effect=[False, True]) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True