├── docs ├── .nojekyll ├── objects.inv ├── _static │ ├── up.png │ ├── down.png │ ├── file.png │ ├── minus.png │ ├── plus.png │ ├── comment.png │ ├── up-pressed.png │ ├── ajax-loader.gif │ ├── down-pressed.png │ ├── comment-bright.png │ ├── comment-close.png │ ├── fonts │ │ ├── Lato-Bold.ttf │ │ ├── Inconsolata.ttf │ │ ├── Lato-Regular.ttf │ │ ├── Lato │ │ │ ├── lato-bold.eot │ │ │ ├── lato-bold.ttf │ │ │ ├── lato-bold.woff │ │ │ ├── lato-bold.woff2 │ │ │ ├── lato-italic.eot │ │ │ ├── lato-italic.ttf │ │ │ ├── lato-italic.woff │ │ │ ├── lato-italic.woff2 │ │ │ ├── lato-regular.eot │ │ │ ├── lato-regular.ttf │ │ │ ├── lato-regular.woff │ │ │ ├── lato-regular.woff2 │ │ │ ├── lato-bolditalic.eot │ │ │ ├── lato-bolditalic.ttf │ │ │ ├── lato-bolditalic.woff │ │ │ └── lato-bolditalic.woff2 │ │ ├── Inconsolata-Bold.ttf │ │ ├── RobotoSlab-Bold.ttf │ │ ├── Inconsolata-Regular.ttf │ │ ├── RobotoSlab-Regular.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ └── RobotoSlab │ │ │ ├── roboto-slab-v7-bold.eot │ │ │ ├── roboto-slab-v7-bold.ttf │ │ │ ├── roboto-slab-v7-bold.woff │ │ │ ├── roboto-slab-v7-bold.woff2 │ │ │ ├── roboto-slab-v7-regular.eot │ │ │ ├── roboto-slab-v7-regular.ttf │ │ │ ├── roboto-slab-v7-regular.woff │ │ │ └── roboto-slab-v7-regular.woff2 │ ├── documentation_options.js │ ├── css │ │ └── badge_only.css │ ├── js │ │ └── theme.js │ ├── pygments.css │ └── doctools.js ├── doctrees │ ├── index.doctree │ ├── environment.pickle │ ├── examples.doctree │ ├── quickstart.doctree │ ├── apireference.doctree │ └── howtoinstall.doctree ├── .buildinfo ├── _sources │ ├── apireference.rst.txt │ ├── howtoinstall.rst.txt │ ├── index.rst.txt │ └── examples.rst.txt ├── search.html ├── py-modindex.html ├── howtoinstall.html ├── index.html └── searchindex.js ├── tests ├── __init__.py ├── test_helpers.py ├── test_has_node.py ├── test_create_collection.py ├── test_viewers_editors.py ├── test_delete_node.py ├── test_create_group.py ├── test_save_graph.py ├── test_delete_link.py ├── test_clone_graph.py ├── test_link.py ├── test_add_node.py ├── test_load_graph.py ├── resources │ └── virustotal_graph_id.json └── test_expand.py ├── docsrc ├── _static │ ├── up.png │ ├── down.png │ ├── file.png │ ├── minus.png │ ├── plus.png │ ├── comment.png │ ├── up-pressed.png │ ├── ajax-loader.gif │ ├── comment-close.png │ ├── down-pressed.png │ ├── comment-bright.png │ ├── fonts │ │ ├── Lato-Bold.ttf │ │ ├── Inconsolata.ttf │ │ ├── Lato-Regular.ttf │ │ ├── Lato │ │ │ ├── lato-bold.eot │ │ │ ├── lato-bold.ttf │ │ │ ├── lato-bold.woff │ │ │ ├── lato-bold.woff2 │ │ │ ├── lato-italic.eot │ │ │ ├── lato-italic.ttf │ │ │ ├── lato-italic.woff │ │ │ ├── lato-regular.eot │ │ │ ├── lato-regular.ttf │ │ │ ├── lato-italic.woff2 │ │ │ ├── lato-regular.woff │ │ │ ├── lato-regular.woff2 │ │ │ ├── lato-bolditalic.eot │ │ │ ├── lato-bolditalic.ttf │ │ │ ├── lato-bolditalic.woff │ │ │ └── lato-bolditalic.woff2 │ │ ├── Inconsolata-Bold.ttf │ │ ├── RobotoSlab-Bold.ttf │ │ ├── Inconsolata-Regular.ttf │ │ ├── RobotoSlab-Regular.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ └── RobotoSlab │ │ │ ├── roboto-slab-v7-bold.eot │ │ │ ├── roboto-slab-v7-bold.ttf │ │ │ ├── roboto-slab-v7-bold.woff │ │ │ ├── roboto-slab-v7-bold.woff2 │ │ │ ├── roboto-slab-v7-regular.eot │ │ │ ├── roboto-slab-v7-regular.ttf │ │ │ ├── roboto-slab-v7-regular.woff │ │ │ └── roboto-slab-v7-regular.woff2 │ ├── documentation_options.js │ ├── css │ │ └── badge_only.css │ ├── js │ │ └── theme.js │ ├── pygments.css │ └── doctools.js ├── apireference.rst ├── howtoinstall.rst ├── Makefile ├── index.rst ├── make.bat ├── conf.py └── examples.rst ├── vt_graph_api ├── version.py ├── __init__.py ├── errors.py ├── helpers.py └── node.py ├── examples ├── download_screenshot.py ├── load_graph_vt.py ├── collection_from_graph.py ├── basic_search.py ├── basic_graph.py ├── advanced_search.py └── advanced_graph.py ├── .github └── workflows │ └── tests.yaml ├── setup.py ├── .gitignore └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/objects.inv -------------------------------------------------------------------------------- /docs/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/up.png -------------------------------------------------------------------------------- /docs/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/down.png -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/minus.png -------------------------------------------------------------------------------- /docs/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/plus.png -------------------------------------------------------------------------------- /docsrc/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/up.png -------------------------------------------------------------------------------- /docs/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/comment.png -------------------------------------------------------------------------------- /docsrc/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/down.png -------------------------------------------------------------------------------- /docsrc/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/file.png -------------------------------------------------------------------------------- /docsrc/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/minus.png -------------------------------------------------------------------------------- /docsrc/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/plus.png -------------------------------------------------------------------------------- /docs/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/index.doctree -------------------------------------------------------------------------------- /docsrc/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/comment.png -------------------------------------------------------------------------------- /docs/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/down-pressed.png -------------------------------------------------------------------------------- /docsrc/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/comment-close.png -------------------------------------------------------------------------------- /docs/_static/fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /docs/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/doctrees/examples.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/examples.doctree -------------------------------------------------------------------------------- /docs/doctrees/quickstart.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/quickstart.doctree -------------------------------------------------------------------------------- /docsrc/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docsrc/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/comment-close.png -------------------------------------------------------------------------------- /docsrc/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/_static/fonts/Inconsolata.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Inconsolata.ttf -------------------------------------------------------------------------------- /docs/doctrees/apireference.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/apireference.doctree -------------------------------------------------------------------------------- /docs/doctrees/howtoinstall.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/doctrees/howtoinstall.doctree -------------------------------------------------------------------------------- /docsrc/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/comment-bright.png -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bold.eot -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bold.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Inconsolata.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Inconsolata.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bold.woff -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-italic.eot -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-italic.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab-Bold.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bold.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-italic.woff -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-italic.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-regular.eot -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-regular.woff -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-regular.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bold.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bold.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-italic.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-italic.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-italic.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-regular.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-regular.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bolditalic.eot -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bolditalic.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bolditalic.woff -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-italic.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-regular.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-regular.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab-Regular.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato/lato-bolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/Lato/lato-bolditalic.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bolditalic.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bolditalic.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bolditalic.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/Lato/lato-bolditalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/Lato/lato-bolditalic.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 -------------------------------------------------------------------------------- /vt_graph_api/version.py: -------------------------------------------------------------------------------- 1 | """vt_graph_api.version. 2 | 3 | This module provides version information. 4 | """ 5 | 6 | 7 | __version__ = '2.2.0' 8 | __x_tool__ = 'Graph' 9 | -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff -------------------------------------------------------------------------------- /docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusTotal/vt-graph-api/HEAD/docsrc/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 -------------------------------------------------------------------------------- /docs/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 699ad53166c0c12ba2bfed9132a678b1 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /vt_graph_api/__init__.py: -------------------------------------------------------------------------------- 1 | """vt_graph_api. 2 | 3 | vt_graph_api package exports. 4 | """ 5 | 6 | 7 | from vt_graph_api.graph import VTGraph, RepresentationType 8 | from vt_graph_api.node import Node 9 | from vt_graph_api.version import __version__ 10 | 11 | 12 | __all__ = ["Node", "VTGraph", "RepresentationType", "__version__"] 13 | -------------------------------------------------------------------------------- /examples/download_screenshot.py: -------------------------------------------------------------------------------- 1 | """VTGraph get screenshot.""" 2 | 3 | import vt_graph_api 4 | 5 | API_KEY = "" # Insert your VT API here. 6 | GRAPH_ID = "" # Insert yout graph id here. 7 | 8 | # Retrieve the graph. 9 | graph = vt_graph_api.VTGraph.load_graph(GRAPH_ID, API_KEY) 10 | 11 | # Download screenshot 12 | graph.download_screenshot(path=".") 13 | -------------------------------------------------------------------------------- /docs/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '1.0.1', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | FILE_SUFFIX: '.html', 7 | HAS_SOURCE: true, 8 | SOURCELINK_SUFFIX: '.txt', 9 | NAVIGATION_WITH_KEYS: false, 10 | }; -------------------------------------------------------------------------------- /docsrc/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '1.0.1', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | FILE_SUFFIX: '.html', 7 | HAS_SOURCE: true, 8 | SOURCELINK_SUFFIX: '.txt', 9 | NAVIGATION_WITH_KEYS: false, 10 | }; -------------------------------------------------------------------------------- /examples/load_graph_vt.py: -------------------------------------------------------------------------------- 1 | """VirusTotal Graph id load example.""" 2 | 3 | 4 | import vt_graph_api 5 | 6 | 7 | API_KEY = "" # Insert your VT API here. 8 | GRAPH_ID = "" # Insert yout graph id here. 9 | 10 | 11 | # Retrieve the graph. 12 | graph = vt_graph_api.VTGraph.load_graph(GRAPH_ID, API_KEY) 13 | 14 | # Modify your graph here 15 | 16 | # Save it in VirusTotal. 17 | graph.save_graph() 18 | 19 | # Get the graph id 20 | print("Graph Id: %s" % graph.graph_id) 21 | 22 | # Visualizing the Graph 23 | print(graph.get_ui_link()) # Open the url in the browser 24 | -------------------------------------------------------------------------------- /docsrc/apireference.rst: -------------------------------------------------------------------------------- 1 | .. _APIReference overview: 2 | 3 | ************** 4 | API Reference 5 | ************** 6 | 7 | graph 8 | --------------------------- 9 | 10 | .. automodule:: vt_graph_api.graph 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | node 16 | -------------------------- 17 | 18 | .. automodule:: vt_graph_api.node 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | errors 24 | ---------------------------- 25 | 26 | .. automodule:: vt_graph_api.errors 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/_sources/apireference.rst.txt: -------------------------------------------------------------------------------- 1 | .. _APIReference overview: 2 | 3 | ************** 4 | API Reference 5 | ************** 6 | 7 | graph 8 | --------------------------- 9 | 10 | .. automodule:: vt_graph_api.graph 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | node 16 | -------------------------- 17 | 18 | .. automodule:: vt_graph_api.node 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | errors 24 | ---------------------------- 25 | 26 | .. automodule:: vt_graph_api.errors 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docsrc/howtoinstall.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | How to install 3 | ************** 4 | 5 | The easiest way of installing **vt_graph_api** is using `pip`: 6 | 7 | .. code-block:: sh 8 | 9 | $ pip install vt_graph_api 10 | 11 | Alternatively, you can get the source code directly from the GitHub and run 12 | `setup.py`. For getting the code you can clone the public 13 | repository: 14 | 15 | .. code-block:: sh 16 | 17 | $ git clone git://github.com/VirusTotal/vt-graph-api.git 18 | $ cd vt-py 19 | 20 | Once you have the code you can install it with: 21 | 22 | .. code-block:: sh 23 | 24 | $ pip install . --user 25 | -------------------------------------------------------------------------------- /docs/_sources/howtoinstall.rst.txt: -------------------------------------------------------------------------------- 1 | ************** 2 | How to install 3 | ************** 4 | 5 | The easiest way of installing **vt_graph_api** is using `pip`: 6 | 7 | .. code-block:: sh 8 | 9 | $ pip install vt_graph_api 10 | 11 | Alternatively, you can get the source code directly from the GitHub and run 12 | `setup.py`. For getting the code you can clone the public 13 | repository: 14 | 15 | .. code-block:: sh 16 | 17 | $ git clone git://github.com/VirusTotal/vt-graph-api.git 18 | $ cd vt-py 19 | 20 | Once you have the code you can install it with: 21 | 22 | .. code-block:: sh 23 | 24 | $ pip install . --user 25 | -------------------------------------------------------------------------------- /examples/collection_from_graph.py: -------------------------------------------------------------------------------- 1 | """Create a VT Collection from VT Graph.""" 2 | 3 | from vt_graph_api import VTGraph 4 | 5 | API_KEY = "" # Insert your VT API here. 6 | 7 | # Creates the graph. 8 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 9 | 10 | # Adds the node. 11 | graph.add_node( 12 | "malpedia_win_emotet", 13 | "collection", label="Emotet Collection") 14 | 15 | # Expands the graph 1 level. 16 | graph.expand_n_level(level=1, max_nodes_per_relationship=5, max_nodes=100) 17 | 18 | collection_ui_link = graph.create_collection("New Emotet collection") 19 | 20 | # Visualizing Collection link 21 | print(collection_ui_link) 22 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | """Test vt_graph_api.helpers module.""" 2 | 3 | 4 | import vt_graph_api.helpers as helpers 5 | 6 | 7 | def test_safe_get(): 8 | """Test safe_get.""" 9 | my_dict = {"a": {"b": {"c": "my_value"}}} 10 | assert helpers.safe_get(my_dict, "a", "b", "c") == "my_value" 11 | assert not helpers.safe_get(my_dict, "a", "z") 12 | assert ( 13 | helpers.safe_get(my_dict, "a", "z", default="my_other_value") == 14 | "my_other_value") 15 | 16 | my_other_dict = {"a": ["first"]} 17 | assert helpers.safe_get(my_other_dict, "a", 0) == "first" 18 | assert not helpers.safe_get(my_other_dict, "a", 1) 19 | assert helpers.safe_get(my_other_dict, "a", 1, default="second") == "second" 20 | -------------------------------------------------------------------------------- /docsrc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = ./build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | github: 18 | @make html 19 | @cp -a build/html/. ../docs 20 | 21 | # Catch-all target: route all unknown targets to Sphinx using the new 22 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /examples/basic_search.py: -------------------------------------------------------------------------------- 1 | """VTGraph basic search usage example.""" 2 | 3 | from vt_graph_api import VTGraph 4 | 5 | 6 | API_KEY = "" # Insert your VT API here. 7 | 8 | 9 | # Creates the graph. 10 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 11 | 12 | # Add some nodes to graph. 13 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 14 | graph.add_node("nsis.sf.net", "domain", True) 15 | 16 | graph.add_links_if_match( 17 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", "nsis.sf.net", 18 | max_api_quotas=1000, max_depth=10) 19 | 20 | # Try to connect node with graph. 21 | graph.save_graph() 22 | 23 | # Get the graph id 24 | print("Graph Id: %s" % graph.graph_id) 25 | 26 | # Visualizing the Graph 27 | print(graph.get_ui_link()) # Open the url in the browser 28 | -------------------------------------------------------------------------------- /docsrc/index.rst: -------------------------------------------------------------------------------- 1 | .. VirusTotal Graph python API documentation master file, created by 2 | sphinx-quickstart on Thu Sep 19 11:45:47 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to VirusTotal Graph Python API's documentation! 7 | ======================================================= 8 | 9 | 10 | `vt_graph_api `_ is the official Python client library 11 | for the **VirusTotal Graph** that implements the `VirusTotal Graph REST API `_. This library requires Python 3.2+ or Python 2.7. 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Contents: 17 | 18 | howtoinstall.rst 19 | quickstart.rst 20 | apireference.rst 21 | examples.rst 22 | -------------------------------------------------------------------------------- /docs/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. VirusTotal Graph python API documentation master file, created by 2 | sphinx-quickstart on Thu Sep 19 11:45:47 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to VirusTotal Graph Python API's documentation! 7 | ======================================================= 8 | 9 | 10 | `vt_graph_api `_ is the official Python client library 11 | for the **VirusTotal Graph** that implements the `VirusTotal Graph REST API `_. This library requires Python 3.2+ or Python 2.7. 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Contents: 17 | 18 | howtoinstall.rst 19 | quickstart.rst 20 | apireference.rst 21 | examples.rst 22 | -------------------------------------------------------------------------------- /examples/basic_graph.py: -------------------------------------------------------------------------------- 1 | """Basic VTGraph usage example.""" 2 | 3 | 4 | from vt_graph_api import VTGraph, RepresentationType 5 | 6 | 7 | API_KEY = "" # Insert your VT API here. 8 | 9 | 10 | # Creates the graph. 11 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 12 | 13 | # Adds the node. 14 | graph.add_node( 15 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 16 | "file", label="Investigation node") 17 | 18 | # Expands the graph 1 level. 19 | graph.expand_n_level(level=1, max_nodes_per_relationship=5, max_nodes=100) 20 | 21 | # Set representation type 22 | graph.set_representation(RepresentationType.FORCE_GRAPH) 23 | 24 | # Saves the graph 25 | graph.save_graph() 26 | 27 | # Get the graph id 28 | print("Graph Id: %s" % graph.graph_id) 29 | 30 | # Visualizing the Graph 31 | print(graph.get_ui_link()) # Open the url in the browser 32 | -------------------------------------------------------------------------------- /tests/test_has_node.py: -------------------------------------------------------------------------------- 1 | """Test graph has node.""" 2 | 3 | 4 | import vt_graph_api 5 | import vt_graph_api.errors 6 | 7 | 8 | test_graph = vt_graph_api.VTGraph( 9 | "Dummy api key", verbose=False, private=False, name="Graph test", 10 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 11 | 12 | 13 | def test_graph_has_node(mocker): 14 | """Test add node file sha256.""" 15 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 16 | mocker.patch("requests.get", return_value=m) 17 | added_node_id = ( 18 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 19 | test_graph.add_node(added_node_id, "file", label="Investigation node") 20 | assert test_graph.has_node(added_node_id) 21 | mocker.resetall() 22 | 23 | 24 | def test_graph_not_has_node(): 25 | """Test add node file sha256.""" 26 | added_node_id = ("Dummy_id") 27 | assert not test_graph.has_node(added_node_id) 28 | -------------------------------------------------------------------------------- /docsrc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=sources 11 | set BUILDDIR=. 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/advanced_search.py: -------------------------------------------------------------------------------- 1 | """VTGraph advanced search usage example.""" 2 | 3 | from vt_graph_api import VTGraph 4 | 5 | 6 | API_KEY = "" # Insert your VT API here. 7 | 8 | 9 | # Creates the graph. 10 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 11 | 12 | # Add some nodes to graph. 13 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 14 | graph.add_node("nsis.sf.net", "domain", True) 15 | graph.add_node( 16 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", "file") 17 | 18 | graph.add_node("www.openssl.org", "domain", True) 19 | 20 | # Try to connect node with graph. 21 | graph.connect_with_graph( 22 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", max_api_quotas=1000, 23 | max_depth=10) 24 | 25 | graph.save_graph() 26 | 27 | # Get the graph id 28 | print("Graph Id: %s" % graph.graph_id) 29 | 30 | # Visualizing the Graph 31 | print(graph.get_ui_link()) # Open the url in the browser 32 | -------------------------------------------------------------------------------- /vt_graph_api/errors.py: -------------------------------------------------------------------------------- 1 | """vt_graph_api.errors. 2 | 3 | This modules provides all errors that could be raised by 4 | the methods in vt_graph_api package. 5 | """ 6 | 7 | 8 | class NodeNotFoundError(Exception): 9 | pass 10 | 11 | 12 | class LinkNotFoundError(Exception): 13 | pass 14 | 15 | 16 | class NodeNotSupportedTypeError(Exception): 17 | pass 18 | 19 | 20 | class NodeNotSupportedExpansionError(Exception): 21 | pass 22 | 23 | 24 | class CollaboratorNotFoundError(Exception): 25 | pass 26 | 27 | 28 | class SaveGraphError(Exception): 29 | pass 30 | 31 | 32 | class SameNodeError(Exception): 33 | pass 34 | 35 | 36 | class MaximumConnectionRetriesError(Exception): 37 | pass 38 | 39 | 40 | class InvalidJSONError(Exception): 41 | pass 42 | 43 | 44 | class LoadError(Exception): 45 | pass 46 | 47 | class DownloadScreenshotError(Exception): 48 | pass 49 | 50 | class CreateCollectionError(Exception): 51 | pass 52 | 53 | class CreateGroupError(Exception): 54 | pass 55 | -------------------------------------------------------------------------------- /tests/test_create_collection.py: -------------------------------------------------------------------------------- 1 | """Test create collection from graph.""" 2 | 3 | import pytest 4 | import vt_graph_api 5 | import vt_graph_api.errors 6 | 7 | test_graph = vt_graph_api.VTGraph( 8 | "Dummy api key", verbose=False, private=False, name="Graph test", 9 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 10 | 11 | 12 | def test_create_collection(mocker): 13 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={ 14 | 'data': {'id': 'new_collection'} 15 | })) 16 | mocker.patch("requests.post", return_value=m) 17 | test_graph.add_node("virustotal.com", "domain") 18 | collection_url = test_graph.create_collection() 19 | assert collection_url == "https://www.virustotal.com/gui/collection/new_collection" 20 | 21 | 22 | def test_create_collection_fails(mocker): 23 | m = mocker.Mock(status_code=400, json=mocker.Mock({})) 24 | mocker.patch("requests.post", return_value=m) 25 | test_graph.add_node("virustotal.com", "domain") 26 | 27 | with pytest.raises(vt_graph_api.errors.CreateCollectionError): 28 | test_graph.create_collection() 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | Build: 11 | runs-on: '${{ matrix.os }}' 12 | strategy: 13 | matrix: 14 | os: 15 | - ubuntu-18.04 16 | python-version: [2.7, 3.4, 3.5, 3.6, 3.7] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: 'Set up Python ${{ matrix.python-version }}' 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '${{ matrix.python-version }}' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install pytest pytest-mock==1.10.4 pytest-cov coverage requests six futures 27 | - name: Install enum 28 | run: pip install enum34 29 | if: ${{ matrix.python-version == 2.7 }} 30 | - name: Install python/typing 31 | run: pip install typing 32 | if: ${{ matrix.python-version == 3.4 }} 33 | - name: Test with pytest 34 | run: pytest 35 | -------------------------------------------------------------------------------- /vt_graph_api/helpers.py: -------------------------------------------------------------------------------- 1 | """vt_graph_api.helpers. 2 | 3 | This module provides some helper methods. 4 | """ 5 | 6 | 7 | def safe_get(dct, *keys, **kwargs): 8 | """Return the dict value for the given ordered keys. 9 | 10 | Args: 11 | dct (dict): the dictionary that will be consulted. 12 | *keys: positional arguments which contains the key path to the 13 | wanted value. 14 | **kwargs: 15 | default -> If any key is missing, default will be returned. 16 | 17 | Examples: 18 | >>> my_dict = {"a": {"b": {"c": "my_value"}}} 19 | >>> safe_get(my_dict, "a", "b", "c") 20 | 'my_value' 21 | >>> safe_get(my_dict, "a", "z") 22 | >>> safe_get(my_dict, "a", "z", default="my_other_value") 23 | 'my_other_value' 24 | >>> my_other_dict = {"a": ["first"]} 25 | >>> safe_get(my_other_dict, "a", 0) 26 | 'first' 27 | >>> safe_get(my_other_dict, "a", 1) 28 | >>> safe_get(my_other_dict, "a", 1, default="second") 29 | 'second' 30 | 31 | Returns: 32 | Any: the dictionary value for the given ordered keys. 33 | """ 34 | default_value = kwargs.get("default") 35 | for key in keys: 36 | try: 37 | dct = dct[key] 38 | except (KeyError, IndexError): 39 | return default_value 40 | return dct 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup for vt_graph_api module.""" 2 | 3 | 4 | import re 5 | import sys 6 | import setuptools 7 | 8 | 9 | with open("./vt_graph_api/version.py") as f: 10 | version = ( 11 | re.search(r"__version__ = \'([0-9]{1,}.[0-9]{1,}.[0-9]{1,})\'", 12 | f.read()).groups()[0]) 13 | 14 | # check python version >2.7.x and >=3.2.x 15 | installable = True 16 | if sys.version_info.major == 3: 17 | if sys.version_info.minor < 2: 18 | installable = False 19 | else: 20 | if sys.version_info.minor < 7: 21 | installable = False 22 | if not installable: 23 | sys.exit("Sorry, this python version is not supported") 24 | 25 | with open("README.md", "r") as fh: 26 | long_description = fh.read() 27 | 28 | install_requires = [ 29 | "requests", 30 | "six" 31 | ] 32 | 33 | if sys.version_info.major == 2: 34 | # Support enums in python 2. 35 | install_requires.append("enum34") 36 | 37 | setuptools.setup( 38 | name="vt_graph_api", 39 | version=version, 40 | author="VirusTotal", 41 | author_email="vt_graph_api@virustotal.com", 42 | description="The official Python client library for VirusTotal Graph API", 43 | license="Apache 2", 44 | long_description=long_description, 45 | long_description_content_type="text/markdown", 46 | url="https://github.com/virustotal/vt-graph-api", 47 | packages=setuptools.find_packages(), 48 | install_requires=install_requires, 49 | classifiers=[ 50 | "Programming Language :: Python :: 3", 51 | "License :: OSI Approved :: MIT License", 52 | "Operating System :: OS Independent", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # IDE 35 | .vscode/* 36 | .idea/ 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | -------------------------------------------------------------------------------- /tests/test_viewers_editors.py: -------------------------------------------------------------------------------- 1 | """Test VTGraph is_viewer and is_editor.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | VT_USER = "alvarogf" 10 | GRAPH_ID = "Dummy Graph ID" 11 | API_KEY = "Dummy Api Key" 12 | API_RESPONSE = { 13 | "data": [ 14 | { 15 | "id": "alvarogf", 16 | "type": "user" 17 | } 18 | ] 19 | } 20 | 21 | 22 | def test_is_viewer(mocker): 23 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=API_RESPONSE)) 24 | mocker.patch("requests.get", return_value=m) 25 | assert vt_graph_api.VTGraph.is_viewer(VT_USER, GRAPH_ID, API_KEY) 26 | mocker.resetall() 27 | 28 | 29 | def test_is_not_viewer(mocker): 30 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={"data": []})) 31 | mocker.patch("requests.get", return_value=m) 32 | assert not vt_graph_api.VTGraph.is_viewer(VT_USER, GRAPH_ID, API_KEY) 33 | mocker.resetall() 34 | 35 | 36 | def test_viewer_load_error(mocker): 37 | mocker.patch("requests.get", return_value=mocker.Mock(status_code=401)) 38 | with pytest.raises(vt_graph_api.errors.LoadError): 39 | vt_graph_api.VTGraph.is_viewer(VT_USER, GRAPH_ID, API_KEY) 40 | mocker.resetall() 41 | 42 | 43 | def test_is_editor(mocker): 44 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=API_RESPONSE)) 45 | mocker.patch("requests.get", return_value=m) 46 | assert vt_graph_api.VTGraph.is_editor(VT_USER, GRAPH_ID, API_KEY) 47 | mocker.resetall() 48 | 49 | 50 | def test_is_not_editor(mocker): 51 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={"data": []})) 52 | mocker.patch("requests.get", return_value=m) 53 | assert not vt_graph_api.VTGraph.is_editor(VT_USER, GRAPH_ID, API_KEY) 54 | mocker.resetall() 55 | 56 | 57 | def test_editor_load_error(mocker): 58 | mocker.patch("requests.get", return_value=mocker.Mock(status_code=401)) 59 | with pytest.raises(vt_graph_api.errors.LoadError): 60 | vt_graph_api.VTGraph.is_editor(VT_USER, GRAPH_ID, API_KEY) 61 | mocker.resetall() 62 | -------------------------------------------------------------------------------- /tests/test_delete_node.py: -------------------------------------------------------------------------------- 1 | """Test delete node from graph.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_delete_existing_node(mocker): 15 | """Test delete existing node.""" 16 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 17 | mocker.patch("requests.get", return_value=m) 18 | added_node_id = ( 19 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 20 | test_graph.add_node(added_node_id, "file", label="Investigation node") 21 | test_graph.delete_node(added_node_id) 22 | assert not test_graph.nodes 23 | mocker.resetall() 24 | 25 | 26 | def test_delete_node_with_links(mocker): 27 | """Test delete existing node with links.""" 28 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 29 | mocker.patch("requests.get", return_value=m) 30 | added_node_id_a = ( 31 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 32 | added_node_id_b = ( 33 | "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16") 34 | test_graph.add_node(added_node_id_a, "file", label="Investigation node") 35 | test_graph.add_node(added_node_id_b, "file", label="Investigation node 2") 36 | test_graph.add_link(added_node_id_a, added_node_id_b, "similar_files") 37 | assert test_graph.links[(added_node_id_a, added_node_id_b, "similar_files")] 38 | test_graph.delete_node(added_node_id_a) 39 | assert added_node_id_a not in test_graph.nodes 40 | assert not test_graph.links.get( 41 | (added_node_id_a, added_node_id_b, "similar_files")) 42 | mocker.resetall() 43 | 44 | 45 | def test_delete_not_existing_node(): 46 | """Test delete not existing node.""" 47 | with pytest.raises(vt_graph_api.errors.NodeNotFoundError, 48 | match=r"Node 'dummy id' not found in nodes."): 49 | test_graph.delete_node("dummy id") 50 | -------------------------------------------------------------------------------- /docsrc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'VirusTotal Graph python API' 21 | copyright = '2019, VirusTotal' 22 | author = 'VirusTotal' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.0.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.napoleon', 36 | 'sphinx_rtd_theme', 37 | 'sphinx.ext.githubpages' 38 | ] 39 | 40 | napoleon_use_ivar = True 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = [] 48 | 49 | master_doc = 'index' 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | -------------------------------------------------------------------------------- /examples/advanced_graph.py: -------------------------------------------------------------------------------- 1 | """Advanced VTGraph usage example.""" 2 | 3 | 4 | from vt_graph_api import VTGraph 5 | from vt_graph_api.errors import NodeNotFoundError 6 | 7 | 8 | API_KEY = "" # Add your VT API Key here. 9 | 10 | 11 | graph = VTGraph( 12 | API_KEY, verbose=False, private=True, name="First Graph", 13 | user_editors=["jinfantes"], group_viewers=["virustotal"]) 14 | 15 | # Adding first node. WannyCry hash 16 | graph.add_node( 17 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 18 | "file", label="Investigation node") 19 | 20 | print("Expanding... this might take a few seconds.") 21 | graph.expand_n_level(level=1, max_nodes_per_relationship=10, max_nodes=200) 22 | 23 | # Adding second node, Kill Switch domain 24 | graph.add_node( 25 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "domain", 26 | label="Kill Switch", fetch_information=True 27 | ) 28 | 29 | # Expanding the communicating files of the kill switch domain. 30 | graph.expand( 31 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "communicating_files", 32 | max_nodes_per_relationship=20 33 | ) 34 | 35 | # Deleting nodes 36 | nodes_to_delete = [ 37 | "52.57.88.48", 38 | "54.153.0.145", 39 | "52.170.89.193", 40 | "184.168.221.43", 41 | "144.217.254.91", 42 | "144.217.254.3", 43 | "98.143.148.47", 44 | "104.41.151.54", 45 | "144.217.74.156", 46 | "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc", 47 | "428f22a9afd2797ede7c0583d34a052c32693cbb55f567a60298587b6e675c6f", 48 | "b43b234012b8233b3df6adb7c0a3b2b13cc2354dd6de27e092873bf58af2693c", 49 | "85ce324b8f78021ecfc9b811c748f19b82e61bb093ff64f2eab457f9ef19b186", 50 | "3f3a9dde96ec4107f67b0559b4e95f5f1bca1ec6cb204bfe5fea0230845e8301", 51 | "2c2d8bc91564050cf073745f1b117f4ffdd6470e87166abdfcd10ecdff040a2e", 52 | "a93ee7ea13238bd038bcbec635f39619db566145498fe6e0ea60e6e76d614bd3", 53 | "7a828afd2abf153d840938090d498072b7e507c7021e4cdd8c6baf727cafc545", 54 | "a897345b68191fd36f8cefb52e6a77acb2367432abb648b9ae0a9d708406de5b", 55 | "5c1f4f69c45cff9725d9969f9ffcf79d07bd0f624e06cfa5bcbacd2211046ed6" 56 | ] 57 | 58 | for node in nodes_to_delete: 59 | try: 60 | graph.delete_node(node) 61 | except NodeNotFoundError: 62 | pass # Ignoring if the node does not exist in the graph. 63 | 64 | graph.save_graph() 65 | 66 | print("Graph ID: %s" % graph.graph_id) 67 | -------------------------------------------------------------------------------- /tests/test_create_group.py: -------------------------------------------------------------------------------- 1 | """Test create a Group of nodes.""" 2 | 3 | import pytest 4 | import vt_graph_api 5 | import vt_graph_api.errors 6 | 7 | 8 | def create_dummy_graph(): 9 | return vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["dummy_user"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_create_empty_group(): 15 | """Test create a group without nodes.""" 16 | test_graph = create_dummy_graph() 17 | test_graph.add_node("virustotal.com", "domain") 18 | test_graph.add_node("google.com", "domain") 19 | 20 | with pytest.raises(vt_graph_api.errors.CreateGroupError, 21 | match=r"A group must contain at least one node."): 22 | test_graph.create_group([], "Group 1") 23 | 24 | 25 | def test_create_group_with_nodes_already_grouped(): 26 | """Test create a group with nodes already grouped.""" 27 | test_graph = create_dummy_graph() 28 | test_graph.add_node("virustotal.com", "domain") 29 | test_graph.add_node("google.com", "domain") 30 | test_graph.create_group(['virustotal.com', 'google.com'], 'Group 1') 31 | 32 | with pytest.raises(vt_graph_api.errors.CreateGroupError, 33 | match=r"Node .+ is already in a group."): 34 | test_graph.create_group(['virustotal.com', 'google.com'], "Group 1") 35 | 36 | 37 | def test_create_group_with_node_that_does_not_exist(): 38 | """Test create a group with nodes that are not in the graph.""" 39 | test_graph = create_dummy_graph() 40 | test_graph.add_node("virustotal.com", "domain") 41 | test_graph.add_node("google.com", "domain") 42 | test_graph.create_group(['virustotal.com', 'google.com'], 'Group 1') 43 | 44 | with pytest.raises(vt_graph_api.errors.CreateGroupError, 45 | match=r"Node hola.es is not in the Graph."): 46 | test_graph.create_group(['hola.es'], "Group 1") 47 | 48 | 49 | def test_create_group(mocker): 50 | """Test create a group.""" 51 | test_graph = create_dummy_graph() 52 | 53 | test_graph.add_node("virustotal.com", "domain") 54 | test_graph.add_node("google.com", "domain") 55 | test_graph.create_group(['virustotal.com', 'google.com'], 'Group 1') 56 | 57 | mocker.patch.object(test_graph, "_push_editors") 58 | mocker.patch.object(test_graph, "_push_viewers") 59 | event_mocked = mocker.patch.object(test_graph, "_push_graph_to_vt") 60 | 61 | test_graph.save_graph() 62 | 63 | # Assert group relationship node is generated 64 | group_node = event_mocked.call_args[0][0]['data']['attributes']['nodes'][-1] 65 | assert set(group_node['entity_attributes']['grouped_node_ids']) == { 66 | 'virustotal.com', 'google.com'} 67 | assert len(group_node['entity_attributes']['grouped_node_ids']) == 2 68 | 69 | # Assert group relationship links are generated 70 | links = event_mocked.call_args[0][0]['data']['attributes']['links'] 71 | group_links = [link for link in links if link['connection_type'] == 'group'] 72 | assert len(group_links) == 2 73 | mocker.resetall() 74 | -------------------------------------------------------------------------------- /tests/test_save_graph.py: -------------------------------------------------------------------------------- 1 | """Test save graph on VT and get links.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_save_graph(mocker): 15 | """Test save graph without errors.""" 16 | request_data = { 17 | "data": { 18 | "id": "437502384758sdafasdfadsfas9873452938cgf" 19 | } 20 | } 21 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 22 | mocker.patch("requests.post", return_value=m) 23 | mocker.patch.object(test_graph, "_fetch_node_information") 24 | added_node_id_a = ( 25 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 26 | added_node_id_b = ( 27 | "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16") 28 | test_graph.add_node(added_node_id_a, "file", label="Investigation node") 29 | test_graph.add_node(added_node_id_b, "file", label="Investigation node 2") 30 | test_graph.add_link(added_node_id_a, added_node_id_b, "similar_files") 31 | mocker.patch.object(test_graph, "_push_editors") 32 | mocker.patch.object(test_graph, "_push_viewers") 33 | test_graph.save_graph() 34 | mocker.resetall() 35 | 36 | 37 | def test_save_graph_collaborator_not_found(mocker): 38 | """Test save graph on VT with collaborators not found in VT.""" 39 | with pytest.raises(vt_graph_api.errors.CollaboratorNotFoundError): 40 | mocker.patch.object(test_graph, "_push_graph_to_vt") 41 | test_graph.save_graph() 42 | mocker.resetall() 43 | 44 | 45 | def test_save_graph_error(mocker): 46 | """Test save graph on VT with error.""" 47 | with pytest.raises(vt_graph_api.errors.SaveGraphError): 48 | m = mocker.Mock(status_code=400, json=mocker.Mock(return_value={})) 49 | mocker.patch("requests.post", return_value=m) 50 | mocker.patch.object(test_graph, "_push_editors") 51 | mocker.patch.object(test_graph, "_push_viewers") 52 | test_graph.save_graph() 53 | mocker.resetall() 54 | 55 | 56 | def test_get_link(): 57 | """Test get VT graph link.""" 58 | graph_id = "dfadsfasd7fa9ds8f7asd9f87dsfasd6f6s8d76fa6sd87f6adsfsdfasd687" 59 | test_graph.graph_id = graph_id 60 | assert (test_graph.get_ui_link() == 61 | "https://www.virustotal.com/graph/%s" % graph_id) 62 | 63 | 64 | def test_get_link_error(): 65 | test_graph.graph_id = "" 66 | with pytest.raises(vt_graph_api.errors.SaveGraphError): 67 | test_graph.get_ui_link() 68 | 69 | 70 | def test_get_iframe(): 71 | """Test get VT graph iframe.""" 72 | graph_id = "dfadsfasd7fa9ds8f7asd9f87dsfasd6f6s8d76fa6sd87f6adsfsdfasd687" 73 | test_graph.graph_id = graph_id 74 | assert test_graph.get_iframe_code() == ( 75 | "" 77 | .format(graph_id=graph_id)) 78 | 79 | 80 | def test_get_iframe_error(): 81 | test_graph.graph_id = "" 82 | with pytest.raises(vt_graph_api.errors.SaveGraphError): 83 | test_graph.get_iframe_code() 84 | -------------------------------------------------------------------------------- /docs/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} 2 | -------------------------------------------------------------------------------- /docsrc/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests](https://github.com/VirusTotal/vt-graph-api/actions/workflows/tests.yaml/badge.svg) 2 | 3 | # VirusTotal Graph API 4 | 5 | VirusTotal Graph API allows you programatically interact with VirusTotal dataset. 6 | 7 | ## Installing the API 8 | Install VirusTotal Graph Python API. 9 | ``` 10 | git clone https://github.com/VirusTotal/vt_graph_api 11 | cd vt_graph_api 12 | pip install . --user 13 | ``` 14 | 15 | ## Verifying the installation 16 | 17 | ```python 18 | >>> import vt_graph_api 19 | >>> vt_graph_api.__version__ 20 | X.X.X 21 | ``` 22 | 23 | ## Documentation 24 | 25 | For more information about how to use **vt_graph_api** visit the [documentation](https://virustotal.github.io/vt-graph-api/) page. 26 | 27 | You may also want to take a look at some of our [example scripts](https://github.com/VirusTotal/vt-graph-api/tree/master/examples), 28 | which besides doing useful work for you can be used as a guidance on how to use **vt_graph_api**. 29 | 30 | In addition, you can find the documentation for the VirusTotal Graph REST API at the [API reference](https://developers.virustotal.com/v3.0/reference#graphs) 31 | 32 | # Test it! 33 | 34 | Use tox to test: 35 | 36 | ``` 37 | >>> tox 38 | ``` 39 | 40 | # Changelog 41 | 42 | ### V2.2.0 43 | - Support for loading Graphs with special relationships (Groups, Intelligence, Livehunt, Retrohunt, Commonalities). 44 | - New method for creating groups of nodes. 45 | 46 | ### V2.1.0 47 | - Support for setting Graph representation. 48 | 49 | ### V2.0.0 50 | - Removed `carbonblack_children` and `carbonblack_parent` relationships in File entity. 51 | - Create a Collection from a Graph. 52 | - Added **new entity** types: 53 | - collection 54 | - reference 55 | - whois 56 | - ssl_cert 57 | - Added **new relationships**: 58 | - Files: *dropped_files, collections, email_attachments, itw_ips, overlay_children, pe_resource_children, references, urls_for_embedded_js* 59 | - Domains: *historical_ssl_certificates, 60 | historical_whois, 61 | caa_records, 62 | cname_records, 63 | mx_records, 64 | ns_records, 65 | soa_records, 66 | collections, 67 | references.* 68 | - IP Addresses: *historical_ssl_certificates, 69 | historical_whois, 70 | collections, 71 | references.* 72 | - Urls: *contacted_domains, 73 | contacted_ips, 74 | redirects_to, 75 | urls_related_by_tracker_id, 76 | communicating_files, 77 | referrer_files, 78 | embedded_js_files, 79 | collections, 80 | references* 81 | - Collections: *files, 82 | domains, 83 | ip_addresses, 84 | urls, 85 | references.* 86 | - Whois: *network_location.* 87 | 88 | ### V1.1.3 89 | - Bug fixing. 90 | 91 | ### V1.1.2 92 | - Bug fixing. 93 | 94 | ### V1.1.1 95 | - Bug fixing. 96 | - Fixing documentation. 97 | 98 | ### V1.1.0 99 | - Added download graph screenshot from VirusTotal. 100 | 101 | ### V1.0.1 102 | - Fixing documentation. 103 | 104 | ### V1.0.0 105 | --- 106 | - Added autosearch algorithm to find links between graph's nodes. 107 | - Accept **MD5** and **SHA1** as valid ID for nodes with **file type**. 108 | - Added **VTIntelligence** search for nodes without any information. 109 | - Accept custom node types. 110 | - Added load graph from VirusTotal. 111 | - Added clone graph from VirusTotal. 112 | -------------------------------------------------------------------------------- /tests/test_delete_link.py: -------------------------------------------------------------------------------- 1 | """Test delete node from graph.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | node_a = test_graph.add_node("dummy_hash_1", "file") 14 | node_b = test_graph.add_node("dummy_hash_2", "file") 15 | node_not_found_id = "dummy_hash_3" 16 | 17 | 18 | def test_delete_link(): 19 | test_graph.add_link(node_a.node_id, node_b.node_id, "bundled_files") 20 | assert test_graph.links[((node_a.node_id, node_b.node_id, "bundled_files"))] 21 | test_graph.delete_link(node_a.node_id, node_b.node_id, "bundled_files") 22 | assert not test_graph.links.get( 23 | (node_a.node_id, node_b.node_id, "bundled_files")) 24 | 25 | 26 | def test_delete_link_node_not_found_error(): 27 | with pytest.raises( 28 | vt_graph_api.errors.NodeNotFoundError, 29 | match=(r"Node '{node_id}' not found in nodes.". 30 | format(node_id=node_not_found_id))): 31 | test_graph.delete_link(node_not_found_id, node_b.node_id, "bundled_files") 32 | 33 | with pytest.raises( 34 | vt_graph_api.errors.NodeNotFoundError, 35 | match=(r"Node '{node_id}' not found in nodes.". 36 | format(node_id=node_not_found_id))): 37 | test_graph.delete_link(node_a.node_id, node_not_found_id, "bundled_files") 38 | 39 | 40 | def test_delete_link_same_node_error(): 41 | with pytest.raises( 42 | vt_graph_api.errors.SameNodeError, 43 | match=(r"It is no possible to delete links between the same node; id: " + 44 | "{node_id}.".format(node_id=node_a.node_id))): 45 | test_graph.delete_link(node_a.node_id, node_a.node_id, "bundled_files") 46 | 47 | 48 | def test_delete_link_link_not_found_error(): 49 | connection_type = "dummy connection" 50 | with pytest.raises( 51 | vt_graph_api.errors.LinkNotFoundError, 52 | match=(r"Link between {source} and {target} with {connection_type} does" + 53 | " not exists.").format( 54 | source=node_a.node_id, target=node_b.node_id, 55 | connection_type=connection_type)): 56 | test_graph.delete_link(node_a.node_id, node_b.node_id, connection_type) 57 | 58 | 59 | def test_delete_links(): 60 | test_graph.add_link(node_a.node_id, node_b.node_id, "bundled_files") 61 | test_graph.add_link(node_a.node_id, node_b.node_id, "carbonblack_children") 62 | test_graph.add_link(node_a.node_id, node_b.node_id, "carbonblack_parents") 63 | assert test_graph.links[((node_a.node_id, node_b.node_id, "bundled_files"))] 64 | assert test_graph.links[ 65 | ((node_a.node_id, node_b.node_id, "carbonblack_children"))] 66 | assert test_graph.links[ 67 | ((node_a.node_id, node_b.node_id, "carbonblack_parents"))] 68 | test_graph.delete_links(node_a.node_id) 69 | assert not test_graph.links.get( 70 | (node_a.node_id, node_b.node_id, "bundled_files")) 71 | assert not test_graph.links.get( 72 | (node_a.node_id, node_b.node_id, "carbonblack_children")) 73 | assert not test_graph.links.get( 74 | (node_a.node_id, node_b.node_id, "carbonblack_parents")) 75 | 76 | 77 | def test_delete_links_node_not_found_error(): 78 | with pytest.raises( 79 | vt_graph_api.errors.NodeNotFoundError, 80 | match=(r"Node '{node_id}' not found in nodes.". 81 | format(node_id=node_not_found_id))): 82 | test_graph.delete_links(node_not_found_id) 83 | -------------------------------------------------------------------------------- /tests/test_clone_graph.py: -------------------------------------------------------------------------------- 1 | """Test clone graph from VT.""" 2 | 3 | 4 | import json 5 | import os 6 | import pytest 7 | import vt_graph_api.errors 8 | import vt_graph_api.graph 9 | 10 | 11 | with ( 12 | open(os.path.join( 13 | os.path.dirname(os.path.abspath(__file__)), 14 | "resources/virustotal_graph_id.json"))) as fp: 15 | GRAPH_RESPONSE_DATA = json.load(fp) 16 | 17 | VIEWERS_RESPONSE_DATA = { 18 | "data": [ 19 | { 20 | "id": "alvarogf", 21 | "type": "user" 22 | } 23 | ] 24 | } 25 | 26 | EDITORS_RESPONSE_DATA = { 27 | "data": [ 28 | { 29 | "id": "virustotal", 30 | "type": "group" 31 | } 32 | ] 33 | } 34 | 35 | 36 | API_KEY = "DUMMY_API_KEY" 37 | GRAPH_ID = "DUMMY_ID" 38 | 39 | 40 | def test_clone_graph(mocker): 41 | """Test clone graph without errors.""" 42 | side_effects = [ 43 | GRAPH_RESPONSE_DATA, 44 | VIEWERS_RESPONSE_DATA, 45 | EDITORS_RESPONSE_DATA 46 | ] 47 | new_user_viewers = ["jinfantes"] 48 | m = mocker.Mock(status_code=200, json=mocker.Mock(side_effect=side_effects)) 49 | mocker.patch("requests.get", return_value=m) 50 | test_graph = vt_graph_api.graph.VTGraph.clone_graph( 51 | GRAPH_ID, API_KEY, user_viewers=new_user_viewers) 52 | nodes = [ 53 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 54 | "178.62.125.244", 55 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 56 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 57 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 58 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 59 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 60 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885" 61 | ] 62 | links = [ 63 | ( 64 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 65 | "178.62.125.244", 66 | "contacted_ips", 67 | ), 68 | ( 69 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 70 | "178.62.125.244", 71 | "contacted_ips", 72 | ), 73 | ( 74 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 75 | "178.62.125.244", 76 | "contacted_ips", 77 | ), 78 | ( 79 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 80 | "178.62.125.244", 81 | "contacted_ips", 82 | ), 83 | ( 84 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 85 | "178.62.125.244", 86 | "contacted_ips", 87 | ), 88 | ( 89 | "178.62.125.244", 90 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 91 | "communicating_files", 92 | ), 93 | ( 94 | "178.62.125.244", 95 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885", 96 | "communicating_files", 97 | ) 98 | ] 99 | for node in nodes: 100 | assert test_graph.nodes[node] 101 | for source, target, connection_type in links: 102 | assert test_graph.links[(source, target, connection_type)] 103 | assert not test_graph.group_editors 104 | assert not test_graph.user_editors 105 | assert not test_graph.group_viewers 106 | assert "alvarogf" not in test_graph.user_viewers 107 | assert "jinfantes" in test_graph.user_viewers 108 | mocker.resetall() 109 | -------------------------------------------------------------------------------- /docs/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | /* sphinx_rtd_theme version 0.4.3 | MIT license */ 2 | /* Built 20190212 16:02 */ 3 | require=function r(s,a,l){function c(e,n){if(!a[e]){if(!s[e]){var i="function"==typeof require&&require;if(!n&&i)return i(e,!0);if(u)return u(e,!0);var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}var o=a[e]={exports:{}};s[e][0].call(o.exports,function(n){return c(s[e][1][n]||n)},o,o.exports,r,s,a,l)}return a[e].exports}for(var u="function"==typeof require&&require,n=0;n"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var e=i(this);expand=i(''),expand.on("click",function(n){return t.toggleCurrent(e),n.stopPropagation(),!1}),e.prepend(expand)})},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),i=e.find('[href="'+n+'"]');if(0===i.length){var t=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(i=e.find('[href="#'+t.attr("id")+'"]')).length&&(i=e.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current"),e.siblings().find("li.current").removeClass("current"),e.find("> ul li.current").removeClass("current"),e.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:e.exports.ThemeNav,StickyNav:e.exports.ThemeNav}),function(){for(var r=0,n=["ms","moz","webkit","o"],e=0;e"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var e=i(this);expand=i(''),expand.on("click",function(n){return t.toggleCurrent(e),n.stopPropagation(),!1}),e.prepend(expand)})},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),i=e.find('[href="'+n+'"]');if(0===i.length){var t=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(i=e.find('[href="#'+t.attr("id")+'"]')).length&&(i=e.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current"),e.siblings().find("li.current").removeClass("current"),e.find("> ul li.current").removeClass("current"),e.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:e.exports.ThemeNav,StickyNav:e.exports.ThemeNav}),function(){for(var r=0,n=["ms","moz","webkit","o"],e=0;e 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Search — VirusTotal Graph python API 1.0.1 documentation 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 | 95 | 96 |
97 | 98 | 99 | 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 |
  • Docs »
  • 132 | 133 |
  • Search
  • 134 | 135 | 136 |
  • 137 | 138 | 139 | 140 |
  • 141 | 142 |
