From 919f70c357cff47277ddc2a337eb6ec3b4d69fef Mon Sep 17 00:00:00 2001 From: Kevin Breit Date: Wed, 5 Jun 2019 09:36:39 -0500 Subject: [PATCH] New module - meraki_malware (#56929) * Initial commit for meraki_malware module - Allows for manipulation of malware configuration * Add full documentation and improve code coverage * Add diff support * Type change * Sanity check fixes * Convert org_id from str to int for consistency * Sanity fixes again * Fix argument type errors * Remove ignore items for some Meraki modules so shippable is happy --- .../network/meraki/meraki_config_template.py | 2 - .../modules/network/meraki/meraki_malware.py | 276 ++++++++++++++++++ .../network/meraki/meraki_organization.py | 3 +- .../modules/network/meraki/meraki_snmp.py | 2 - lib/ansible/plugins/doc_fragments/meraki.py | 2 +- .../targets/meraki_malware/aliases | 1 + .../targets/meraki_malware/tasks/main.yml | 247 ++++++++++++++++ test/sanity/validate-modules/ignore.txt | 5 - 8 files changed, 527 insertions(+), 11 deletions(-) create mode 100644 lib/ansible/modules/network/meraki/meraki_malware.py create mode 100644 test/integration/targets/meraki_malware/aliases create mode 100644 test/integration/targets/meraki_malware/tasks/main.yml diff --git a/lib/ansible/modules/network/meraki/meraki_config_template.py b/lib/ansible/modules/network/meraki/meraki_config_template.py index 604069924fa..eedc9d898a6 100644 --- a/lib/ansible/modules/network/meraki/meraki_config_template.py +++ b/lib/ansible/modules/network/meraki/meraki_config_template.py @@ -182,8 +182,6 @@ def main(): # the module argument_spec = meraki_argument_spec() argument_spec.update(state=dict(type='str', choices=['absent', 'query', 'present'], default='query'), - org_name=dict(type='str', aliases=['organization']), - org_id=dict(type='int'), config_template=dict(type='str', aliases=['name']), net_name=dict(type='str'), net_id=dict(type='str'), diff --git a/lib/ansible/modules/network/meraki/meraki_malware.py b/lib/ansible/modules/network/meraki/meraki_malware.py new file mode 100644 index 00000000000..93de3202852 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_malware.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Kevin Breit (@kbreit) +# 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': 'community' +} + +DOCUMENTATION = r''' +--- +module: meraki_malware +short_description: Manage Malware Protection in the Meraki cloud +version_added: "2.9" +description: +- Fully configure malware protection in a Meraki environment. +notes: +- Some of the options are likely only used for developers within Meraki. +options: + state: + description: + - Specifies whether object should be queried, created/modified, or removed. + choices: [absent, present, query] + default: query + type: str + net_name: + description: + - Name of network which configuration is applied to. + aliases: [network] + type: str + net_id: + description: + - ID of network which configuration is applied to. + type: str + allowed_urls: + description: + - List of URLs to whitelist. + suboptions: + url: + description: + - URL string to allow. + type: str + comment: + description: + - Human readable information about URL. + type: str + allowed_files: + description: + - List of files to whitelist. + suboptions: + sha256: + description: + - 256-bit hash of file. + type: str + aliases: [ hash ] + comment: + description: + - Human readable information about file. + type: str + mode: + description: + - Enabled or disabled state of malware protection. + choices: [disabled, enabled] + + +author: +- Kevin Breit (@kbreit) +extends_documentation_fragment: meraki +''' + +EXAMPLES = r''' + - name: Enable malware protection + meraki_malware: + auth_key: abc123 + state: present + org_name: YourOrg + net_name: YourNet + mode: enabled + delegate_to: localhost + + - name: Set whitelisted url + meraki_malware: + auth_key: abc123 + state: present + org_name: YourOrg + net_name: YourNet + mode: enabled + allowed_urls: + - url: www.google.com + comment: Google + delegate_to: localhost + + - name: Set whitelisted file + meraki_malware: + auth_key: abc123 + state: present + org_name: YourOrg + net_name: YourNet + mode: enabled + allowed_files: + - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: random zip + delegate_to: localhost + + - name: Get malware settings + meraki_malware: + auth_key: abc123 + state: query + org_name: YourNet + net_name: YourOrg + delegate_to: localhost +''' + +RETURN = r''' +data: + description: List of administrators. + returned: success + type: complex + contains: + mode: + description: Mode to enable or disable malware scanning. + returned: success + type: str + sample: enabled + allowed_files: + description: List of files which are whitelisted. + returned: success + type: complex + contains: + sha256: + description: sha256 hash of whitelisted file. + returned: success + type: str + sample: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: + description: Comment about the whitelisted entity + returned: success + type: str + sample: TPS report + allowed_urls: + description: List of URLs which are whitelisted. + returned: success + type: complex + contains: + url: + description: URL of whitelisted site. + returned: success + type: str + sample: site.com + comment: + description: Comment about the whitelisted entity + returned: success + type: str + sample: Corporate HQ +''' + +import os +from ansible.module_utils.basic import AnsibleModule, json, env_fallback +from ansible.module_utils._text import to_native +from ansible.module_utils.common.dict_transformations import recursive_diff +from ansible.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec + + +def main(): + # define the available arguments/parameters that a user can pass to + # the module + + urls_arg_spec = dict(url=dict(type='str'), + comment=dict(type='str'), + ) + + files_arg_spec = dict(sha256=dict(type='str', aliases=['hash']), + comment=dict(type='str'), + ) + + argument_spec = meraki_argument_spec() + argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'), + net_name=dict(type='str', aliases=['network']), + net_id=dict(type='str'), + mode=dict(type='str', choices=['enabled', 'disabled']), + allowed_urls=dict(type='list', default=None, elements='dict', options=urls_arg_spec), + allowed_files=dict(type='list', default=None, elements='dict', options=files_arg_spec), + ) + + # seed the result dict in the object + # we primarily care about changed and state + # change is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + ) + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + ) + meraki = MerakiModule(module, function='malware') + + meraki.params['follow_redirects'] = 'all' + + query_url = {'malware': '/networks/{net_id}/security/malwareSettings'} + update_url = {'malware': '/networks/{net_id}/security/malwareSettings'} + + meraki.url_catalog['get_one'].update(query_url) + meraki.url_catalog['update'] = update_url + + org_id = meraki.params['org_id'] + if org_id is None: + org_id = meraki.get_org_id(meraki.params['org_name']) + net_id = meraki.params['net_id'] + if net_id is None: + nets = meraki.get_nets(org_id=org_id) + net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets) + + # Check for argument completeness + if meraki.params['state'] == 'present': + if meraki.params['allowed_files'] is not None or meraki.params['allowed_urls'] is not None: + if meraki.params['mode'] is None: + meraki.fail_json(msg="mode must be set when allowed_files or allowed_urls is set.") + + # Assemble payload + if meraki.params['state'] == 'present': + payload = dict() + if meraki.params['mode'] is not None: + payload['mode'] = meraki.params['mode'] + if meraki.params['allowed_urls'] is not None: + payload['allowedUrls'] = meraki.params['allowed_urls'] + if meraki.params['allowed_files'] is not None: + payload['allowedFiles'] = meraki.params['allowed_files'] + + if meraki.params['state'] == 'query': + path = meraki.construct_path('get_one', net_id=net_id) + data = meraki.request(path, method='GET') + if meraki.status == 200: + meraki.result['data'] = data + elif meraki.params['state'] == 'present': + path = meraki.construct_path('get_one', net_id=net_id) + original = meraki.request(path, method='GET') + if meraki.is_update_required(original, payload): + if meraki.module.check_mode is True: + diff = recursive_diff(original, payload) + original.update(payload) + meraki.result['diff'] = {'before': diff[0], + 'after': diff[1], + } + meraki.result['data'] = original + meraki.result['changed'] = True + meraki.exit_json(**meraki.result) + path = meraki.construct_path('update', net_id=net_id) + data = meraki.request(path, method='PUT', payload=json.dumps(payload)) + if meraki.status == 200: + diff = recursive_diff(original, payload) + meraki.result['diff'] = {'before': diff[0], + 'after': diff[1], + } + meraki.result['data'] = data + meraki.result['changed'] = True + else: + meraki.result['data'] = original + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + meraki.exit_json(**meraki.result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/meraki/meraki_organization.py b/lib/ansible/modules/network/meraki/meraki_organization.py index f46da8065b8..95ccef02c40 100644 --- a/lib/ansible/modules/network/meraki/meraki_organization.py +++ b/lib/ansible/modules/network/meraki/meraki_organization.py @@ -38,6 +38,7 @@ options: description: - ID of organization. aliases: [ id ] + type: str author: - Kevin Breit (@kbreit) extends_documentation_fragment: meraki @@ -124,7 +125,7 @@ def main(): argument_spec.update(clone=dict(type='str'), state=dict(type='str', choices=['present', 'query'], default='present'), org_name=dict(type='str', aliases=['name', 'organization']), - org_id=dict(type='int', aliases=['id']), + org_id=dict(type='str', aliases=['id']), ) # seed the result dict in the object diff --git a/lib/ansible/modules/network/meraki/meraki_snmp.py b/lib/ansible/modules/network/meraki/meraki_snmp.py index d57e1da1ec4..8cbd91c77c8 100644 --- a/lib/ansible/modules/network/meraki/meraki_snmp.py +++ b/lib/ansible/modules/network/meraki/meraki_snmp.py @@ -219,8 +219,6 @@ def main(): # the module argument_spec = meraki_argument_spec() argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'), - org_name=dict(type='str', aliases=['organization']), - org_id=dict(type='int'), v2c_enabled=dict(type='bool'), v3_enabled=dict(type='bool'), v3_auth_mode=dict(type='str', choices=['SHA', 'MD5']), diff --git a/lib/ansible/plugins/doc_fragments/meraki.py b/lib/ansible/plugins/doc_fragments/meraki.py index 00935146d17..07df281996b 100644 --- a/lib/ansible/plugins/doc_fragments/meraki.py +++ b/lib/ansible/plugins/doc_fragments/meraki.py @@ -56,5 +56,5 @@ options: org_id: description: - ID of organization. - type: int + type: str ''' diff --git a/test/integration/targets/meraki_malware/aliases b/test/integration/targets/meraki_malware/aliases new file mode 100644 index 00000000000..ad7ccf7ada2 --- /dev/null +++ b/test/integration/targets/meraki_malware/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_malware/tasks/main.yml b/test/integration/targets/meraki_malware/tasks/main.yml new file mode 100644 index 00000000000..b387479debb --- /dev/null +++ b/test/integration/targets/meraki_malware/tasks/main.yml @@ -0,0 +1,247 @@ +# Test code for the Meraki VLAN module +# Copyright: (c) 2018, Kevin Breit (@kbreit) + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- block: + - name: Test an API key is provided + fail: + msg: Please define an API key + when: auth_key is not defined + + - name: Create test network + meraki_network: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + type: appliance + delegate_to: localhost + register: net + + - set_fact: + net_id: '{{net.data.id}}' + + - name: Enable malware protection with check mode + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + delegate_to: localhost + check_mode: yes + register: get_malware_check + + - assert: + that: + - get_malware_check is changed + - get_malware_check.data is defined + + - name: Enable malware protection + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + delegate_to: localhost + register: get_malware + + - debug: + var: get_malware + + - assert: + that: + - get_malware is changed + - get_malware.data.mode is defined + + - name: Enable malware protection with idempotency + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + delegate_to: localhost + register: get_malware_idempotent + + - debug: + var: get_malware_idempotent + + - assert: + that: + - get_malware_idempotent is not changed + - get_malware_idempotent.data is defined + + - name: Test error when mode is not set + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + allowed_files: + - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: random zip + delegate_to: localhost + register: test_mode_err + ignore_errors: yes + + - assert: + that: + - test_mode_err.msg == "mode must be set when allowed_files or allowed_urls is set." + + - name: Set whitelisted file with check mode + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + allowed_files: + - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: random zip + delegate_to: localhost + check_mode: yes + register: set_file_check + + - debug: + var: + set_file_check + + - assert: + that: + - set_file_check is changed + - set_file_check.data is defined + + - name: Set whitelisted file + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_id: '{{net_id}}' + mode: enabled + allowed_files: + - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: random zip + delegate_to: localhost + register: set_file + + - debug: + var: set_file + + - assert: + that: + - set_file is changed + - set_file.data.mode is defined + + - name: Set whitelisted file with idempotency + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + allowed_files: + - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503 + comment: random zip + delegate_to: localhost + register: set_file_idempotent + + - debug: + var: set_file_idempotent + + - assert: + that: + - set_file_idempotent is not changed + - set_file_idempotent.data is defined + + - name: Set whitelisted url with check mode + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + allowed_urls: + - url: www.google.com + comment: Google + delegate_to: localhost + check_mode: yes + register: set_url_check + + - debug: + var: + set_url_check + + - assert: + that: + - set_url_check is changed + - set_url_check.data is defined + + - name: Set whitelisted url + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + allowed_urls: + - url: www.google.com + comment: Google + delegate_to: localhost + register: set_url + + - debug: + var: set_url + + - assert: + that: + - set_url is changed + - set_url.data.mode is defined + + - name: Set whitelisted url with idempotency + meraki_malware: + auth_key: '{{auth_key}}' + state: present + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + mode: enabled + allowed_urls: + - url: www.google.com + comment: Google + delegate_to: localhost + register: set_url_idempotent + + - debug: + var: set_url_idempotent + + - assert: + that: + - set_url_idempotent is not changed + - set_url_idempotent.data is defined + + - name: Get malware settings + meraki_malware: + auth_key: '{{auth_key}}' + state: query + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + delegate_to: localhost + register: get_malware + + - assert: + that: + - get_malware.data is defined + + ############################################################################# + # Tear down starts here + ############################################################################# + always: + - name: Delete test network + meraki_network: + auth_key: '{{auth_key}}' + state: absent + org_name: '{{test_org_name}}' + net_name: '{{test_net_name}} - Malware' + delegate_to: localhost diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index 3c8f8d342a7..9a0cebac467 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -536,13 +536,8 @@ lib/ansible/modules/network/junos/junos_logging.py E322 lib/ansible/modules/network/junos/junos_rpc.py E326 lib/ansible/modules/network/junos/junos_static_route.py E322 lib/ansible/modules/network/junos/junos_vrf.py E324 -lib/ansible/modules/network/meraki/meraki_device.py E325 lib/ansible/modules/network/meraki/meraki_mr_l3_firewall.py E325 -lib/ansible/modules/network/meraki/meraki_mx_l3_firewall.py E325 -lib/ansible/modules/network/meraki/meraki_network.py E325 lib/ansible/modules/network/meraki/meraki_ssid.py E325 -lib/ansible/modules/network/meraki/meraki_switchport.py E325 -lib/ansible/modules/network/meraki/meraki_vlan.py E325 lib/ansible/modules/network/netact/netact_cm_command.py E326 lib/ansible/modules/network/netconf/netconf_config.py E326 lib/ansible/modules/network/netscaler/netscaler_cs_action.py E323