DTA: implement excluded types and reverse analysis.

closes #25
closes #26
This commit is contained in:
Chris PeBenito 2015-02-07 15:07:40 -05:00
parent 094dbe3906
commit da44b3592b
3 changed files with 289 additions and 46 deletions

11
sedta
View File

@ -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:

View File

@ -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

View File

@ -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)