-
Notifications
You must be signed in to change notification settings - Fork 176
/
tree.py
561 lines (487 loc) · 19.8 KB
/
tree.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
"""
Defines an action tree. Action trees are comprised of a trigger and a tree of actions.
"""
import random
import re
import anytree
from anytree.exporter import DotExporter
import actions.utils
import actions.trigger
class ActionTreeParseError(Exception):
"""
Exception thrown when an action tree is malformed or cannot be parsed.
"""
class ActionTree():
"""
Defines an ActionTree for the Geneva system.
"""
def __init__(self, direction, trigger=None):
"""
Creates this action tree.
Args:
direction (str): Direction this tree is facing ("out", "in")
trigger (:obj:`actions.trigger.Trigger`): Trigger to use with this tree
"""
self.trigger = trigger
self.action_root = None
self.direction = direction
self.environment_id = None
self.ran = False
def initialize(self, num_actions, environment_id, allow_terminal=True, disabled=None):
"""
Sets up this action tree with a given number of random actions.
Note that the returned action trees may have less actions than num_actions
if terminal actions are used.
"""
self.environment_id = environment_id
self.trigger = actions.trigger.Trigger(None, None, None, environment_id=environment_id)
if not allow_terminal or random.random() > 0.1:
allow_terminal = False
for _ in range(num_actions):
new_action = self.get_rand_action(self.direction, disabled=disabled)
self.add_action(new_action)
return self
def __iter__(self):
"""
Sets up a preoder iterator for the tree.
"""
for node in self.preorder(self.action_root):
yield node
def __getitem__(self, index):
"""
Implements indexing
"""
if index > len(self):
return None
# Wrap around if given negative number to allow for negative indexing
if index < 0:
index = index + len(self)
idx = 0
for action in self:
if idx == index:
return action
idx += 1
def __len__(self):
"""
Calculates the number of actions in the tree.
"""
num = 0
for node in self:
if node:
num += 1
return num
def __str__(self):
"""
Returns a string representation for the action tree.
"""
rep = "[%s]-" % str(self.trigger)
for string in self.string_repr(self.action_root):
rep += string
if not rep.endswith("-"):
rep += "-"
rep += "|"
return rep
def do_parse(self, node, string, logger):
"""
Handles the preorder recursive parsing.
"""
# If we're passed an empty string, return None
if not string.strip():
return None
# If there is no subtree defined here, the action string is the string we've been given,
# and there are no left or right actions left
if "(" not in string:
action_string = string
left_actions, right_actions = "", ""
else:
# Find the outermost (first) occurance of "(" - this defines the boundaries between
# this current action and it's subtree actions
subtree_idx = string.find("(")
# Split this string into the action and it's subtree string
action_string, rest = string[:subtree_idx], string[subtree_idx:]
# We need to split the remaining string at the correct comma that splits this subtree's
# left and right actions. To find the correct comma to split on, we need to find the
# comma that splits the current tree 'in the middle' - where the depth is the number of
# splits. This occurs when we cound the same number of commas as open parenthesis "(".
depth = 0
comma = 0
idx = 0
for char in rest:
if char == "(":
depth += 1
if char == ",":
comma += 1
if comma == depth and depth > 0:
break
idx += 1
# If we did not break, we didn't find where to split. Raise an exception
else:
raise ActionTreeParseError("Given string %s is malformed" % string)
# Split on this index, and ignore the first character "(" and last character ")"
left_actions, right_actions = rest[1:idx], rest[idx+1:-1]
# Parse the action_string using action.utils
action_obj = actions.action.Action.parse_action(action_string, self.direction, logger)
if not action_obj:
raise ActionTreeParseError("Did not get a legitimate action object from %s" %
action_string)
# Assign this action_obj to the node
node = action_obj
# Sanity check - if this is not a branching action but it has right actions, raise
if not node.branching and right_actions:
raise ActionTreeParseError("Cannot have a non branching action with right subtree")
# Sanity check = if this is a termainal action but it has sub actions, raise
if node.terminal and (right_actions or left_actions):
raise ActionTreeParseError("Cannot have a terminal action with children")
# If we have a left action and were given a packet to pass on, run
# on the left packet
if left_actions:
node.left = self.do_parse(node.left, left_actions, logger)
# If we have a left action and were given a packet to pass on, run
# on the left packet
if right_actions:
node.right = self.do_parse(node.right, right_actions, logger)
return node
def parse(self, string, logger):
"""
Parses a string representation of an action tree into this object.
"""
# Individual action trees always end in "|" to signify the end - refuse
# to parse if this is malformed
if not string.strip().endswith("|"):
msg = "Tree does not end with |. Was I given an entire strategy or part of a tree?"
logger.error(msg)
return False
# The format of each action matches this regular expression. For example, given
# the action tree: [TCP:flags:SA]-tamper{TCP:flags:corrupt}-|
# it would parse out the trigger "TCP:flags:SA" and the tree as
# "-tamper{TCP:flags:corrupt}"
match = re.match(r"\[(\S*)\]-(\S*)|", string)
if not match or not match.group(0):
logger.error("Could not identify trigger or tree")
return False
# Ask the trigger class to parse this trigger to define a new object
trigger = actions.trigger.Trigger.parse(match.group(1))
# If we couldn't parse the trigger, bail
if not trigger:
logger.error("Trigger failed to parse")
return False
tree = match.group(2)
# Depending on the form of the action tree, there might be a hanging "-|" or "|"
# Remove them
if tree.endswith("-|"):
tree = tree.replace("-|", "")
if tree.endswith("|"):
tree = tree.replace("|", "")
# Parse the rest of the tree and setup the action tree
try:
self.action_root = self.do_parse(self.action_root, tree, logger)
except ActionTreeParseError:
logger.exception("Exception caught from parser")
return False
self.trigger = trigger
return self
def check(self, packet, logger):
"""
Checks if this action tree should run on this packet.
"""
return self.trigger.is_applicable(packet, logger)
def do_run(self, node, packet, logger):
"""
Handles recursively running a packet down the tree.
"""
# If there is no action here, yield None
if not node:
yield None
else:
# Run this current action against the given packet.
# It will give us a packet to pass to the left and right child
left_packet, right_packet = node.run(packet, logger)
# If there is no left child, yield the left packet
if not node.left:
yield left_packet
# If we have a left action and were given a packet to pass on, run
# on the left packet
if node.left and left_packet:
for lpacket in self.do_run(node.left, left_packet, logger):
yield lpacket
# If there is no right child, yield the right packet
if not node.right:
yield right_packet
# If we have a right action and were given a packet to pass on, run
# on the right packet
if node.right and right_packet:
for rpacket in self.do_run(node.right, right_packet, logger):
yield rpacket
def run(self, packet, logger):
"""
Runs a packet through the action tree.
"""
self.ran = True
packets = []
for processed in self.do_run(self.action_root, packet, logger):
if processed:
packets.append(processed)
return packets
def preorder(self, node):
"""
Yields a preorder traversal of the tree.
"""
yield node
if node and node.left:
for lnode in self.preorder(node.left):
yield lnode
if node and node.right:
for rnode in self.preorder(node.right):
yield rnode
def string_repr(self, node):
"""
Yields a preorder traversal of the tree to create a string representation.
"""
if not node:
yield ""
else:
yield "%s" % node
# Only yield a subtree start representation if there is a subtree to build
if node.left or node.right:
yield "("
# Setup the left subtree representation
if node.left:
for lnode in self.string_repr(node.left):
yield str(lnode)
# Only yield subtree representation if there is a subtree to build
if node.left or node.right:
yield ","
# Setup the right subtree representation
if node and node.right:
for rnode in self.string_repr(node.right):
yield str(rnode)
# Only yield subtree representation if there is a subtree to build
if node.left or node.right:
yield ")"
def remove_action(self, action):
"""
Removes a given action from the tree.
"""
# If there is only an action root and no other actions, just delete the root
if action == self.action_root and not self.action_root.left and not self.action_root.right:
self.action_root = None
return True
# If there is no tree at all, nothing to remove
if not self.action_root:
return False
for node in self:
# If the node we're removing is the root of the tree, replace it with the left child
# if it exists; if not, the right child.
if node == action and action == self.action_root:
self.action_root = action.left or action.right
return True
if node.left == action:
node.left = action.left
return True
if node.right == action:
node.right = action.left
return True
return False
def get_slots(self):
"""
Returns the number of locations a new action could be added.
"""
slots = 0
for action in self:
# Terminal actions have no children
if action.terminal:
continue
if not action.left:
slots += 1
if not action.right and action.branching:
slots += 1
return slots
def count_leaves(self):
"""
Counts the number of leaves.
"""
leaves = 0
for action in self:
if not action.left and not action.right:
leaves += 1
return leaves
def contains(self, action):
"""
Checks if an action is contained in the tree.
"""
for node in self:
if node == action:
return True
return False
def add_action(self, new_action):
"""
Adds an action to this action tree.
"""
# Refuse to add None actions
if not new_action:
return False
# If no actions are in this tree yet, this given action is the new root
if not self.action_root:
self.action_root = new_action
return True
# We cannot add an action if it is already in the tree, or we could recurse
# forever
if self.contains(new_action):
return False
# Count the open spaces that we could put a new action to.
# This is effectively counting the leaves we could have if all the leaves had children
slots = self.get_slots()
# We will visit each available slot and add the action there with probability
# 1/slots. Since it's possible we could visit every slot without having hit that
# probability yet, keep iterating until we do.
action_added = False
while not action_added and slots > 0:
for action in self:
if not action.left and not action.terminal and random.random() < 1/float(slots):
action.left = new_action
action_added = True
break
# We can only add to the right child if this action can introduce branching,
# such as (duplicate, fragment)
if not action.right and not action.terminal and action.branching and \
random.random() < 1/float(slots):
action.right = new_action
action_added = True
break
return action_added
def get_rand_action(self, direction, request=None, allow_terminal=True, disabled=None):
"""
Retrieves and initializes a random action that can run in the given direction.
"""
pick = random.random()
action_options = actions.action.Action.get_actions(direction, disabled=disabled, allow_terminal=allow_terminal)
# Check to make sure there are still actions available to use
assert action_options, "No actions were available"
act_dict = {}
all_opts = []
for action_name, act_cls in action_options:
act_dict[action_name] = act_cls
all_opts += ([act_cls] * act_cls.frequency)
new_action = act_dict.get(request, random.choice(all_opts))
return new_action(environment_id=self.environment_id)
def remove_one(self):
"""
Removes a random leaf from the tree.
"""
if not self.action_root:
return False
action = random.choice(self)
return self.remove_action(action)
def mutate(self):
"""
Mutates this action tree with respect to a given direction.
"""
pick = random.uniform(0, 1)
if pick < 0.20 or not self.action_root:
new_action = self.get_rand_action(direction=self.direction)
self.add_action(new_action)
elif pick < 0.65 and self.action_root:
action = random.choice(self)
action.mutate(environment_id=self.environment_id)
# If this individual has never been run under the evaluator,
# or if it ran and it failed, it won't have an environment_id,
# which means it has no saved packets to read from.
elif pick < 0.80 and self.environment_id:
self.trigger.mutate(self.environment_id)
else:
self.remove_one()
return self
def choose_one(self):
"""
Picks a random element in the tree.
"""
# If this is an empty tree, return None
if not self.action_root:
return None
return random.choice(self)
def get_parent(self, node):
"""
Returns the parent of the given node and direction of the child.
"""
# If we're given None, bail with None, None
if not node:
return None, None
for action in self:
if action.left == node:
return action, "left"
if action.right == node:
return action, "right"
return None, None
def swap(self, my_donation, other_tree, other_donation):
"""
Swaps a node in this tree with a node in another tree.
"""
parent, direction = self.get_parent(my_donation)
other_parent, other_direction = other_tree.get_parent(other_donation)
# If this tree is empty or I'm trying to donate my root
if not my_donation or not parent:
parent = self
direction = "action_root"
# if the other tree is empty or they are trying to donate their root
if not other_donation or not other_parent:
other_parent = other_tree
other_direction = "action_root"
setattr(parent, direction, other_donation)
setattr(other_parent, other_direction, my_donation)
return True
def mate(self, other_tree):
"""
Mates this tree with another tree.
"""
# If both trees are empty, nothing to do
if not self.action_root and not other_tree.action_root:
return False
# Chose an action node in this tree to swap
my_swap_node = self.choose_one()
other_swap_node = other_tree.choose_one()
return self.swap(my_swap_node, other_tree, other_swap_node)
def pretty_print_help(self, root, visual=False, parent=None):
"""
Pretty prints the tree.
- root is the highest-level node you wish to start printing
- [visual] controls whether a png should be created, by default, this is false.
- [parent] is an optional parameter specifying the parent of a given node, should
only be used by this function.
Returns the root with its children as an anytree node.
"""
if not root:
return None
if visual:
newroot = anytree.Node(str(root) + "(" + str(root.ident) + ")", parent=parent)
else:
newroot = anytree.Node(str(root), parent=parent)
if root.left:
newroot.left = self.pretty_print_help(root.left, visual, parent=newroot)
else:
if not root.terminal:
# Drop never sends packets
newroot.left = anytree.Node(' ===> ', parent=newroot)
if root.right:
newroot.right = self.pretty_print_help(root.right, visual, parent=newroot)
else:
if (not root.terminal and root.branching):
# Tamper only has one child
newroot.right = anytree.Node(' ===> ', parent=newroot)
return newroot
def pretty_print(self, visual=False):
"""
Pretty prints the tree.
"""
if visual:
newroot = self.pretty_print_help(self.action_root, visual=True)
if newroot:
DotExporter(newroot).to_picture("tree.png")
else:
newroot = self.pretty_print_help(self.action_root, visual=False)
if not newroot:
return ""
# use an array and join so there's never an issue with newlines at the end
pretty_string = []
for pre, _fill, node in anytree.RenderTree(newroot):
pretty_string.append(("%s%s" % (pre, node.name)))
return "%s\n%s" % (str(self.trigger), '\n'.join(pretty_string))