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 # 19 import itertools 20 import logging 21 from collections import namedtuple 22 23 import networkx as nx 24 from networkx.exception import NetworkXError, NetworkXNoPath 25 26 from .descriptors import EdgeAttrIntMax, EdgeAttrList 27 28 __all__ = ['InfoFlowAnalysis'] 29 30 # Return values for the analysis 31 # are in the following tuple format: 32 step_output = namedtuple("step", ["source", 33 "target", 34 "rules"]) 35 36 37 class InfoFlowAnalysis(object): 38 39 """Information flow analysis.""" 40 41 def __init__(self, policy, perm_map, min_weight=1, exclude=None): 42 """ 43 Parameters: 44 policy The policy to analyze. 45 perm_map The permission map or path to the permission map file. 46 minweight The minimum permission weight to include in the analysis. 47 (default is 1) 48 exclude The types excluded from the information flow analysis. 49 (default is none) 50 """ 51 self.log = logging.getLogger(self.__class__.__name__) 52 53 self.policy = policy 54 55 self.min_weight = min_weight 56 self.perm_map = perm_map 57 self.exclude = exclude 58 self.rebuildgraph = True 59 self.rebuildsubgraph = True 60 61 self.G = nx.DiGraph() 62 self.subG = None 63 64 @property 65 def min_weight(self): 66 return self._min_weight 67 68 @min_weight.setter 69 def min_weight(self, weight): 70 if not 1 <= weight <= 10: 71 raise ValueError( 72 "Min information flow weight must be an integer 1-10.") 73 74 self._min_weight = weight 75 self.rebuildsubgraph = True 76 77 @property 78 def perm_map(self): 79 return self._perm_map 80 81 @perm_map.setter 82 def perm_map(self, perm_map): 83 self._perm_map = perm_map 84 self.rebuildgraph = True 85 self.rebuildsubgraph = True 86 87 @property 88 def exclude(self): 89 return self._exclude 90 91 @exclude.setter 92 def exclude(self, types): 93 if types: 94 self._exclude = [self.policy.lookup_type(t) for t in types] 95 else: 96 self._exclude = [] 97 98 self.rebuildsubgraph = True 99 100 def shortest_path(self, source, target): 101 """ 102 Generator which yields one shortest path between the source 103 and target types (there may be more). 104 105 Parameters: 106 source The source type. 107 target The target type. 108 109 Yield: generator(steps) 110 111 steps Yield: tuple(source, target, rules) 112 113 source The source type for this step of the information flow. 114 target The target type for this step of the information flow. 115 rules The list of rules creating this information flow step. 116 """ 117 s = self.policy.lookup_type(source) 118 t = self.policy.lookup_type(target) 119 120 if self.rebuildsubgraph: 121 self._build_subgraph() 122 123 self.log.info("Generating one shortest path from {0} to {1}...".format(s, t)) 124 125 try: 126 yield self.__generate_steps(nx.shortest_path(self.subG, s, t)) 127 except (NetworkXNoPath, NetworkXError): 128 # NetworkXError: the type is valid but not in graph, e.g. 129 # excluded or disconnected due to min weight 130 # NetworkXNoPath: no paths or the target type is 131 # not in the graph 132 pass 133 134 def all_paths(self, source, target, maxlen=2): 135 """ 136 Generator which yields all paths between the source and target 137 up to the specified maximum path length. This algorithm 138 tends to get very expensive above 3-5 steps, depending 139 on the policy complexity. 140 141 Parameters: 142 source The source type. 143 target The target type. 144 maxlen Maximum length of paths. 145 146 Yield: generator(steps) 147 148 steps Yield: tuple(source, target, rules) 149 150 source The source type for this step of the information flow. 151 target The target type for this step of the information flow. 152 rules The list of rules creating this information flow step. 153 """ 154 if maxlen < 1: 155 raise ValueError("Maximum path length must be positive.") 156 157 s = self.policy.lookup_type(source) 158 t = self.policy.lookup_type(target) 159 160 if self.rebuildsubgraph: 161 self._build_subgraph() 162 163 self.log.info("Generating all paths from {0} to {1}, max len {2}...".format(s, t, maxlen)) 164 165 try: 166 for path in nx.all_simple_paths(self.subG, s, t, maxlen): 167 yield self.__generate_steps(path) 168 except (NetworkXNoPath, NetworkXError): 169 # NetworkXError: the type is valid but not in graph, e.g. 170 # excluded or disconnected due to min weight 171 # NetworkXNoPath: no paths or the target type is 172 # not in the graph 173 pass 174 175 def all_shortest_paths(self, source, target): 176 """ 177 Generator which yields all shortest paths between the source 178 and target types. 179 180 Parameters: 181 source The source type. 182 target The target type. 183 184 Yield: generator(steps) 185 186 steps Yield: tuple(source, target, rules) 187 188 source The source type for this step of the information flow. 189 target The target type for this step of the information flow. 190 rules The list of rules creating this information flow step. 191 """ 192 s = self.policy.lookup_type(source) 193 t = self.policy.lookup_type(target) 194 195 if self.rebuildsubgraph: 196 self._build_subgraph() 197 198 self.log.info("Generating all shortest paths from {0} to {1}...".format(s, t)) 199 200 try: 201 for path in nx.all_shortest_paths(self.subG, s, t): 202 yield self.__generate_steps(path) 203 except (NetworkXNoPath, NetworkXError, KeyError): 204 # NetworkXError: the type is valid but not in graph, e.g. 205 # excluded or disconnected due to min weight 206 # NetworkXNoPath: no paths or the target type is 207 # not in the graph 208 # KeyError: work around NetworkX bug 209 # when the source node is not in the graph 210 pass 211 212 def infoflows(self, type_, out=True): 213 """ 214 Generator which yields all information flows in/out of a 215 specified source type. 216 217 Parameters: 218 source The starting type. 219 220 Keyword Parameters: 221 out If true, information flows out of the type will 222 be returned. If false, information flows in to the 223 type will be returned. Default is true. 224 225 Yield: generator(steps) 226 227 steps A generator that returns the tuple of 228 source, target, and rules for each 229 information flow. 230 """ 231 s = self.policy.lookup_type(type_) 232 233 if self.rebuildsubgraph: 234 self._build_subgraph() 235 236 self.log.info("Generating all infoflows out of {0}...".format(s)) 237 238 if out: 239 flows = self.subG.out_edges_iter(s) 240 else: 241 flows = self.subG.in_edges_iter(s) 242 243 try: 244 for source, target in flows: 245 edge = Edge(self.subG, source, target) 246 yield step_output(source, target, edge.rules) 247 except NetworkXError: 248 # NetworkXError: the type is valid but not in graph, e.g. 249 # excluded or disconnected due to min weight 250 pass 251 252 def get_stats(self): # pragma: no cover 253 """ 254 Get the information flow graph statistics. 255 256 Return: tuple(nodes, edges) 257 258 nodes The number of nodes (types) in the graph. 259 edges The number of edges (information flows between types) 260 in the graph. 261 """ 262 return (self.G.number_of_nodes(), self.G.number_of_edges()) 263 264 # 265 # Internal functions follow 266 # 267 268 def __generate_steps(self, path): 269 """ 270 Generator which returns the source, target, and associated rules 271 for each information flow step. 272 273 Parameter: 274 path A list of graph node names representing an information flow path. 275 276 Yield: tuple(source, target, rules) 277 278 source The source type for this step of the information flow. 279 target The target type for this step of the information flow. 280 rules The list of rules creating this information flow step. 281 """ 282 for s in range(1, len(path)): 283 edge = Edge(self.subG, path[s - 1], path[s]) 284 yield step_output(edge.source, edge.target, edge.rules) 285 286 # 287 # 288 # Graph building functions 289 # 290 # 291 # 1. _build_graph determines the flow in each direction for each TE 292 # rule and then expands the rule. All information flows are 293 # included in this main graph: memory is traded off for efficiency 294 # as the main graph should only need to be rebuilt if permission 295 # weights change. 296 # 2. _build_subgraph derives a subgraph which removes all excluded 297 # types (nodes) and edges (information flows) which are below the 298 # minimum weight. This subgraph is rebuilt only if the main graph 299 # is rebuilt or the minimum weight or excluded types change. 300 301 def _build_graph(self): 302 self.G.clear() 303 304 self.perm_map.map_policy(self.policy) 305 306 self.log.info("Building graph from {0}...".format(self.policy)) 307 308 for rule in self.policy.terules(): 309 if rule.ruletype != "allow": 310 continue 311 312 (rweight, wweight) = self.perm_map.rule_weight(rule) 313 314 for s, t in itertools.product(rule.source.expand(), rule.target.expand()): 315 # only add flows if they actually flow 316 # in or out of the source type type 317 if s != t: 318 if wweight: 319 edge = Edge(self.G, s, t, create=True) 320 edge.rules.append(rule) 321 edge.weight = wweight 322 323 if rweight: 324 edge = Edge(self.G, t, s, create=True) 325 edge.rules.append(rule) 326 edge.weight = rweight 327 328 self.rebuildgraph = False 329 self.rebuildsubgraph = True 330 self.log.info("Completed building graph.") 331 332 def _build_subgraph(self): 333 if self.rebuildgraph: 334 self._build_graph() 335 336 self.log.info("Building subgraph...") 337 self.log.debug("Excluding {0!r}".format(self.exclude)) 338 self.log.debug("Min weight {0}".format(self.min_weight)) 339 340 # delete excluded types from subgraph 341 nodes = [n for n in self.G.nodes() if n not in self.exclude] 342 self.subG = self.G.subgraph(nodes) 343 344 # delete edges below minimum weight. 345 # no need if weight is 1, since that 346 # does not exclude any edges. 347 if self.min_weight > 1: 348 delete_list = [] 349 for s, t in self.subG.edges_iter(): 350 edge = Edge(self.subG, s, t) 351 if edge.weight < self.min_weight: 352 delete_list.append(edge) 353 354 self.subG.remove_edges_from(delete_list) 355 356 self.rebuildsubgraph = False 357 self.log.info("Completed building subgraph.") 358 359 360 class Edge(object): 361 362 """ 363 A graph edge. Also used for returning information flow steps. 364 365 Parameters: 366 source The source type of the edge. 367 target The target type of the edge. 368 369 Keyword Parameters: 370 create (T/F) create the edge if it does not exist. 371 The default is False. 372 """ 373 374 rules = EdgeAttrList('rules') 375 376 # use capacity to store the info flow weight so 377 # we can use network flow algorithms naturally. 378 # The weight for each edge is 1 since each info 379 # flow step is no more costly than another 380 # (see below add_edge() call) 381 weight = EdgeAttrIntMax('capacity') 382 383 def __init__(self, graph, source, target, create=False): 384 self.G = graph 385 self.source = source 386 self.target = target 387 388 # a bit of a hack to make edges work 389 # in NetworkX functions that work on 390 # 2-tuples of (source, target) 391 # (see __getitem__ below) 392 self.st_tuple = (source, target) 393 394 if not self.G.has_edge(source, target): 395 if create: 396 self.G.add_edge(source, target, weight=1) 397 self.rules = None 398 self.weight = None 399 else: 400 raise ValueError("Edge does not exist in graph") 401 402 def __getitem__(self, key): 403 return self.st_tuple[key] 404