143 | 144 | 145 |
146 |
147 |
148 |
149 | 150 | 158 | 159 | 160 |
161 | 162 |
163 | 164 |
165 | 166 |
167 |
168 | 169 | 170 |
171 | 172 |
173 |

174 | © Copyright 2019, VirusTotal 175 | 176 |

177 |
178 | Built with Sphinx using a theme provided by Read the Docs. 179 | 180 |
181 | 182 |
183 |
184 | 185 |
186 | 187 |
188 | 189 | 190 | 191 | 196 | 197 | 198 | 199 | 200 | 201 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /docs/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Python Module Index — VirusTotal Graph python API 1.0.1 documentation 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 | 97 | 98 |
99 | 100 | 101 | 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 |
  • Docs »
  • 134 | 135 |
  • Python Module Index
  • 136 | 137 | 138 |
  • 139 | 140 |
  • 141 | 142 |
143 | 144 | 145 |
146 |
147 |
148 |
149 | 150 | 151 |

Python Module Index

152 | 153 |
154 | v 155 |
156 | 157 | 158 | 159 | 161 | 162 | 164 | 167 | 168 | 169 | 172 | 173 | 174 | 177 | 178 | 179 | 182 |
 
160 | v
165 | vt_graph_api 166 |
    170 | vt_graph_api.errors 171 |
    175 | vt_graph_api.graph 176 |
    180 | vt_graph_api.node 181 |
