Skip to main content

1E SDK

Instruction flow control

Early versions of the Tachyon Agent Language (SCALE) were sequential, and deliberately did not include constructs for branching, but later versions introduced additional keywords for controlling instruction flow and termination:

  • EVALUATE - allows an instruction to terminate early if the preceding statement did not yield any results; it is in effect "continue if data available".

  • FOREACH - (new in v3.0.0) allows looping to iterate over each row within a @table and execute one or more statements per row

  • IF/ELSE/ENDIF- (new in v3.3) provides conditional branching

  • RETURN, NOCONTENT, ERROR, and NOTIMPLEMENTED - (new in v3.3) allows Early exit termination of the instruction.

Inconsistent branching results

The reason why SCALE did not originally support branching is because poor coding could return a different "shape" of data as its output. The following example works fine as it is because both the IF and ELSE statements contain the same number of columns, with the same column names, and can be output as the results of the instruction, or saved in a @table for processing in later statements.

Example - ensuring IF and ELSE statements have the same shape

@n =
Network.GetConnections();
@c = SELECT COUNT(1), 1 AS X FROM @n GROUP BY X HAVING COUNT(1)>10;
@results = IF (@c)
       SELECT Count(1) AS Connections FROM @n;
       -- SELECT * FROM @n;
       -- SELECT RemoteAddress, RemotePort FROM @n;
ELSE
       SELECT "Not many connections here" AS Connections;
ENDIF;

However, if instead the IF statement used either of the commented lines, then it would return a different number of columns to the ELSE statement, depending on the number of network connections detected. One would produce multiple columns such as "IpAddress", "Port" and the other a single column "Connections". You could only use one of these shapes as the instruction schema, and Tachyon would then be unable to process the different shape data because it does not match the instruction schema.

Note

TIMS does not detect inconsistent branching results (shape shifting). Extra attention to code testing is required when using IF/ELSE/ENDIF and Early exit statements, which is good practice anyway.

Using "EVALUATE" to break on no output

If an error occurs while the Agent executes the instruction, execution of the whole instruction will terminate and return corresponding error details back to the Tachyon Server. See Exit Codes for more details on how error status is reported.

There is no support for exception handling (e.g. TRY/CATCH), nor is there support for transactional logic (e.g. COMMIT/ROLLBACK).

EVALUATE is the simplest form of flow control, and allows you to optimize your instruction and improve its readability, by avoiding unnecessary execution of subsequent statements.

For example:

Example - breaking on no results

@connections = Network.GetConnections();
@https = SELECT * FROM @connections WHERE RemotePort = 443;

EVALUATE;

Scripting.Run(Language: "PowerShell", LanguageVersion: "3.0",
              Script: "AuditCertificates.ps1");

In this example, we execute a PowerShell script if (and only if) we detect that there are any HTTPS connections detected (port 443); otherwise execution stops at the EVALUATE statement, and the instruction returns the result of "Success with no data". The AuditCertificates.ps1 PowerShell script may itself contain logic to handle the case of no active HTTPS connections, but by explicitly including an EVALUATE keyword the overhead of launching PowerShell is avoided.

Note that using EVALUATE inside a FOREACH loop will exit the entire instruction if the previous statement had returned no data; it will not just exit the loop. This behaviour may change in a future version of Tachyon.

Looping with FOREACH

The FOREACH construct – like an iterator in C++, or an enumerator in .NET - allows you to repeat a block of one or more statements for each row in a @table, and optionally save in a @table the output of the last statement in the block.

Syntax

FOREACH @newTable IN @existingTable
DO
   //block of statements using @newTable (SELECT statements, Methods and/or Functions)
   //optional 'early exit' termination
DONE;

FOREACH does not have any conditional flow control (eg. WHILE or BREAK) but this may change in a future version of Tachyon. However, you can cause the instruction to terminate early using EVALUATE, RETURN, NOCONTENT, ERROR, and NOTIMPLEMENTED..

Here’s an example, which deletes any files bigger than 500k and also any ".tmp" files.

Example - looping with FOREACH

@files = FileSystem.GetFilesInFolder(Folder: "C:\\BigFiles");
@filesToDelete = SELECT FileName
                 FROM @files
                 WHERE FileSize > 500000
                 OR FileName LIKE "%.tmp";

EVALUATE;

FOREACH @f IN @filesToDelete
DO
   FileSystem.DeleteFileByName(FileName: @f.FileName);
DONE;

In this example, we build a @table containing the files we want to delete, and then, for each row in that @table, we create a new @table called "@f", and execute the code between the "DO" and "DONE" keywords. Since our new @table "@f" has exactly one row, we can pass its values as parameters to the DeleteFileByName method. In this way, we delete each of the selected files, one by one.

