In [ ]:
import os, sys
try:
    from synapse.lib.jupyter import *
except ImportError as e:
    # Insert the root path of the repository to sys.path.
    # This assumes the notebook is located three directories away
    # From the root synapse directory. It may need to be varied
    synroot = os.path.abspath('../../../')
    sys.path.insert(0, synroot)
    from synapse.lib.jupyter import *

In [ ]:
# Get a temp cortex
core = await getTempCoreCmdr()
.. highlight:: none .. _storm-ref-cmd: Storm Reference - Storm Commands ================================ Storm commands are built-in or custom commands that can be used with the Synapse :ref:`syn-storm` command itself. As such, Storm commands must be executed following the ``storm`` command: **storm** ** or **storm** ** **|** ** [ **|** ** ] The pipe symbol ( ``|`` ) is used to send (pipe) the output from a Storm query to any of the Storm commands, and to send the output from a Storm command back to a Storm query. **Built-in commands** are native to the Storm library and loaded by default within a given Cortex. Built-in commands comprise a set of helper commands that perform a variety of specialized tasks that are useful regardless of the types of data stored in the Cortex or the types of analysis performed. **Custom commands** are Storm commands that have been added to a Cortex to invoke the execution of dynamically loaded modules. Dynamically loaded modules are typically custom modules that have been added to Synapse to support domain-specific analysis. For example, a knowledge domain that requires tracking of IP addresses might have access to a third-party service such as Maxmind to obtain up-to-date data on the assigned Autonomous System (AS) or geographical location of a given IP address. A custom ``maxmind`` module and associated Storm command could be added to Synapse to query the Maxmind database and update the appropriate secondary properties on the associated ``inet:ipv4`` nodes directly from Storm. The full list of storm commands (built-in and custom) available in a given Cortex can be displayed with ``storm help``. Help for a specific Storm command can be displayed with ``storm --help``. This section details the usage and syntax for built-in Storm commands. Many of the commands below, such as ``count``, ``limit``, ``max`` / ``min``, or ``delnode``, directly support analyst tasks within Storm. Other commands, such as those used to manage daemons, queues, packages, or services, may be primarily of interest to Syanpse administrators or developers. - `help`_ - `count`_ - `cron`_ - `delnode`_ - `dmon`_ - `feed`_ - `graph`_ - `iden`_ - `layer`_ - `limit`_ - `macro`_ - `max`_ - `min`_ - `model`_ - `movetag`_ - `package`_ - `queue`_ - `reindex`_ - `scrape`_ - `service`_ - `sleep`_ - `spin`_ - `splice`_ - `tee`_ - `tree`_ - `trigger`_ - `uniq`_ - `view`_ See :ref:`storm-ref-syntax` for an explanation of the syntax format used below. The Storm query language is covered in detail starting with the :ref:`storm-ref-intro` section of the Synapse User Guide. .. NOTE:: Storm commands, including custom commands, are added to the Cortex as **runtime nodes** ("runt nodes" - see :ref:`gloss-node-runt`) of the form ``syn:cmd``. These runt nodes can be lifted and filtered just like standard nodes in a Cortex. **Example** Lift the ``syn:cmd`` node for the Storm ``movetag`` command:

In [ ]:
# Run the command and display output
q = 'syn:cmd=movetag'
podes = await core.eval(q, cmdr=True)
.. NOTE:: While the documentation for built-in Storm commands is relatively basic, the ``syn:cmd`` form within the data model includes additional secondary propeties that can be used to make commands (particularly custom commands) more "self-documenting" to users, because detail about the command can be introspected directly within the data model from within Storm. For example, the ``:input`` and ``:output`` properties can be used to specify a list (:ref:`type-array`) of forms that can be input (submitted) to the command and a list of forms that may be created by the command, respectively.
.. _storm-help: help ---- The ``help`` command (``storm help``) displays the list of available built-in commands and a brief message describing each command. Help on individual commands is available via `` --help``. **Syntax:**

In [ ]:
# Run the command and display output
q = 'help'
podes = await core.eval(q, cmdr=True)
.. _storm-count: count ----- The ``count`` command enumerates the number of nodes returned from a given Storm query and displays the resultant nodes and associated node count. **Syntax**

In [ ]:
# Run the command and display output
q = 'count --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Count the number of email address nodes:

