Repository: ansible/ansible

Issue:
azure_rm_securitygroup cannot create rules that allow comma-separated IP address prefixes
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and devel branch are affected too.
Always add information AFTER of these html comments.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
<!---
Insert below this comment the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path
-->

`azure_rm_securitygroup`

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/vish/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]

```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

```
DEFAULT_LOG_PATH(/etc/ansible/ansible.cfg) = /var/log/ansible/ansible.log
```

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->

Control Machine Details:

```
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"
```

##### SUMMARY
<!--- Explain the problem briefly -->

The `azure_rm_securitygroup` module is unable to set security rules where the `source_address_prefix` is a sequence of IP addresses. It _is able to set security rules_ when the `source_address_prefix` is a _single IP address_.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

* Save the playbook below e.g. `bug.yml`.
* Replace IP addresses with a valid set of IPs of your own. 
* Run playbook `ansible-playbook bug.yml`

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- name: Reproducing a Bug
  hosts: localhost
  connection: local
  tasks:
  - name: Ensure resource group exists
    azure_rm_resourcegroup:
      name: "TESTRG"
      location: "westeurope"
    state: present
  - name: Set Rules for NSG 
    azure_rm_securitygroup:
      resource_group: "TESTRG"
      name: "TESTNSG"
      purge_rules: yes
      rules: 
      - name: WinRM-Secure
        protocol: Tcp
        destination_port_range: 5986
        access: Allow
        priority: 1021
        direction: Inbound
        source_address_prefix: "X.X.X.X,Y.Y.Y.Y,Z.Z.Z.Z"

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

* Create an Azure NSG Security Group `TESTNSG`
* Security group should have one rule "WinRM-Secure". 
* The Source IP Addresses field should have 3 IP addresses added.

An example screenshot. The Security Rule here was created manually from Azure GUI. 

![image](https://user-images.githubusercontent.com/13379978/38228641-0f244b26-36f4-11e8-9a52-24efbdc36170.png)


##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
The full traceback is:
  File "/tmp/ansible_8WSk8C/ansible_module_azure_rm_securitygroup.py", line 707, in create_or_update
    parameters=parameters)
  File "/usr/local/lib/python2.7/dist-packages/azure/mgmt/network/v2017_11_01/operations/network_security_groups_operations.py", line 278, in create_or_update
    get_long_running_status, long_running_operation_timeout)
  File "/usr/local/lib/python2.7/dist-packages/msrestazure/azure_operation.py", line 360, in __init__
    raise CloudError(self._response)

fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "ad_user": null,
            "api_profile": "latest",
            "append_tags": true,
            "auth_source": null,
            "cert_validation_mode": null,
            "client_id": null,
            "cloud_environment": null,
            "default_rules": null,
            "location": null,
            "name": "TESTNSG",
            "password": null,
            "profile": null,
            "purge_default_rules": false,
            "purge_rules": true,
            "resource_group": "TESTRG",
            "rules": [
                {
                    "access": "Allow",
                    "destination_address_prefix": "*",
                    "destination_port_range": 5986,
                    "direction": "Inbound",
                    "name": "WinRM-Secure",
                    "priority": 1021,
                    "protocol": "Tcp",
                    "source_address_prefix": "X.X.X.X,Y.Y.Y.Y,Z.Z.Z.Z",
                    "source_port_range": "*"
                }
            ],
            "secret": null,
            "state": "present",
            "subscription_id": null,
            "tags": null,
            "tenant": null
        }
    },
    "msg": "Error creating/updating security group TESTNSG - Azure Error: SecurityRuleInvalidAddressPrefix\nMessage: Security rule /subscriptions/XXXXXXXXXXXXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/securityRules/WinRM-Secure has invalid Address prefix. Value provided: X.X.X.X,Y.Y.Y.Y,Z.Z.Z.Z"
}

```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @haroldwongms @nitzmahone @trstringer @xscript @yuwzho @zikalino
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@savishy Please try the following playbook