It is important to note what the "output" value of FOREACH is. The FOREACH statement will take each output of the inner block (in our example, the output of the call to DeleteFileByName) and combine the results together. The output of FOREACH can also be assigned to a @table if required.

For example, here is how we could get the SHA256 hash of each file in a folder:

Example - capturing the output of FOREACH

@files = FileSystem.GetFilesInFolder(Folder: "C:\\BigFiles");

@hashes = FOREACH @f IN @files
DO
   FileSystem.GetHash(FileName: @f.FileName, Algorithm: "SHA256");
DONE;

SELECT FileHash, FileName FROM @hashes;

With each successive call to the GetHash method, the output of the FOREACH statement is combined with the output of the method.

We could run multiple statements for each file, although this is less common. If multiple statements occur between the DO and DONE keywords, it is the output of the last of those statements which is merged into the overall result of the FOREACH statement.

Conditional branching using IF/ELSE/ENDIF

Allows conditional branching of instruction flow based on a condition.

The condition cannot be a SELECT statement, but can be one of the following:

  • A @table, such as "@myTable"

  • A method invocation, such as "Network.GetConnections()"

  • A literal, such as "1" (although use of this is uncommon)

In each of the above cases, the condition (which represents a table of data) will evaluate to "true" – and will therefore pass the IF check – if that table has one or more rows. A table with zero rows (including tables which have an undefined structure) will evaluate to "false".

The IF condition is followed by a block of code to execute if the condition evaluates to true, and then a closing ENDIF. You may optionally include an ELSE statement with a corresponding block of code to execute if the condition evaluates to false.

Simple branching

The following simple example runs a different executable depending on whether the target Agent is running on a 32- or 64-bit operating system.

@device = Device.GetSummary();
@x64 = SELECT * FROM @device WHERE OsArchitecture = "x64";
@results = IF (@x64)
    NativeServices.RunCommand(CommandLine: "my-64-bit-process.exe");
ELSE
    NativeServices.RunCommand(CommandLine: "my-32-bit-process.exe");
ENDIF;

Note that the expression to test – in preceding case @x64 – needs to be enclosed in parentheses.

In this case, because both the IF and ELSE statements run the same command, the results will have the same "shape" and can be saved in a @table for later processing.

As described earlier, the ELSE is optional – so the following example runs an executable if and only if the target Agent is running on a 64-bit operating system (but will do nothing on 32-bit operating systems).

@device = Device.GetSummary();
@x64 = SELECT * FROM @device WHERE OsArchitecture = "x64";

IF (@x64)
    NativeServices.RunCommand(CommandLine: "my-64-bit-process.exe");
ENDIF;

The body of an IF block (or an ELSE block) can contain multiple statements, such as in the following example:

@device = Device.GetSummary();
@x64 = SELECT * FROM @device WHERE OsArchitecture = "x64";

IF (@x64)
    NativeServices.RunCommand(CommandLine: "my-first-64-bit-process.exe");
    NativeServices.RunCommand(CommandLine: "my-second-64-bit-process.exe");
ENDIF;
Nesting

IF blocks can also be nested; the following example shows running an executable if the target machine is running on Windows, and a different executable depending on the operating system is 32- or 64-bit:

@device = Device.GetSummary();

@isWindows = SELECT * FROM @device WHERE OperatingSystemType = "Windows";
@x64 = SELECT * FROM @device WHERE OsArchitecture = "x64";

IF (@isWindows)
   IF (@x64)
       NativeServices.RunCommand(CommandLine: "my-64-bit-process.exe");
   ELSE
       NativeServices.RunCommand(CommandLine: "my-32-bit-process.exe");
   ENDIF;
ELSE
   // Other logic to handle other operating systems
ENDIF;

IF blocks can also contain any other statements, such as FOREACH loops and SELECT statements.

Warning

SCALE does not presently support an "ELSEIF" keyword; to emulate this behaviour (e.g. if A then do X, else if B then do Y, else if C then do Z, etc.) you can use the nesting approach described above.

IF Conditions

The previous examples all use the result of a SELECT statement on the result of a method call as their expression in the IF condition. You can however put the method call, including any required parameters, directly in the IF condition. The following example will run a command if a specified file exists:

IF (FileSystem.GetFileDetails(FilePath: "c:\\MyFolder\\MyFile.dat"))
    NativeServices.RunCommand(CommandLine: "process-my-file.exe");
ENDIF;

The example above works because FileSystem.GetFileDetails will return no rows (and therefore the condition will evaluate to false) if the file does not exist, or will return a single row (in which case the condition is true) if the file does exist.

Warning

SCALE does not presently support using operators or SELECT statements directly as the IF condition; you must instead perform the SELECT before the IF, assigning the result to a @table, and then use the @table as the condition.

Lastly, the expression may be a literal value, such as a string (enclosed within double-quotes) or a number. Any literal value – including NULL – evaluates to a table with a single column (named "Value") and a single row (with the literal value), so will always evaluate to true. This can be useful for testing code paths during development of an instruction – in other words, you may wish to temporarily force a condition to be true.

