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 # pylint: disable=unsubscriptable-object 20 21 import itertools 22 import logging 23 from collections import defaultdict, namedtuple 24 25 import networkx as nx 26 from networkx.exception import NetworkXError, NetworkXNoPath 27 28 from .descriptors import EdgeAttrDict, EdgeAttrList 29 30 __all__ = ['DomainTransitionAnalysis'] 31 32 # Return values for the analysis 33 # are in the following tuple formats: 34 step_output = namedtuple("step", ["source", 35 "target", 36 "transition", 37 "entrypoints", 38 "setexec", 39 "dyntransition", 40 "setcurrent"]) 41 42 entrypoint_output = namedtuple("entrypoints", ["name", 43 "entrypoint", 44 "execute", 45 "type_transition"]) 46 47 48 class DomainTransitionAnalysis(object): 49 50 """Domain transition analysis.""" 51 52 def __init__(self, policy, reverse=False, exclude=None): 53 """ 54 Parameter: 55 policy The policy to analyze. 56 """ 57 self.log = logging.getLogger(__name__) 58 59 self.policy = policy 60 self.exclude = exclude 61 self.reverse = reverse 62 self.rebuildgraph = True 63 self.rebuildsubgraph = True 64 self.G = nx.DiGraph() 65 self.subG = None 66 67 @property 68 def reverse(self): 69 return self._reverse 70 71 @reverse.setter 72 def reverse(self, direction): 73 self._reverse = bool(direction) 74 self.rebuildsubgraph = True 75 76 @property 77 def exclude(self): 78 return self._exclude 79 80 @exclude.setter 81 def exclude(self, types): 82 if types: 83 self._exclude = [self.policy.lookup_type(t) for t in types] 84 else: 85 self._exclude = [] 86 87 self.rebuildsubgraph = True 88 89 def shortest_path(self, source, target): 90 """ 91 Generator which yields one shortest domain transition path 92 between the source and target types (there may be more). 93 94 Parameters: 95 source The source type. 96 target The target type. 97 98 Yield: generator(steps) 99 100 steps A generator that returns the tuple of 101 source, target, and rules for each 102 domain transition. 103 """ 104 s = self.policy.lookup_type(source) 105 t = self.policy.lookup_type(target) 106 107 if self.rebuildsubgraph: 108 self._build_subgraph() 109 110 self.log.info("Generating one domain transition path from {0} to {1}...".format(s, t)) 111 112 try: 113 yield self.__generate_steps(nx.shortest_path(self.subG, s, t)) 114 except (NetworkXNoPath, NetworkXError): 115 # NetworkXError: the type is valid but not in graph, e.g. excluded 116 # NetworkXNoPath: no paths or the target type is 117 # not in the graph 118 pass 119 120 def all_paths(self, source, target, maxlen=2): 121 """ 122 Generator which yields all domain transition paths between 123 the source and target up to the specified maximum path 124 length. 125 126 Parameters: 127 source The source type. 128 target The target type. 129 maxlen Maximum length of paths. 130 131 Yield: generator(steps) 132 133 steps A generator that returns the tuple of 134 source, target, and rules for each 135 domain transition. 136 """ 137 if maxlen < 1: 138 raise ValueError("Maximum path length must be positive.") 139 140 s = self.policy.lookup_type(source) 141 t = self.policy.lookup_type(target) 142 143 if self.rebuildsubgraph: 144 self._build_subgraph() 145 146 self.log.info("Generating all domain transition paths from {0} to {1}, max length {2}...". 147 format(s, t, maxlen)) 148 149 try: 150 for path in nx.all_simple_paths(self.subG, s, t, maxlen): 151 yield self.__generate_steps(path) 152 except (NetworkXNoPath, NetworkXError): 153 # NetworkXError: the type is valid but not in graph, e.g. excluded 154 # NetworkXNoPath: no paths or the target type is 155 # not in the graph 156 pass 157 158 def all_shortest_paths(self, source, target): 159 """ 160 Generator which yields all shortest domain transition paths 161 between the source and target types. 162 163 Parameters: 164 source The source type. 165 target The target type. 166 167 Yield: generator(steps) 168 169 steps A generator that returns the tuple of 170 source, target, and rules for each 171 domain transition. 172 """ 173 s = self.policy.lookup_type(source) 174 t = self.policy.lookup_type(target) 175 176 if self.rebuildsubgraph: 177 self._build_subgraph() 178 179 self.log.info("Generating all shortest domain transition paths from {0} to {1}...". 180 format(s, t)) 181 182 try: 183 for path in nx.all_shortest_paths(self.subG, s, t): 184 yield self.__generate_steps(path) 185 except (NetworkXNoPath, NetworkXError, KeyError): 186 # NetworkXError: the type is valid but not in graph, e.g. excluded 187 # NetworkXNoPath: no paths or the target type is 188 # not in the graph 189 # KeyError: work around NetworkX bug 190 # when the source node is not in the graph 191 pass 192 193 def transitions(self, type_): 194 """ 195 Generator which yields all domain transitions out of a 196 specified source type. 197 198 Parameters: 199 type_ The starting type. 200 201 Yield: generator(steps) 202 203 steps A generator that returns the tuple of 204 source, target, and rules for each 205 domain transition. 206 """ 207 s = self.policy.lookup_type(type_) 208 209 if self.rebuildsubgraph: 210 self._build_subgraph() 211 212 self.log.info("Generating all domain transitions {1} {0}". 213 format(s, "in to" if self.reverse else "out from")) 214 215 try: 216 for source, target in self.subG.out_edges_iter(s): 217 edge = Edge(self.subG, source, target) 218 219 if self.reverse: 220 real_source, real_target = target, source 221 else: 222 real_source, real_target = source, target 223 224 yield step_output(real_source, real_target, 225 edge.transition, 226 self.__generate_entrypoints(edge), 227 edge.setexec, 228 edge.dyntransition, 229 edge.setcurrent) 230 231 except NetworkXError: 232 # NetworkXError: the type is valid but not in graph, e.g. excluded 233 pass 234 235 def get_stats(self): # pragma: no cover 236 """ 237 Get the domain transition graph statistics. 238 239 Return: str 240 """ 241 if self.rebuildgraph: 242 self._build_graph() 243 244 return nx.info(self.G) 245 246 # 247 # Internal functions follow 248 # 249 @staticmethod 250 def __generate_entrypoints(edge): 251 """ 252 Creates a list of entrypoint, execute, and 253 type_transition rules for each entrypoint. 254 255 Parameter: 256 data The dictionary of entrypoints. 257 258 Return: list of tuple(type, entry, exec, trans) 259 260 type The entrypoint type. 261 entry The list of entrypoint rules. 262 exec The list of execute rules. 263 trans The list of type_transition rules. 264 """ 265 return [entrypoint_output(e, edge.entrypoint[e], edge.execute[e], edge.type_transition[e]) 266 for e in edge.entrypoint] 267 268 def __generate_steps(self, path): 269 """ 270 Generator which yields the source, target, and associated rules 271 for each domain transition. 272 273 Parameter: 274 path A list of graph node names representing an information flow path. 275 276 Yield: tuple(source, target, transition, entrypoints, 277 setexec, dyntransition, setcurrent) 278 279 source The source type for this step of the domain transition. 280 target The target type for this step of the domain transition. 281 transition The list of transition rules. 282 entrypoints Generator which yields entrypoint-related rules. 283 setexec The list of setexec rules. 284 dyntranstion The list of dynamic transition rules. 285 setcurrent The list of setcurrent rules. 286 """ 287 288 for s in range(1, len(path)): 289 source = path[s - 1] 290 target = path[s] 291 edge = Edge(self.subG, source, target) 292 293 # Yield the actual source and target. 294 # The above perspective is reversed 295 # if the graph has been reversed. 296 if self.reverse: 297 real_source, real_target = target, source 298 else: 299 real_source, real_target = source, target 300 301 yield step_output(real_source, real_target, 302 edge.transition, 303 self.__generate_entrypoints(edge), 304 edge.setexec, 305 edge.dyntransition, 306 edge.setcurrent) 307 308 # 309 # Graph building functions 310 # 311 312 # Domain transition requirements: 313 # 314 # Standard transitions a->b: 315 # allow a b:process transition; 316 # allow a b_exec:file execute; 317 # allow b b_exec:file entrypoint; 318 # 319 # and at least one of: 320 # allow a self:process setexec; 321 # type_transition a b_exec:process b; 322 # 323 # Dynamic transition x->y: 324 # allow x y:process dyntransition; 325 # allow x self:process setcurrent; 326 # 327 # Algorithm summary: 328 # 1. iterate over all rules 329 # 1. skip non allow/type_transition rules 330 # 2. if process transition or dyntransition, create edge, 331 # initialize rule lists, add the (dyn)transition rule 332 # 3. if process setexec or setcurrent, add to appropriate dict 333 # keyed on the subject 334 # 4. if file exec, entrypoint, or type_transition:process, 335 # add to appropriate dict keyed on subject,object. 336 # 2. Iterate over all graph edges: 337 # 1. if there is a transition rule (else add to invalid 338 # transition list): 339 # 1. use set intersection to find matching exec 340 # and entrypoint rules. If none, add to invalid 341 # transition list. 342 # 2. for each valid entrypoint, add rules to the 343 # edge's lists if there is either a 344 # type_transition for it or the source process 345 # has setexec permissions. 346 # 3. If there are neither type_transitions nor 347 # setexec permissions, add to the invalid 348 # transition list 349 # 2. if there is a dyntransition rule (else add to invalid 350 # dyntrans list): 351 # 1. If the source has a setcurrent rule, add it 352 # to the edge's list, else add to invalid 353 # dyntransition list. 354 # 3. Iterate over all graph edges: 355 # 1. if the edge has an invalid trans and dyntrans, delete 356 # the edge. 357 # 2. if the edge has an invalid trans, clear the related 358 # lists on the edge. 359 # 3. if the edge has an invalid dyntrans, clear the related 360 # lists on the edge. 361 # 362 def _build_graph(self): 363 self.G.clear() 364 self.G.name = "Domain transition graph for {0}.".format(self.policy) 365 366 self.log.info("Building domain transition graph from {0}...".format(self.policy)) 367 368 # hash tables keyed on domain type 369 setexec = defaultdict(list) 370 setcurrent = defaultdict(list) 371 372 # hash tables keyed on (domain, entrypoint file type) 373 # the parameter for defaultdict has to be callable 374 # hence the lambda for the nested defaultdict 375 execute = defaultdict(lambda: defaultdict(list)) 376 entrypoint = defaultdict(lambda: defaultdict(list)) 377 378 # hash table keyed on (domain, entrypoint, target domain) 379 type_trans = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) 380 381 for rule in self.policy.terules(): 382 if rule.ruletype == "allow": 383 if rule.tclass not in ["process", "file"]: 384 continue 385 386 perms = rule.perms 387 388 if rule.tclass == "process": 389 if "transition" in perms: 390 for s, t in itertools.product(rule.source.expand(), rule.target.expand()): 391 # only add edges if they actually 392 # transition to a new type 393 if s != t: 394 edge = Edge(self.G, s, t, create=True) 395 edge.transition.append(rule) 396 397 if "dyntransition" in perms: 398 for s, t in itertools.product(rule.source.expand(), rule.target.expand()): 399 # only add edges if they actually 400 # transition to a new type 401 if s != t: 402 e = Edge(self.G, s, t, create=True) 403 e.dyntransition.append(rule) 404 405 if "setexec" in perms: 406 for s in rule.source.expand(): 407 setexec[s].append(rule) 408 409 if "setcurrent" in perms: 410 for s in rule.source.expand(): 411 setcurrent[s].append(rule) 412 413 else: 414 if "execute" in perms: 415 for s, t in itertools.product( 416 rule.source.expand(), 417 rule.target.expand()): 418 execute[s][t].append(rule) 419 420 if "entrypoint" in perms: 421 for s, t in itertools.product(rule.source.expand(), rule.target.expand()): 422 entrypoint[s][t].append(rule) 423 424 elif rule.ruletype == "type_transition": 425 if rule.tclass != "process": 426 continue 427 428 d = rule.default 429 for s, t in itertools.product(rule.source.expand(), rule.target.expand()): 430 type_trans[s][t][d].append(rule) 431 432 invalid_edge = [] 433 clear_transition = [] 434 clear_dyntransition = [] 435 436 for s, t in self.G.edges_iter(): 437 edge = Edge(self.G, s, t) 438 invalid_trans = False 439 invalid_dyntrans = False 440 441 if edge.transition: 442 # get matching domain exec w/entrypoint type 443 entry = set(entrypoint[t].keys()) 444 exe = set(execute[s].keys()) 445 match = entry.intersection(exe) 446 447 if not match: 448 # there are no valid entrypoints 449 invalid_trans = True 450 else: 451 # TODO try to improve the 452 # efficiency in this loop 453 for m in match: 454 if s in setexec or type_trans[s][m]: 455 # add key for each entrypoint 456 edge.entrypoint[m] += entrypoint[t][m] 457 edge.execute[m] += execute[s][m] 458 459 if type_trans[s][m][t]: 460 edge.type_transition[m] += type_trans[s][m][t] 461 462 if s in setexec: 463 edge.setexec.extend(setexec[s]) 464 465 if not edge.setexec and not edge.type_transition: 466 invalid_trans = True 467 else: 468 invalid_trans = True 469 470 if edge.dyntransition: 471 if s in setcurrent: 472 edge.setcurrent.extend(setcurrent[s]) 473 else: 474 invalid_dyntrans = True 475 else: 476 invalid_dyntrans = True 477 478 # cannot change the edges while iterating over them, 479 # so keep appropriate lists 480 if invalid_trans and invalid_dyntrans: 481 invalid_edge.append(edge) 482 elif invalid_trans: 483 clear_transition.append(edge) 484 elif invalid_dyntrans: 485 clear_dyntransition.append(edge) 486 487 # Remove invalid transitions 488 self.G.remove_edges_from(invalid_edge) 489 for edge in clear_transition: 490 # if only the regular transition is invalid, 491 # clear the relevant lists 492 del edge.transition 493 del edge.execute 494 del edge.entrypoint 495 del edge.type_transition 496 del edge.setexec 497 for edge in clear_dyntransition: 498 # if only the dynamic transition is invalid, 499 # clear the relevant lists 500 del edge.dyntransition 501 del edge.setcurrent 502 503 self.rebuildgraph = False 504 self.rebuildsubgraph = True 505 self.log.info("Completed building domain transition graph.") 506 self.log.debug("Graph stats: nodes: {0}, edges: {1}.".format( 507 nx.number_of_nodes(self.G), 508 nx.number_of_edges(self.G))) 509 510 def __remove_excluded_entrypoints(self): 511 invalid_edges = [] 512 for source, target in self.subG.edges_iter(): 513 edge = Edge(self.subG, source, target) 514 entrypoints = set(edge.entrypoint) 515 entrypoints.intersection_update(self.exclude) 516 517 if not entrypoints: 518 # short circuit if there are no 519 # excluded entrypoint types on 520 # this edge. 521 continue 522 523 for e in entrypoints: 524 # clear the entrypoint data 525 del edge.entrypoint[e] 526 del edge.execute[e] 527 528 try: 529 del edge.type_transition[e] 530 except KeyError: # setexec 531 pass 532 533 # cannot delete the edges while iterating over them 534 if not edge.entrypoint and not edge.dyntransition: 535 invalid_edges.append(edge) 536 537 self.subG.remove_edges_from(invalid_edges) 538 539 def _build_subgraph(self): 540 if self.rebuildgraph: 541 self._build_graph() 542 543 self.log.info("Building domain transition subgraph.") 544 self.log.debug("Excluding {0}".format(self.exclude)) 545 self.log.debug("Reverse {0}".format(self.reverse)) 546 547 # reverse graph for reverse DTA 548 if self.reverse: 549 self.subG = self.G.reverse(copy=True) 550 else: 551 self.subG = self.G.copy() 552 553 if self.exclude: 554 # delete excluded domains from subgraph 555 self.subG.remove_nodes_from(self.exclude) 556 557 # delete excluded entrypoints from subgraph 558 self.__remove_excluded_entrypoints() 559 560 self.rebuildsubgraph = False 561 self.log.info("Completed building domain transition subgraph.") 562 self.log.debug("Subgraph stats: nodes: {0}, edges: {1}.".format( 563 nx.number_of_nodes(self.subG), 564 nx.number_of_edges(self.subG))) 565 566 567 class Edge(object): 568 569 """ 570 A graph edge. Also used for returning domain transition steps. 571 572 Parameters: 573 graph The NetworkX graph. 574 source The source type of the edge. 575 target The target tyep of the edge. 576 577 Keyword Parameters: 578 create (T/F) create the edge if it does not exist. 579 The default is False. 580 """ 581 582 transition = EdgeAttrList('transition') 583 setexec = EdgeAttrList('setexec') 584 dyntransition = EdgeAttrList('dyntransition') 585 setcurrent = EdgeAttrList('setcurrent') 586 entrypoint = EdgeAttrDict('entrypoint') 587 execute = EdgeAttrDict('execute') 588 type_transition = EdgeAttrDict('type_transition') 589 590 def __init__(self, graph, source, target, create=False): 591 self.G = graph 592 self.source = source 593 self.target = target 594 595 if not self.G.has_edge(source, target): 596 if not create: 597 raise ValueError("Edge does not exist in graph") 598 else: 599 self.G.add_edge(source, target) 600 self.transition = None 601 self.entrypoint = None 602 self.execute = None 603 self.type_transition = None 604 self.setexec = None 605 self.dyntransition = None 606 self.setcurrent = None 607 608 def __getitem__(self, key): 609 # This is implemented so this object can be used in NetworkX 610 # functions that operate on (source, target) tuples 611 if isinstance(key, slice): 612 return [self._index_to_item(i) for i in range(* key.indices(2))] 613 else: 614 return self._index_to_item(key) 615 616 def _index_to_item(self, index): 617 """Return source or target based on index.""" 618 if index == 0: 619 return self.source 620 elif index == 1: 621 return self.target 622 else: 623 raise IndexError("Invalid index (edges only have 2 items): {0}".format(index)) 624