```yaml
---
- name: Reproducing a Bug
  hosts: localhost
  connection: local
  tasks:
  - name: Ensure resource group exists
    azure_rm_resourcegroup:
      name: "TESTRG"
      location: "westeurope"
    state: present
  - name: Set Rules for NSG 
    azure_rm_securitygroup:
      resource_group: "TESTRG"
      name: "TESTNSG"
      purge_rules: yes
      rules: 
      - name: WinRM-Secure
        protocol: Tcp
        destination_port_range: 5986
        access: Allow
        priority: 1021
        direction: Inbound
        source_address_prefixes:
        - 52.100.120.240
        - 53.100.250.190
        - 54.110.200.200
```
@yuwzho  Could I request this issue be reopened?

* I ran the playbook again, making note of the parameter name: `source_address_prefixes` instead of `source_address_prefix`.
* It seems this parameter was silently ignored. In the invocation I see it has passed in `"source_address_prefix": "*"` 
* This causes the NSG to be created with no `source_address_prefix` i.e. it is open to the world. (This is a serious security risk and raises automated alarms at my organization, which I really want to avoid :)  )

```
changed: [localhost] => {
    "changed": true,
    "invocation": {
        "module_args": {
            "ad_user": null,
            "api_profile": "latest",
            "append_tags": true,
            "auth_source": null,
            "cert_validation_mode": null,
            "client_id": null,
            "cloud_environment": null,
            "default_rules": null,
            "location": null,
            "name": "TESTNSG",
            "password": null,
            "profile": null,
            "purge_default_rules": false,
            "purge_rules": true,
            "resource_group": "TESTRG",
            "rules": [
                {
                    "access": "Allow",
                    "destination_address_prefix": "*",
                    "destination_port_range": 5986,
                    "direction": "Inbound",
                    "name": "WinRM-Secure",
                    "priority": 1021,
                    "protocol": "Tcp",
                    "source_address_prefix": "*",
                    "source_address_prefixes": [
                        "52.138.X.X",
                        "52.164.X.X",
                        "52.138.X.X"
                    ],
                    "source_port_range": "*"
                }
            ],
            "secret": null,
            "state": "present",
            "subscription_id": null,
            "tags": null,
            "tenant": null
        }
    },
    "state": {
        "default_rules": [
            {
                "access": "Allow",
                "description": "Allow inbound traffic from all VMs in VNET",
                "destination_address_prefix": "VirtualNetwork",
                "destination_port_range": "*",
                "direction": "Inbound",
                "etag": "W/\"01de6124-c502-45ed-XXXX-b969X78fa5574\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/AllowVnetInBound",
                "name": "AllowVnetInBound",
                "priority": 65000,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "VirtualNetwork",
                "source_port_range": "*"
            },
            {
                "access": "Allow",
                "description": "Allow inbound traffic from azure load balancer",
                "destination_address_prefix": "*",
                "destination_port_range": "*",
                "direction": "Inbound",
                "etag": "W/\"01de6124-c502-XX-87f9-XCXC\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/AllowAzureLoadBalancerInBound",
                "name": "AllowAzureLoadBalancerInBound",
                "priority": 65001,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "AzureLoadBalancer",
                "source_port_range": "*"
            },
            {
                "access": "Deny",
                "description": "Deny all inbound traffic",
                "destination_address_prefix": "*",
                "destination_port_range": "*",
                "direction": "Inbound",
                "etag": "W/\"X-c502-45ed-87f9-X\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/DenyAllInBound",
                "name": "DenyAllInBound",
                "priority": 65500,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "*",
                "source_port_range": "*"
            },
            {
                "access": "Allow",
                "description": "Allow outbound traffic from all VMs to all VMs in VNET",
                "destination_address_prefix": "VirtualNetwork",
                "destination_port_range": "*",
                "direction": "Outbound",
                "etag": "W/\"01de6124-c502-45ed-87f9-XX\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/AllowVnetOutBound",
                "name": "AllowVnetOutBound",
                "priority": 65000,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "VirtualNetwork",
                "source_port_range": "*"
            },
            {
                "access": "Allow",
                "description": "Allow outbound traffic from all VMs to Internet",
                "destination_address_prefix": "Internet",
                "destination_port_range": "*",
                "direction": "Outbound",
                "etag": "W/\"01de6124-c502-45ed-87f9-XX\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/AllowInternetOutBound",
                "name": "AllowInternetOutBound",
                "priority": 65001,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "*",
                "source_port_range": "*"
            },
            {
                "access": "Deny",
                "description": "Deny all outbound traffic",
                "destination_address_prefix": "*",
                "destination_port_range": "*",
                "direction": "Outbound",
                "etag": "W/\"01de6124-c502-45ed-87f9-X\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/defaultSecurityRules/DenyAllOutBound",
                "name": "DenyAllOutBound",
                "priority": 65500,
                "protocol": "*",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "*",
                "source_port_range": "*"
            }
        ],
        "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG",
        "location": "westeurope",
        "name": "TESTNSG",
        "network_interfaces": [],
        "rules": [
            {
                "access": "Allow",
                "description": null,
                "destination_address_prefix": "*",
                "destination_port_range": "5986",
                "direction": "Inbound",
                "etag": "W/\"01de6124-c502-45ed-87f9-X\"",
                "id": "/subscriptions/XXXXXXXXXXX/resourceGroups/TESTRG/providers/Microsoft.Network/networkSecurityGroups/TESTNSG/securityRules/WinRM-Secure",
                "name": "WinRM-Secure",
                "priority": 1021,
                "protocol": "Tcp",
                "provisioning_state": "Succeeded",
                "source_address_prefix": "*",
                "source_port_range": "*"
            }
        ],
        "subnets": [],
        "tags": {},
        "type": "Microsoft.Network/networkSecurityGroups"
    }
}
```

@savishy Sure, I get your question, please reopen it and I will add it to my backlog
@yuwzho I don't see a Reopen option. Just an option to comment. Perhaps you can reopen from your side.
I also don't have the reopen button...
bot_skip
@yuwzho I got the issue reopened (thanks @abadger ) - could you take a look at this now please?

@savishy I already add this to my backlog, but I don't think I have bandwidth this three weeks. I can know the problem is the following two line: 

https://github.com/ansible/ansible/blob/6300e71903ef02212f792b09c9b6720a55bfdf24/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py#L372-L373

It should also check whether the `source_address_prefixes` is none. Besides these lines, there also need document modification and full test. I add `easyfix` here, to see if someone volunteer can help to solve this problem before I get time.

+label easyfix
@yuwzho take your time, this isn't urgent. I'll let you know if I have some bandwidth, perhaps I can make a PR with your assistance. 
working on it

Resolving patch:
diff --git a/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py b/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py
--- a/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py
+++ b/lib/ansible/modules/cloud/azure/azure_rm_securitygroup.py
@@ -73,10 +73,12 @@
             source_port_range:
                 description:
                   - Port or range of ports from which traffic originates.
+                  - It can accept string type or a list of string type.
                 default: "*"
             destination_port_range:
                 description:
                   - Port or range of ports to which traffic is headed.
+                  - It can accept string type or a list of string type.
                 default: "*"
             source_address_prefix:
                 description:
@@ -84,6 +86,7 @@
                   - Asterix C(*) can also be used to match all source IPs.
                   - Default tags such as C(VirtualNetwork), C(AzureLoadBalancer) and C(Internet) can also be used.
                   - If this is an ingress rule, specifies where network traffic originates from.
+                  - It can accept string type or a list of string type.
                 default: "*"
             destination_address_prefix:
                 description:
@@ -91,6 +94,7 @@
                   - CIDR or destination IP range.
                   - Asterix C(*) can also be used to match all source IPs.
                   - Default tags such as C(VirtualNetwork), C(AzureLoadBalancer) and C(Internet) can also be used.
+                  - It can accept string type or a list of string type.
                 default: "*"
             access:
                 description:
@@ -145,7 +149,9 @@
             direction: Inbound
           - name: 'AllowSSH'
             protocol: TCP
-            source_address_prefix: '174.109.158.0/24'
+            source_address_prefix:
+              - '174.109.158.0/24'
+              - '174.109.159.0/24'
             destination_port_range: 22
             access: Allow
             priority: 101