183 | 184 | 185 |
186 | 187 |
188 |
189 | 190 | 191 |
192 | 193 |
194 |

195 | © Copyright 2019, VirusTotal 196 | 197 |

198 |
199 | Built with Sphinx using a theme provided by Read the Docs. 200 | 201 |
202 | 203 |
204 |
205 | 206 |
207 | 208 |
209 | 210 | 211 | 212 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /docsrc/examples.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | Examples 3 | ************** 4 | 5 | Basic Graph 6 | ========================== 7 | 8 | .. code-block:: python 9 | 10 | """Basic VTGraph usage example.""" 11 | 12 | 13 | from vt_graph_api import VTGraph 14 | 15 | 16 | API_KEY = "" # Insert your VT API here. 17 | 18 | 19 | # Creates the graph. 20 | graph = VTGraph(API_KEY, private=False, name="First Graph") 21 | 22 | # Adds the node. 23 | graph.add_node( 24 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 25 | "file", label="Investigation node") 26 | 27 | # Expands the graph 1 level. 28 | graph.expand_n_level(level=1, max_nodes_per_relationship=5, max_nodes=100) 29 | 30 | # Saves the graph. 31 | graph.save_graph() 32 | 33 | # Get the graph id. 34 | print("Graph Id: %s" % graph.graph_id) 35 | 36 | # Visualizing the Graph. 37 | print(graph.get_ui_link()) # Open the url in the browser 38 | 39 | 40 | Advanced Graph 41 | ========================== 42 | 43 | .. code-block:: python 44 | 45 | """Advanced VTGraph usage example.""" 46 | 47 | 48 | from vt_graph_api import VTGraph 49 | from vt_graph_api.errors import NodeNotFoundError 50 | 51 | 52 | API_KEY = "" # Add your VT API Key here. 53 | 54 | # Make sure you have private graph quota. Otherwise it will fail to save 55 | graph = VTGraph( 56 | API_KEY, verbose=False, private=True, name="First Graph", 57 | user_editors=["jinfantes"], group_viewers=["virustotal"]) 58 | 59 | # Adding first node, WannyCry hash. 60 | graph.add_node( 61 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 62 | "file", label="Investigation node") 63 | 64 | print("Expanding... this might take a few seconds.") 65 | graph.expand_n_level(level=1, max_nodes_per_relationship=10, max_nodes=200) 66 | 67 | # Adding second node, Kill Switch domain. 68 | graph.add_node( 69 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "domain", 70 | label="Kill Switch", fetch_information=True 71 | ) 72 | 73 | # Expanding the communicating files of the kill switch domain. 74 | graph.expand( 75 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "communicating_files", 76 | max_nodes_per_relationship=20 77 | ) 78 | 79 | # Deleting nodes. 80 | nodes_to_delete = [ 81 | "52.57.88.48", 82 | "54.153.0.145", 83 | "52.170.89.193", 84 | "184.168.221.43", 85 | "144.217.254.91", 86 | "144.217.254.3", 87 | "98.143.148.47", 88 | "104.41.151.54", 89 | "144.217.74.156", 90 | "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc", 91 | "428f22a9afd2797ede7c0583d34a052c32693cbb55f567a60298587b6e675c6f", 92 | "b43b234012b8233b3df6adb7c0a3b2b13cc2354dd6de27e092873bf58af2693c", 93 | "85ce324b8f78021ecfc9b811c748f19b82e61bb093ff64f2eab457f9ef19b186", 94 | "3f3a9dde96ec4107f67b0559b4e95f5f1bca1ec6cb204bfe5fea0230845e8301", 95 | "2c2d8bc91564050cf073745f1b117f4ffdd6470e87166abdfcd10ecdff040a2e", 96 | "a93ee7ea13238bd038bcbec635f39619db566145498fe6e0ea60e6e76d614bd3", 97 | "7a828afd2abf153d840938090d498072b7e507c7021e4cdd8c6baf727cafc545", 98 | "a897345b68191fd36f8cefb52e6a77acb2367432abb648b9ae0a9d708406de5b", 99 | "5c1f4f69c45cff9725d9969f9ffcf79d07bd0f624e06cfa5bcbacd2211046ed6" 100 | ] 101 | 102 | for node in nodes_to_delete: 103 | try: 104 | graph.delete_node(node) 105 | except NodeNotFoundError: 106 | pass # Ignoring if the node does not exist in the graph. 107 | 108 | graph.save_graph() 109 | 110 | print("Graph ID: %s" % graph.graph_id) 111 | 112 | 113 | Basic Graph Search 114 | ========================== 115 | 116 | .. code-block:: python 117 | 118 | 119 | """VTGraph basic search usage example.""" 120 | 121 | from vt_graph_api import VTGraph 122 | 123 | 124 | API_KEY = "" # Insert your VT API here. 125 | 126 | 127 | # Creates the graph. 128 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 129 | 130 | # Add some nodes to graph. 131 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 132 | graph.add_node("nsis.sf.net", "domain", True) 133 | 134 | graph.add_links_if_match( 135 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", "nsis.sf.net", 136 | max_api_quotas=1000, max_depth=10) 137 | 138 | # Try to connect node with graph. 139 | graph.save_graph() 140 | 141 | # Get the graph id. 142 | print("Graph Id: %s" % graph.graph_id) 143 | 144 | # Visualizing the Graph. 145 | print(graph.get_ui_link()) # Open the url in the browser 146 | 147 | 148 | Advanced Graph Search 149 | ========================== 150 | 151 | .. code-block:: python 152 | 153 | """VTGraph advanced search usage example.""" 154 | 155 | from vt_graph_api import VTGraph 156 | 157 | 158 | API_KEY = "" # Insert your VT API here. 159 | 160 | 161 | # Creates the graph. 162 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 163 | 164 | # Add some nodes to graph. 165 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 166 | graph.add_node("nsis.sf.net", "domain", True) 167 | graph.add_node( 168 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", "file") 169 | 170 | graph.add_node("www.openssl.org", "domain", True) 171 | 172 | # Try to connect node with graph. 173 | graph.connect_with_graph( 174 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", max_api_quotas=1000, 175 | max_depth=10) 176 | 177 | graph.save_graph() 178 | 179 | # Get the graph id. 180 | print("Graph Id: %s" % graph.graph_id) 181 | 182 | # Visualizing the Graph. 183 | print(graph.get_ui_link()) # Open the url in the browser 184 | 185 | Load Graph 186 | ========================== 187 | 188 | .. code-block:: python 189 | 190 | """VirusTotal Graph id load example.""" 191 | 192 | 193 | import vt_graph_api 194 | 195 | 196 | API_KEY = "" # Insert your VT API here. 197 | GRAPH_ID = "" # Insert yout graph id here. 198 | 199 | 200 | # Retrieve the graph. 201 | graph = vt_graph_api.VTGraph.load_graph(GRAPH_ID, API_KEY) 202 | 203 | # Modify your graph here. 204 | 205 | # Save it in VirusTotal. 206 | graph.save_graph() 207 | 208 | # Get the graph id. 209 | print("Graph Id: %s" % graph.graph_id) 210 | 211 | # Visualizing the Graph. 212 | print(graph.get_ui_link()) # Open the url in the browser. 213 | -------------------------------------------------------------------------------- /docs/_sources/examples.rst.txt: -------------------------------------------------------------------------------- 1 | ************** 2 | Examples 3 | ************** 4 | 5 | Basic Graph 6 | ========================== 7 | 8 | .. code-block:: python 9 | 10 | """Basic VTGraph usage example.""" 11 | 12 | 13 | from vt_graph_api import VTGraph 14 | 15 | 16 | API_KEY = "" # Insert your VT API here. 17 | 18 | 19 | # Creates the graph. 20 | graph = VTGraph(API_KEY, private=False, name="First Graph") 21 | 22 | # Adds the node. 23 | graph.add_node( 24 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 25 | "file", label="Investigation node") 26 | 27 | # Expands the graph 1 level. 28 | graph.expand_n_level(level=1, max_nodes_per_relationship=5, max_nodes=100) 29 | 30 | # Saves the graph. 31 | graph.save_graph() 32 | 33 | # Get the graph id. 34 | print("Graph Id: %s" % graph.graph_id) 35 | 36 | # Visualizing the Graph. 37 | print(graph.get_ui_link()) # Open the url in the browser 38 | 39 | 40 | Advanced Graph 41 | ========================== 42 | 43 | .. code-block:: python 44 | 45 | """Advanced VTGraph usage example.""" 46 | 47 | 48 | from vt_graph_api import VTGraph 49 | from vt_graph_api.errors import NodeNotFoundError 50 | 51 | 52 | API_KEY = "" # Add your VT API Key here. 53 | 54 | # Make sure you have private graph quota. Otherwise it will fail to save 55 | graph = VTGraph( 56 | API_KEY, verbose=False, private=True, name="First Graph", 57 | user_editors=["jinfantes"], group_viewers=["virustotal"]) 58 | 59 | # Adding first node, WannyCry hash. 60 | graph.add_node( 61 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 62 | "file", label="Investigation node") 63 | 64 | print("Expanding... this might take a few seconds.") 65 | graph.expand_n_level(level=1, max_nodes_per_relationship=10, max_nodes=200) 66 | 67 | # Adding second node, Kill Switch domain. 68 | graph.add_node( 69 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "domain", 70 | label="Kill Switch", fetch_information=True 71 | ) 72 | 73 | # Expanding the communicating files of the kill switch domain. 74 | graph.expand( 75 | "www.ifferfsodp9ifjaposdfjhgosurijfaewrwergwea.com", "communicating_files", 76 | max_nodes_per_relationship=20 77 | ) 78 | 79 | # Deleting nodes. 80 | nodes_to_delete = [ 81 | "52.57.88.48", 82 | "54.153.0.145", 83 | "52.170.89.193", 84 | "184.168.221.43", 85 | "144.217.254.91", 86 | "144.217.254.3", 87 | "98.143.148.47", 88 | "104.41.151.54", 89 | "144.217.74.156", 90 | "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc", 91 | "428f22a9afd2797ede7c0583d34a052c32693cbb55f567a60298587b6e675c6f", 92 | "b43b234012b8233b3df6adb7c0a3b2b13cc2354dd6de27e092873bf58af2693c", 93 | "85ce324b8f78021ecfc9b811c748f19b82e61bb093ff64f2eab457f9ef19b186", 94 | "3f3a9dde96ec4107f67b0559b4e95f5f1bca1ec6cb204bfe5fea0230845e8301", 95 | "2c2d8bc91564050cf073745f1b117f4ffdd6470e87166abdfcd10ecdff040a2e", 96 | "a93ee7ea13238bd038bcbec635f39619db566145498fe6e0ea60e6e76d614bd3", 97 | "7a828afd2abf153d840938090d498072b7e507c7021e4cdd8c6baf727cafc545", 98 | "a897345b68191fd36f8cefb52e6a77acb2367432abb648b9ae0a9d708406de5b", 99 | "5c1f4f69c45cff9725d9969f9ffcf79d07bd0f624e06cfa5bcbacd2211046ed6" 100 | ] 101 | 102 | for node in nodes_to_delete: 103 | try: 104 | graph.delete_node(node) 105 | except NodeNotFoundError: 106 | pass # Ignoring if the node does not exist in the graph. 107 | 108 | graph.save_graph() 109 | 110 | print("Graph ID: %s" % graph.graph_id) 111 | 112 | 113 | Basic Graph Search 114 | ========================== 115 | 116 | .. code-block:: python 117 | 118 | 119 | """VTGraph basic search usage example.""" 120 | 121 | from vt_graph_api import VTGraph 122 | 123 | 124 | API_KEY = "" # Insert your VT API here. 125 | 126 | 127 | # Creates the graph. 128 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 129 | 130 | # Add some nodes to graph. 131 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 132 | graph.add_node("nsis.sf.net", "domain", True) 133 | 134 | graph.add_links_if_match( 135 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", "nsis.sf.net", 136 | max_api_quotas=1000, max_depth=10) 137 | 138 | # Try to connect node with graph. 139 | graph.save_graph() 140 | 141 | # Get the graph id. 142 | print("Graph Id: %s" % graph.graph_id) 143 | 144 | # Visualizing the Graph. 145 | print(graph.get_ui_link()) # Open the url in the browser 146 | 147 | 148 | Advanced Graph Search 149 | ========================== 150 | 151 | .. code-block:: python 152 | 153 | """VTGraph advanced search usage example.""" 154 | 155 | from vt_graph_api import VTGraph 156 | 157 | 158 | API_KEY = "" # Insert your VT API here. 159 | 160 | 161 | # Creates the graph. 162 | graph = VTGraph(API_KEY, verbose=True, private=True, name="First Graph") 163 | 164 | # Add some nodes to graph. 165 | graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", True) 166 | graph.add_node("nsis.sf.net", "domain", True) 167 | graph.add_node( 168 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", "file") 169 | 170 | graph.add_node("www.openssl.org", "domain", True) 171 | 172 | # Try to connect node with graph. 173 | graph.connect_with_graph( 174 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", max_api_quotas=1000, 175 | max_depth=10) 176 | 177 | graph.save_graph() 178 | 179 | # Get the graph id. 180 | print("Graph Id: %s" % graph.graph_id) 181 | 182 | # Visualizing the Graph. 183 | print(graph.get_ui_link()) # Open the url in the browser 184 | 185 | Load Graph 186 | ========================== 187 | 188 | .. code-block:: python 189 | 190 | """VirusTotal Graph id load example.""" 191 | 192 | 193 | import vt_graph_api 194 | 195 | 196 | API_KEY = "" # Insert your VT API here. 197 | GRAPH_ID = "" # Insert yout graph id here. 198 | 199 | 200 | # Retrieve the graph. 201 | graph = vt_graph_api.VTGraph.load_graph(GRAPH_ID, API_KEY) 202 | 203 | # Modify your graph here. 204 | 205 | # Save it in VirusTotal. 206 | graph.save_graph() 207 | 208 | # Get the graph id. 209 | print("Graph Id: %s" % graph.graph_id) 210 | 211 | # Visualizing the Graph. 212 | print(graph.get_ui_link()) # Open the url in the browser. 213 | -------------------------------------------------------------------------------- /docs/howtoinstall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | How to install — VirusTotal Graph python API 1.0.1 documentation 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 | 96 | 97 |
98 | 99 | 100 | 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 |
  • Docs »
  • 133 | 134 |
  • How to install
  • 135 | 136 | 137 |
  • 138 | 139 | 140 | View page source 141 | 142 | 143 |
  • 144 | 145 |