IF ("hello")
    Agent.Echo(Message: "This will always be executed");
ENDIF;

IF (1)
    Agent.Echo(Message: "So will this");
ELSE
    Agent.Echo(Message: "And this will NEVER be executed");
ENDIF;

Tip

This syntax is generally useful only for testing purposes; you would be unlikely to use this form of IF in a live instruction.

Negating an IF

You can use the NOT keyword in conjunction with IF to negate the expression being tested; for example:

@procs = OperatingSystem.GetProcesses();
@oneDrive = SELECT * FROM @procs WHERE Executable LIKE "OneDrive.exe";

IF NOT (@oneDrive)
    // OneDrive is not running - take further action here
ENDIF;

In the example above, the IF block is executed if @oneDrive contains no rows (which it won’t if there is no running process whose executable name is like "OneDrive.exe".

Note

The NOT operator occurs outside of the parentheses, and not inside. Technically speaking, the "NOT" does not form part of the expression, but is actually an extended form of the IF statement.

Capturing the result of an IF statement

Like FOREACH loops, IF statements themselves are an expression, in that they yield a value. The value returned by an IF statement is the last result of the block of statements executed. This can then be assigned to a @table, for example:

// Get lines from configuration file if it exists
@lines = FileSystem.GetFileByLine(FilePath: "C:\\MyFolder\\MyConfiguration.txt"); 

@setting = IF (@lines)
    // Configuration file exists - get the setting from the content
    // of line number 10
    SELECT Content AS MySetting FROM @lines WHERE LineNumber = 10;
ELSE
    // Configuration file does not exist (no lines were read)
    // so fall back to reading from registry
    @reg = NativeServices.RegistryGetValue(
        Hive:"HKLM", Subkey:"SOFTWARE\\MyVendor\\MyConfiguration",
        Name:"MySetting");

    // Append default domain suffix to whatever we have read
    SELECT Value || ".acme.local" AS MySetting FROM @reg;
ENDIF;

// Do something with the setting
SELECT "My setting is || " MySetting AS Value FROM @setting;

In this example, the @setting table is assigned the result of the IF statement, which will be either the result of SELECTing from @lines or from @reg, depending on the path of execution.

The same result could also be achieved by having both the IF and the ELSE branch of the code assign to the same @table:

@lines = FileSystem.GetFileByLine(FilePath: "C:\\MyFolder\\MyConfiguration.txt");

IF (@lines)
    @setting = SELECT Content AS MySetting
               FROM @lines
               WHERE LineNumber = 10;
ELSE
    @reg = NativeServices.RegistryGetValue(
        Hive:"HKLM", Subkey:"SOFTWARE\\MyVendor\\MyConfiguration",
        Name:"MySetting");
 
    @setting = SELECT Value || ".acme.local" AS MySetting FROM @reg;
ENDIF;

SELECT "My setting is || " MySetting AS Value FROM @setting;
Different data from different branches

Something to be careful of when using IF statements is having the IF branch of the code return data which is a different "shape" (i.e. different columns) to the ELSE branch of the code.

Consider the following:

@result = IF (@myTable)
    SELECT A, B, C FROM @myTable;
ELSE
    SELECT X, Y, Z FROM @myOtherTable;
ENDIF;

// Problem! - @result will have different columns depending on
// which branch of the IF statement was executed

In this example, depending on the path taken, the @result table will either have columns called A, B and C, or will have X, Y and Z. This means that subsequent code which uses @result may fail.

You should take steps to avoid this situation. If you intend to use the result of an IF statement, ensure that the column names (and datatypes) are consistent between the IF and ELSE branches.

Do you really need to use an IF?

IF statements are useful for executing different code paths depending on some condition – for example, one branch of your code might have to read some registry values and manipulate them, while the other branch may need to execute a command-line or run a WMI query.

However, often you may simply want to deal with different data in each code branch, but what you do with that data may be identical.

In that case, it can sometimes be easier to express this logic in SQL rather than using an IF statement – especially if you need to deal with multiple conditions.

Consider the case where you want to execute a different command depending on the Agent’s operating system; you could use a series of IF/ELSE statements to perform this, but an easier way to achieve the result could be as follows:

@device = Device.GetSummary();
@cmd = SELECT CASE OperatingSystemType
              WHEN  "Windows" THEN "my-windows-command-line.exe -a -b -c"
              WHEN  "MacOS"   THEN "/my/mac/command-line -d -e -f"
                            ELSE  "unsupported"
              END AS CommandToExecute
       FROM @device;

NativeServices.RunCommand(CommandLine: @cmd.CommandToExecute);

The SQL above builds a different command-line (using the SELECT CASE construct) depending on the operating system type, and then runs that command.