Storm Reference - Advanced - Control Flow

Storm includes a number of common programming control flow structures to facilitate more advanced or complex Storm queries. These include:

The examples below are for illustrative purposes. This guide is not meant as a Storm programming tutorial, but to introduce Storm users who may not be familiar with programming concepts to possible use cases for these structures.

See also the follwing User Guide and reference sections for additional information:

It may also be helpful to review the general Storm Operating Concepts and keep them in mind for control flow operations.

If-Else Statement

An if-else statement matches inbound objects against a specified condition. If that condition is met, a set of Storm operations are performed. If the condition is not met, a different set of Storm operations are performed.

Syntax:

if <condition> { <storm> }
else { <storm> }

Example:

You have a subscription to a third-party malware service that allows you to download malware binaries via the service’s API. However, the service has a query limit, so you don’t want to make any unnecessary API requests that might exhaust your limit.

You can use a simple if-else statement to check whether you already have a copy of the binary in your storage Axon before attempting to download it.

file:bytes=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
$sha256 = :sha256

if $lib.bytes.has($sha256) { }

else { | malware.download }

The Storm query above:

  • takes an inbound file:bytes node;
  • sets the variable $sha256 to the file’s SHA256 hash;
  • checks for the file in the Axon ($lib.bytes.has(sha256));
  • if $lib.bytes.has($sha256) returns true (i.e., we have the file), do nothing ({  });
  • otherwise call the malware.download service to attempt to download the file.

Note: malware.download is used as an example Storm service name only, it does not exist in the base Synapse code.

Switch Statement

A switch statement matches inbound objects against a set of specified constants. Depending on which constant is matched, a set of Storm operations is performed. The switch statement can include an optional default case to perform a set of Storm operations in the case where none of the explicitly defined constants are matched.

Syntax:

<inbound objects>

switch <constant> {

  <case1>: { <storm> }
  <case2>: { <storm> }
  <case3>: { <storm> }
  *: { <storm for optional default case> }
}

Example:

You want to write a macro (see Macros) to automatically enrich a set of indicators (i.e., query third-party data sources for additional data). Instead of writing separate macros for each type of indicator, you want a single macro that can take any type of indicator and send it to the appropriate Storm services.

A switch statement can send your indicators to the correct services based on the kind of node (e.g., the node’s form).

<inbound nodes>

switch $node.form() {

    "hash:md5": { | malware.service }

    "hash:sha1": { | malware.service }

    "hash:sha256": { | malware.service }

    "inet:fqdn": { | pdns.service | whois.service }

    "inet:ipv4": { | pdns.service }

    "inet:email": { | whois.service }

    *: { $lib.print("{form} is not supported.", form=$node.form()) }
}

The Storm query above:

  • takes a set of inbound nodes;
  • checks the switch conditions based on the form of the node (see $node.form());
  • matches the form name against the list of forms;
  • handles each form differently (e.g., hashes are submitted to a malware service, domains are submitted to passive DNS and whois services, etc.)
  • if the inbound form does not match any of the specified cases, print ($lib.print(mesg, **kwargs)) the specified statement (e.g., "file:bytes is not supported.").

The default case above is not strictly necessary - any inbound nodes that fail to match a condition will simply pass through the switch statement with no action taken. It is used above to illustrate the use of a default case for any non-matching nodes.

Note: the Storm service names used above are examples only. Services with those names do not exist in the base Synapse code.

For Loop

A for loop will iterate over a set of objects, performing the specified Storm operations on each object in the set.

Syntax:

for $<var> in $<vars> {

    <storm>
}

Note: The user documentation for the Synapse csvtool (Synapse Tools - csvtool) includes additional examples of using a for loop to iterate over the rows of a CSV-formatted file (i.e., for $row in $rows { <storm> }).

Example:

You are writing a “threat hunting” macro (see Macros) that:

  • lifts FQDN nodes associated with various threat clusters (e.g., tagged threat.<cluster_name>);
  • pivots to DNS request nodes (inet:dns:request) that:
    • query the FQDN (:query:name:fqdn), and
    • have an associated file (:exe) (that is, the DNS query was made by a file)
  • pivots to the file node(s) (file:bytes)
  • tags the file(s) for review to see if they should be added to the threat cluster.

This may help to identify previously unknown binaries that query for known malicious FQDNs.

The macro can be run periodically to search for new threat indicators, based on nodes that may have been recently added to the Cortex.

Because your macro may flag a large number of files associated with a broad range of threat clusters, you want your #review tag to include the name of the potentially associated threat cluster to help with the review process.

You can use a for loop to iterate over the threat cluster tags on the inbound nodes and parse those tags to create equivalent review tags for each threat cluster.

for $tag in $node.tags(threat.*) {

   $threat = $tag.split(".").index(1)
   $reviewtag = $lib.str.format("review.{threat}", threat=$threat)
}

For each inbound node, the for loop:

  • takes each tag on the inbound node (see $node.tags()) that matches the specified pattern (threat.*);
  • splits the tag along the dot (.) separator (split(text)), creating a list of elements;
  • takes the element located at index 1 (i.e., the second element in the tag, since we count from zero) (index(valu)) and assigns it to the variable $threat;
  • formats a string representing the new “review” tag, using the value of the $threat variable ($lib.str.format(text, **kwargs)).

In other words, if an inbound node has the tag #threat.viciouswombat, the for loop will:

  • split the tag into elements threat and viciouswombat;
  • take the element at index 1 (viciouswombat);
  • set $threat = viciouswombat
  • set $reviewtag = review.viciouswombat.

The for loop can be incorporated into your larger “threat hunting” macro to identify files that query for known malicious FQDNs:

inet:fqdn#threat

for $tag in $node.tags(threat.*) {

    $threat = $tag.split(".").index(1)
    $reviewtag = $lib.str.format("review.{threat}", threat=$threat)
}

-> inet:dns:request +:exe -> file:bytes [ +#$reviewtag ]

Note

A for loop will iterate over “all the things” as defined by the for loop syntax. In the example above, a single inbound node may have multiple tags that match the pattern defined by the for loop. This means that the for loop operations will execute once per matching tag per node and yield the inbound node to the pipeline for each iteration. In other words, for each inbound node:

  • the first matching tag enters the for loop;
  • the loop operations are performed on that tag (i.e., the tag is split and an associated #review tag constructed);
  • the variable $reviewtag, which contains the newly-constructed tag, exits the for loop;
  • the FQDN node that was inbound to the for loop is subject to the post-loop pivot and filter operations;
  • if the operations are successful, the appropriate $reviewtag is applied to the node.
  • If there are additional tags to process from the inbound node, repeat these steps for each tag.

This means that if a single inbound FQDN node has the tags #threat.viciouswombat and #threat.spuriousunicorn, a file:bytes node that queries the FQDN will receive both #review.viciouswombat and #review.spuriousunicorn.

This is by design, and is the way Storm variables - specifically non-runtime safe variables (Non-Runtsafe) - and the Storm execution pipeline (see Storm Operating Concepts) are intended to work. (Of course, if your analysis has attributed a single FQDN to two different threat groups, you’d want to know about it.)

While Loop

A while loop checks inbound nodes against a specified condition and performs the specified Storm operations for as long as the condition is met.

Syntax:

while <condition> {

    <storm>
}

While loops are more frequently used for developer tasks, such as consuming from Queues; and are less common for day-to-day user use cases.