146 | 147 | 148 |
149 |
150 |
151 |
152 | 153 |
154 |

How to install

155 |

The easiest way of installing vt_graph_api is using pip:

156 |
$ pip install vt_graph_api
157 | 
158 |
159 |

Alternatively, you can get the source code directly from the GitHub and run 160 | setup.py. For getting the code you can clone the public 161 | repository:

162 |
$ git clone https://github.com/VirusTotal/vt-graph-api.git
163 | $ cd vt-graph-api
164 | 
165 |
166 |

Once you have the code you can install it with:

167 |
$ pip install . --user
168 | 
169 |
170 |
171 | 172 | 173 |
174 | 175 |
176 |
177 | 178 | 186 | 187 | 188 |
189 | 190 |
191 |

192 | © Copyright 2019, VirusTotal 193 | 194 |

195 |
196 | Built with Sphinx using a theme provided by Read the Docs. 197 | 198 |
199 | 200 |
201 |
202 | 203 |
204 | 205 |
206 | 207 | 208 | 209 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /tests/test_link.py: -------------------------------------------------------------------------------- 1 | """Test add node to graph.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_add_link(mocker): 15 | """Test add link.""" 16 | mocker.patch.object(test_graph, "_fetch_node_information") 17 | node_1 = test_graph.add_node( 18 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 19 | "file", label="Investigation node") 20 | node_2 = test_graph.add_node( 21 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41bb", 22 | "file", label="Investigation node") 23 | test_graph.add_link(node_1.node_id, node_2.node_id, "compressed_parents") 24 | assert test_graph.links[ 25 | (node_1.node_id, node_2.node_id, "compressed_parents")] 26 | mocker.resetall() 27 | 28 | 29 | def test_add_link_not_existing_node(): 30 | """Test link between not existing nodes.""" 31 | with pytest.raises(vt_graph_api.errors.NodeNotFoundError, 32 | match=r"Node 'dummy id 1' not found in nodes."): 33 | test_graph.add_link("dummy id 1", "dummy id 2", "compressed_parents") 34 | 35 | 36 | def test_add_link_between_the_same_node(): 37 | """Test add link between the same node.""" 38 | dummy_id = "dummy id" 39 | with pytest.raises( 40 | vt_graph_api.errors.SameNodeError, 41 | match=r"It is no possible to add links between the same node; id: {gid}." 42 | .format(gid=dummy_id) 43 | ): 44 | test_graph.add_link(dummy_id, dummy_id, "compressed_parents") 45 | 46 | 47 | def test_add_links_if_match(mocker): 48 | """Test add links if match.""" 49 | mocker.patch.object(test_graph, "_fetch_node_information") 50 | search_connection_response = [[( 51 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 52 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41cc", 53 | "similar_files", 54 | "file" 55 | )]] 56 | mocker.patch("vt_graph_api.VTGraph._search_connection", 57 | return_value=search_connection_response) 58 | node_1 = test_graph.add_node( 59 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 60 | "file", label="Investigation node") 61 | node_2 = test_graph.add_node( 62 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41cc", 63 | "file", label="Investigation node") 64 | assert test_graph.add_links_if_match(node_1.node_id, node_2.node_id) 65 | assert test_graph.links[ 66 | (node_1.node_id, node_2.node_id, "similar_files")] 67 | mocker.resetall() 68 | 69 | 70 | def test_add_links_if_match_link_already_exists(mocker): 71 | """Test add links if match if link already exists.""" 72 | mocker.patch.object(test_graph, "_fetch_node_information") 73 | node_1 = test_graph.add_node( 74 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 75 | "file", label="Investigation node") 76 | node_2 = test_graph.add_node( 77 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41bb", 78 | "file", label="Investigation node") 79 | test_graph.add_link(node_1.node_id, node_2.node_id, "compressed_parents") 80 | assert test_graph.add_links_if_match(node_1.node_id, node_2.node_id) 81 | assert test_graph.links[ 82 | (node_1.node_id, node_2.node_id, "compressed_parents")] 83 | assert not test_graph.links.get( 84 | (node_1.node_id, node_2.node_id, "similar_files")) 85 | mocker.resetall() 86 | 87 | 88 | def test_add_links_if_match_not_existing_node(): 89 | """Test add links if match between not existing nodes.""" 90 | with pytest.raises(vt_graph_api.errors.NodeNotFoundError, 91 | match=r"Node 'dummy id 1' not found in nodes."): 92 | test_graph.add_links_if_match("dummy id 1", "dummy id 2") 93 | 94 | 95 | def test_add_links_if_match_between_the_same_node(): 96 | """Test add links if match between the same node.""" 97 | dummy_id = "dummy id" 98 | with pytest.raises( 99 | vt_graph_api.errors.SameNodeError, 100 | match=r"It is no possible to add links between the same node; id: {gid}." 101 | .format(gid=dummy_id) 102 | ): 103 | test_graph.add_links_if_match(dummy_id, dummy_id, "compressed_parents") 104 | 105 | 106 | def test_connect_with_graph_and_found(mocker): 107 | """Test connect node with graph resolving connections.""" 108 | search_connection_response = [ 109 | [ 110 | ( 111 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", 112 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 113 | "execution_parents", 114 | "file" 115 | ), 116 | ( 117 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 118 | "nsis.sf.net", 119 | "embedded_domains", 120 | "domain" 121 | ) 122 | ], 123 | [ 124 | ( 125 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", 126 | "www.openssl.org", 127 | "embedded_domains", 128 | "domain" 129 | ) 130 | ] 131 | ] 132 | mocker.patch("vt_graph_api.VTGraph._search_connection", 133 | return_value=search_connection_response) 134 | mocker.spy(test_graph, "_search_connection") 135 | test_graph.add_node("b3b7d8a4daee86280c7e54b0ff3283afe3579480", "file", False) 136 | test_graph.add_node("nsis.sf.net", "domain", False) 137 | test_graph.add_node( 138 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 139 | "file", False) 140 | test_graph.add_node("www.openssl.org", "domain", False) 141 | assert test_graph.connect_with_graph( 142 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", 143 | max_api_quotas=1000, max_depth=10) 144 | assert test_graph._search_connection.call_count == 1 145 | assert test_graph.links[ 146 | ( 147 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", 148 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 149 | "execution_parents" 150 | ) 151 | ] 152 | assert test_graph.links[ 153 | ( 154 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 155 | "nsis.sf.net", 156 | "embedded_domains" 157 | ) 158 | ] 159 | assert test_graph.links[ 160 | ( 161 | "b3b7d8a4daee86280c7e54b0ff3283afe3579480", 162 | "www.openssl.org", 163 | "embedded_domains" 164 | ) 165 | ] 166 | mocker.resetall() 167 | 168 | 169 | def test_connect_with_graph_and_not_found(mocker): 170 | """Test connect node with graph resolving connections.""" 171 | search_connection_response = [] 172 | mocker.patch("vt_graph_api.VTGraph._search_connection", 173 | return_value=search_connection_response) 174 | mocker.spy(test_graph, "_search_connection") 175 | test_graph.add_node("98374253453454352345fdgdsfg3grgh", "file", False) 176 | assert not test_graph.connect_with_graph( 177 | "98374253453454352345fdgdsfg3grgh", 178 | max_api_quotas=1000, max_depth=10) 179 | assert test_graph._search_connection.call_count == 1 180 | mocker.resetall() 181 | -------------------------------------------------------------------------------- /tests/test_add_node.py: -------------------------------------------------------------------------------- 1 | """Test add node to graph.""" 2 | 3 | 4 | import requests 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_add_node_file_sha256(mocker): 15 | """Test add node file sha256.""" 16 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 17 | mocker.patch("requests.get", return_value=m) 18 | node_id = "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa" 19 | added_node = test_graph.add_node(node_id, "file", label="Investigation node") 20 | assert test_graph.nodes[node_id] == added_node 21 | assert len(test_graph.nodes) == 1 22 | # add the same node again to check that graph's nodes not increases 23 | test_graph.add_node(node_id, "file", label="Investigation node") 24 | assert len(test_graph.nodes) == 1 25 | mocker.resetall() 26 | 27 | 28 | def test_add_node_file_sha1(mocker): 29 | """Test add node file sha1.""" 30 | rq_id = "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa" 31 | request_data = { 32 | "data": { 33 | "attributes": { 34 | "sha256": rq_id 35 | } 36 | } 37 | } 38 | mocker.patch.object(test_graph, "_fetch_node_information") 39 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 40 | mocker.patch("requests.get", return_value=m) 41 | node_id = "5ff465afaabcbf0150d1a3ab2c2e74f3a4426467" 42 | added_node = test_graph.add_node(node_id, "file", label="Investigation node") 43 | assert not test_graph.nodes.get(node_id) 44 | assert test_graph.nodes[added_node.node_id] == added_node 45 | mocker.resetall() 46 | 47 | 48 | def test_add_node_file_md5(mocker): 49 | """Test add node file md5.""" 50 | rq_id = "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa" 51 | request_data = { 52 | "data": { 53 | "attributes": { 54 | "sha256": rq_id 55 | } 56 | } 57 | } 58 | mocker.patch.object(test_graph, "_fetch_node_information") 59 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 60 | mocker.patch("requests.get", return_value=m) 61 | added_node_id = "84c82835a5d21bbcf75a61706d8ab549" 62 | added_node = test_graph.add_node( 63 | added_node_id, "file", label="Investigation node") 64 | assert test_graph.nodes.get(added_node_id) is None 65 | assert test_graph.nodes[added_node.node_id] == added_node 66 | mocker.resetall() 67 | 68 | 69 | def test_add_node_url(mocker): 70 | """Test add node URL.""" 71 | rq_id = "u-afb80d6e2f84fbe2248ad78-1566543875" 72 | request_data = { 73 | "data": { 74 | "id": rq_id, 75 | "type": "analysis" 76 | } 77 | } 78 | mocker.patch.object(test_graph, "_fetch_node_information") 79 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 80 | mocker.patch("requests.post", return_value=m) 81 | added_node_id = "http://cwwnhwhlz52maqm7.onion/" 82 | added_node = test_graph.add_node( 83 | added_node_id, "url", label="Investigation node") 84 | assert not test_graph.nodes.get(added_node_id) 85 | assert test_graph.nodes[added_node.node_id] == added_node 86 | mocker.resetall() 87 | 88 | 89 | def test_add_node_domain(mocker): 90 | """Test add node domain.""" 91 | m = mocker.Mock(status_code=400, json=mocker.Mock(return_value={})) 92 | mocker.patch("requests.get", return_value=m) 93 | added_node_id = "google.com" 94 | added_node = test_graph.add_node( 95 | added_node_id, "domain", label="Investigation node") 96 | assert test_graph.nodes[added_node_id] == added_node 97 | mocker.resetall() 98 | 99 | 100 | def test_add_node_ip(mocker): 101 | """Test add node IP.""" 102 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 103 | mocker.patch("requests.get", return_value=m) 104 | added_node_id = "104.17.38.137" 105 | added_node = test_graph.add_node( 106 | added_node_id, "ip_address", label="Investigation node") 107 | assert test_graph.nodes[added_node_id] == added_node 108 | mocker.resetall() 109 | 110 | 111 | def test_add_node_with_fetch_vt_enterprise_search_and_found(mocker): 112 | """Test add node calling fetch_vt_enterprise to search for the node id.""" 113 | rq_id = "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 114 | request_data = { 115 | "data": [ 116 | { 117 | "id": rq_id, 118 | "type": "file" 119 | } 120 | ], 121 | "meta": { 122 | "total_hits": 1 123 | } 124 | } 125 | mocker.patch.object(test_graph, "_fetch_node_information") 126 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 127 | mocker.patch("requests.get", return_value=m) 128 | added_node_id = "dummy file.bak" 129 | added_node = test_graph.add_node( 130 | added_node_id, "file", label="Investigation node") 131 | assert not test_graph.nodes.get(added_node_id) 132 | assert test_graph.nodes[added_node.node_id] == added_node 133 | url = "https://www.virustotal.com/api/v3/intelligence/search?query={query}".format( 134 | query=added_node_id) 135 | requests.get.assert_called_with(url, headers=test_graph._get_headers()) 136 | mocker.resetall() 137 | 138 | 139 | def test_add_node_with_fetch_vt_enterprise_search_and_not_found(mocker): 140 | """Test add node with calling fetch_vt_enterprise without exact result.""" 141 | rq_id_1 = "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f880571" 142 | rq_id_2 = "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab912" 143 | request_data = { 144 | "data": [ 145 | { 146 | "id": rq_id_1, 147 | "type": "file" 148 | }, 149 | { 150 | "id": rq_id_2, 151 | "type": "file" 152 | } 153 | ], 154 | "meta": { 155 | "total_hits": 2 156 | } 157 | } 158 | mocker.patch.object(test_graph, "_fetch_node_information") 159 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 160 | mocker.patch("requests.get", return_value=m) 161 | added_node_id = "dummy file 2.bak" 162 | added_node = test_graph.add_node( 163 | added_node_id, "file", label="Investigation node") 164 | assert test_graph.nodes[added_node_id] == added_node 165 | url = "https://www.virustotal.com/api/v3/intelligence/search?query={query}".format( 166 | query=added_node_id) 167 | requests.get.assert_called_with(url, headers=test_graph._get_headers()) 168 | mocker.resetall() 169 | 170 | 171 | def test_add_nodes(mocker): 172 | """Test add nodes.""" 173 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value={})) 174 | mocker.patch("requests.get", return_value=m) 175 | added_node_id_1 = ( 176 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 177 | added_node_type_1 = "file" 178 | added_node_id_2 = "dummy.com" 179 | added_node_type_2 = "domain" 180 | nodes_to_add = [ 181 | { 182 | "node_id": added_node_id_1, 183 | "node_type": added_node_type_1, 184 | }, 185 | { 186 | "node_id": added_node_id_2, 187 | "node_type": added_node_type_2, 188 | } 189 | ] 190 | 191 | added_nodes = test_graph.add_nodes(nodes_to_add) 192 | assert test_graph.nodes[added_node_id_1] in added_nodes 193 | assert test_graph.nodes[added_node_id_2] in added_nodes 194 | mocker.resetall() 195 | -------------------------------------------------------------------------------- /tests/test_load_graph.py: -------------------------------------------------------------------------------- 1 | """Test load graph from VT.""" 2 | 3 | import json 4 | import os 5 | import pytest 6 | import unittest 7 | import vt_graph_api.errors 8 | import vt_graph_api.graph 9 | 10 | with ( 11 | open(os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 13 | "resources/virustotal_graph_id.json"))) as fp: 14 | GRAPH_RESPONSE_DATA = json.load(fp) 15 | 16 | VIEWERS_RESPONSE_DATA = { 17 | "data": [ 18 | { 19 | "id": "alvarogf", 20 | "type": "user" 21 | } 22 | ] 23 | } 24 | 25 | EDITORS_RESPONSE_DATA = { 26 | "data": [ 27 | { 28 | "id": "virustotal", 29 | "type": "group" 30 | } 31 | ] 32 | } 33 | 34 | GRAPH_WRONG_RESPONSE_DATA = { 35 | "dummy": "dummy_value" 36 | } 37 | 38 | API_KEY = "DUMMY_API_KEY" 39 | GRAPH_ID = "DUMMY_ID" 40 | 41 | 42 | def test_load_graph_with_match(mocker): 43 | """Test load from graph id without errors.""" 44 | side_effects = [ 45 | GRAPH_RESPONSE_DATA, 46 | VIEWERS_RESPONSE_DATA, 47 | EDITORS_RESPONSE_DATA 48 | ] 49 | m = mocker.Mock(status_code=200, json=mocker.Mock(side_effect=side_effects)) 50 | mocker.patch("requests.get", return_value=m) 51 | test_graph = vt_graph_api.graph.VTGraph.load_graph(GRAPH_ID, API_KEY) 52 | nodes = [ 53 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 54 | "178.62.125.244", 55 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 56 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 57 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 58 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 59 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 60 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885" 61 | ] 62 | links = [ 63 | ( 64 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 65 | "178.62.125.244", 66 | "contacted_ips", 67 | ), 68 | ( 69 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 70 | "178.62.125.244", 71 | "contacted_ips", 72 | ), 73 | ( 74 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 75 | "178.62.125.244", 76 | "contacted_ips", 77 | ), 78 | ( 79 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 80 | "178.62.125.244", 81 | "contacted_ips", 82 | ), 83 | ( 84 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 85 | "178.62.125.244", 86 | "contacted_ips", 87 | ), 88 | ( 89 | "178.62.125.244", 90 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 91 | "communicating_files", 92 | ), 93 | ( 94 | "178.62.125.244", 95 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885", 96 | "communicating_files", 97 | ) 98 | ] 99 | for node in nodes: 100 | assert test_graph.nodes[node] 101 | for source, target, connection_type in links: 102 | assert test_graph.links[(source, target, connection_type)] 103 | assert "virustotal" in test_graph.group_editors 104 | assert "alvarogf" in test_graph.user_viewers 105 | 106 | special_relationship_nodes = [ 107 | "relationships_commonality_1670398662", 108 | "relationships_retrohunt_user1626193170" 109 | ] 110 | 111 | special_relationship_links = [ 112 | ("relationships_commonality_1670398662", 113 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885", 114 | "commonality"), 115 | ("relationships_retrohunt_user1626193170", 116 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885", 117 | "retrohunt") 118 | ] 119 | 120 | for node in special_relationship_nodes: 121 | assert test_graph.special_relationship_nodes[node] 122 | 123 | for source, target, connection_type in special_relationship_links: 124 | link = {'source': source, 'target': target, 125 | 'connection_type': connection_type} 126 | assert link in test_graph.special_relationship_links 127 | 128 | group_nodes = ["relationships_group_123456789"] 129 | 130 | for node in group_nodes: 131 | assert test_graph.group_nodes[node] 132 | 133 | mocker.resetall() 134 | 135 | 136 | def test_load_graph_without_editors_and_viewers(mocker): 137 | """Test load from id without editors and viewers.""" 138 | side_effects = [ 139 | mocker.Mock(status_code=200, 140 | json=mocker.Mock(return_value=GRAPH_RESPONSE_DATA)), 141 | mocker.Mock(status_code=200, json=mocker.Mock(return_value={"data": []})), 142 | mocker.Mock(status_code=200, json=mocker.Mock(return_value={"data": []})) 143 | ] 144 | mocker.patch("requests.get", side_effect=side_effects) 145 | test_graph = vt_graph_api.graph.VTGraph.load_graph(GRAPH_ID, API_KEY) 146 | nodes = [ 147 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 148 | "178.62.125.244", 149 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 150 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 151 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 152 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 153 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e" 154 | ] 155 | links = [ 156 | ( 157 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 158 | "178.62.125.244", 159 | "contacted_ips", 160 | ), 161 | ( 162 | "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 163 | "178.62.125.244", 164 | "contacted_ips", 165 | ), 166 | ( 167 | "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 168 | "178.62.125.244", 169 | "contacted_ips", 170 | ), 171 | ( 172 | "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 173 | "178.62.125.244", 174 | "contacted_ips", 175 | ), 176 | ( 177 | "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 178 | "178.62.125.244", 179 | "contacted_ips", 180 | ), 181 | ( 182 | "178.62.125.244", 183 | "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 184 | "communicating_files", 185 | ) 186 | ] 187 | for node in nodes: 188 | assert test_graph.nodes[node] 189 | for source, target, connection_type in links: 190 | assert test_graph.links[(source, target, connection_type)] 191 | assert "virustotal" not in test_graph.group_editors 192 | assert "alvarogf" not in test_graph.user_viewers 193 | mocker.resetall() 194 | 195 | 196 | def test_load_graph_with_fail_request(mocker): 197 | """Test load from id with errors.""" 198 | with pytest.raises( 199 | vt_graph_api.errors.LoadError, 200 | match=r"Error to find graph with id: DUMMY_ID. Response code: 400."): 201 | mocker.patch("requests.get", return_value=mocker.Mock(status_code=400)) 202 | vt_graph_api.graph.VTGraph.load_graph(GRAPH_ID, API_KEY) 203 | mocker.resetall() 204 | 205 | 206 | def test_load_graph_wrong_json(mocker): 207 | """Test load from id with error in JSON structure.""" 208 | with pytest.raises( 209 | vt_graph_api.errors.InvalidJSONError): 210 | side_effects = [ 211 | GRAPH_WRONG_RESPONSE_DATA, 212 | VIEWERS_RESPONSE_DATA, 213 | EDITORS_RESPONSE_DATA 214 | ] 215 | m = mocker.Mock(status_code=200, json=mocker.Mock(side_effect=side_effects)) 216 | mocker.patch("requests.get", return_value=m) 217 | vt_graph_api.graph.VTGraph.load_graph(GRAPH_ID, API_KEY) 218 | mocker.resetall() 219 | 220 | 221 | def test_load_graph_with_group_nodes(mocker): 222 | mocker.resetall() 223 | -------------------------------------------------------------------------------- /tests/resources/virustotal_graph_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "attributes": { 4 | "comments_count": 0, 5 | "creation_date": 1567094335, 6 | "graph_data": { 7 | "description": "First Graph API test", 8 | "version": "api-1.0.0" 9 | }, 10 | "last_modified_date": 1567094335, 11 | "links": [ 12 | { 13 | "connection_type": "contacted_ips", 14 | "source": "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 15 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 16 | }, 17 | { 18 | "connection_type": "contacted_ips", 19 | "source": "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 20 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 21 | }, 22 | { 23 | "connection_type": "contacted_ips", 24 | "source": "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 25 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 26 | }, 27 | { 28 | "connection_type": "contacted_ips", 29 | "source": "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 30 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 31 | }, 32 | { 33 | "connection_type": "contacted_ips", 34 | "source": "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 35 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 36 | }, 37 | { 38 | "connection_type": "contacted_ips", 39 | "source": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 40 | "target": "178.62.125.244" 41 | }, 42 | { 43 | "connection_type": "communicating_files", 44 | "source": "178.62.125.244", 45 | "target": "relationships_communicating_files_17862125244" 46 | }, 47 | { 48 | "connection_type": "communicating_files", 49 | "source": "relationships_communicating_files_17862125244", 50 | "target": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e" 51 | }, 52 | { 53 | "connection_type": "communicating_files", 54 | "source": "relationships_communicating_files_17862125244", 55 | "target": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885" 56 | }, 57 | { 58 | "connection_type": "commonality", 59 | "source": "relationships_commonality_1670398662", 60 | "target": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885" 61 | }, 62 | { 63 | "connection_type": "retrohunt", 64 | "source": "relationships_retrohunt_user1626193170", 65 | "target": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885" 66 | }, 67 | { 68 | "connection_type": "contacted_ips", 69 | "source": "relationships_group_123456789", 70 | "target": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 71 | }, 72 | { 73 | "connection_type": "group", 74 | "source": "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 75 | "target": "relationships_group_123456789" 76 | } 77 | ], 78 | "nodes": [ 79 | { 80 | "entity_attributes": { 81 | "has_detections": 45, 82 | "type_tag": "docx" 83 | }, 84 | "entity_id": "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 85 | "index": 0, 86 | "type": "file", 87 | "x": 0, 88 | "y": 0 89 | }, 90 | { 91 | "entity_attributes": { 92 | "country": "GB" 93 | }, 94 | "entity_id": "178.62.125.244", 95 | "index": 1, 96 | "type": "ip_address", 97 | "x": 0, 98 | "y": 0 99 | }, 100 | { 101 | "entity_id": "relationships_contacted_ips_5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91", 102 | "index": 2, 103 | "type": "relationship", 104 | "x": 0, 105 | "y": 0 106 | }, 107 | { 108 | "entity_attributes": { 109 | "has_detections": 51, 110 | "type_tag": "peexe" 111 | }, 112 | "entity_id": "efa0b414a831cbf724d1c67808b7483dec22a981ae670947793d114048f88057", 113 | "index": 3, 114 | "type": "file", 115 | "x": 0, 116 | "y": 0 117 | }, 118 | { 119 | "entity_attributes": { 120 | "has_detections": 55, 121 | "type_tag": "peexe" 122 | }, 123 | "entity_id": "720d6a4288fa43357151bdeb8dc9cdb7c27fd7db1b5f76345f5ff094d48ae5a0", 124 | "index": 4, 125 | "type": "file", 126 | "x": 0, 127 | "y": 0 128 | }, 129 | { 130 | "entity_attributes": { 131 | "has_detections": 52, 132 | "type_tag": "peexe" 133 | }, 134 | "entity_id": "b20ce00a6864225f05de6407fac80ddb83cd0aec00ada438c1e354cdd0d7d5df", 135 | "index": 5, 136 | "type": "file", 137 | "x": 0, 138 | "y": 0 139 | }, 140 | { 141 | "entity_attributes": { 142 | "has_detections": 59, 143 | "type_tag": "peexe" 144 | }, 145 | "entity_id": "5961861d2b9f50d05055814e6bfd1c6291b30719f8a4d02d4cf80c2e87753fa1", 146 | "index": 6, 147 | "type": "file", 148 | "x": 0, 149 | "y": 0 150 | }, 151 | { 152 | "entity_attributes": { 153 | "has_detections": 57, 154 | "type_tag": "peexe" 155 | }, 156 | "entity_id": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f88e", 157 | "index": 7, 158 | "type": "file", 159 | "x": 0, 160 | "y": 0 161 | }, 162 | { 163 | "entity_id": "relationships_communicating_files_17862125244", 164 | "index": 8, 165 | "type": "relationship", 166 | "x": 0, 167 | "y": 0 168 | }, 169 | { 170 | "entity_id": "e6ecb146f469d243945ad8a5451ba1129c5b190f7d50c64580dbad4b8246f885", 171 | "index": 9, 172 | "type": "file", 173 | "x": 0, 174 | "y": 0 175 | }, 176 | { 177 | "entity_id": "relationships_commonality_1670398662", 178 | "index": 10, 179 | "type": "relationship", 180 | "entity_attributes": { 181 | "commonalities": [ 182 | { 183 | "commonality": "1670398662" 184 | } 185 | ] 186 | }, 187 | "x": 0, 188 | "y": 0 189 | }, 190 | { 191 | "entity_id": "relationships_retrohunt_user1626193170", 192 | "index": 11, 193 | "type": "relationship", 194 | "entity_attributes": { 195 | "relationship_type": "retrohunt", 196 | "retrohunt_job_id": "user-1626193170" 197 | }, 198 | "x": 0, 199 | "y": 0 200 | }, 201 | { 202 | "entity_id": "relationships_group_123456789", 203 | "index": 12, 204 | "type": "relationship", 205 | "entity_attributes": { 206 | "relationship_type": "group", 207 | "grouped_node_ids": [ 208 | "5504e04083d6146a67cb0d671d8ad5885315062c9ee08a62e40e264c2d5eab91" 209 | ] 210 | }, 211 | "x": 0, 212 | "y": 0 213 | } 214 | ], 215 | "private": true, 216 | "views_count": 6 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/test_expand.py: -------------------------------------------------------------------------------- 1 | """Test expansion nodes from graph.""" 2 | 3 | 4 | import pytest 5 | import vt_graph_api 6 | import vt_graph_api.errors 7 | 8 | 9 | test_graph = vt_graph_api.VTGraph( 10 | "Dummy api key", verbose=False, private=False, name="Graph test", 11 | user_editors=["agfernandez"], group_viewers=["virustotal"]) 12 | 13 | 14 | def test_get_expansion_nodes_one_level(mocker): 15 | """Test get expansion nodes at once level.""" 16 | rq_id = "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16" 17 | request_data = { 18 | "data": [ 19 | { 20 | "attributes": {}, 21 | "id": rq_id, 22 | "type": "file" 23 | } 24 | ] 25 | } 26 | mocker.spy(test_graph, "_get_expansion_nodes") 27 | node_a = vt_graph_api.Node( 28 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 29 | "file") 30 | node_b = vt_graph_api.Node( 31 | "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16", 32 | "file") 33 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 34 | mocker.patch("requests.get", return_value=m) 35 | expansion_nodes, _ = test_graph._get_expansion_nodes( 36 | node_a, "similar_files", 20) 37 | assert test_graph._get_expansion_nodes.call_count == 1 38 | assert node_b in expansion_nodes 39 | mocker.resetall() 40 | 41 | 42 | def test_get_expansion_nodes_n_level_with_cursor(mocker): 43 | """Test get expansion nodes at n level without cursor.""" 44 | rq_id = "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16" 45 | request_data = { 46 | "data": [ 47 | { 48 | "attributes": {}, 49 | "id": rq_id, 50 | "type": "file" 51 | } 52 | ], 53 | "meta": { 54 | "cursor": "dummy cursor" 55 | } 56 | } 57 | mocker.spy(test_graph, "_get_expansion_nodes") 58 | node_a = vt_graph_api.Node( 59 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 60 | "file") 61 | node_b = vt_graph_api.Node( 62 | "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16", 63 | "file") 64 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 65 | mocker.patch("requests.get", return_value=m) 66 | expansion_nodes, _ = test_graph._get_expansion_nodes( 67 | node_a, "similar_files", 40) 68 | assert test_graph._get_expansion_nodes.call_count == 40 69 | assert node_b in expansion_nodes 70 | mocker.resetall() 71 | 72 | 73 | def test_get_expansion_nodes_n_level_without_cursor(mocker): 74 | """Test get expansion nodes at n level without cursor.""" 75 | rq_id = "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16" 76 | request_data = { 77 | "data": [ 78 | { 79 | "attributes": {}, 80 | "id": rq_id, 81 | "type": "file" 82 | } 83 | ] 84 | } 85 | mocker.spy(test_graph, "_get_expansion_nodes") 86 | node_a = vt_graph_api.Node( 87 | "26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906", 88 | "file") 89 | node_b = vt_graph_api.Node( 90 | "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16", 91 | "file") 92 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 93 | mocker.patch("requests.get", return_value=m) 94 | expansion_nodes, _ = test_graph._get_expansion_nodes( 95 | node_a, "similar_files", 1000) 96 | assert test_graph._get_expansion_nodes.call_count == 1 97 | assert node_b in expansion_nodes 98 | mocker.resetall() 99 | 100 | 101 | def test_expansion_existing_node(mocker): 102 | """Test expansion existing node in graph.""" 103 | mocker.patch.object(test_graph, "_fetch_node_information") 104 | rq_id = "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc" 105 | first_level = { 106 | "data": [ 107 | { 108 | "attributes": {}, 109 | "id": rq_id, 110 | "type": "file" 111 | } 112 | ] 113 | } 114 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=first_level)) 115 | mocker.patch("requests.get", return_value=m) 116 | added_node_id = ( 117 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 118 | added_node = test_graph.add_node(added_node_id, "file", 119 | label="Investigation node") 120 | expansion_node = vt_graph_api.Node( 121 | "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc", 122 | "file") 123 | assert ( 124 | [expansion_node] == 125 | test_graph.expand(added_node_id, "compressed_parents", 40)) 126 | assert len(test_graph.nodes) == 2 127 | assert test_graph.nodes[expansion_node.node_id] == expansion_node 128 | assert test_graph.links[ 129 | (added_node.node_id, expansion_node.node_id, "compressed_parents")] 130 | mocker.resetall() 131 | 132 | 133 | def test_expand_not_existing_node(): 134 | """Test expansion not existing node.""" 135 | with pytest.raises(vt_graph_api.errors.NodeNotFoundError, 136 | match=r"Node 'dummy id' not found in nodes."): 137 | test_graph.expand("dummy id", "compressed_parents", 40) 138 | 139 | 140 | def test_not_supported_expansion(mocker): 141 | """Test not suported expansion type.""" 142 | mocker.patch.object(test_graph, "_fetch_node_information") 143 | added_node_id = ( 144 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 145 | test_graph.add_node(added_node_id, "file", 146 | label="Investigation node") 147 | expansion = "dummy expansion" 148 | with pytest.raises(vt_graph_api.errors.NodeNotSupportedExpansionError, 149 | match=r"Node %s cannot be expanded with %s expansion." % 150 | (added_node_id, expansion)): 151 | test_graph.expand(added_node_id, expansion, 40) 152 | mocker.resetall() 153 | 154 | 155 | def test_expand_one_level_existing_node(mocker): 156 | """Test expand one level for existing node.""" 157 | mocker.patch.object(test_graph, "_fetch_node_information") 158 | added_node_id = ( 159 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa") 160 | added_node = test_graph.add_node(added_node_id, "file", 161 | label="Investigation node") 162 | mocker.spy(test_graph, "expand") 163 | assert not test_graph.expand_one_level(added_node_id, 40) 164 | assert test_graph.expand.call_count == len(added_node.expansions_available) 165 | mocker.resetall() 166 | 167 | 168 | def test_expand_one_level_not_existing_node(): 169 | """Test expand one level for not existing node.""" 170 | with pytest.raises(vt_graph_api.errors.NodeNotFoundError, 171 | match=r"Node 'dummy id' not found in nodes."): 172 | test_graph.expand_one_level("dummy id", 40) 173 | 174 | 175 | def test_expand_n_level(mocker): 176 | """Test expand graph n levels.""" 177 | mocker.patch.object(test_graph, "_fetch_node_information") 178 | test_graph.add_node( 179 | "ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa", 180 | "file", label="Investigation node") 181 | test_graph.add_node( 182 | "fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc", 183 | "file", label="Investigation node 2") 184 | test_graph.add_node("www.google.es", "domain", label="Investigation node 3") 185 | mocker.patch.object(test_graph, "expand_one_level") 186 | mocker.spy(test_graph, "expand_one_level") 187 | assert not test_graph.expand_n_level(1) 188 | assert test_graph.expand_one_level.call_count == len(test_graph.nodes) 189 | mocker.resetall() 190 | 191 | def test_expand_node_that_returns_itself_in_the_expansion(mocker): 192 | rq_id = "7c11c7ccd384fd9f377da499fc059fa08fdc33a1bb870b5bc3812d24dd421a16" 193 | request_data = { 194 | "data": [ 195 | { 196 | "attributes": {}, 197 | "id": rq_id, 198 | "type": "file" 199 | } 200 | ] 201 | } 202 | m = mocker.Mock(status_code=200, json=mocker.Mock(return_value=request_data)) 203 | mocker.patch("requests.get", return_value=m) 204 | test_graph.add_node(rq_id, "file", label="Investigation Node File") 205 | test_graph.expand(rq_id, "similar_files") 206 | assert not (rq_id, rq_id, "similar_files") in test_graph.links 207 | mocker.resetall() 208 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Welcome to VirusTotal Graph Python API’s documentation! — VirusTotal Graph python API 1.0.1 documentation 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 | 95 | 96 |
97 | 98 | 99 | 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 |
  • Docs »
  • 132 | 133 |
  • Welcome to VirusTotal Graph Python API’s documentation!
  • 134 | 135 | 136 |
  • 137 | 138 | 139 | View page source 140 | 141 | 142 |
  • 143 | 144 |
145 | 146 | 147 |
148 |
149 |
150 |
151 | 152 |
153 |

Welcome to VirusTotal Graph Python API’s documentation!

154 |

vt_graph_api is the official Python client library 155 | for the VirusTotal Graph that implements the VirusTotal Graph REST API. This library requires Python 3.2+ or Python 2.7.

156 |
157 |

Contents:

158 | 186 |
187 |
188 | 189 | 190 |
191 | 192 |
193 |
194 | 195 | 201 | 202 | 203 |
204 | 205 |
206 |

207 | © Copyright 2019, VirusTotal 208 | 209 |

210 |
211 | Built with Sphinx using a theme provided by Read the Docs. 212 | 213 |
214 | 215 |
216 |
217 | 218 |
219 | 220 |
221 | 222 | 223 | 224 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /docs/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["apireference","examples","howtoinstall","index","quickstart"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,sphinx:54},filenames:["apireference.rst","examples.rst","howtoinstall.rst","index.rst","quickstart.rst"],objects:{"vt_graph_api.errors":{CollaboratorNotFoundError:[0,1,1,""],InvalidJSONError:[0,1,1,""],LinkNotFoundError:[0,1,1,""],LoadError:[0,1,1,""],MaximumConnectionRetriesError:[0,1,1,""],NodeNotFoundError:[0,1,1,""],NodeNotSupportedExpansionError:[0,1,1,""],NodeNotSupportedTypeError:[0,1,1,""],SameNodeError:[0,1,1,""],SaveGraphError:[0,1,1,""]},"vt_graph_api.graph":{VTGraph:[0,2,1,""]},"vt_graph_api.graph.VTGraph":{MAX_API_EXPANSION_LIMIT:[0,3,1,""],MAX_CHARACTERS:[0,3,1,""],MAX_PARALLEL_REQUESTS:[0,3,1,""],MIN_API_EXPANSION_NUMBER:[0,3,1,""],REQUEST_TIMEOUT:[0,3,1,""],add_link:[0,4,1,""],add_links_if_match:[0,4,1,""],add_node:[0,4,1,""],add_nodes:[0,4,1,""],clone_graph:[0,5,1,""],connect_with_graph:[0,4,1,""],delete_link:[0,4,1,""],delete_links:[0,4,1,""],delete_node:[0,4,1,""],expand:[0,4,1,""],expand_n_level:[0,4,1,""],expand_one_level:[0,4,1,""],get_api_calls:[0,4,1,""],get_iframe_code:[0,4,1,""],get_ui_link:[0,4,1,""],has_node:[0,4,1,""],is_editor:[0,5,1,""],is_viewer:[0,5,1,""],load_graph:[0,5,1,""],save_graph:[0,4,1,""]},"vt_graph_api.node":{Node:[0,2,1,""]},"vt_graph_api.node.Node":{NODE_EXPANSIONS:[0,3,1,""],SUPPORTED_NODE_TYPES:[0,3,1,""],add_attributes:[0,4,1,""],add_child:[0,4,1,""],add_label:[0,4,1,""],delete_child:[0,4,1,""],get_detections:[0,4,1,""],is_domain:[0,5,1,""],is_ipv4:[0,5,1,""],is_md5:[0,5,1,""],is_sha1:[0,5,1,""],is_sha256:[0,5,1,""],is_url:[0,5,1,""],reset_relationship_ids:[0,4,1,""]},vt_graph_api:{errors:[0,0,0,"-"],graph:[0,0,0,"-"],node:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"],"4":["py","method","Python method"],"5":["py","staticmethod","Python static method"]},objtypes:{"0":"py:module","1":"py:exception","2":"py:class","3":"py:attribute","4":"py:method","5":"py:staticmethod"},terms:{"26c808a1eb3eaa7bb29ec2ab834559f06f2636b87d5f542223426d6f238ff906":1,"2c2d8bc91564050cf073745f1b117f4ffdd6470e87166abdfcd10ecdff040a2":1,"3f3a9dde96ec4107f67b0559b4e95f5f1bca1ec6cb204bfe5fea0230845e8301":[1,4],"428f22a9afd2797ede7c0583d34a052c32693cbb55f567a60298587b6e675c6f":1,"5c1f4f69c45cff9725d9969f9ffcf79d07bd0f624e06cfa5bcbacd2211046ed6":1,"7a828afd2abf153d840938090d498072b7e507c7021e4cdd8c6baf727cafc545":1,"7ed0707be56fe3a7f5f2eb1747fdb929bbb9879e6c22b6825da67be5e93b6bd2":4,"85ce324b8f78021ecfc9b811c748f19b82e61bb093ff64f2eab457f9ef19b186":[1,4],"class":0,"default":0,"import":[0,1,4],"int":0,"new":[0,4],"public":[0,2,4],"return":[0,4],"static":0,"switch":1,"true":[0,1,4],"try":[0,1],Adding:1,For:[0,2,4],IDs:0,NOT:0,The:[0,2,4],There:4,a897345b68191fd36f8cefb52e6a77acb2367432abb648b9ae0a9d708406de5b:1,a93ee7ea13238bd038bcbec635f39619db566145498fe6e0ea60e6e76d614bd3:1,about:4,accord:0,action:4,actor:4,actual:4,add:[0,1,4],add_attribut:0,add_child:0,add_label:0,add_link:[0,4],add_links_if_match:[0,1,4],add_nod:[0,1,4],added:0,adding:4,advanc:[3,4],after:0,algorithm:4,all:[0,4],allow:4,alreadi:0,also:[0,4],altern:[2,4],althought:4,ani:[0,4],api:[1,2],api_cal:0,api_kei:[0,1],apikei:4,appear:0,append:0,appli:0,attribut:[0,4],avail:[0,4],b3b7d8a4daee86280c7e54b0ff3283afe3579480:1,b43b234012b8233b3df6adb7c0a3b2b13cc2354dd6de27e092873bf58af2693c:1,bad:0,badgui:4,base:0,basic:3,been:[0,4],befor:4,belong:0,between:[0,4],bool:0,browser:1,bundled_fil:[0,4],call:[0,4],can:[0,2,4],cannot:[0,4],carbonblack_children:0,carbonblack_par:0,check:0,child:0,children:0,client:3,clone:[0,2,3],clone_graph:[0,4],code:2,collabor:0,collaboratornotfounderror:0,com:[0,1,2,4],commun:1,communicating_fil:[0,1],compressed_par:0,comput:0,concurr:0,connect:[0,1],connect_with_graph:[0,1,4],connection_typ:[0,4],consum:0,contact:4,contacted_domain:[0,4],contacted_ip:0,contacted_url:0,contain:0,content:3,coordin:0,correct:0,could:[0,4],counter:0,creat:[1,4],current:0,custom:4,databas:0,delet:[0,1],delete_child:0,delete_link:[0,4],delete_nod:[0,1,4],detect:0,develop:[],dict:0,dictionari:[0,4],differ:4,directli:2,discov:0,displai:0,document:[0,4],doe:[0,1,4],doesn:0,domain:[0,1,4],dot:0,down:0,downloaded_fil:0,draw:0,each:[0,4],easiest:2,ed01ebfbc9eb5bbea545af4d01bf5f1071661840480439c6e5babe8e080e41aa:1,edit:0,either:4,email:4,email_par:0,emb:4,embedded_domain:0,embedded_ip:0,embedded_url:0,enabl:0,ensur:4,error:[1,3,4],everi:4,exampl:[0,3,4],except:[0,1],execution_par:0,exist:[0,1],expand:[0,1],expand_n_level:[0,1,4],expand_one_level:[0,4],expans:[0,3],expansions_avail:0,fail:1,fals:[0,1,4],fb0b6044347e972e21b6c376e37e1115dab494a2c6b9fb28b92b1e45b45d0ebc:1,featur:0,fetch:[0,4],fetch_info_collected_nod:0,fetch_inform:[0,1,4],fetch_vt_enterpris:[0,4],few:1,file:[0,1,4],find:[0,4],finish:4,first:[0,1,4],flag:4,follow:[0,4],found:[0,4],from:[0,1,2],get:[0,1,2],get_api_cal:[0,4],get_detect:0,get_iframe_cod:[0,4],get_ui_link:[0,1,4],git:2,github:[0,2],give:0,given:0,graph:2,graph_id:[0,1,4],graphid:4,group:[0,4],group_editor:0,group_view:[0,1],gui:0,has:[0,4],has_nod:[0,4],hash:[0,1,4],have:[0,1,2,4],height:4,here:1,his:[0,4],hop:0,how:[3,4],http:[0,4],idea:4,identifi:[0,4],ids:4,ifferfsodp9ifjaposdfjhgosurijfaewrwergwea:1,ifram:0,ignor:1,implement:3,improv:4,includ:0,infer:4,inform:[0,4],inmediate_par:0,insert:1,instal:3,instead:[0,4],intellig:[0,4],invalid:0,invalidjsonerror:0,investig:1,involv:4,ip_address:[0,4],ipv4:0,is_domain:0,is_editor:0,is_ipv4:0,is_md5:0,is_sha1:0,is_sha256:0,is_url:0,is_view:[0,4],itw_domain:0,itw_url:0,jinfant:1,json:0,just:4,kei:[0,1,4],kill:1,know:[0,4],known:0,label:[0,1,4],last_serving_ip_address:0,layer:0,least:0,level:[0,1],librari:3,limit:[0,4],link:[0,3],linknotfounderror:0,list:[0,4],load:[0,3],load_graph:[0,1,4],loaderror:0,log:0,lot:4,mai:4,make:[0,1],mani:4,match:4,max:0,max_api_expansion_limit:0,max_api_quota:[0,1,4],max_charact:0,max_depth:[0,1,4],max_nod:[0,1,4],max_nodes_per_queri:4,max_nodes_per_relationship:[0,1,4],max_parallel_request:0,max_qp:0,maximum:[0,4],maximumconnectionretrieserror:0,md5:0,method:[0,4],might:1,min_api_expansion_numb:0,minimum:[0,4],mode:0,modifi:[1,4],modul:[0,4],more:[0,4],much:[0,4],multipl:4,must:0,my_hash_1:4,my_hash_2:4,mynod:4,name:[0,1,4],necessari:4,need:0,net:1,network_loc:0,next:0,node:[1,3],node_attribut:0,node_expans:0,node_id:[0,4],node_list:[0,4],node_typ:[0,4],nodenotfounderror:[0,1,4],nodenotsupportedexpansionerror:0,nodenotsupportedtypeerror:0,nodes_to_delet:1,none:0,normal:0,nsi:1,number:[0,4],object:0,offici:3,onc:[2,4],one:0,ones:4,onli:4,open:1,openssl:1,opportun:4,option:[0,4],order:4,org:1,origin:4,otherwis:[1,4],our:4,overlay_par:0,overview:4,own:4,owner:4,packag:0,paramet:[0,4],parent:0,pass:1,path:4,pcap_par:0,pe_resource_par:0,per:[0,4],permiss:4,pip:2,pleas:4,point:[0,4],possibl:4,premium:[0,4],pretty_id:0,print:1,privat:[0,1,4],provid:0,python:0,queri:4,quickstart:3,quota:[0,1],rais:[0,4],raw:4,reach:0,receiv:4,recov:4,redirecting_url:0,refer:[3,4],referrer_fil:0,relat:[0,4],relationship:[0,4],relationship_id:0,replac:4,repositori:2,repres:4,represent:[0,4],request:[0,4],request_timeout:0,requir:[0,3],reset:0,reset_relationship_id:0,resolut:0,respons:0,rest:3,result:0,retriev:[1,4],run:2,safe:0,same:[0,4],samenodeerror:[0,4],save:[0,1,3],save_graph:[0,1,4],savegrapherror:0,script:[0,4],search:[0,3,4],searchabl:[0,4],second:[0,1],see:[0,4],select:0,set:[0,4],setup:2,sha1:0,sha256:[0,4],sibl:0,similar_fil:0,sinc:4,singl:4,some:[1,4],someth:0,soon:0,sourc:[0,2,4],source_id:4,source_nod:0,specifi:4,src:4,standard:0,start:4,stop:0,str:0,string:0,structur:[0,4],subdomain:0,subscript:0,suppli:[0,4],supported_node_typ:0,sure:1,take:[1,4],target:[0,4],target_id:4,target_nod:0,than:[0,4],thei:4,them:4,thi:[0,1,3,4],those:0,thread:0,three:0,time:4,titl:0,too:4,top:0,total:[0,4],two:4,type:[0,4],understand:0,unit:4,unknown:[0,4],url:[0,1,4],usag:[1,3],use:4,user:[0,2,4],user_editor:[0,1],user_view:0,usernam:[0,4],uses:4,using:[0,2],util:3,valu:0,variabl:0,verbos:[0,1],victim:4,view:0,virustot:[0,1,2,4],visual:1,vt_graph_api:[0,1,2,3,4],vt_user:0,vtgraph:[0,1,4],wai:2,wannycri:1,want:4,websit:4,well:0,went:0,wether:0,when:0,whether:0,which:[0,4],why:4,width:4,without:[0,4],wraper:0,wrapper:0,www:[0,1,4],x_posit:[0,4],y_posit:[0,4],yet:0,yoi:4,you:[0,1,2,4],your:[0,1,4],yout:1},titles:["API Reference","Examples","How to install","Welcome to VirusTotal Graph Python API\u2019s documentation!","Quickstart"],titleterms:{Adding:4,advanc:1,api:[0,3,4],autoexplor:4,basic:[1,4],belong:4,check:4,clone:4,connect:4,consum:4,delet:4,document:3,editor:4,error:0,exampl:1,expand:4,expans:4,get:4,given:4,graph:[0,1,3,4],how:2,ifram:4,instal:2,level:4,link:4,load:[1,4],node:[0,4],one:4,python:3,quickstart:4,quota:4,refer:0,save:4,search:1,someon:4,usag:4,using:4,util:4,viewer:4,virustot:3,welcom:3,whole:4}}) -------------------------------------------------------------------------------- /vt_graph_api/node.py: -------------------------------------------------------------------------------- 1 | """vt_graph_api.node. 2 | 3 | This module provides the Python object wrapper for 4 | VTGraph node representation. 5 | """ 6 | 7 | 8 | import re 9 | 10 | 11 | URL_RE = re.compile(r"https?://", re.IGNORECASE) 12 | SHA1_RE = re.compile(r"^[0-9a-fA-F]{40}$") 13 | MD5_RE = re.compile(r"^[0-9a-fA-F]{32}$") 14 | SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$") 15 | IPV4_RE = re.compile(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$") 16 | DOMAIN_RE = re.compile(r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$") 17 | 18 | 19 | class Node(object): 20 | """Python object wraper for the VT Graph Node representation. 21 | 22 | Attributes: 23 | node_id (str): node identifier. 24 | node_type (str): node type, must be one of the SUPPORTED_NODE_TYPES. 25 | pretty_id (str): node identifier without dots. 26 | x (int, optional): X coordinate for Node representation in VT Graph GUI. 27 | y (int, optional): Y coordinate for Node representation in VT Graph GUI. 28 | expansions_available ([str]): available expansions for the node. 29 | attributes (dict): VirusTotal attribute dict. 30 | label (str): node name. 31 | children (dict): dict with the children for each expansion type. 32 | relationship_ids (dict): dict with the relationship id for each 33 | expansion type. 34 | """ 35 | 36 | SUPPORTED_NODE_TYPES = ( 37 | "file", 38 | "url", 39 | "domain", 40 | "ip_address", 41 | "whois", 42 | "ssl_cert", 43 | "collection", 44 | "reference", 45 | ) 46 | NODE_EXPANSIONS = { 47 | "file": [ 48 | "bundled_files", 49 | "collections", 50 | "compressed_parents", 51 | "contacted_domains", 52 | "contacted_ips", 53 | "contacted_urls", 54 | "dropped_files", 55 | "email_parents", 56 | "email_attachments", 57 | "embedded_domains", 58 | "embedded_urls", 59 | "embedded_ips", 60 | "execution_parents", 61 | "itw_domains", 62 | "itw_urls", 63 | "itw_ips", 64 | "overlay_parents", 65 | "overlay_children", 66 | "pcap_parents", 67 | "pe_resource_parents", 68 | "pe_resource_children", 69 | "references", 70 | "similar_files", 71 | "urls_for_embedded_js" 72 | ], 73 | "url": [ 74 | "downloaded_files", 75 | "last_serving_ip_address", 76 | "network_location", 77 | "redirecting_urls", 78 | "contacted_domains", 79 | "contacted_ips", 80 | "redirects_to", 81 | "urls_related_by_tracker_id", 82 | "communicating_files", 83 | "referrer_files", 84 | "embedded_js_files", 85 | "collections", 86 | "references" 87 | ], 88 | "domain": [ 89 | "immediate_parent", 90 | "parent", 91 | "communicating_files", 92 | "downloaded_files", 93 | "referrer_files", 94 | "resolutions", 95 | "siblings", 96 | "subdomains", 97 | "urls", 98 | "historical_ssl_certificates", 99 | "historical_whois", 100 | "caa_records", 101 | "cname_records", 102 | "mx_records", 103 | "ns_records", 104 | "soa_records", 105 | "collections", 106 | "references" 107 | ], 108 | "ip_address": [ 109 | "communicating_files", 110 | "downloaded_files", 111 | "referrer_files", 112 | "resolutions", 113 | "urls", 114 | "historical_ssl_certificates", 115 | "historical_whois", 116 | "collections", 117 | "references" 118 | ], 119 | "reference": [ 120 | "files", 121 | "domains", 122 | "urls", 123 | "ip_addresses", 124 | "collections" 125 | ], 126 | "collection": [ 127 | "files", 128 | "domains", 129 | "ip_addresses", 130 | "urls", 131 | "references" 132 | ], 133 | "whois": ["network_location"], 134 | "ssl_cert": [] 135 | } 136 | 137 | def __init__(self, node_id, node_type, x=0, y=0): 138 | """Creates an instance of a node object. 139 | 140 | Args: 141 | node_id (str): node identifier. 142 | node_type (str): node type, must be one of the SUPPORTED_NODE_TYPES 143 | x (int, optional): X coordinate for Node representation in VT Graph GUI. 144 | y (int, optional): Y coordinate for Node representation in VT Graph GUI. 145 | """ 146 | self.pretty_id = node_id.replace(".", "") 147 | self.node_id = node_id 148 | self.node_type = node_type 149 | self.x = x 150 | self.y = y 151 | self.expansions_available = self.NODE_EXPANSIONS.get(node_type, []) 152 | self.attributes = None 153 | self.label = "" 154 | self.children = { 155 | expansion_type: [] for expansion_type in self.expansions_available 156 | } 157 | self.relationship_ids = {} 158 | 159 | def get_detections(self): 160 | """Get the node detections from attributes. 161 | 162 | Returns: 163 | int: the number of detections. 164 | """ 165 | if "has_detections" in self.attributes: 166 | return self.attributes["has_detections"] 167 | else: 168 | stats = self.attributes.get("last_analysis_stats", {}) 169 | return stats.get("malicious", 0) + stats.get("suspicious", 0) 170 | 171 | @staticmethod 172 | def is_url(node_id): 173 | """Check if node_id belongs to url. 174 | 175 | Args: 176 | node_id (str): node ID. 177 | 178 | Returns: 179 | bool: whether node_id belongs to a url 180 | """ 181 | return URL_RE.match(node_id) 182 | 183 | @staticmethod 184 | def is_md5(node_id): 185 | """Check if node_id belongs to md5 hash. 186 | 187 | Args: 188 | node_id (str): node ID. 189 | 190 | Returns: 191 | bool: wether node_id belongs to a md5 hash. 192 | """ 193 | return MD5_RE.match(node_id) 194 | 195 | @staticmethod 196 | def is_sha1(node_id): 197 | """Check if node_id belongs to sha1 hash. 198 | 199 | Args: 200 | node_id (str): node ID. 201 | 202 | Returns: 203 | bool: wether node_id belongs to a sha1 hash. 204 | """ 205 | return SHA1_RE.match(node_id) 206 | 207 | @staticmethod 208 | def is_sha256(node_id): 209 | """Check if node_id belongs to a sha256 hash. 210 | 211 | Args: 212 | node_id (str): node ID. 213 | 214 | Returns: 215 | bool: wether node_id belongs to a sha256 hash. 216 | """ 217 | return SHA256_RE.match(node_id) 218 | 219 | @staticmethod 220 | def is_ipv4(node_id): 221 | """Check if node_id belongs to ipv4. 222 | 223 | Args: 224 | node_id (str): node ID. 225 | 226 | Returns: 227 | bool: wether node_id belongs to a ipv4. 228 | """ 229 | return IPV4_RE.match(node_id) 230 | 231 | @staticmethod 232 | def is_domain(node_id): 233 | """Check if node_id belongs to domain name. 234 | 235 | Args: 236 | node_id (str): node ID. 237 | 238 | Returns: 239 | bool: wether node_id belongs to a domain name. 240 | """ 241 | return DOMAIN_RE.match(node_id) 242 | 243 | def add_attributes(self, attributes): 244 | """Adds the attributes if the node doesn't have it yet. 245 | 246 | Args: 247 | attributes (dict): VirusTotal attribute dict. 248 | """ 249 | if not self.attributes: 250 | self.attributes = attributes 251 | 252 | def add_label(self, label): 253 | """Adds a label to the node. 254 | 255 | Args: 256 | label (str): value of the label. 257 | """ 258 | self.label = label 259 | 260 | def add_child(self, node_id, expansion): 261 | """Add child to Node in the given expansion. 262 | 263 | Args: 264 | node_id (str): child node id. 265 | expansion (str): expansion for the given node_id. 266 | """ 267 | if expansion not in self.children: 268 | self.children[expansion] = [] 269 | 270 | self.children[expansion].append(node_id) 271 | 272 | def delete_child(self, node_id, expansion): 273 | """Delete child from Node in the given expansion. 274 | 275 | Args: 276 | node_id (str): child node id. 277 | expansion (str): expansion for the given node_id. 278 | """ 279 | if expansion in self.children: 280 | self.children[expansion].remove(node_id) 281 | 282 | def reset_relationship_ids(self): 283 | """Reset relationship_ids.""" 284 | self.relationship_ids.clear() 285 | 286 | def __str__(self): 287 | return "%s" % (self.node_id) 288 | 289 | def __repr__(self): 290 | return str(self) 291 | 292 | def __eq__(self, other): 293 | return isinstance(other, Node) and self.node_id == other.node_id 294 | 295 | def __hash__(self): 296 | return hash(self.node_id) 297 | -------------------------------------------------------------------------------- /docs/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s === 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node, addItems) { 70 | if (node.nodeType === 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && 74 | !jQuery(node.parentNode).hasClass(className) && 75 | !jQuery(node.parentNode).hasClass("nohighlight")) { 76 | var span; 77 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 78 | if (isInSVG) { 79 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 80 | } else { 81 | span = document.createElement("span"); 82 | span.className = className; 83 | } 84 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 85 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 86 | document.createTextNode(val.substr(pos + text.length)), 87 | node.nextSibling)); 88 | node.nodeValue = val.substr(0, pos); 89 | if (isInSVG) { 90 | var bbox = span.getBBox(); 91 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 92 | rect.x.baseVal.value = bbox.x; 93 | rect.y.baseVal.value = bbox.y; 94 | rect.width.baseVal.value = bbox.width; 95 | rect.height.baseVal.value = bbox.height; 96 | rect.setAttribute('class', className); 97 | var parentOfText = node.parentNode.parentNode; 98 | addItems.push({ 99 | "parent": node.parentNode, 100 | "target": rect}); 101 | } 102 | } 103 | } 104 | else if (!jQuery(node).is("button, select, textarea")) { 105 | jQuery.each(node.childNodes, function() { 106 | highlight(this, addItems); 107 | }); 108 | } 109 | } 110 | var addItems = []; 111 | var result = this.each(function() { 112 | highlight(this, addItems); 113 | }); 114 | for (var i = 0; i < addItems.length; ++i) { 115 | jQuery(addItems[i].parent).before(addItems[i].target); 116 | } 117 | return result; 118 | }; 119 | 120 | /* 121 | * backward compatibility for jQuery.browser 122 | * This will be supported until firefox bug is fixed. 123 | */ 124 | if (!jQuery.browser) { 125 | jQuery.uaMatch = function(ua) { 126 | ua = ua.toLowerCase(); 127 | 128 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 129 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 130 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 131 | /(msie) ([\w.]+)/.exec(ua) || 132 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 133 | []; 134 | 135 | return { 136 | browser: match[ 1 ] || "", 137 | version: match[ 2 ] || "0" 138 | }; 139 | }; 140 | jQuery.browser = {}; 141 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 142 | } 143 | 144 | /** 145 | * Small JavaScript module for the documentation. 146 | */ 147 | var Documentation = { 148 | 149 | init : function() { 150 | this.fixFirefoxAnchorBug(); 151 | this.highlightSearchWords(); 152 | this.initIndexTable(); 153 | if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { 154 | this.initOnKeyListeners(); 155 | } 156 | }, 157 | 158 | /** 159 | * i18n support 160 | */ 161 | TRANSLATIONS : {}, 162 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, 163 | LOCALE : 'unknown', 164 | 165 | // gettext and ngettext don't access this so that the functions 166 | // can safely bound to a different name (_ = Documentation.gettext) 167 | gettext : function(string) { 168 | var translated = Documentation.TRANSLATIONS[string]; 169 | if (typeof translated === 'undefined') 170 | return string; 171 | return (typeof translated === 'string') ? translated : translated[0]; 172 | }, 173 | 174 | ngettext : function(singular, plural, n) { 175 | var translated = Documentation.TRANSLATIONS[singular]; 176 | if (typeof translated === 'undefined') 177 | return (n == 1) ? singular : plural; 178 | return translated[Documentation.PLURALEXPR(n)]; 179 | }, 180 | 181 | addTranslations : function(catalog) { 182 | for (var key in catalog.messages) 183 | this.TRANSLATIONS[key] = catalog.messages[key]; 184 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 185 | this.LOCALE = catalog.locale; 186 | }, 187 | 188 | /** 189 | * add context elements like header anchor links 190 | */ 191 | addContextElements : function() { 192 | $('div[id] > :header:first').each(function() { 193 | $('\u00B6'). 194 | attr('href', '#' + this.id). 195 | attr('title', _('Permalink to this headline')). 196 | appendTo(this); 197 | }); 198 | $('dt[id]').each(function() { 199 | $('\u00B6'). 200 | attr('href', '#' + this.id). 201 | attr('title', _('Permalink to this definition')). 202 | appendTo(this); 203 | }); 204 | }, 205 | 206 | /** 207 | * workaround a firefox stupidity 208 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 209 | */ 210 | fixFirefoxAnchorBug : function() { 211 | if (document.location.hash && $.browser.mozilla) 212 | window.setTimeout(function() { 213 | document.location.href += ''; 214 | }, 10); 215 | }, 216 | 217 | /** 218 | * highlight the search words provided in the url in the text 219 | */ 220 | highlightSearchWords : function() { 221 | var params = $.getQueryParameters(); 222 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 223 | if (terms.length) { 224 | var body = $('div.body'); 225 | if (!body.length) { 226 | body = $('body'); 227 | } 228 | window.setTimeout(function() { 229 | $.each(terms, function() { 230 | body.highlightText(this.toLowerCase(), 'highlighted'); 231 | }); 232 | }, 10); 233 | $('') 235 | .appendTo($('#searchbox')); 236 | } 237 | }, 238 | 239 | /** 240 | * init the domain index toggle buttons 241 | */ 242 | initIndexTable : function() { 243 | var togglers = $('img.toggler').click(function() { 244 | var src = $(this).attr('src'); 245 | var idnum = $(this).attr('id').substr(7); 246 | $('tr.cg-' + idnum).toggle(); 247 | if (src.substr(-9) === 'minus.png') 248 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 249 | else 250 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 251 | }).css('display', ''); 252 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 253 | togglers.click(); 254 | } 255 | }, 256 | 257 | /** 258 | * helper function to hide the search marks again 259 | */ 260 | hideSearchWords : function() { 261 | $('#searchbox .highlight-link').fadeOut(300); 262 | $('span.highlighted').removeClass('highlighted'); 263 | }, 264 | 265 | /** 266 | * make the url absolute 267 | */ 268 | makeURL : function(relativeURL) { 269 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 270 | }, 271 | 272 | /** 273 | * get the current relative url 274 | */ 275 | getCurrentURL : function() { 276 | var path = document.location.pathname; 277 | var parts = path.split(/\//); 278 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 279 | if (this === '..') 280 | parts.pop(); 281 | }); 282 | var url = parts.join('/'); 283 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 284 | }, 285 | 286 | initOnKeyListeners: function() { 287 | $(document).keyup(function(event) { 288 | var activeElementType = document.activeElement.tagName; 289 | // don't navigate when in search box or textarea 290 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { 291 | switch (event.keyCode) { 292 | case 37: // left 293 | var prevHref = $('link[rel="prev"]').prop('href'); 294 | if (prevHref) { 295 | window.location.href = prevHref; 296 | return false; 297 | } 298 | case 39: // right 299 | var nextHref = $('link[rel="next"]').prop('href'); 300 | if (nextHref) { 301 | window.location.href = nextHref; 302 | return false; 303 | } 304 | } 305 | } 306 | }); 307 | } 308 | }; 309 | 310 | // quick alias for translations 311 | _ = Documentation.gettext; 312 | 313 | $(document).ready(function() { 314 | Documentation.init(); 315 | }); 316 | -------------------------------------------------------------------------------- /docsrc/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s === 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node, addItems) { 70 | if (node.nodeType === 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && 74 | !jQuery(node.parentNode).hasClass(className) && 75 | !jQuery(node.parentNode).hasClass("nohighlight")) { 76 | var span; 77 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 78 | if (isInSVG) { 79 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 80 | } else { 81 | span = document.createElement("span"); 82 | span.className = className; 83 | } 84 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 85 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 86 | document.createTextNode(val.substr(pos + text.length)), 87 | node.nextSibling)); 88 | node.nodeValue = val.substr(0, pos); 89 | if (isInSVG) { 90 | var bbox = span.getBBox(); 91 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 92 | rect.x.baseVal.value = bbox.x; 93 | rect.y.baseVal.value = bbox.y; 94 | rect.width.baseVal.value = bbox.width; 95 | rect.height.baseVal.value = bbox.height; 96 | rect.setAttribute('class', className); 97 | var parentOfText = node.parentNode.parentNode; 98 | addItems.push({ 99 | "parent": node.parentNode, 100 | "target": rect}); 101 | } 102 | } 103 | } 104 | else if (!jQuery(node).is("button, select, textarea")) { 105 | jQuery.each(node.childNodes, function() { 106 | highlight(this, addItems); 107 | }); 108 | } 109 | } 110 | var addItems = []; 111 | var result = this.each(function() { 112 | highlight(this, addItems); 113 | }); 114 | for (var i = 0; i < addItems.length; ++i) { 115 | jQuery(addItems[i].parent).before(addItems[i].target); 116 | } 117 | return result; 118 | }; 119 | 120 | /* 121 | * backward compatibility for jQuery.browser 122 | * This will be supported until firefox bug is fixed. 123 | */ 124 | if (!jQuery.browser) { 125 | jQuery.uaMatch = function(ua) { 126 | ua = ua.toLowerCase(); 127 | 128 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 129 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 130 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 131 | /(msie) ([\w.]+)/.exec(ua) || 132 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 133 | []; 134 | 135 | return { 136 | browser: match[ 1 ] || "", 137 | version: match[ 2 ] || "0" 138 | }; 139 | }; 140 | jQuery.browser = {}; 141 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 142 | } 143 | 144 | /** 145 | * Small JavaScript module for the documentation. 146 | */ 147 | var Documentation = { 148 | 149 | init : function() { 150 | this.fixFirefoxAnchorBug(); 151 | this.highlightSearchWords(); 152 | this.initIndexTable(); 153 | if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { 154 | this.initOnKeyListeners(); 155 | } 156 | }, 157 | 158 | /** 159 | * i18n support 160 | */ 161 | TRANSLATIONS : {}, 162 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, 163 | LOCALE : 'unknown', 164 | 165 | // gettext and ngettext don't access this so that the functions 166 | // can safely bound to a different name (_ = Documentation.gettext) 167 | gettext : function(string) { 168 | var translated = Documentation.TRANSLATIONS[string]; 169 | if (typeof translated === 'undefined') 170 | return string; 171 | return (typeof translated === 'string') ? translated : translated[0]; 172 | }, 173 | 174 | ngettext : function(singular, plural, n) { 175 | var translated = Documentation.TRANSLATIONS[singular]; 176 | if (typeof translated === 'undefined') 177 | return (n == 1) ? singular : plural; 178 | return translated[Documentation.PLURALEXPR(n)]; 179 | }, 180 | 181 | addTranslations : function(catalog) { 182 | for (var key in catalog.messages) 183 | this.TRANSLATIONS[key] = catalog.messages[key]; 184 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 185 | this.LOCALE = catalog.locale; 186 | }, 187 | 188 | /** 189 | * add context elements like header anchor links 190 | */ 191 | addContextElements : function() { 192 | $('div[id] > :header:first').each(function() { 193 | $('\u00B6'). 194 | attr('href', '#' + this.id). 195 | attr('title', _('Permalink to this headline')). 196 | appendTo(this); 197 | }); 198 | $('dt[id]').each(function() { 199 | $('\u00B6'). 200 | attr('href', '#' + this.id). 201 | attr('title', _('Permalink to this definition')). 202 | appendTo(this); 203 | }); 204 | }, 205 | 206 | /** 207 | * workaround a firefox stupidity 208 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 209 | */ 210 | fixFirefoxAnchorBug : function() { 211 | if (document.location.hash && $.browser.mozilla) 212 | window.setTimeout(function() { 213 | document.location.href += ''; 214 | }, 10); 215 | }, 216 | 217 | /** 218 | * highlight the search words provided in the url in the text 219 | */ 220 | highlightSearchWords : function() { 221 | var params = $.getQueryParameters(); 222 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 223 | if (terms.length) { 224 | var body = $('div.body'); 225 | if (!body.length) { 226 | body = $('body'); 227 | } 228 | window.setTimeout(function() { 229 | $.each(terms, function() { 230 | body.highlightText(this.toLowerCase(), 'highlighted'); 231 | }); 232 | }, 10); 233 | $('') 235 | .appendTo($('#searchbox')); 236 | } 237 | }, 238 | 239 | /** 240 | * init the domain index toggle buttons 241 | */ 242 | initIndexTable : function() { 243 | var togglers = $('img.toggler').click(function() { 244 | var src = $(this).attr('src'); 245 | var idnum = $(this).attr('id').substr(7); 246 | $('tr.cg-' + idnum).toggle(); 247 | if (src.substr(-9) === 'minus.png') 248 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 249 | else 250 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 251 | }).css('display', ''); 252 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 253 | togglers.click(); 254 | } 255 | }, 256 | 257 | /** 258 | * helper function to hide the search marks again 259 | */ 260 | hideSearchWords : function() { 261 | $('#searchbox .highlight-link').fadeOut(300); 262 | $('span.highlighted').removeClass('highlighted'); 263 | }, 264 | 265 | /** 266 | * make the url absolute 267 | */ 268 | makeURL : function(relativeURL) { 269 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 270 | }, 271 | 272 | /** 273 | * get the current relative url 274 | */ 275 | getCurrentURL : function() { 276 | var path = document.location.pathname; 277 | var parts = path.split(/\//); 278 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 279 | if (this === '..') 280 | parts.pop(); 281 | }); 282 | var url = parts.join('/'); 283 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 284 | }, 285 | 286 | initOnKeyListeners: function() { 287 | $(document).keyup(function(event) { 288 | var activeElementType = document.activeElement.tagName; 289 | // don't navigate when in search box or textarea 290 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { 291 | switch (event.keyCode) { 292 | case 37: // left 293 | var prevHref = $('link[rel="prev"]').prop('href'); 294 | if (prevHref) { 295 | window.location.href = prevHref; 296 | return false; 297 | } 298 | case 39: // right 299 | var nextHref = $('link[rel="next"]').prop('href'); 300 | if (nextHref) { 301 | window.location.href = nextHref; 302 | return false; 303 | } 304 | } 305 | } 306 | }); 307 | } 308 | }; 309 | 310 | // quick alias for translations 311 | _ = Documentation.gettext; 312 | 313 | $(document).ready(function() { 314 | Documentation.init(); 315 | }); 316 | --------------------------------------------------------------------------------