In [ ]:
# Make some email nodes
q = '[inet:email=me@gmail.com inet:email=you@yahoo.com]'
# Run the query and test
podes = await core.eval(q, num=2, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:email | count'
print(q)
# Execute the query and test
podes = await core.eval(q, num=2, cmdr=False)
- Count the number of DNS A records for the domain woot.com:

In [ ]:
# Make some DNS A records
q = '[inet:dns:a=(woot.com,1.2.3.4) inet:dns:a=(woot.com,5.6.7.8)]'
# Run the query and test
podes = await core.eval(q, num=2, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:dns:a:fqdn=woot.com | count'
print(q)
# Execute the query and test
podes = await core.eval(q, num=2, cmdr=False)
**Usage Notes:** - ``count`` does not consume nodes, so Storm will stream the nodes being counted to the CLI output while the command executes. To count nodes without streaming the output, ``count`` can be piped to the `spin`_ command (i.e., ** | count | spin). ``Spin`` consumes nodes and so will prevent nodes processed by the ``count`` command from streaming.
.. _storm-cron: cron ---- Storm includes ``cron.*`` commands that allow you to create scheduled :ref:`gloss-cron` jobs. Within Synapse, jobs are Storm queries that execute within a Cortex on a recurring or non-recurring (``cron.at``) basis. - `cron.add`_ - `cron.at`_ - `cron.list`_ - `cron.stat`_ - `cron.mod`_ - `cron.disable`_ - `cron.enable`_ - `cron.del`_ Help for individual ``cron.*`` commands can be displayed using: ``storm --help | -h`` Cron jobs (including jobs created with ``cron.at``) are added to the Cortex as **runtime nodes** ("runt nodes" - see :ref:`gloss-node-runt`) of the form ``syn:cron``. These runt nodes can be lifted and filtered just like standard nodes in a Cortex. See the :ref:`storm-ref-automation` document for additional information. .. NOTE:: The Storm ``cron.*`` commands replace the Synapse cmdr-based :ref:`syn-cron` command, which is being deprecated.
.. _storm-cron-add: cron.add ++++++++ The ``cron.add`` command creates an individual cron job within a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'cron.add --help'
podes = await core.eval(q, cmdr=True)
**Example** Add a cron job to run on the first day of every month to lift all IPv4 nodes with missing geolocation data (i.e., the ``:loc`` property is set to the string ``??``) and submit them to the maxmind service. .. NOTE:: This example assumes a Storm service or other module has been implemented to interface with Maxmind; such a service is not included in Synpase by default.

In [ ]:
# Define query
q = 'cron.add --day 1 { inet:ipv4:loc="??" | maxmind }'
# Execute the query
podes = await core.eval(q, cmdr=True)
.. _storm-cron-at: cron.at +++++++ The ``cron.at`` command creates a non-recurring cron job within a Cortex. Note that just like standard cron jobs, jobs created with ``cron.at`` will persist (remain in the list of cron jobs and as ``syn:cron`` runt nodes) until they are explicitly removed using ``cron.del``. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.at --help'
podes = await core.eval(q, cmdr=True)
**Example** Add a non-recurring cron job to run at 0200 UTC to create the set of IPv4 addresses in the 172.16.0.0/16 range.

In [ ]:
# Define query
q = 'cron.at --hour 2 { [ inet:ipv4=172.16.0.0/16 ] }'
# Execute the query
podes = await core.eval(q, cmdr=True)
.. _storm-cron-list: cron.list +++++++++ The ``cron.list`` command displays the set of cron jobs in the Cortex that the current user can view / modify based on their permissions. Cron jobs are displayed in alphanumeric order by job :ref:`gloss-iden`. Jobs are sorted upon Cortex initialization, so newly-created jobs will be displayed at the bottom of the list until the list is re-sorted the next time the Cortex is restarted. .. NOTE:: Jobs can also be viewed in runt node form as ``syn:cron`` nodes. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.list --help'
podes = await core.eval(q, cmdr=True)
**Examples** *cron.list* List cron jobs (including non-recurring jobs created with ``cron.at``).

In [ ]:
# Define query
q = 'cron.list'
# Execute the query
podes = await core.eval(q, cmdr=True)
**Notes:** The output of ``cron.list`` contains the following columns: - The username used to create the job. - The first eight characters of the job's identifier (iden). - Whether the job is currently enabled or disabled. - Whether the job is scheduled to repeat. - Whether the job is currently executing. - Whether the last job execution encountered an error. - The number of times the job has started. - The date and time of the job's last start and last end. - The query executed by the cron job.
*cron jobs as runt nodes* List cron jobs whose Storm command contains the string "maxmind" by lifting ``syn:cron`` nodes.

In [ ]:
# Define query
q = 'syn:cron +:storm~=maxmind'
# Execute the query
podes = await core.eval(q, cmdr=True)
**Notes:** While runt nodes (:ref:`gloss-node-runt`) are typically read-only, ``syn:cron`` nodes include ``:name`` and ``:doc`` secondary properties that can be set and modified via Storm. This allows management of cron jobs by providing them with meaningful names / categoreies and / or descriptions of their purpose. Changes to these properties will persist even after a Cortex restart.
.. _storm-cron-stat: cron.stat +++++++++ The ``cron.stat`` command displays statistics for an individual cron job and provides more detail on an individual job vs. ``cron.list``, including any errors and the interval at which the job executes. To view the stats for a job, you must provide the first portion of the job's iden (i.e., enough of the iden that the job can be uniquely identified), which can be obtained using ``cron.list`` or by lifting the appropriate ``syn:cron`` node. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.stat --help'
podes = await core.eval(q, cmdr=True)
.. _storm-cron-mod: cron.mod ++++++++ The ``cron.mod`` command modifies the Storm query associated with a specific cron job. To modify a job, you must provide the first portion of the job's iden (i.e., enough of the iden that the job can be uniquely identified), which can be obtained using ``cron.list`` or by lifting the appropriate ``syn:cron`` node. .. NOTE:: Other aspects of the cron job, such as its schedule for execution, cannot be modified once the job has been created. To change these aspects you must delete and re-add the job. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.mod --help'
podes = await core.eval(q, cmdr=True)
.. _storm-cron-disable: cron.disable ++++++++++++ The ``cron.disable`` command disables a job and prevents it from executing without removing it from the Cortex. To disable a job, you must provide the first portion of the job's iden (i.e., enough of the iden that the job can be uniquely identified), which can be obtained using ``cron.list`` or by lifting the appropriate ``syn:cron`` node. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.disable --help'
podes = await core.eval(q, cmdr=True)
.. _storm-cron-enable: cron.enable +++++++++++ The ``cron.enable`` command enables a disabled cron job. To enable a job, you must provide the first portion of the job's iden (i.e., enough of the iden that the job can be uniquely identified), which can be obtained using ``cron.list`` or by lifting the appropriate ``syn:cron`` node. .. NOTE:: Cron jobs, including non-recurring jobs added with ``cron.at``, are enabled by default upon creation. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.enable --help'
podes = await core.eval(q, cmdr=True)
.. _storm-cron-del: cron.del ++++++++ The ``cron.del`` command permanently removes a cron job from the Cortex. To delete a job, you must provide the first portion of the job's iden (i.e., enough of the iden that the job can be uniquely identified), which can be obtained using ``cron.list`` or by lifting the appropriate ``syn:cron`` node. **Syntax:**

In [ ]:
# Run the command and display output
q = 'cron.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-delnode: delnode ------- The ``delnode`` command deletes a node or set of nodes from a Cortex. .. WARNING:: The Storm ``delnode`` command has the potential to be destructive if executed on an incorrect, badly formed, or mistyped query. Users are strongly encouraged to validate their query by first executing it on its own to confirm it returns the expected nodes before piping the query to the ``delnode`` command. **Syntax:**

In [ ]:
# Run the command and display output
q = 'delnode --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Delete the node for the domain woowoo.com:

In [ ]:
# Make a node
q = '[inet:fqdn=woowoo.com]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:fqdn=woowoo.com | delnode'
print(q)
# Execute the query and test
podes = await core.eval(q, num=0, cmdr=False)
- Forcibly delete all nodes with the #testing tag:

In [ ]:
# Make and tag some nodes
q = '[inet:dns:a=(woowoo.com,1.2.3.4)]'
q1 = '[inet:fqdn=woowoo.com inet:fqdn=hurr.com inet:fqdn=derp.com +#testing]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)
podes = await core.eval(q1, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = '#testing | delnode --force'
print(q)
# Execute the query and test
podes = await core.eval(q, num=0, cmdr=False)
**Usage Notes:** - ``delnode`` operates on the output of a previous Storm query. - ``delnode`` will attempt to perform some basic sanity-checking to help prevent egregious mistakes. For example, ``delnode`` will return an error if you attempt to delete a node that is still referenced by another node (such as an ``inet:fqdn`` that is referenced by an ``inet:dns:a`` node). Similarly, delnode will return an error if you attempt to delete a ``syn:tag`` node if that tag is still applied to other nodes. **However, delnode cannot prevent all mistakes.** - Edge / digraph nodes (such as ``edge:refs`` or ``edge:has``) are **exempt** from these "reference checks". It is possible to delete an edge node where ``n1`` and / or ``n2`` still exist and without affecting ``n1`` or ``n2``. This is generally desired behavior as the relationship between the nodes (the edge) may need to be removed without affecting the nodes themselves. - However, the opposite is also true - since ``delnode`` does not check for references to edge nodes when deleting nodes, it is possible leave "orphaned" edge nodes with a missing ``n1`` or ``n2``. - The ``--force`` parameter will forcibly delete the nodes input to the command, regardless of any sanity-checking errors or other conditions. **This parameter should be used with extreme caution as it may result in broken references within the Cortex.**
.. _storm-dmon: dmon ---- Storm includes ``dmon.*`` commands that allow you work with daemons (see :ref:`gloss-daemon`). - `dmon.list`_ Help for individual ``dmon.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-dmon-list: dmon.list +++++++++ The ``dmon.list`` command displays the set of running dmon queries in the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'dmon.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-feed: feed ---- Storm includes ``feed.*`` commands that allow you work with feeds (see :ref:`gloss-feed`). - `feed.list`_ Help for individual ``feed.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-feed-list: feed.list +++++++++ The ``feed.list`` command displays available feed functions in the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'feed.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-graph: graph ----- The ``graph`` command generates a subgraph based on a specified set of nodes and parameters. **Syntax:**

In [ ]:
# Run the command and display output
q = 'graph --help'
podes = await core.eval(q, cmdr=True)
.. _storm-iden: iden ---- The ``iden`` command lifts one or more nodes by their node identifier (node ID / iden). **Syntax:**

In [ ]:
# Run the command and display output
q = 'iden --help'
podes = await core.eval(q, cmdr=True)
**Example:** - Lift the node with node ID d7fb3ae625e295c9279c034f5d91a7ad9132c79a9c2b16eecffc8d1609d75849:

In [ ]:
# Define and print test query
q = 'iden d7fb3ae625e295c9279c034f5d91a7ad9132c79a9c2b16eecffc8d1609d75849'
print(q)
# Execute the query and test
podes = await core.eval(q, num=1, cmdr=False)
**Usage Notes:** - The node ID (iden) for a given node can be obtained by lifting the node using the ``--debug`` or ``--raw`` option to the :ref:`syn-storm` command: - ``storm --debug inet:fqdn=woot.com``
.. _storm-layer: layer ----- Storm includes ``layer.*`` commands that allow you work with layers (see :ref:`gloss-layer`). - `layer.add`_ - `layer.set`_ - `layer.get`_ - `layer.list`_ - `layer.del`_ Help for individual ``layer.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-layer-add: layer.add +++++++++ The ``layer.add`` command adds a layer to the Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'layer.add --help'
podes = await core.eval(q, cmdr=True)
.. _storm-layer-set: layer.set +++++++++ The ``layer.set`` command sets an option for the specified layer. **Syntax**

In [ ]:
# Run the command and display output
q = 'layer.set --help'
podes = await core.eval(q, cmdr=True)
.. _storm-layer-get: layer.get +++++++++ The ``layer.get`` command retrieves the specified layer from a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'layer.get --help'
podes = await core.eval(q, cmdr=True)
.. _storm-layer-list: layer.list ++++++++++ The ``layer.list`` command lists the available layers in a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'layer.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-layer-del: layer.del +++++++++ The ``layer.del`` command deletes a layer from a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'layer.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-limit: limit ----- The ``limit`` command restricts the number of nodes returned from a given Storm query to the specified number of nodes. **Syntax:**

In [ ]:
# Run the command and display output
q = 'limit --help'
podes = await core.eval(q, cmdr=True)
**Example:** - Lift ten IP address nodes:

In [ ]:
# Make some nodes
q = '[inet:ipv4=192.168.0.0/24]'
# Run the query and test
podes = await core.eval(q, num=256, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:ipv4 | limit 10'
print(q)
# Execute the query and test
podes = await core.eval(q, num=10, cmdr=False)
**Usage Notes:** - If the limit number specified (i.e., ``limit 100``) is greater than the total number of nodes returned from the Storm query, no limit will be applied to the resultant nodes (i.e., all nodes will be returned). - By design, ``limit`` imposes an artificial limit on the nodes returned by a query, which may impair effective analysis of data by restricting results. As such, ``limit`` is most useful for viewing a subset of a large result set or an exemplar node for a given form. - While ``limit`` returns a sampling of nodes, it is not statistically random for the purposes of population sampling for algorithmic use.
.. _storm-macro: macro ----- Storm includes ``macro.*`` commands that allow you work with macros (see :ref:`gloss-macro`). - `macro.list`_ - `macro.set`_ - `macro.get`_ - `macro.exec`_ - `macro.del`_ Help for individual ``macro.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-macro-list: macro.list ++++++++++ The ``macro.list`` command lists the macros in a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'macro.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-macro-set: macro.set +++++++++ The ``macro.set`` command creates (or modifies) a macro in a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'macro.set --help'
podes = await core.eval(q, cmdr=True)
.. _storm-macro-get: macro.get +++++++++ The ``macro.get`` command retrieves and displays the specified macro. **Syntax**

In [ ]:
# Run the command and display output
q = 'macro.get --help'
podes = await core.eval(q, cmdr=True)
.. _storm-macro-exec: macro.exec ++++++++++ The ``macro.exec`` command executes the specified macro. **Syntax**

In [ ]:
# Run the command and display output
q = 'macro.exec --help'
podes = await core.eval(q, cmdr=True)
.. _storm-macro-del: macro.del +++++++++ The ``macro.del`` command deletes the specified macro from a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'macro.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-max: max --- The ``max`` command returns the node from a given set that contains the highest value for a specified secondary property, tag interval, or variable. **Syntax:**

In [ ]:
# Run the command and display output
q = 'max --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Return the DNS A record for woot.com with the most recent ``.seen`` value:

In [ ]:
# Make some DNS A nodes
q = '[inet:dns:a=(woot.com,107.21.53.159) .seen=(2014/08/13,2014/08/14)]'
q1 = '[inet:dns:a=(woot.com,75.101.146.4) .seen=(2013/09/21,2013/09/22)]'
q2 = '[inet:dns:a=(woot.com,52.206.255.234) .seen=(2018/01/23,2018/01/24)]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)
podes = await core.eval(q1, num=1, cmdr=False)
podes = await core.eval(q2, num=1, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:dns:a:fqdn=woot.com | max .seen'
print(q)
# Execute the query and test
podes = await core.eval(q, num=1, cmdr=False)
max_seen =(1516665600000, 1516752000000)
assert max_seen == podes[0][1].get('props', {}).get('.seen')
- Return the most recent WHOIS record for domain woot.com:

In [ ]:
# Make some WHOIS records
q = '[inet:whois:rec=(woot.com,2018/05/22) :text="domain name: woot.com"]'
q1 = '[inet:whois:rec=(woot.com,2018/01/17) :text="domain name: woot.com"]'
q2 = '[inet:whois:rec=(woot.com,2018/03/30) :text="domain name: woot.com"]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)
podes = await core.eval(q1, num=1, cmdr=False)
podes = await core.eval(q2, num=1, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:whois:rec:fqdn=woot.com | max :asof'
print(q)
# Execute the query and test
podes = await core.eval(q, num=1, cmdr=False)
max_asof = 1526947200000
assert max_asof == podes[0][1].get('props', {}).get('asof')
.. _storm-min: min --- The ``min`` command returns the node from a given set that contains the lowest value for a specified secondary property, tag interval, or variable. **Syntax:**

In [ ]:
# Run the command and display output
q = 'min --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Return the DNS A record for woot.com with the oldest ``.seen`` value:

In [ ]:
# Define and print test query
q = 'inet:dns:a:fqdn=woot.com | min .seen'
print(q)
# Execute the query and test
podes = await core.eval(q, num=1, cmdr=False)
min_seen = (1379721600000, 1379808000000)
assert min_seen  == podes[0][1].get('props', {}).get('.seen')
- Return the oldest WHOIS record for domain woot.com:

In [ ]:
# Define and print test query
q = 'inet:whois:rec:fqdn=woot.com | min :asof'
print(q)
# Execute the query and test
podes = await core.eval(q, num=1, cmdr=False)
min_asof = 1516147200000
assert min_asof  == podes[0][1].get('props', {}).get('asof')
.. _storm-model: model ----- Storm includes ``model.*`` commands (specifically ``model.edge.*``) that allow you work with lightweight (light) edges (see :ref:`gloss-edge-light`). - `model.edge.list`_ - `model.edge.set`_ - `model.edge.get`_ - `model.edge.del`_ Help for individual ``model.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-model-edge-list: model.edge.list +++++++++++++++ The ``model.edge.list`` command displays the set of light edges currently defined in the Cortex and any ``doc`` values they have set on them. **Syntax:**

In [ ]:
# Run the command and display output
q = 'model.edge.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-model-edge-set: model.edge.set ++++++++++++++ The ``model.edge.set`` command allows you to set the value of an given key on a light edge (such as a ``doc`` value to specify a definition for the light edge). The current list of valid keys include the following: - ``doc`` **Syntax:**

In [ ]:
# Run the command and display output
q = 'model.edge.set --help'
podes = await core.eval(q, cmdr=True)
.. _storm-model-edge-get: model.edge.get ++++++++++++++ The ``model.edge.get`` command allows you to retrieve all of the keys that have been set on a light edge. **Syntax:**

In [ ]:
# Run the command and display output
q = 'model.edge.get --help'
podes = await core.eval(q, cmdr=True)
.. _storm-model-edge-del: model.edge.del ++++++++++++++ The ``model.edge.del`` command allows you to delete the key from a light edge (such as a ``doc`` property to specify a definition for the light edge). Deleting an key from a specific light edge does not delete the key from Synapse (e.g., the property can be re-added to the light edge or to other light edges). **Syntax:**

In [ ]:
# Run the command and display output
q = 'model.edge.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-movetag: movetag ------- The ``movetag`` command moves a Synapse tag and its associated tag tree from one location in a tag hierarchy to another location. It is equivalent to "renaming" a given tag and all of its subtags. Moving a tag consists of: - Creating the new ``syn:tag`` node(s). - Copying the definitions (``:title`` and ``:doc`` properties) from the old ``syn:tag`` node to the new ``syn:tag`` node. - Applying the new tag(s) to the nodes with the old tag(s). - If the old tag(s) have associated timestamps / time intervals, they will be applied to the new tag(s). - Deleting the old tag(s) from the nodes. - Setting the ``:isnow`` property of the old ``syn:tag`` node(s) to reference the new ``syn:tag`` node. - The old ``syn:tag`` nodes are **not** deleted. - Once the ``:isnow`` property is set, attempts to apply the old tag will automatically result in the new tag being applied. **Syntax:**

In [ ]:
# Run the command and display output
q = 'movetag --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Move the tag named #research to #internal.research:

In [ ]:
# Make some tagged nodes
q = '[inet:fqdn=hurr.com inet:fqdn=derp.com inet:fqdn=umwut.com +#research]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = 'movetag research internal.research'
print(q)
# Execute the query and test
podes = await core.eval(q, num=0, cmdr=False)
podes = await core.eval('#research', num=0, cmdr=False)
podes = await core.eval('#internal.research', num=3, cmdr=False)
- Move the tag tree #aka.fireeye.malware to #aka.feye.mal:

In [ ]:
# Make some tagged nodes
q = '[inet:fqdn=newsonet.net inet:fqdn=staycools.net inet:fqdn=firefoxupdata.com +#aka.fireeye.malware]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = 'movetag aka.fireeye.malware aka.feye.mal'
print(q)
# Execute the query and test
podes = await core.eval(q, num=0, cmdr=False)
podes = await core.eval('#aka', num=3, cmdr=False)
podes = await core.eval('#aka.fireeye', num=3, cmdr=False)
podes = await core.eval('#aka.fireeye.malware', num=0, cmdr=False)
podes = await core.eval('#aka.feye', num=3, cmdr=False)
podes = await core.eval('#aka.feye.mal', num=3, cmdr=False)
**Usage Notes:** .. WARNING:: ``movetag`` should be used with caution as when used incorrectly it can result in "deleted" (inadvertently moved / removed) or orphaned (inadvertently retained) tags. For example, in the second example query above, all ``aka.fireeye.malware`` tags are renamed ``aka.feye.mal``, but the tag ``aka.fireeye`` still exists and is still applied to all of the original nodes. In other words, the result of the above command will be that nodes previously tagged ``aka.fireeye.malware`` will now be tagged both ``aka.feye.mal`` **and** ``aka.fireeye``. Users may wish to test the command on sample data first to understand its effects before applying it in a live Cortex.
.. _storm-pkg: package ------- Storm includes ``pkg.*`` commands that allow you work with Storm packages (see :ref:`gloss-package`). - `pkg.list`_ - `pkg.del`_ Help for individual ``pkg.*`` commands can be displayed using: ``storm --help | -h`` Packages typically contain extended Storm commands and Storm library code used to implement a Storm :ref:`gloss-service`.
.. _storm-pkg-list: pkg.list ++++++++ The ``pkg.list`` command lists each Storm package loaded in the Cortex. Output is displayed in tabluar form and includes the package name and version information. **Syntax:**

In [ ]:
# Run the command and display output
q = 'pkg.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-pkg-del: pkg.del +++++++ The ``pkg.del`` command removes a Storm package from the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'pkg.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-queue: queue ----- Storm includes ``queue.*`` commands that allow you work with queues (see :ref:`gloss-queue`). - `queue.add`_ - `queue.list`_ - `queue.del`_ Help for individual ``queue.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-queue-add: queue.add +++++++++ The ``queue.add`` command adds a queue to the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'queue.add --help'
podes = await core.eval(q, cmdr=True)
.. _storm-queue-list: queue.list ++++++++++ The ``queue.list`` command lists each queue in the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'queue.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-queue-del: queue.del +++++++++ The ``queue.del`` command removes a queue from the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'queue.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-reindex: reindex ------- The ``reindex`` command reindexes a given node property. This is an administrative command that is typically used when data model updates have been pushed to a Cortex and existing node properties must be migrated to the new model. **Syntax:**

In [ ]:
# Run the command and display output
q = 'reindex --help'
podes = await core.eval(q, cmdr=True)
.. _storm-scrape: scrape ------ The ``scrape`` command parses one or more secondary properties or variables of the inbound node(s), attempts to identify ("scrape") common forms from the content, and returns nodes for the identified forms (creating the nodes if they do not already exist). This is useful (for example) to extract forms such as email addresses, domains, URLs, hashes, etc. from unstructured text. ``scrape`` can optionally link the source nodes(s) to the scraped forms via ``refs`` light edges. By default, the scrape command will yield the nodes that it got as input to allow command chaining, however if executed with the option ``--yield`` the ``scrape`` command will yield the scraped nodes rather than the input nodes.

In [ ]:
# Run the command and display output
q = 'scrape --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Scrape common forms and create nodes from the body of domain WHOIS record(s) for domain woot.com:

In [ ]:
# Make a whois record node
q = '[inet:whois:rec=(woot.com,20191216)  :created=20000112 :updated ="20190507" :expires=20240112 :registrant="woot, inc." :registrar="markmonitor inc." :text="Here is some text that contains an email address abusecomplaints@markmonitor.com and a url http://www.markmonitor.com"]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:whois:rec:fqdn=woot.com | scrape :text'
print(q)
# Execute the query and test
podes = await core.eval(q, num=4, cmdr=False)
- Scrape common forms from the content of a set of Internet posts from a given account, link the created nodes to the original posts, and uniq the results:

In [ ]:
# Make a node for a Twitter post
q = '[inet:web:post=((twitter.com,finley1589),20160918105200) :acct=(twitter.com,finley1589) :time=20160918105200 :url=http://twitter.com/finley1589/statuses/777460180225261568 :text="THOSE THAT LOVE THE USA PRAY! https://t.co/q5EPZrxGKk RT #STOPIslam #TCOT #CCOT #MakeDCListen #TeaParty #Conservatives"]'
# Run the query and test
podes = await core.eval(q, num=1, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:web:post:acct=(twitter.com,finley1589) | scrape :text --refs --yield | uniq'
print(q)
# Execute the query and test
podes = await core.eval(q, num=2, cmdr=False)
**Usage Notes:** - If no properties or variables are specified, ``scrape`` will attempt to scrape all properties by default. - If any scraped secondary properties are already recognizable / common properties, those properties will also be scraped (and optionally linked, if ``--refs`` is also specified). That is, scraping the ``:md5`` secondary property of a node will return a ``hash:md5`` node with that property's value. - ``scrape`` will only scrape node properties; it will not scrape the content of any files that may be referenced by those properties. For example, attempting to scrape the ``:file`` property of a ``media:news`` node will scrape and extract the **value** of the property (typically a SHA256 hash) if present; it will not extract indicators from the referenced file itself. - With respect to cyber threat data in particular, ``scrape`` cannot identify and extract "defanged" values such as "hxxp://" for URLs or "woot[.]com" for domains.
.. _storm-service: service ------- Storm includes ``service.*`` commands that allow you work with Storm services (see :ref:`gloss-service`). - `service.add`_ - `service.list`_ - `service.del`_ Help for individual ``service.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-service-add: service.add +++++++++++ The ``service.add`` command adds a Storm service to the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'service.add --help'
podes = await core.eval(q, cmdr=True)
.. _storm-service-list: service.list ++++++++++++ The ``service.list`` command lists each Storm service in the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'service.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-service-del: service.del +++++++++++ The ``service.del`` command removes a Storm service from the Cortex. **Syntax:**

In [ ]:
# Run the command and display output
q = 'service.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-sleep: sleep ----- The ``sleep`` command adds a delay in returning each result for a given Storm query. By default, query results are streamed back and displayed as soon as they arrive for optimal performance. A ``sleep`` delay effectively slows the display of results. **Syntax:**

In [ ]:
# Run the command and display output
q = 'sleep --help'
podes = await core.eval(q, cmdr=True)
**Example:** - Retrieve domain nodes from a Cortex every second:

In [ ]:
# Make some nodes
q = '[inet:email=me@gmail.com inet:email=you@yahoo.com inet:email=him@live.com inet:email=her@gmx.com]'
# Run the query and test
podes = await core.eval(q, num=4, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:email | sleep 1.0'
print(q)
# Execute the query and test
podes = await core.eval(q, num=5, cmdr=False)
.. _storm-spin: spin ---- The ``spin`` command is used to suppress the output of a Storm query. ``Spin`` simply consumes all nodes sent to the command, so no nodes are output to the CLI. This allows you to execute a Storm query and view messages and results without displaying the associated nodes. **Syntax:**

In [ ]:
# Run the command and display output
q = 'spin --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Count the number of email addresses without displaying the inet:email nodes:

In [ ]:
# Define and print test query
q = 'inet:email | count | spin'
print(q)
# Execute the query and test
# This may not be a good test because it only checks that 'spin' returns zero nodes
podes = await core.eval(q, num=0, cmdr=False)
podes = await core.eval('inet:email | count', num=5, cmdr=False)
- Add the tag #int.research to any domain containing the string "firefox" but do not display the nodes.

In [ ]:
# Make some domains
q = '[inet:fqdn=myfirefox.com inet:fqdn=fakefirefox.net inet:fqdn=usefirefoxbrowser.org]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:fqdn~=firefox [+#int.research] | spin'
print(q)
# Execute the query and test
podes = await core.eval(q, num=0, cmdr=False)
podes = await core.eval('inet:fqdn~=firefox', num=4, cmdr=False)
.. _storm-splice: splice ------ Storm includes ``splice.*`` commands that allow you work with splices (see :ref:`gloss-splice`). - `splice.list`_ - `splice.undo`_ Splices are represented as **runtime nodes** ("runt nodes" - see :ref:`gloss-node-runt`) of the form ``syn:splice``. These runt nodes can be lifted and filtered just like standard nodes in a Cortex. Help for individual ``splice.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-splice-list: splice.list +++++++++++ The ``splice.list`` command allows you to list (view) splices in the splice log. By default, splices are displayed starting with the most recent and working backwards through the log. **Syntax**

In [ ]:
# Run the command and display output
q = 'splice.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-splice-undo: splice.undo +++++++++++ The ``splice.undo`` command allows a user with appropriate permissions to roll back or undo the specified set of splices (changes). **Syntax**

In [ ]:
# Run the command and display output
q = 'splice.undo --help'
podes = await core.eval(q, cmdr=True)
.. _storm-tee: tee --- The ``tee`` command executes multiple Storm queries on the inbound nodes and returns the combined result set. **Syntax:**

In [ ]:
# Run the command and display output
q = 'tee --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Return the set of domains and IP addresses associated with a set of DNS A records.

In [ ]:
# Make some DNS A records
q = '[inet:dns:a=(foo.mydomain.com,8.8.8.8) inet:dns:a=(bar.mydomain.com,34.56.78.90) inet:dns:a=(baz.mydomain.com,127.0.0.2)]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:fqdn:zone=mydomain.com -> inet:dns:a | tee { -> inet:fqdn } { -> inet:ipv4 }'
print(q)
# Execute the query and test
podes = await core.eval(q, num=6, cmdr=False)
- Return the set of domains and IP addresses associated with a set of DNS A records along with the original DNS A records.

In [ ]:
# Define and print test query
q = 'inet:fqdn:zone=mydomain.com -> inet:dns:a | tee --join { -> inet:fqdn } { -> inet:ipv4 }'
print(q)
# Execute the query and test
podes = await core.eval(q, num=9, cmdr=False)
**Usage Notes:** - ``tee`` can take an arbitrary number of Storm queries (i.e., 1 to n queries) as arguments.
.. _storm-tree: tree ---- The ``tree`` command recursively performs the specified pivot until no additional nodes are returned. **Syntax:**

In [ ]:
# Run the command and display output
q = 'tree --help'
podes = await core.eval(q, cmdr=True)
**Example:** - List the full set of tags in the "TTP" tag hierarchy.

In [ ]:
# Make some tags
q = '[syn:tag=ttp.net.c2.proto.dns syn:tag=ttp.se.masq.microsoft syn:tag=ttp.opsec.anon]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)

In [ ]:
# Define and print test query
q = 'syn:tag=ttp | tree { $node.value() -> syn:tag:up }'
print(q)
# Execute the query and test
podes = await core.eval(q, num=10, cmdr=False)
**Usage Notes:** - ``tree`` is useful for "walking" a set of properties with a single command vs. performing an arbitrary nubmer of pivots until the end of the data is reached.
.. _storm-trigger: trigger ------- Storm includes ``trigger.*`` commands that allow you to create automated event-driven triggers (see :ref:`gloss-trigger`) using the Storm query syntax. - `trigger.add`_ - `trigger.list`_ - `trigger.mod`_ - `trigger.disable`_ - `trigger.enable`_ - `trigger.del`_ Help for individual ``trigger.*`` commands can be displayed using: ``storm --help | -h`` Triggers are added to the Cortex as **runtime nodes** ("runt nodes" - see :ref:`gloss-node-runt`) of the form ``syn:trigger``. These runt nodes can be lifted and filtered just like standard nodes in a Cortex. See the :ref:`storm-ref-automation` document for additional information. .. NOTE:: The Storm ``trigger.*`` commands replace the Synapse cmdr-based :ref:`syn-trigger` command, which is being deprecated.
.. _storm-trigger-add: trigger.add +++++++++++ The ``trigger.add`` command adds a trigger to a Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.add --help'
podes = await core.eval(q, cmdr=True)
**Example** Create a trigger so that when the tag ``#foo`` is added to an ``inet:fqdn`` node, the tag ``#bar`` is added as well.

In [ ]:
# Define query
q = 'trigger.add tag:add --form inet:fqdn --tag foo --query { [ +#bar ] }'
# Execute the query
podes = await core.eval(q, cmdr=True)
.. _storm-trigger-list: trigger.list ++++++++++++ The ``trigger-list`` command displays the set of triggers in the Cortex that the current user can view / modify based on their permissions. Triggers are displayed at the cmdr CLI in tabular format, with columns including the user who created the trigger, the :ref:`gloss-iden` of the trigger, the condition that fires the trigger (i.e., ``node:add``), and the Storm query associated with the trigger. Triggers are displayed in alphanumeric order by iden. Triggers are sorted upon Cortex initialization, so newly-created triggers will be displayed at the bottom of the list until the list is re-sorted the next time the Cortex is restarted. .. NOTE:: Triggers can also be viewed in runt node form as ``syn:trigger`` nodes. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.list --help'
podes = await core.eval(q, cmdr=True)
**Examples** *trigger.list* List triggers in a Cortex.

In [ ]:
# Define query
q = 'trigger.list'
# Execute the query
podes = await core.eval(q, cmdr=True)
**Notes:** The output of ``trigger.list`` contains the following columns: - The username used to create the trigger. - The first eight characters of the trigger's identifier (iden). - Whether the trigger is currently enabled or disabled. - The condition that causes the trigger to fire. - The object that the condition operates on, if any. - The tag or tag expression used by the condition (for ``tag:add`` or ``tag:del`` conditions). - The query to be executed when the trigger fires.
*triggers as runt nodes* List triggers that fire on ``tag:add`` events by lifting ``syn:trigger`` nodes.

In [ ]:
# Define query
q = 'syn:trigger:cond=tag:add'
# Execute the query
podes = await core.eval(q, cmdr=True)
**Notes:** While runt nodes (:ref:`gloss-node-runt`) are typically read-only, ``syn:trigger`` nodes include ``:name`` and ``:doc`` secondary properties that can be set and modified via Storm. This allows management of triggers by providing them with meaningful names / categoreies and / or descriptions of their purpose. Changes to these properties will persist even after a Cortex restart.
.. _storm-trigger-mod: trigger.mod +++++++++++ The ``trigger.mod`` command modifies the Storm query associated with a specific trigger. To modify a trigger, you must provide the first portion of the trigger's iden (i.e., enough of the iden that the trigger can be uniquely identified), which can be obtained using ``trigger.list`` or by lifting the appropriate ``syn:trigger`` node. .. NOTE:: Other aspects of the trigger, such as the condition used to fire the trigger or the tag or property associated with the trigger, cannot be modified once the trigger has been created. To change these aspects, you must delete and re-add the trigger. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.mod --help'
podes = await core.eval(q, cmdr=True)
.. _storm-trigger-disable: trigger.disable +++++++++++++++ The ``trigger.disable`` command disables a trigger and prevents it from firing without removing it from the Cortex. To disable a trigger, you must provide the first portion of the trigger's iden (i.e., enough of the iden that the trigger can be uniquely identified), which can be obtained using ``trigger.list`` or by lifting the appropriate ``syn:trigger`` node. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.disable --help'
podes = await core.eval(q, cmdr=True)
.. _storm-trigger-enable: trigger.enable ++++++++++++++ The ``trigger-enable`` command enables a disabled trigger. To enable a trigger, you must provide the first portion of the trigger's iden (i.e., enough of the iden that the trigger can be uniquely identified), which can be obtained using ``trigger.list`` or by lifting the appropriate ``syn:trigger`` node. .. NOTE:: Triggers are enabled by default upon creation. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.enable --help'
podes = await core.eval(q, cmdr=True)
.. _storm-trigger-del: trigger.del +++++++++++ The ``trigger.del`` command permanently removes a trigger from the Cortex. To delete a trigger, you must provide the first portion of the trigger's iden (i.e., enough of the iden that the trigger can be uniquely identified), which can be obtained using ``trigger.list`` or by lifting the appropriate ``syn:trigger`` node. **Syntax**

In [ ]:
# Run the command and display output
q = 'trigger.del --help'
podes = await core.eval(q, cmdr=True)
.. _storm-uniq: uniq ---- The ``uniq`` command removes duplicate results from a Storm query. Results are uniqued based on each node's node identifier (node ID / iden) so that only the first node with a given node ID is returned. **Syntax:**

In [ ]:
# Run the command and display output
q = 'uniq --help'
podes = await core.eval(q, cmdr=True)
**Examples:** - Lift all of the unique IP addresses that domains associated with the Fancy Bear threat group have resolved to:

In [ ]:
# Make some tagged nodes and A records
q = '[inet:fqdn=autoupdater.org inet:fqdn=actblues.com inet:fqdn=euronews24.info +#aka.threatconnect.thr.fancybear]'
q1 = '[inet:dns:a=(autoupdater.org,1.2.3.4) inet:dns:a=(autoupdater.org,5.6.7.8) inet:dns:a=(actblues.com,5.6.7.8) inet:dns:a=(euronews24.info,1.2.3.4) inet:dns:a=(euronews24.info,8.8.8.8) inet:dns:a=(euronews24.info,255.255.255.254)]'
# Run the query and test
podes = await core.eval(q, num=3, cmdr=False)
podes = await core.eval(q1, num=6, cmdr=False)

In [ ]:
# Define and print test query
q = 'inet:fqdn#aka.threatconnect.thr.fancybear -> inet:dns:a -> inet:ipv4 | uniq'
print(q)
# Execute the query and test
podes = await core.eval(q, num=4, cmdr=False)
q = 'inet:fqdn#aka.threatconnect.thr.fancybear -> inet:dns:a -> inet:ipv4'
podes = await core.eval(q, num=6, cmdr=False)
.. _storm-view: view ---- Storm includes ``view.*`` commands that allow you work with views (see :ref:`gloss-view`). - `view.add`_ - `view.fork`_ - `view.get`_ - `view.list`_ - `view.merge`_ - `view.del`_ Help for individual ``view.*`` commands can be displayed using: ``storm --help | -h``
.. _storm-view-add: view.add ++++++++ The ``view.add`` command adds a view to the Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.add --help'
podes = await core.eval(q, cmdr=True)
.. _storm-view-fork: view.fork +++++++++ The ``view.fork`` command forks an existing view from the Cortex. Forking a view creates a new view with a new writeable layer on top of the set of layers from the previous (forked) view. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.fork --help'
podes = await core.eval(q, cmdr=True)
.. _storm-view-get: view.get ++++++++ The ``view.get`` command retrieves an existing view from the Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.get --help'
podes = await core.eval(q, cmdr=True)
.. _storm-view-list: view.list +++++++++ The ``view.list`` command lists the views in the Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.list --help'
podes = await core.eval(q, cmdr=True)
.. _storm-view-merge: view.merge ++++++++++ The ``view.merge`` command merges data from a forked view into its parent view. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.merge --help'
podes = await core.eval(q, cmdr=True)
.. _storm-view-del: view.del ++++++++ The ``view.del`` command permanently deletes a view from the Cortex. **Syntax**

In [ ]:
# Run the command and display output
q = 'view.del --help'
podes = await core.eval(q, cmdr=True)

In [ ]:
# Close cortex because done
_ = await core.fini()