Deployed version (https://attack-trees.vercel.app).
- React (https://reactjs.org)
- Typescript (https://www.typescriptlang.org/)
- Tailwind (https://tailwindcss.com/)
- CSS
- Cloud Database (https://fauna.com/)
- Cloudinary for file storage (https://cloudinary.com/)
The frontend application is built using a javascript
framework called React.js
with a supporting library React-Flow
to help drawing svg boxes to the HTML canvas.
<ReactFlow
elements={elements}
nodeTypes={nodeTypes}
onConnect={onConnect}
onNodeDragStop={handleNodeDragStop}
onDragOver={onDragOver}
onDrop={onDrop}
onLoad={onLoad}
>
<Background
variant={BackgroundVariant.Dots}
gap={12}
size={0.5}
/>
<Controls />
</ReactFlow>
This is a cloud NoSQL database that stores data about attack trees including but not limited to nodes
, file-names
, comments
. This cloud database provides an API/SDK to integrate seamlessly with the frontend applications. This is atypical to conventional development approach where there is a dedicated backend.
This is the cloud file storage used in this application to store the files. While the Faunadb
stores the source_url
for the files itself. This also provides an easy to integrate API/SDK to integrate directly with the fronend application.
const uploadFile = async (
treeId: string,
nodeTitle: string,
nodeId: string,
filename: string,
files: FileList
) => {
const data = new FormData();
data.append("file", files[0]);
data.append("public_id", `${treeId}/${filename}`);
data.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET!);
try {
//upload to cloudinary
const response = await (
await fetch(process.env.REACT_APP_CLOUDINARY_URL!, {
method: "POST",
body: data,
})
).json();
//upload to database
await db.client.query(
db.q.Create(db.q.Collection(FILE_COLLECTION), {
data: {
name: filename,
treeId,
nodeTitle,
nodeId,
url: response.url,
format: response.format,
},
})
);
} catch (err) {}
};
- Arrays
- Tree
- Breadth first traversal:
The initial nodes rendering in the attack-tree workflow area was represented using an Array data structure. However, to calculate the cheapest path, that had to be copied into a Tree Data structure. The Breadth first traversal algorithm was useful in this regard. see below:
//breadth first traversal
traverse = (cb: (node: TreeNode) => void) => {
if (!this.root) return;
const queue: TreeNode[] = [this.root];
while (queue.length) {
const next = queue.shift();
cb(next!);
queue.push(...next!.children);
}
};
- Depth first traversal:
In calculating the cheapest path itself, depth first algorithm was a better choice, because all nodes from the parent source node to the leaf nodes had to be traverse to get the true cheapest path.
//depth first
getCheapestPath = (): FullPath[""] | undefined => {
if (!this.root) return;
const validPath: FullPath = {};
let counter = 0;
const stack = [this.root];
while (stack.length) {
const next = stack.pop();
if (next!.children.length) {
stack.push(...next!.children);
} else {
//is a leaf node
let weightSum = next!.data.data.nodeWeight;
validPath[`${counter}`] = {
path: [
...next!.getAncestors((weight) => (weightSum += weight ?? 0)),
next!,
],
weightSum,
};
++counter;
}
}
return Object.entries(validPath).reduce(
(cheapestPath: FullPath[""], entry) => {
const [, { path, weightSum }] = entry;
if (weightSum < cheapestPath.weightSum) {
cheapestPath.path = path;
cheapestPath.weightSum = weightSum;
}
return cheapestPath;
},
{
path: [],
weightSum: Infinity,
}
);
};
git clone
cd attack-trees
yarn
yarn start
head to your browser and type in http://localhost:3000
you should see this.