# Copyright 2018 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import uuid
from c7n.actions import BaseAction
from c7n.filters import Filter, FilterValidationError
from c7n.filters.core import PolicyValidationError
from c7n.utils import type_schema
from c7n_azure.provider import resources
from c7n_azure.resources.arm import ArmResourceManager
from c7n_azure.utils import StringUtils, PortsRangeHelper
from msrestazure.azure_exceptions import CloudError
[docs]@resources.register('networksecuritygroup')
class NetworkSecurityGroup(ArmResourceManager):
[docs] class resource_type(object):
service = 'azure.mgmt.network'
client = 'NetworkManagementClient'
enum_spec = ('network_security_groups', 'list_all', None)
id = 'id'
name = 'name'
default_report_fields = (
'name',
'location',
'resourceGroup'
)
DIRECTION = 'direction'
PORTS = 'ports'
MATCH = 'match'
EXCEPT_PORTS = 'exceptPorts'
IP_PROTOCOL = 'ipProtocol'
ACCESS = 'access'
ALLOW_OPERATION = 'Allow'
DENY_OPERATION = 'Deny'
PRIORITY_STEP = 10
[docs]class NetworkSecurityGroupFilter(Filter):
"""
Filter Network Security Groups using opened/closed ports configuration
"""
schema = {
'type': 'object',
'properties': {
'type': {'enum': []},
MATCH: {'type': 'string', 'enum': ['all', 'any']},
PORTS: {'type': 'string'},
EXCEPT_PORTS: {'type': 'string'},
IP_PROTOCOL: {'type': 'string', 'enum': ['TCP', 'UDP', '*']},
ACCESS: {'type': 'string', 'enum': [ALLOW_OPERATION, DENY_OPERATION]},
},
'required': ['type', ACCESS]
}
[docs] def validate(self):
# Check that variable values are valid
if PORTS in self.data:
if not PortsRangeHelper.validate_ports_string(self.data[PORTS]):
raise FilterValidationError("ports string has wrong format.")
if EXCEPT_PORTS in self.data:
if not PortsRangeHelper.validate_ports_string(self.data[EXCEPT_PORTS]):
raise FilterValidationError("exceptPorts string has wrong format.")
return True
[docs] def process(self, network_security_groups, event=None):
# Get variables
self.ip_protocol = self.data.get(IP_PROTOCOL, '*')
self.IsAllowed = StringUtils.equal(self.data.get(ACCESS), ALLOW_OPERATION)
self.match = self.data.get(MATCH, 'all')
# Calculate ports from the settings:
# If ports not specified -- assuming the entire range
# If except_ports not specifed -- nothing
ports_set = PortsRangeHelper.get_ports_set_from_string(self.data.get(PORTS, '0-65535'))
except_set = PortsRangeHelper.get_ports_set_from_string(self.data.get(EXCEPT_PORTS, ''))
self.ports = ports_set.difference(except_set)
nsgs = [nsg for nsg in network_security_groups if self._check_nsg(nsg)]
return nsgs
def _check_nsg(self, nsg):
nsg_ports = PortsRangeHelper.build_ports_dict(nsg, self.direction_key, self.ip_protocol)
num_allow_ports = len([p for p in self.ports if nsg_ports.get(p)])
num_deny_ports = len(self.ports) - num_allow_ports
if self.match == 'all':
if self.IsAllowed:
return num_deny_ports == 0
else:
return num_allow_ports == 0
if self.match == 'any':
if self.IsAllowed:
return num_allow_ports > 0
else:
return num_deny_ports > 0
[docs]@NetworkSecurityGroup.filter_registry.register('ingress')
class IngressFilter(NetworkSecurityGroupFilter):
direction_key = 'Inbound'
schema = type_schema('ingress', rinherit=NetworkSecurityGroupFilter.schema)
[docs]@NetworkSecurityGroup.filter_registry.register('egress')
class EgressFilter(NetworkSecurityGroupFilter):
direction_key = 'Outbound'
schema = type_schema('egress', rinherit=NetworkSecurityGroupFilter.schema)
[docs]class NetworkSecurityGroupPortsAction(BaseAction):
"""
Action to perform on Network Security Groups
"""
schema = {
'type': 'object',
'properties': {
'type': {'enum': []},
PORTS: {'type': 'string'},
EXCEPT_PORTS: {'type': 'string'},
IP_PROTOCOL: {'type': 'string', 'enum': ['TCP', 'UDP', '*']},
DIRECTION: {'type': 'string', 'enum': ['Inbound', 'Outbound']}
},
'required': ['type', DIRECTION]
}
[docs] def validate(self):
# Check that variable values are valid
if PORTS in self.data:
if not PortsRangeHelper.validate_ports_string(self.data[PORTS]):
raise PolicyValidationError("ports string has wrong format.")
if EXCEPT_PORTS in self.data:
if not PortsRangeHelper.validate_ports_string(self.data[EXCEPT_PORTS]):
raise PolicyValidationError("exceptPorts string has wrong format.")
return True
def _build_ports_strings(self, nsg, direction_key, ip_protocol):
nsg_ports = PortsRangeHelper.build_ports_dict(nsg, direction_key, ip_protocol)
IsAllowed = StringUtils.equal(self.access_action, ALLOW_OPERATION)
# Find ports with different access level from NSG and this action
diff_ports = sorted([p for p in self.action_ports if nsg_ports.get(p, False) != IsAllowed])
return PortsRangeHelper.get_ports_strings_from_list(diff_ports)
[docs] def process(self, network_security_groups):
ip_protocol = self.data.get(IP_PROTOCOL, '*')
direction = self.data[DIRECTION]
# Build a list of ports described in the action.
ports = PortsRangeHelper.get_ports_set_from_string(self.data.get(PORTS, '0-65535'))
except_ports = PortsRangeHelper.get_ports_set_from_string(self.data.get(EXCEPT_PORTS, ''))
self.action_ports = ports.difference(except_ports)
for nsg in network_security_groups:
nsg_name = nsg['name']
resource_group = nsg['resourceGroup']
# Get list of ports to Deny or Allow access to.
ports = self._build_ports_strings(nsg, direction, ip_protocol)
if not ports:
# If its empty, it means NSG already blocks/allows access to all ports,
# no need to change.
self.manager.log.info("Network security group %s satisfies provided "
"ports configuration, no actions scheduled.", nsg_name)
continue
rules = nsg['properties']['securityRules']
rules = sorted(rules, key=lambda k: k['properties']['priority'])
rules = [r for r in rules
if StringUtils.equal(r['properties']['direction'], direction)]
lowest_priority = rules[0]['properties']['priority'] if len(rules) > 0 else 4096
# Create new top-priority rule to allow/block ports from the action.
rule_name = 'c7n-policy-' + str(uuid.uuid1())
new_rule = {
'name': rule_name,
'properties': {
'access': self.access_action,
'destinationAddressPrefix': '*',
'destinationPortRanges': ports,
'direction': self.data[DIRECTION],
'priority': lowest_priority - PRIORITY_STEP,
'protocol': ip_protocol,
'sourceAddressPrefix': '*',
'sourcePortRange': '*',
}
}
self.manager.log.info("NSG %s. Creating new rule to %s access for ports %s",
nsg_name, self.access_action, ports)
try:
self.manager.get_client().security_rules.create_or_update(
resource_group,
nsg_name,
rule_name,
new_rule
)
except CloudError as e:
self.manager.log.error('Failed to create or update security rule for %s NSG.',
nsg_name)
self.manager.log.error(e)
[docs]@NetworkSecurityGroup.action_registry.register('close')
class CloseRules(NetworkSecurityGroupPortsAction):
"""
Deny access to Security Rule
"""
schema = type_schema('close', rinherit=NetworkSecurityGroupPortsAction.schema)
access_action = DENY_OPERATION
[docs]@NetworkSecurityGroup.action_registry.register('open')
class OpenRules(NetworkSecurityGroupPortsAction):
"""
Allow access to Security Rule
"""
schema = type_schema('open', rinherit=NetworkSecurityGroupPortsAction.schema)
access_action = ALLOW_OPERATION