Skip to content
Snippets Groups Projects
Commit 7e820933 authored by Leendertz, R.R. (Rens, Student M-CS)'s avatar Leendertz, R.R. (Rens, Student M-CS)
Browse files

added a python file capable of converting an attack tree to a simpler version

parent 5013b93d
No related branches found
No related tags found
No related merge requests found
.ionide/
\ No newline at end of file
.ionide/
.idea/
fault-tree-examples.iml
import json
import os,argparse
#create a new node of any type
def makeAnd(name, causes):
return {"name":name, "causes":causes, "gate":{"type":"and"}}
def makeOr(name, causes):
return {"name":name, "causes":causes, "gate":{"type":"or"}}
def makeVote(name, causes, amount):
return {"name":name, "causes":causes, "gate":{"type":"vote", "count":amount}}
def updateReferences(newNodes, nodes, name, newName):
"""if a name of a node is updated in the process, this fixes any references that got mismatched"""
for n in newNodes:
if "causes" not in newNodes[n]: continue
causes = newNodes[n]["causes"]
for i in range(len(causes)):
if causes[i] == name: causes[i]= newName
for n in nodes:
if "causes" not in n: continue
causes = n["causes"]
for i in range(len(causes)):
if causes[i] == name: causes[i]= newName
def updateName(name, gate, addition):
"""Extend a name to be more specific, used for gates with a lot of children"""
if name.startswith(gate):
parts =name.split('_',2)
parts[1]+=addition
return "_".join(parts)
else:
return gate+"_"+addition+"_"+name
def takeN(ls, n, base=[], offset=0):
""" Collects all possible subsets of size N of a list, and yields each exactly once.
Use an iterator or next() to retrieve these subsets"""
if n <= 0: yield base
else:
for i in range(offset, len(ls)):
yield from takeN(ls, n-1, base+[ls[i]], i+1)
def reduceVote(node):
""" Reduce a voting gate to a large subtree of ands and ors
First, collect all possible subsets of children of the right size and put them into an AND gate.
Then have an or gate as a parent for all those and gates. """
name = node["name"]
count = node["gate"]["count"]
causes = node["causes"]
ands = []
# all possible subsets of size count.
for i,subset in enumerate(takeN(causes, count)):
# voting gate fails if all nodes in this subset fail, thus an and
andN = makeAnd("vote_"+str(i)+"_"+name, subset)
# reduce the and if it has too many children.
newName = reduceAnd(andN)
ands.append(newName)
# voting gate fails if any of the and gates fails.
orN = makeOr("vote_or_"+name, ands)
# new name of this subtree is the name of the or gate.
return reduceOr(orN)
def reduceAnd(node):
"""reduce 1 AND gate with many children to many AND gates with 2 children."""
name = node["name"]
causes = node["causes"]
if len(causes) == 1:
# an and gate of 1 child is simply that child.
return causes[0]
if len(causes) == 2:
# this is a simple and gate. add it to the new tree and return name.
newNodes[name]=node
return name
halfway = len(causes)//2
left, right = causes[:halfway], causes[halfway:]
left = reduceAnd(makeAnd(updateName(name, "and", "l"), left))
right = reduceAnd(makeAnd(updateName(name, "and", "r"), right))
return addNode(makeAnd(updateName(name, "and", ""), [left,right]))
def reduceOr(node):
"""reduce 1 OR gate with many children to many OR gates with 2 children."""
name = node["name"]
causes = node["causes"]
if len(causes) == 1:
# an or gate of 1 child is simply that child.
return causes[0]
if len(causes) == 2:
# this is a simple or gate. add it to the new tree and return name.
newNodes[name]=node
return name
#split it in half
halfway = len(causes)//2
left, right = causes[:halfway], causes[halfway:]
left = reduceOr(makeOr(updateName(name, "or", "l"), left))
right = reduceOr(makeOr(updateName(name, "or", "r"), right))
return addNode(makeOr(updateName(name, "or", ""), [left,right]))
def reduceLeaf(node):
"""Insert leaf nodes into the new tree"""
return addNode(node)
def addNode(node):
""" Adds a node to the new tree. """
name = node["name"]
if name in newNodes:
print("2 nodes with identical names detected:",name)
exit()
newNodes[name]=node
return name
def reduce(node):
""" Takes a node from the original, converts it to some set of nodes for the new tree."""
if "gate" not in node:
return reduceLeaf(node)
mapping = {"or":reduceOr,"and":reduceAnd,"vote":reduceVote}
return mapping[node["gate"]["type"]](node)
def simplify(json):
""" Converts a standard attack tree to a simple one suitable for a binary decision diagram.
Fixes issues caused by nodes changing their names."""
global newNodes
newNodes = {}
nodes = json
for node in nodes:
name = node["name"]
newName = reduce(node)
if name != newName:
updateReferences(newNodes, nodes, name, newName)
return list(newNodes.values())
######## DOT
def toDot(out):
def formatNode(node):
nodeString = '"{}" [ shape = {} ]'
if "gate" not in node:
return nodeString.format(node["name"], "circle")
if node['gate']['type']=='and':
return nodeString.format(node["name"], "trapezium")
if node['gate']['type']=='or':
return nodeString.format(node["name"], "cilinder")
if node['gate']['type']=='vote':
return nodeString.format(node["name"], "square")
return nodeString.format(node["name"], "star")
def formatEdge(ns):
n1,n2=ns
return '"{}" -> "{}"'.format(n1,n2)
nodes = list(map(formatNode, out))
edges = []
for n in out:
if "causes" not in n: continue
for c in n["causes"]:
edges.append((n["name"], c))
edges = list(map(formatEdge, edges))
return"digraph G{{ \n\t{}\n\t{}\n}}".format("\n\t".join(nodes), "\n\t".join(edges))
############# IO
def readJSON(fileName):
with open(fileName) as f:
return json.loads(f.read())
def writeJSON(fileName, data):
with open(fileName, 'w') as f:
json.dump(data, f, indent=4)
def writeDOT(fileName, data):
with open(fileName, 'w') as f:
f.write(data)
def convertFile(inpFile, outFile, dotFile=None):
inp = readJSON(inpFile)
out = simplify(inp)
writeJSON(outFile, out)
if dotFile:
writeDOT(dotFile, toDot(out))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Simplify attack trees', epilog="""
If the input is a directory, output (and dot) have to be directories too.
If the input is a file, but output/dot is a directory, the name of the input file will be used as name for the created file.
""")
parser.add_argument('input_file', metavar='INfile/dir', type=str,
help='A file/directory to be converted')
parser.add_argument('output_file', metavar='OUTfile/dir', type=str,
help='The file/directory to store the new attack tree.')
parser.add_argument('--dot', metavar="DOTfile/dir", type=str,
help="Optionally, also create a DOT file of the new attack tree.")
parsed = parser.parse_args()
inpFile = parsed.input_file
outFile = parsed.output_file
dotFile = parsed.dot
doDot = dotFile is not None
if not os.path.isfile(inpFile):
if not os.path.isdir(inpFile):
raise FileNotFoundError("the specified input file/directory does not exist")
if not os.path.isdir(outFile):
raise NotADirectoryError("if the input is a directory, the output should be too!")
if doDot and not os.path.isdir(dotFile):
raise NotADirectoryError("if the input is a directory, the DOT should be too!")
# an entire directory at once
for file in os.scandir(inpFile):
if file.path.lower().endswith(".json") and file.is_file():
basename = os.path.basename(file)
output = os.path.join(outFile, basename)
if doDot:
dot = os.path.join(dotFile, os.path.splitext(basename)[0]+".dot")
else: dot=None
convertFile(file, output, dot)
exit()
#single file
basename = os.path.basename(inpFile)
if os.path.isdir(outFile):
outFile = os.path.join(outFile, basename)
if doDot:
dot = os.path.join(dotFile, os.path.splitext(basename)[0]+".dot")
else:
dot=None
convertFile(inpFile, outFile, dot)
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment