1# Copyright 2014-2015, Tresys Technology, LLC
2#
3# This file is part of SETools.
4#
5# SETools is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as
7# published by the Free Software Foundation, either version 2.1 of
8# the License, or (at your option) any later version.
9#
10# SETools is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with SETools.  If not, see
17# <http://www.gnu.org/licenses/>.
18#
19try:
20    import ipaddress
21except ImportError:  # pragma: no cover
22    pass
23
24import logging
25from socket import AF_INET, AF_INET6
26
27from .mixins import MatchContext
28from .query import PolicyQuery
29
30
31class NodeconQuery(MatchContext, PolicyQuery):
32
33    """
34    Query nodecon statements.
35
36    Parameter:
37    policy          The policy to query.
38
39    Keyword Parameters/Class attributes:
40    network         The IPv4/IPv6 address or IPv4/IPv6 network address
41                    with netmask, e.g. 192.168.1.0/255.255.255.0 or
42                    "192.168.1.0/24".
43    network_overlap If true, the net will match if it overlaps with
44                    the nodecon's network instead of equality.
45    ip_version      The IP version of the nodecon to match. (socket.AF_INET
46                    for IPv4 or socket.AF_INET6 for IPv6)
47    user            The criteria to match the context's user.
48    user_regex      If true, regular expression matching
49                    will be used on the user.
50    role            The criteria to match the context's role.
51    role_regex      If true, regular expression matching
52                    will be used on the role.
53    type_           The criteria to match the context's type.
54    type_regex      If true, regular expression matching
55                    will be used on the type.
56    range_          The criteria to match the context's range.
57    range_subset    If true, the criteria will match if it is a subset
58                    of the context's range.
59    range_overlap   If true, the criteria will match if it overlaps
60                    any of the context's range.
61    range_superset  If true, the criteria will match if it is a superset
62                    of the context's range.
63    range_proper    If true, use proper superset/subset operations.
64                    No effect if not using set operations.
65    """
66
67    _network = None
68    network_overlap = False
69    _ip_version = None
70
71    @property
72    def ip_version(self):
73        return self._ip_version
74
75    @ip_version.setter
76    def ip_version(self, value):
77        if value:
78            if not (value == AF_INET or value == AF_INET6):
79                raise ValueError(
80                    "The address family must be {0} for IPv4 or {1} for IPv6.".
81                    format(AF_INET, AF_INET6))
82
83            self._ip_version = value
84        else:
85            self._ip_version = None
86
87    @property
88    def network(self):
89        return self._network
90
91    @network.setter
92    def network(self, value):
93        if value:
94            try:
95                self._network = ipaddress.ip_network(value)
96            except NameError:  # pragma: no cover
97                raise RuntimeError("Nodecon IP address/network functions require Python 3.3+.")
98        else:
99            self._network = None
100
101    def __init__(self, policy, **kwargs):
102        super(NodeconQuery, self).__init__(policy, **kwargs)
103        self.log = logging.getLogger(__name__)
104
105    def results(self):
106        """Generator which yields all matching nodecons."""
107        self.log.info("Generating nodecon results from {0.policy}".format(self))
108        self.log.debug("Network: {0.network!r}, overlap: {0.network_overlap}".format(self))
109        self.log.debug("IP Version: {0.ip_version}".format(self))
110        self._match_context_debug(self.log)
111
112        for nodecon in self.policy.nodecons():
113
114            if self.network:
115                try:
116                    netmask = ipaddress.ip_address(nodecon.netmask)
117                except NameError:  # pragma: no cover
118                    # Should never actually hit this since the self.network
119                    # setter raises the same exception.
120                    raise RuntimeError("Nodecon IP address/network functions require Python 3.3+.")
121
122                # Python 3.3's IPv6Network constructor does not support
123                # expanded netmasks, only CIDR numbers. Convert netmask
124                # into CIDR.
125                # This is Brian Kernighan's method for counting set bits.
126                # If the netmask happens to be invalid, this will
127                # not detect it.
128                CIDR = 0
129                int_netmask = int(netmask)
130                while int_netmask:
131                    int_netmask &= int_netmask - 1
132                    CIDR += 1
133
134                net = ipaddress.ip_network('{0}/{1}'.format(nodecon.address, CIDR))
135
136                if self.network_overlap:
137                    if not self.network.overlaps(net):
138                        continue
139                else:
140                    if not net == self.network:
141                        continue
142
143            if self.ip_version and self.ip_version != nodecon.ip_version:
144                continue
145
146            if not self._match_context(nodecon.context):
147                continue
148
149            yield nodecon
150