diff --git a/sedta b/sedta index 0ad7637..a435890 100755 --- a/sedta +++ b/sedta @@ -1,5 +1,5 @@ #!/usr/bin/python -# Copyright 2014, Tresys Technology, LLC +# Copyright 2014-2015, Tresys Technology, LLC # # This file is part of SETools. # @@ -84,6 +84,12 @@ alg.add_argument("-S", "--shortest_path", action="store_true", alg.add_argument("-A", "--all_paths", help="Calculate all paths, with the specified maximum path length. (Expensive)", type=int, metavar="MAX_STEPS") +opts = parser.add_argument_group("Analysis options") +opts.add_argument("-r", "--reverse", action="store_true", default=False, + help="Perform a reverse DTA.") +opts.add_argument( + "exclude", help="List of excluded types in the analysis.", nargs="*") + args = parser.parse_args() if not args.target and (args.shortest_path or args.all_paths): @@ -94,7 +100,7 @@ if args.target and not (args.shortest_path or args.all_paths): try: p = setools.SELinuxPolicy(args.policy) - g = setools.dta.DomainTransitionAnalysis(p) + g = setools.dta.DomainTransitionAnalysis(p, reverse=args.reverse, exclude=args.exclude) except Exception as err: print(err) sys.exit(1) @@ -131,7 +137,6 @@ else: # single transition print("Transition {0}: {1} -> {2}\n".format(i, src, tgt)) print_transition(trans, entrypoints, setexec, dyntrans, setcur) - print(i, "domain transition(s) found.") if args.stats: diff --git a/setools/dta.py b/setools/dta.py index 6292210..7dadc94 100644 --- a/setools/dta.py +++ b/setools/dta.py @@ -1,4 +1,4 @@ -# Copyright 2014, Tresys Technology, LLC +# Copyright 2014-2015, Tresys Technology, LLC # # This file is part of SETools. # @@ -26,13 +26,16 @@ class DomainTransitionAnalysis(object): """Domain transition analysis.""" - def __init__(self, policy): + def __init__(self, policy, reverse=False, exclude=[]): """ Parameter: policy The policy to analyze. """ self.policy = policy + self.set_exclude(exclude) + self.set_reverse(reverse) self.rebuildgraph = True + self.rebuildsubgraph = True self.G = nx.DiGraph() def __get_entrypoints(self, source, target): @@ -51,24 +54,18 @@ class DomainTransitionAnalysis(object): exec The execute rules. trans The type_transition rules. """ - for e in self.G.edge[source][target]['entrypoint']: - if self.G.edge[source][target]['type_transition'][e]: + for e in self.subG.edge[source][target]['entrypoint']: + if self.subG.edge[source][target]['type_transition'][e]: yield e, \ - self.G.edge[source][target]['entrypoint'][e], \ - self.G.edge[source][target]['execute'][e], \ - self.G.edge[source][target]['type_transition'][e] + self.subG.edge[source][target]['entrypoint'][e], \ + self.subG.edge[source][target]['execute'][e], \ + self.subG.edge[source][target]['type_transition'][e] else: yield e, \ - self.G.edge[source][target]['entrypoint'][e], \ - self.G.edge[source][target]['execute'][e], \ + self.subG.edge[source][target]['entrypoint'][e], \ + self.subG.edge[source][target]['execute'][e], \ [] - # TODO: consider making sure source and target are valid - # both as types and also in graph - # TODO: make reverse an option. on that option, - # simply reverse the graph. Will probably have to fix up - # __get_steps to output correctly so sedta doesn't have to - # change. def __get_steps(self, path): """ Generator which returns the source, target, and associated rules @@ -93,12 +90,43 @@ class DomainTransitionAnalysis(object): source = path[s - 1] target = path[s] - yield source, target, \ - self.G.edge[source][target]['transition'], \ - self.__get_entrypoints(source, target), \ - self.G.edge[source][target]['setexec'], \ - self.G.edge[source][target]['dyntransition'], \ - self.G.edge[source][target]['setcurrent'] + if self.reverse: + real_source, real_target = target, source + else: + real_source, real_target = source, target + + # It seems that NetworkX does not reverse the dictionaries + # that store the attributes, so real_* is used everywhere + # below, rather than just the first line. + yield real_source, real_target, \ + self.subG.edge[real_source][real_target]['transition'], \ + self.__get_entrypoints(real_source, real_target), \ + self.subG.edge[real_source][real_target]['setexec'], \ + self.subG.edge[real_source][real_target]['dyntransition'], \ + self.subG.edge[real_source][real_target]['setcurrent'] + + def set_reverse(self, reverse): + """ + Set forward/reverse DTA direction. + + Parameter: + reverse If true, a reverse DTA is performed, otherwise a + forward DTA is performed. + """ + + self.reverse = bool(reverse) + self.rebuildsubgraph = True + + def set_exclude(self, exclude): + """ + Set the domains to exclude from the domain transition analysis. + + Parameter: + exclude A list of types. + """ + + self.exclude = [self.policy.lookup_type(t) for t in exclude] + self.rebuildsubgraph = True def shortest_path(self, source, target): """ @@ -118,12 +146,12 @@ class DomainTransitionAnalysis(object): s = self.policy.lookup_type(source) t = self.policy.lookup_type(target) - if self.rebuildgraph: - self._build_graph() + if self.rebuildsubgraph: + self._build_subgraph() - if s in self.G and t in self.G: + if s in self.subG and t in self.subG: try: - path = nx.shortest_path(self.G, s, t) + path = nx.shortest_path(self.subG, s, t) except nx.exception.NetworkXNoPath: pass else: @@ -149,12 +177,12 @@ class DomainTransitionAnalysis(object): s = self.policy.lookup_type(source) t = self.policy.lookup_type(target) - if self.rebuildgraph: - self._build_graph() + if self.rebuildsubgraph: + self._build_subgraph() - if s in self.G and t in self.G: + if s in self.subG and t in self.subG: try: - paths = nx.all_simple_paths(self.G, s, t, maxlen) + paths = nx.all_simple_paths(self.subG, s, t, maxlen) except nx.exception.NetworkXNoPath: pass else: @@ -179,12 +207,12 @@ class DomainTransitionAnalysis(object): s = self.policy.lookup_type(source) t = self.policy.lookup_type(target) - if self.rebuildgraph: - self._build_graph() + if self.rebuildsubgraph: + self._build_subgraph() - if s in self.G and t in self.G: + if s in self.subG and t in self.subG: try: - paths = nx.all_shortest_paths(self.G, s, t) + paths = nx.all_shortest_paths(self.subG, s, t) except nx.exception.NetworkXNoPath: pass else: @@ -207,16 +235,24 @@ class DomainTransitionAnalysis(object): """ s = self.policy.lookup_type(type_) - if self.rebuildgraph: - self._build_graph() + if self.rebuildsubgraph: + self._build_subgraph() - for source, target in self.G.out_edges_iter(s): - yield source, target, \ - self.G.edge[source][target]['transition'], \ - self.__get_entrypoints(source, target), \ - self.G.edge[source][target]['setexec'], \ - self.G.edge[source][target]['dyntransition'], \ - self.G.edge[source][target]['setcurrent'] + for source, target in self.subG.out_edges_iter(s): + if self.reverse: + real_source, real_target = target, source + else: + real_source, real_target = source, target + + # It seems that NetworkX does not reverse the dictionaries + # that store the attributes, so real_* is used everywhere + # below, rather than just the first line. + yield real_source, real_target, \ + self.subG.edge[real_source][real_target]['transition'], \ + self.__get_entrypoints(real_source, real_target), \ + self.subG.edge[real_source][real_target]['setexec'], \ + self.subG.edge[real_source][real_target]['dyntransition'], \ + self.subG.edge[real_source][real_target]['setcurrent'] def get_stats(self): """ @@ -442,3 +478,46 @@ class DomainTransitionAnalysis(object): del self.G[s][t]['setcurrent'][:] self.rebuildgraph = False + self.rebuildsubgraph = True + + def _build_subgraph(self): + if self.rebuildgraph: + self._build_graph() + + # delete excluded domains from subgraph + nodes = [n for n in self.G.nodes() if n not in self.exclude] + # subgraph created this way to get copies of the edge + # attributes. otherwise the edge attributes point to the + # original graph, and the entrypoint removal below would also + # affect the main graph. + self.subG = nx.DiGraph(self.G.subgraph(nodes)) + + # delete excluded entrypoints from subgraph + invalid_edge = [] + for source, target in self.subG.edges_iter(): + # can't change a dictionary that you're iterating over + entrypoints = list(self.subG.edge[source][target]['entrypoint']) + + for e in entrypoints: + # clear the entrypoint data + if e in self.exclude: + del self.subG.edge[source][target]['entrypoint'][e] + del self.subG.edge[source][target]['execute'][e] + + try: + del self.subG.edge[source][ + target]['type_transition'][e] + except KeyError: # setexec + pass + + # cannot change the edges while iterating over them + if len(self.subG.edge[source][target]['entrypoint']) == 0 and len(self.subG.edge[source][target]['dyntransition']) == 0: + invalid_edge.append((source, target)) + + self.subG.remove_edges_from(invalid_edge) + + # reverse graph for reverse DTA + if self.reverse: + self.subG.reverse(copy=False) + + self.rebuildsubgraph = False diff --git a/tests/dta.py b/tests/dta.py index 1a92256..bb80b66 100644 --- a/tests/dta.py +++ b/tests/dta.py @@ -1,4 +1,4 @@ -# Copyright 2014, Tresys Technology, LLC +# Copyright 2014-2015, Tresys Technology, LLC # # This file is part of SETools. # @@ -477,3 +477,162 @@ class InfoFlowAnalysisTest(unittest.TestCase): # setcurrent r = self.a.G.edge[s][t]["setcurrent"] self.assertEqual(len(r), 0) + + def test_100_forward_subgraph_structure(self): + """DTA: verify forward subgraph structure.""" + # The purpose is to ensure the subgraph is reversed + # only when the reverse option is set, not that + # graph reversal is correct (assumed that NetworkX + # does it correctly). + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(False) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(dyntrans100, bothtrans200), + (start, dyntrans100), + (start, trans1), + (trans1, trans2), + (trans2, trans3), + (trans3, trans5)]), edges) + + def test_101_reverse_subgraph_structure(self): + """DTA: verify reverse subgraph structure.""" + # The purpose is to ensure the subgraph is reversed + # only when the reverse option is set, not that + # graph reversal is correct (assumed that NetworkX + # does it correctly). + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(True) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(bothtrans200, dyntrans100), + (dyntrans100, start), + (trans1, start), + (trans2, trans1), + (trans3, trans2), + (trans5, trans3)]), edges) + + def test_200_exclude_domain(self): + """DTA: exclude domain type.""" + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(False) + self.a.set_exclude(["trans1"]) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(dyntrans100, bothtrans200), + (start, dyntrans100), + (trans2, trans3), + (trans3, trans5)]), edges) + + def test_201_exclude_entryoint_with_2entrypoints(self): + """DTA: exclude entrypoint type without transition deletion (other entrypoints).""" + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(False) + self.a.set_exclude(["trans3_exec1"]) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(dyntrans100, bothtrans200), + (start, dyntrans100), + (start, trans1), + (trans1, trans2), + (trans2, trans3), + (trans3, trans5)]), edges) + + def test_202_exclude_entryoint_with_dyntrans(self): + """DTA: exclude entrypoint type without transition deletion (dyntrans).""" + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(False) + self.a.set_exclude(["bothtrans200_exec"]) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(dyntrans100, bothtrans200), + (start, dyntrans100), + (start, trans1), + (trans1, trans2), + (trans2, trans3), + (trans3, trans5)]), edges) + + def test_203_exclude_entryoint_delete_transition(self): + """DTA: exclude entrypoint type with transition deletion.""" + # Don't check node list since the disconnected nodes are not + # removed after removing invalid domain transitions + + self.a.set_reverse(False) + self.a.set_exclude(["trans2_exec"]) + self.a._build_subgraph() + + start = self.p.lookup_type("start") + trans1 = self.p.lookup_type("trans1") + trans2 = self.p.lookup_type("trans2") + trans3 = self.p.lookup_type("trans3") + trans4 = self.p.lookup_type("trans4") + trans5 = self.p.lookup_type("trans5") + dyntrans100 = self.p.lookup_type("dyntrans100") + bothtrans200 = self.p.lookup_type("bothtrans200") + + edges = set(self.a.subG.out_edges_iter()) + self.assertSetEqual(set([(dyntrans100, bothtrans200), + (start, dyntrans100), + (start, trans1), + (trans2, trans3), + (trans3, trans5)]), edges)