# adtree-viz
## Intro
An Attack-Defense Tree modelling lib that allows user to model attack-defense scenarios using an internal DSL.
Project inspired by https://github.com/hyakuhei/attackTrees and https://github.com/tahti/ADTool2.
The main goals are:
- add support for AND nodes
- be able to break down a large tree into multiple subtrees.
- keep it simple, only Attack and Defense nodes
## Usage
Requirements:
- `Graphviz`
- `Python 3.9`
Install the library
```shell
pip install adtree-viz
```
Quick start
```python
from adtree.models import Attack, Defence, AndGate, ADTree
from adtree.renderer import Renderer
from adtree.themes import RedBlueFillTheme
tree = ADTree("REFS.01", Attack("the goal", [
Attack("path1", [
Defence("defend path1", [
Attack("path1 defence defeated")
])
]),
Attack("path2", [
Attack("path2.1"),
AndGate([
Attack("path3.1"),
Attack("path3.2"),
]),
]),
]))
theme = RedBlueFillTheme()
renderer = Renderer(theme=theme, output_format="png", view=True)
renderer.render(tree=tree, filename="my-adtree")
```
The above should produce an attack-defence tree like this:
![attack-defence tree](images/test_theme.test_render_outline.expected.dot.png)
## Composing trees
Trees can be composed of multiple subtrees.
Which of the subtrees get expanded is decided at render time based on the `subtrees_to_expand` variable.
```python
from adtree.models import Attack, ADTree, ExternalADTree
from adtree.renderer import Renderer
from adtree.themes import NoFormatTheme
some_external_ref = ExternalADTree("EXT.01", "External resource covered by other docs")
some_internal_ref1 = ADTree("INT.01", root_node=Attack("internal path1", [
Attack("path 1.1", [
ADTree("INT.01.A", Attack("nested path 1.1A"))
])
]))
some_internal_ref2 = ADTree("INT.02", root_node=Attack("internal path2", [
Attack("path 2.1")
]))
tree = ADTree("REFS.01", Attack("node1", [
some_external_ref,
some_internal_ref1,
some_internal_ref2
]))
theme = NoFormatTheme()
renderer = Renderer(theme=theme, output_format="png", view=False)
# Default is to not expand
renderer.render(tree=tree, filename="default")
# Optionally expand some nodes
renderer.render(tree=tree, subtrees_to_expand=[some_internal_ref1], filename="partially_expanded")
```
The above will render two files.
One with all the subtrees collapsed (the default):
![attack-defence tree](images/test_trees.test_references_default.expected.dot.png)
And another file with one subtree expanded:
![attack-defence tree](images/test_trees.test_references_some_toggled.expected.dot.png)
## Analysing trees
Currently, there is only one analyser available, the IsDefendedAnalyser.
Traverse the tree and mark each nodes as either defended or undefended
A node is considered defended if:
1. is a Defence node and has no children
2. is an Attack node and has a direct defended Defence node as child
3. is an Attack or Defence node and all child nodes are defended nodes
4. is an AndGate and at least one child node is defended
Example with custom rendering of the defended nodes
```python
from adtree.models import NodeType, Node, Attack, ADTree, Defence, AndGate
from adtree.analysers import IsDefendedAnalyser
from adtree.renderer import Renderer
from adtree.themes import NoFormatTheme
class CustomIsDefendedTheme(NoFormatTheme):
def get_node_attrs_for(self, node: Node):
metadata_attrs = {
"style": "filled"
}
if node.get_node_type() == NodeType.DEFENCE:
metadata_attrs |= {
"shape": "box",
}
if node.get_node_type() == NodeType.AND_GATE:
metadata_attrs |= {
"shape": "triangle",
}
if node.has_metadata(IsDefendedAnalyser.METADATA_KEY):
fillcolor = "#C8FFCB" if node.get_metadata(IsDefendedAnalyser.METADATA_KEY) else "#FFD3D6"
metadata_attrs |= {
"fillcolor": fillcolor,
}
return metadata_attrs
tree = ADTree("REFS.01", Attack("the goal", [
Attack("path1", [
Defence("defend path1", [
Attack("path1 defence defeated")
])
]),
Attack("path2", [
Attack("path2.1", [
Defence("def2.1"),
Attack("path2.1.1")
]),
AndGate([
Attack("path3.1"),
Attack("path3.2", [
Defence("defended")
]),
]),
]),
]))
analyser = IsDefendedAnalyser()
analyser.analyse_tree(tree)
theme = CustomIsDefendedTheme()
renderer = Renderer(theme=theme, output_format="png", view=False)
# Default is to not expand
renderer.render(tree=tree, filename="default")
```
The above should produce an attack-defence tree like this:
![attack-defence tree](images/test_analysers.test_is_defended.expected.dot.png)
## Development
Create a venv
```shell
python3.9 -m venv venv
```
Activate
```shell
. venv/bin/activate
```
Install deps
```shell
pip install -r requirements.txt
```
Run tests
```shell
PYTHONPATH=src python -m pytest
```
Run individual test file
```shell
PYTHONPATH=src python -m pytest ./test/adtree/test_theme.py
```
Run individual test methods
```shell
PYTHONPATH=src python -m pytest --capture=no ./test/adtree/test_theme.py -k "metadata"
```
## Release to Github and PyPi
Create tag and push
```
./release.sh
```
## Manually build and release
Run the below to generate a distributable archive:
```bash
python3 -m build
```
The `adtree-viz-x.xx.x.tar.gz` archive can be found in the `dist` folder.
Deploy to PyPi
```shell
python3 -m twine upload -r pypi dist/*
# Use __token__ as username
# Use PyPi API TOKEN as password
```
Raw data
{
"_id": null,
"home_page": "https://github.com/julianghionoiu/adtree-viz",
"name": "adtree-viz",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "attack-defence,adtree",
"author": "Julian Ghionoiu",
"author_email": "julian.ghionoiu@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/37/66/cb169f0f739369ffce799e7907b657495402f6a2ac78720952372f6edb92/adtree-viz-0.0.10.tar.gz",
"platform": null,
"description": "# adtree-viz\n\n## Intro\n\nAn Attack-Defense Tree modelling lib that allows user to model attack-defense scenarios using an internal DSL.\n\nProject inspired by https://github.com/hyakuhei/attackTrees and https://github.com/tahti/ADTool2.\n\nThe main goals are:\n- add support for AND nodes\n- be able to break down a large tree into multiple subtrees.\n- keep it simple, only Attack and Defense nodes\n\n## Usage\n\nRequirements:\n- `Graphviz`\n- `Python 3.9`\n\n\nInstall the library\n```shell\npip install adtree-viz\n```\n\nQuick start\n\n```python\nfrom adtree.models import Attack, Defence, AndGate, ADTree\nfrom adtree.renderer import Renderer\nfrom adtree.themes import RedBlueFillTheme\n\ntree = ADTree(\"REFS.01\", Attack(\"the goal\", [\n Attack(\"path1\", [\n Defence(\"defend path1\", [\n Attack(\"path1 defence defeated\")\n ])\n ]),\n Attack(\"path2\", [\n Attack(\"path2.1\"),\n AndGate([\n Attack(\"path3.1\"),\n Attack(\"path3.2\"),\n ]),\n ]),\n]))\n\ntheme = RedBlueFillTheme()\nrenderer = Renderer(theme=theme, output_format=\"png\", view=True)\nrenderer.render(tree=tree, filename=\"my-adtree\")\n```\n\nThe above should produce an attack-defence tree like this:\n![attack-defence tree](images/test_theme.test_render_outline.expected.dot.png)\n\n## Composing trees\n\nTrees can be composed of multiple subtrees.\nWhich of the subtrees get expanded is decided at render time based on the `subtrees_to_expand` variable.\n```python\nfrom adtree.models import Attack, ADTree, ExternalADTree\nfrom adtree.renderer import Renderer\nfrom adtree.themes import NoFormatTheme\n\nsome_external_ref = ExternalADTree(\"EXT.01\", \"External resource covered by other docs\")\nsome_internal_ref1 = ADTree(\"INT.01\", root_node=Attack(\"internal path1\", [\n Attack(\"path 1.1\", [\n ADTree(\"INT.01.A\", Attack(\"nested path 1.1A\"))\n ])\n]))\nsome_internal_ref2 = ADTree(\"INT.02\", root_node=Attack(\"internal path2\", [\n Attack(\"path 2.1\")\n]))\ntree = ADTree(\"REFS.01\", Attack(\"node1\", [\n some_external_ref,\n some_internal_ref1,\n some_internal_ref2\n]))\n\ntheme = NoFormatTheme()\nrenderer = Renderer(theme=theme, output_format=\"png\", view=False)\n\n# Default is to not expand\nrenderer.render(tree=tree, filename=\"default\")\n\n# Optionally expand some nodes\nrenderer.render(tree=tree, subtrees_to_expand=[some_internal_ref1], filename=\"partially_expanded\")\n```\n\nThe above will render two files.\n\nOne with all the subtrees collapsed (the default):\n![attack-defence tree](images/test_trees.test_references_default.expected.dot.png)\n\nAnd another file with one subtree expanded:\n![attack-defence tree](images/test_trees.test_references_some_toggled.expected.dot.png)\n\n\n## Analysing trees\n\nCurrently, there is only one analyser available, the IsDefendedAnalyser.\nTraverse the tree and mark each nodes as either defended or undefended\nA node is considered defended if:\n1. is a Defence node and has no children\n2. is an Attack node and has a direct defended Defence node as child\n3. is an Attack or Defence node and all child nodes are defended nodes\n4. is an AndGate and at least one child node is defended\n\nExample with custom rendering of the defended nodes\n```python\nfrom adtree.models import NodeType, Node, Attack, ADTree, Defence, AndGate\nfrom adtree.analysers import IsDefendedAnalyser\nfrom adtree.renderer import Renderer\nfrom adtree.themes import NoFormatTheme\n\nclass CustomIsDefendedTheme(NoFormatTheme):\n def get_node_attrs_for(self, node: Node):\n metadata_attrs = {\n \"style\": \"filled\"\n }\n if node.get_node_type() == NodeType.DEFENCE:\n metadata_attrs |= {\n \"shape\": \"box\",\n }\n if node.get_node_type() == NodeType.AND_GATE:\n metadata_attrs |= {\n \"shape\": \"triangle\",\n }\n if node.has_metadata(IsDefendedAnalyser.METADATA_KEY):\n fillcolor = \"#C8FFCB\" if node.get_metadata(IsDefendedAnalyser.METADATA_KEY) else \"#FFD3D6\"\n metadata_attrs |= {\n \"fillcolor\": fillcolor,\n }\n return metadata_attrs\n\ntree = ADTree(\"REFS.01\", Attack(\"the goal\", [\n Attack(\"path1\", [\n Defence(\"defend path1\", [\n Attack(\"path1 defence defeated\")\n ])\n ]),\n Attack(\"path2\", [\n Attack(\"path2.1\", [\n Defence(\"def2.1\"),\n Attack(\"path2.1.1\")\n ]),\n AndGate([\n Attack(\"path3.1\"),\n Attack(\"path3.2\", [\n Defence(\"defended\")\n ]),\n ]),\n ]),\n]))\n\nanalyser = IsDefendedAnalyser()\nanalyser.analyse_tree(tree)\n\ntheme = CustomIsDefendedTheme()\nrenderer = Renderer(theme=theme, output_format=\"png\", view=False)\n\n# Default is to not expand\nrenderer.render(tree=tree, filename=\"default\")\n```\n\nThe above should produce an attack-defence tree like this:\n![attack-defence tree](images/test_analysers.test_is_defended.expected.dot.png)\n\n\n## Development\n\nCreate a venv\n```shell\npython3.9 -m venv venv\n```\n\nActivate \n```shell\n . venv/bin/activate\n```\n\nInstall deps\n```shell\npip install -r requirements.txt\n```\n\nRun tests\n```shell\nPYTHONPATH=src python -m pytest\n```\n\nRun individual test file\n```shell\nPYTHONPATH=src python -m pytest ./test/adtree/test_theme.py\n```\n\nRun individual test methods\n```shell\nPYTHONPATH=src python -m pytest --capture=no ./test/adtree/test_theme.py -k \"metadata\"\n```\n\n\n## Release to Github and PyPi\n\nCreate tag and push\n```\n./release.sh\n```\n\n## Manually build and release\n\nRun the below to generate a distributable archive:\n```bash\npython3 -m build\n```\n\nThe `adtree-viz-x.xx.x.tar.gz` archive can be found in the `dist` folder.\n\nDeploy to PyPi\n```shell\npython3 -m twine upload -r pypi dist/*\n\n# Use __token__ as username\n# Use PyPi API TOKEN as password\n```\n",
"bugtrack_url": null,
"license": "",
"summary": "adtree-viz",
"version": "0.0.10",
"split_keywords": [
"attack-defence",
"adtree"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "e1140a620a0e1dc0ecf347b717be1cb0b7d6808013759c8e2e32b4afb925c384",
"md5": "31516ad7add401cd4c562fb03b001019",
"sha256": "f7aeacd303b2a2c43fd7a2099ebed74c0b66dad7c99f992a269d1e1c82712dba"
},
"downloads": -1,
"filename": "adtree_viz-0.0.10-py3-none-any.whl",
"has_sig": false,
"md5_digest": "31516ad7add401cd4c562fb03b001019",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 10936,
"upload_time": "2023-01-16T17:28:16",
"upload_time_iso_8601": "2023-01-16T17:28:16.913802Z",
"url": "https://files.pythonhosted.org/packages/e1/14/0a620a0e1dc0ecf347b717be1cb0b7d6808013759c8e2e32b4afb925c384/adtree_viz-0.0.10-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "3766cb169f0f739369ffce799e7907b657495402f6a2ac78720952372f6edb92",
"md5": "a6d83f9dd7365d35c24ffdbb7dcf25ed",
"sha256": "752ca90acdedf81faaeb218b5fecfed7dd11abc0091066c96ebd542696648643"
},
"downloads": -1,
"filename": "adtree-viz-0.0.10.tar.gz",
"has_sig": false,
"md5_digest": "a6d83f9dd7365d35c24ffdbb7dcf25ed",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 11184,
"upload_time": "2023-01-16T17:28:18",
"upload_time_iso_8601": "2023-01-16T17:28:18.474661Z",
"url": "https://files.pythonhosted.org/packages/37/66/cb169f0f739369ffce799e7907b657495402f6a2ac78720952372f6edb92/adtree-viz-0.0.10.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-01-16 17:28:18",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "julianghionoiu",
"github_project": "adtree-viz",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [],
"lcname": "adtree-viz"
}