@@ -343,86 +349,68 @@ def validate_rule(self, rule, rule_type=None):
     :param rule_type: Set to 'default' if the rule is part of the default set of rules.
     :return: None
     '''
-
-    if not rule.get('name'):
-        raise Exception("Rule name value is required.")
-
-    priority = rule.get('priority', None)
-    if not priority:
-        raise Exception("Rule priority is required.")
-    if not isinstance(priority, integer_types):
-        try:
-            priority = int(priority)
-            rule['priority'] = priority
-        except:
-            raise Exception("Rule priority attribute must be an integer.")
+    priority = rule.get('priority', 0)
     if rule_type != 'default' and (priority < 100 or priority > 4096):
         raise Exception("Rule priority must be between 100 and 4096")
 
-    if not rule.get('access'):
-        rule['access'] = 'Allow'
-
-    access_names = [member.value for member in self.nsg_models.SecurityRuleAccess]
-    if rule['access'] not in access_names:
-        raise Exception("Rule access must be one of [{0}]".format(', '.join(access_names)))
-
-    if not rule.get('destination_address_prefix'):
-        rule['destination_address_prefix'] = '*'
-
-    if not rule.get('source_address_prefix'):
-        rule['source_address_prefix'] = '*'
+    def check_plural(src, dest):
+        if isinstance(rule.get(src), list):
+            rule[dest] = rule[src]
+            rule[src] = None
 
-    if not rule.get('protocol'):
-        rule['protocol'] = '*'
+    check_plural('destination_address_prefix', 'destination_address_prefixes')
+    check_plural('source_address_prefix', 'source_address_prefixes')
+    check_plural('source_port_range', 'source_port_ranges')
+    check_plural('destination_port_range', 'destination_port_ranges')
 
-    protocol_names = [member.value for member in self.nsg_models.SecurityRuleProtocol]
-    if rule['protocol'] not in protocol_names:
-        raise Exception("Rule protocol must be one of [{0}]".format(', '.join(protocol_names)))
 
-    if not rule.get('direction'):
-        rule['direction'] = 'Inbound'
-
-    direction_names = [member.value for member in self.nsg_models.SecurityRuleDirection]
-    if rule['direction'] not in direction_names:
-        raise Exception("Rule direction must be one of [{0}]".format(', '.join(direction_names)))
-
-    if not rule.get('source_port_range'):
-        rule['source_port_range'] = '*'
+def compare_rules_change(old_list, new_list, purge_list):
+    old_list = old_list or []
+    new_list = new_list or []
+    changed = False
 
-    if not rule.get('destination_port_range'):
-        rule['destination_port_range'] = '*'
+    for old_rule in old_list:
+        matched = next((x for x in new_list if x['name'] == old_rule['name']), [])
+        if matched:  # if the new one is in the old list, check whether it is updated
+            changed = changed or compare_rules(old_rule, matched)
+        elif not purge_list:  # keep this rule
+            new_list.append(old_rule)
+        else:  # one rule is removed
+            changed = True
+    return changed, new_list
 
 
-def compare_rules(r, rule):
-    matched = False
+def compare_rules(old_rule, rule):
     changed = False
-    if r['name'] == rule['name']:
-        matched = True
-        if rule.get('description', None) != r['description']:
-            changed = True
-            r['description'] = rule.get('description', None)
-        if rule['protocol'] != r['protocol']:
-            changed = True
-            r['protocol'] = rule['protocol']
-        if str(rule['source_port_range']) != str(r['source_port_range']):
-            changed = True
-            r['source_port_range'] = str(rule['source_port_range'])
-        if str(rule['destination_port_range']) != str(r['destination_port_range']):
-            changed = True
-            r['destination_port_range'] = str(rule['destination_port_range'])
-        if rule['access'] != r['access']:
-            changed = True
-            r['access'] = rule['access']
-        if rule['priority'] != r['priority']:
-            changed = True
-            r['priority'] = rule['priority']
-        if rule['direction'] != r['direction']:
-            changed = True
-            r['direction'] = rule['direction']
-        if rule['source_address_prefix'] != str(r['source_address_prefix']):
-            changed = True
-            r['source_address_prefix'] = rule['source_address_prefix']
-    return matched, changed
+    if old_rule['name'] != rule['name']:
+        changed = True
+    if rule.get('description', None) != old_rule['description']:
+        changed = True
+    if rule['protocol'] != old_rule['protocol']:
+        changed = True
+    if str(rule['source_port_range']) != str(old_rule['source_port_range']):
+        changed = True
+    if str(rule['destination_port_range']) != str(old_rule['destination_port_range']):
+        changed = True
+    if rule['access'] != old_rule['access']:
+        changed = True
+    if rule['priority'] != old_rule['priority']:
+        changed = True
+    if rule['direction'] != old_rule['direction']:
+        changed = True
+    if str(rule['source_address_prefix']) != str(old_rule['source_address_prefix']):
+        changed = True
+    if str(rule['destination_address_prefix']) != str(old_rule['destination_address_prefix']):
+        changed = True
+    if set(rule.get('source_address_prefixes') or []) != set(old_rule.get('source_address_prefixes') or []):
+        changed = True
+    if set(rule.get('destination_address_prefixes') or []) != set(old_rule.get('destination_address_prefixes') or []):
+        changed = True
+    if set(rule.get('source_port_ranges') or []) != set(old_rule.get('source_port_ranges') or []):
+        changed = True
+    if set(rule.get('destination_port_ranges') or []) != set(old_rule.get('destination_port_ranges') or []):
+        changed = True
+    return changed
 
 
 def create_rule_instance(self, rule):
@@ -433,16 +421,19 @@ def create_rule_instance(self, rule):
     :return: SecurityRule
     '''
     return self.nsg_models.SecurityRule(
-        protocol=rule['protocol'],
-        source_address_prefix=rule['source_address_prefix'],
-        destination_address_prefix=rule['destination_address_prefix'],
-        access=rule['access'],
-        direction=rule['direction'],
-        id=rule.get('id', None),
         description=rule.get('description', None),
+        protocol=rule.get('protocol', None),
         source_port_range=rule.get('source_port_range', None),
         destination_port_range=rule.get('destination_port_range', None),
+        source_address_prefix=rule.get('source_address_prefix', None),
+        source_address_prefixes=rule.get('source_address_prefixes', None),
+        destination_address_prefix=rule.get('destination_address_prefix', None),
+        destination_address_prefixes=rule.get('destination_address_prefixes', None),
+        source_port_ranges=rule.get('source_port_ranges', None),
+        destination_port_ranges=rule.get('destination_port_ranges', None),
+        access=rule.get('access', None),
         priority=rule.get('priority', None),
+        direction=rule.get('direction', None),
         provisioning_state=rule.get('provisioning_state', None),
         name=rule.get('name', None),
         etag=rule.get('etag', None)
@@ -465,6 +456,10 @@ def create_rule_dict_from_obj(rule):
         destination_port_range=rule.destination_port_range,
         source_address_prefix=rule.source_address_prefix,
         destination_address_prefix=rule.destination_address_prefix,
+        source_port_ranges=rule.source_port_ranges,
+        destination_port_ranges=rule.destination_port_ranges,
+        source_address_prefixes=rule.source_address_prefixes,
+        destination_address_prefixes=rule.destination_address_prefixes,
         access=rule.access,
         priority=rule.priority,
         direction=rule.direction,
@@ -504,18 +499,32 @@ def create_network_security_group_dict(nsg):
     return results
 
 
+rule_spec = dict(
+    name=dict(type='str', required=True),
+    description=dict(type='str'),
+    protocol=dict(type='str', choices=['Udp', 'Tcp', '*'], default='*'),
+    source_port_range=dict(type='raw', default='*'),
+    destination_port_range=dict(type='raw', default='*'),
+    source_address_prefix=dict(type='raw', default='*'),
+    destination_address_prefix=dict(type='raw', default='*'),
+    access=dict(type='str', choices=['Allow', 'Deny'], default='Allow'),
+    priority=dict(type='int', required=True),
+    direction=dict(type='str', choices=['Inbound', 'Outbound'], default='Inbound')
+)
+
+
 class AzureRMSecurityGroup(AzureRMModuleBase):
 
     def __init__(self):
 
         self.module_arg_spec = dict(
-            default_rules=dict(type='list'),
+            default_rules=dict(type='list', elements='dict', options=rule_spec),
             location=dict(type='str'),
             name=dict(type='str', required=True),
             purge_default_rules=dict(type='bool', default=False),
             purge_rules=dict(type='bool', default=False),
             resource_group=dict(required=True, type='str'),
-            rules=dict(type='list'),
+            rules=dict(type='list', elements='dict', options=rule_spec),
             state=dict(type='str', default='present', choices=['present', 'absent']),
         )
 
@@ -591,53 +600,19 @@ def exec_module(self, **kwargs):
             # update the security group
             self.log("Update security group {0}".format(self.name))
 
-            if self.rules:
-                for rule in self.rules:
-                    rule_matched = False
-                    for r in results['rules']:
-                        match, changed = compare_rules(r, rule)
-                        if changed:
-                            changed = True
-                        if match:
-                            rule_matched = True
-
-                    if not rule_matched:
-                        changed = True
-                        results['rules'].append(rule)
-
-            if self.purge_rules:
-                new_rules = []
-                for rule in results['rules']:
-                    for r in self.rules:
-                        if rule['name'] == r['name']:
-                            new_rules.append(rule)
-                results['rules'] = new_rules
-
-            if self.default_rules:
-                for rule in self.default_rules:
-                    rule_matched = False
-                    for r in results['default_rules']:
-                        match, changed = compare_rules(r, rule)
-                        if changed:
-                            changed = True
-                        if match:
-                            rule_matched = True
-                    if not rule_matched:
-                        changed = True
-                        results['default_rules'].append(rule)
-
-            if self.purge_default_rules:
-                new_default_rules = []
-                for rule in results['default_rules']:
-                    for r in self.default_rules:
-                        if rule['name'] == r['name']:
-                            new_default_rules.append(rule)
-                results['default_rules'] = new_default_rules
-
             update_tags, results['tags'] = self.update_tags(results['tags'])
             if update_tags:
                 changed = True
 
+            rule_changed, new_rule = compare_rules_change(results['rules'], self.rules, self.purge_rules)
+            if rule_changed:
+                changed = True
+                results['rules'] = new_rule
+            rule_changed, new_rule = compare_rules_change(results['default_rules'], self.default_rules, self.purge_default_rules)
+            if rule_changed:
+                changed = True
+                results['default_rules'] = new_rule
+
             self.results['changed'] = changed
             self.results['state'] = results
             if not self.check_mode and changed:


Test patch:
diff --git a/test/integration/targets/azure_rm_securitygroup/tasks/main.yml b/test/integration/targets/azure_rm_securitygroup/tasks/main.yml
--- a/test/integration/targets/azure_rm_securitygroup/tasks/main.yml
+++ b/test/integration/targets/azure_rm_securitygroup/tasks/main.yml
@@ -57,7 +57,7 @@
 - assert: 
       that:
           - "{{ output.state.rules | length }} == 3"
-          - output.state.rules[1].source_address_prefix == '174.108.158.0/24'
+          - output.state.rules[0].source_address_prefix == '174.108.158.0/24'
 
 - name: Test idempotence
   azure_rm_securitygroup:
@@ -128,6 +128,59 @@
       that:
           - azure_securitygroups | length > 0
 
+- name: Create security group with source_address_prefixes
+  azure_rm_securitygroup:
+      resource_group: "{{ resource_group }}"
+      name: mysecgroup
+      tags:
+          testing: testing
+          delete: on-exit
+          foo: bar
+      purge_rules: yes
+      rules:
+          - name: AllowSSH
+            protocol: Tcp
+            source_address_prefix:
+            - 52.100.120.240
+            - 53.100.250.190
+            - 54.110.200.200
+            destination_port_range: 22
+            access: Allow
+            priority: 101
+            direction: Inbound
+  register: output
+
+- assert:
+    that:
+    - "{{ output.state.rules | length }} == 1"
+    - "{{ output.state.rules[0].source_address_prefixes | length }} == 3"
+    - not output.state.rules[0].source_address_prefix
+
+- name: Create security group with source_address_prefixes(idempontent)
+  azure_rm_securitygroup:
+      resource_group: "{{ resource_group }}"
+      name: mysecgroup
+      tags:
+          testing: testing
+          delete: on-exit
+          foo: bar
+      purge_rules: yes
+      rules:
+          - name: AllowSSH
+            protocol: Tcp
+            source_address_prefix:
+            - 52.100.120.240
+            - 53.100.250.190
+            - 54.110.200.200
+            destination_port_range: 22
+            access: Allow
+            priority: 101
+            direction: Inbound
+  register: output
+
+- assert:
+      that: not output.changed
+
 - name: Delete all security groups
   azure_rm_securitygroup:
       resource_group: "{{ resource_group }}"